2025/11/28 18:23
Cross-Compiling Common Lisp to WASM
RSS: https://news.ycombinator.com/rss
要約▶
WebAssembly で Common Lisp(WECL)を動かすには、①ECLをホスト版→ターゲットへクロスコンパイルし、emccでWASMモジュールを生成。②WECLはそのランタイムとCレイヤーを組み合わせ、JS‑FFI・REPL・Emacs接続機能を提供する。③ユーザーコードも同様のツールチェーンでコンパイルし、静的ライブラリとしてリンクできる。主要ポイント:クロスビルド手順、WECL構成とインタフェース、ASDF拡張による自動コンパイルとキャッシュ機能。
本文
WebAssembly で Common Lisp を使う(WECL)
著者: Daniel Kochmański
公開日: 2025‑11‑28
目次
ECL のビルド
ECL(Embeddable Common Lisp)を WebAssembly 用にコンパイルするには、まずホスト版を構築し、その後でターゲットへクロスコンパイルします。
git clone https://gitlab.com/embeddable-common-lisp/ecl.git cd ecl export ECL_SRC=$(pwd) export ECL_HOST=${ECL_SRC}/ecl-host ./configure --prefix=${ECL_HOST} && make -j32 && make install
Emscripten SDK のインストール
git clone https://github.com/emscripten-core/emsdk.git pushd emsdk ./emsdk install latest ./emsdk activate latest source ./emsdk_env.sh popd
WebAssembly ターゲットのビルド
make distclean # build/ ディレクトリを削除 export ECL_WASM=${ECL_SRC}/ecl-wasm export ECL_TO_RUN=${ECL_HOST}/bin/ecl emconfigure ./configure \ --host=wasm32-unknown-emscripten \ --build=x86_64-pc-linux-gnu \ --with-cross-config=${ECL_SRC}/src/util/wasm32-unknown-emscripten.cross_config \ --prefix=${ECL_WASM} \ --disable-shared \ --with-tcp=no \ --with-cmp=no emmake make -j32 && emmake make install
生成されたランタイムファイルをコピーします。
cp build/bin/ecl.js build/bin/ecl.wasm ${ECL_WASM}
ホストの実行
Quicklisp と
hunchentoot が必要な小さな Web サーバスクリプトで、ファイルを配信できます。
export WEBSERVER=${ECL_SRC}/src/util/webserver.lisp ${ECL_TO_RUN} --load $WEBSERVER # その後、ブラウザで firefox localhost:8888/ecl-wasm/ecl.html を開く
Node.js で一度だけ評価することも可能です。
node ecl-wasm/ecl.js \ --eval '(format t "Hello world!~%")' \ --eval '(quit)'
生成された
.wasm は、Emscripten が追加したインポート(例:invoke_iii)のために他のランタイムでは移植性がありません。WasmEdge で実行すると次のようなエラーになります。
[error] instantiation failed: unknown import, Code: 0x62 ...
WECL のビルド
WECL はクロスコンパイルされた ECL ランタイムを、C 言語レイヤーと組み合わせて提供します。C レイヤーは WASM モジュールをロードし、JavaScript との相互作用、REPL サポート、および Emacs 接続機能を実装しています。
fossil clone https://fossil.turtleware.eu/wecl cd wecl
アーティファクトと SLIME フォークを
Code/ にコピーします。
pushd Code cp -r ${ECL_WASM} wasm-ecl git clone git@github.com:dkochmanski/slime.git popd
ビルドして起動します。
./make.sh build ./make.sh serve
Emacs へ接続するには
App/lime.el を評価し、 (lime-net-listen "localhost" 8889) を実行してください。ブラウザで
<http://localhost:8888/slug.html> を開き Connect ボタンをクリックします。
リポジトリ構成
| ファイル | 用途 |
|---|---|
| JS 相互作用関数用パッケージ |
| 早期ユーティリティ(例:) |
| JS‑FFI、オブジェクトレジストリ、コンソールストリーム |
| JavaScript オブジェクトとメソッドへのバインディング |
| HTML に埋め込まれた Common Lisp スクリプトをロード |
典型的なエントリポイント:
- main.html – REPL と xterm コンソールを読み込み
- easy.html – JS ↔ CL の相互作用例
- slug.html – Emacs に接続するボタン
最小 HTML スケルトン
<!doctype html> <html> <head> <title>Web Embeddable Common Lisp</title> <script src="boot.js"></script> <script src="wecl.js"></script> </head> <body> <script type="text/common-lisp"> (loop for i from 0 below 3 for p = (|createElement| "document" "p") do (setf (|innerText| p) (format nil "Hello world ~a!" i)) (|appendChild| "document.body" p)) </script> </body> </html>
Tip:
ヘルパーを使うと、JavaScript の演算子やアクセサの Lispy バインディングを自動生成できます。lispify-name
ユーザーコードのビルド
ユーザーコードのクロスコンパイルは ECL と同じツールチェーンを使用します。
以下に例としてソースを示します。
;;; test-file-1.lisp (in-package "CL-USER") (defmacro twice (&body body) `(progn ,@body ,@body)) ;;; test-file-2.lisp (in-package "CL-USER") (defun bam (x) (twice (format t "Hello world ~a~%" (incf x)))) (defvar *target* (c:read-target-info "/path/to/ecl-wasm/target-info.lsp")) (with-compilation-unit (:target *target*) (compile-file "test-file-1.lisp" :system-p t :load t) (compile-file "test-file-2.lisp" :system-p t) (c:build-static-library "test-library" :lisp-files '("test-file-1.o" "test-file-2.o") :init-name "init_test"))
生成された
libtest-library.a は WECL にリンクできます。Emscripten の呼び出しに追加し、
Code/wecl.c から init_test を呼び出します。
extern void init_test(cl_object); ecl_init_module(NULL, init_test);
ASDF を拡張する
以下のモジュールは WebAssembly ターゲット用に静的ライブラリを生成するクロスコンパイル機能を追加します。
パッケージと変数
(defpackage "ASDF-ECL/CC" (:use "CL" "ASDF") (:export "CROSS-COMPILE" "CROSS-COMPILE-PLAN" "CLEAR-CC-CACHE")) (in-package "ASDF-ECL/CC") (defvar *host-target* (c::get-target-info)) (defparameter *cc-target* *host-target*) (defparameter *cc-cache-dir* #P"/tmp/ecl-cc-cache/")
オペレーション
(defclass cross-object-op (downward-operation) ()) (defclass cross-compile-op (sideway-operation downward-operation) ()) (defclass cross-load-op (non-propagating-operation) ())
cross-object-op は個別のソースファイルをオブジェクトファイルにコンパイルします。cross-compile-op はそれらを集めて静的ライブラリを作成し、cross-load-op はターゲット環境での依存関係ロードを処理します。
ヘルパーマクロと関数
(defmacro with-asdf-compilation-unit (() &body body) `(with-compilation-unit (:target *cc-target*) (flet ((cc-path () (merge-pathnames "**/*.*" (uiop:ensure-directory-pathname *cc-cache-dir*)))) (let ((asdf::*output-translations* `(((t ,(cc-path))))) (*load-system-operation* 'load-source-op) (*features* (remove-duplicates (list* :asdf :asdf2 :asdf3 :asdf3.1 *features*)))) ,@body)))) (defun cross-compile (system &rest args &key cache-dir target load-type &allow-other-keys) (let ((*cc-cache-dir* (or cache-dir *cc-cache-dir*)) (*cc-target* (or target *cc-target*)) (*cc-load-type* (or load-type *cc-load-type*))) (apply #'operate (make-operation 'cross-compile-op) system args))) (defun cross-compile-plan (system target) (with-asdf-compilation-unit () (map nil (lambda (a) (format t "~24a: ~a~%" (car a) (cdr a))) (asdf::plan-actions (make-plan 'sequential-plan 'cross-compile-op system)))))
使い方
(cross-compile "flexi-streams" :target *wasm-target*) ;; → #P"/tmp/ecl-cc-cache/libs/flexi-streams-20241012-git/libflexi-streams.a"
生成されたライブラリには
flexi-streams とその依存関係である trivial-gray-streams のオブジェクトが含まれます。キャッシュは変更されたファイルのみを再コンパイルすることでビルド時間を短縮します。
資金調達
本プロジェクトは NGI0 Commons Fund によって資金提供されています。この基金は NLnet が設立し、欧州委員会の Next Generation Internet プログラムからの財政支援を受けています。
詳細については NLnet のプロジェクトページ をご覧ください。