WASI 0.3 コンポーネント(--component)
--component を追加すると、Preview 1 コアモジュールの代わりに WASI 0.3(Preview 3)コンポーネントが出力されます。コンポーネントは wasi:cli/stdout@0.3.0 を通じて印字します:
rontolisp hello.lisp --component -o hello.wasm
wasmtime run -W gc=y hello.wasm
3
WASI 0.3 ではすべてのバイト I/O が組み込みのコンポーネントモデル型 stream<u8> / future<T> と非同期正準 ABI を流れます。rontolisp は同じ Preview 1 コアモジュールを無変更のまま保ち — 依然として 9 つの wasi_snapshot_preview1 関数をインポートします — アダプタコアモジュールがそれらを WASI 0.3(wasi:cli、wasi:filesystem、wasi:clocks、wasi:random)の上に stream.new/stream.read/stream.write と future.read を使って実装します。これらの組み込みは非同期(ノンブロッキング)版です: BLOCKED が報告されると、タスクは完了イベントが届くまでブロッキング待機の waitable-set.wait で待つため、アダプタは直線的なコードのままです。コンポーネントの wasi:cli/run@0.3.0 エクスポート(async func)は非同期型付きのエクスポートとしてリフトされ、そこからこのブロッキング待機は合法です。これらはすべて wasmtime 46+ でデフォルト有効な基本のコンポーネントモデル非同期 ABI の上に成り立っています — ゲートされた機能フラグは残っておらず、必要なのは(wasm-GC コアのための)-W gc=y だけです。
wasmtime の起動方法が出力の種類を選ぶわけではありません。wasmtime run は wasmtime のデフォルトサブコマンドで、コアモジュールかコンポーネントかを自動検出するため、wasmtime run -W gc は Preview 1 の hello.wasm も同様に実行します。Preview 1 コアモジュールと WASI 0.3 コンポーネントのどちらが生成されるかを決めるのは、コンパイル時の --component フラグだけです。(実際上の違いはコンポーネント専用ランタイムで現れます。そこではコンポーネントは動きますが Preview 1 コアモジュールは動きません。)
コンポーネント内で動くもの
コンポーネント内で動くもの、そして各機能が実行時に必要とするもの:
print/標準出力、標準入力(read、0 引数のread-line、wasi:cli/stdin@0.3.0経由)、ファイル I/O(open、close、write-line、ストリームread-line、load、with-open-file)はすべて動作します。async ボディ内では、保留中の標準入力のread-line/read-charはソケット読み取りと同様に自分のタスクだけをサスペンドします — 入力を待っている間も、並行するrontolisp:wait-forタイマーは動き続けます。ファイルアクセスには--dirが必要です(相対パスは最初にプリオープンされたディレクトリ、絶対パスは名前がそのパスの最長の接頭辞になるプリオープンディレクトリに対して解決されます):
cat > fileio.lisp <<'EOF'
(with-open-file (out "greeting.txt" :direction :output)
(write-line "hello" out))
(with-open-file (in "greeting.txt")
(print (read-line in)))
EOF
rontolisp fileio.lisp --component -o fileio.wasm
wasmtime run -W gc=y --dir . fileio.wasm
# "hello"
randomはwasi:random@0.3.0から本物のエントロピーを引きます(Preview 1 はホストのrandom_getを使います)。そのため(random N)は実行ごとに異なります。get-universal-time/get-internal-real-time/get-internal-run-timeはwasi:clocks@0.3.0(system-clock/monotonic-clock)を読み、uiop:getenvはwasi:cli/environment@0.3.0を読みます。- 送信 HTTP(
rontolisp:fetchとrontolisp:await/rontolisp:futurepの future 操作)はコンポーネントモードで動作し、真の非同期性も含みます:fetchはリクエストを送って(処理中のwasi:httpレスポンスハンドルをラップした)future を即座に返すため、awaitが各リクエストをサスペンドする前に複数のリクエストを重ねられます。future 操作自体はどのモードでもコンパイルできます。コンポーネント専用なのはfetchだけです。fetch は非同期のwasi:http@0.3.0(wasi:http/types+wasi:http/client)をインポートします — コンポーネントの他の部分と同じく一様に WASI 0.3 です。fetch コンポーネントは通常のフラグに加えて-S http=y(ホストにwasi:httpを提供させるフラグ)で実行してください。fetch を使わないコンポーネントはwasi:httpをインポートしないため、-S httpは不要です。トランスポートの失敗(接続拒否、名前解決不能)はどのバックエンドでもawait時にrontolisp:wit-errorをシグナルします。nilが返るのはリクエストを開始できなかった場合だけです。リクエスト/レスポンスの形については HTTP fetch ガイドを参照してください。 - TCP ソケット(
rontolisp:tcp-connect/tcp-listen/tcp-accept/tcp-local-port)はコンポーネントモードでwasi:sockets@0.3.0の上で動作します(ネイティブに WASI 0.3 — 0.2 ハイブリッドではありません)。ソケットは双方向ストリームハンドルなので、read-line/write-line/write-string/read-byte/write-byte/closeが直接使えます。ソケットコンポーネントは通常のフラグに加えて-W exceptions=y -S tcp=y -S inherit-network=yで実行してください(tcp コンポーネントは常に exception-handling モードでコンパイルされます)。-Sフラグがなくてもコンポーネントは起動しますが、すべてのソケット操作が失敗してnilを返します。ホストは IPv4 リテラルでなければなりません(ホスト名解決はまだありません)。rontolisp:fetchと tcp 関数は 1 つのコンポーネントで組み合わせられ、tcp はrontolisp:http-handler(serve)コンポーネントの中でも使えます。async 本体では、保留中のtcp-acceptやソケット読み取りはそのタスクだけをサスペンドします — 他のタスク(rontolisp:wait-forタイマーや別のリクエスト)は動き続けます。API 全体は TCP ソケットガイドを参照してください。 - それ以外の点では、コンパイルされた Lisp はサポートされる機能について Preview 1 出力と同一に振る舞います。受信 HTTP のサービング(
rontolisp:http-handler)もコンポーネントにコンパイルされますが、別種のコンポーネント(wasi:http/handler@0.3.0をエクスポート)で、wasmtime serveのもとで動きます — HTTP ハンドラーガイドを参照してください。
インポート表面はツリーシェイキングによって狭められ、プログラムに追随します: 表示だけを行うコンポーネントは wasi:cli/{types,stdout} だけをインポートし、それ以外はありません — wasi:cli/stderr が加わるのは、プログラムが実際にそこへ書ける場合(warn、*error-output*、または捕捉されなかったコンディションが出す報告)だけです。これは wasm-tools component wit の出力にも、後述の --emit-wit の出力にも現れます。実行時のフラグは何も変わりません — ホストが提供すべきものが減るだけです。--optimize=off でビルドしたコンポーネントは、代わりにプログラムがそれらを使うかどうかに関わらず上記すべてを宣言するため、インポート表面はどのプログラムでも同じになります。
コンポーネントモデル関数エクスポート(wasm-export)
--component のもとでは、rontolisp:wasm-export は型付きコンポーネントモデルエクスポートになり、正準 ABI を通じて WAVE 構文(wasmtime run --invoke 'name(args)'、experimental 警告なし)で呼び出せます — しかも wasi:cli/run のコマンドエントリと共存するため、同じコンポーネントは引き続きコマンドとしても実行できます:
rontolisp sumsq.lisp --component -o sumsq.wasm
wasmtime run -W gc=y --invoke 'sumsquared(2, 3)' sumsq.wasm
# 25 (the export's return value, rendered by wasmtime)
wasmtime run -W gc=y sumsq.wasm
# 400 (the ordinary run entry executes the top-level program)
2 つのコマンドは異なるものを表示します: --invoke は名前付きエクスポートだけを呼び出し、トップレベルのプログラム(wasi:cli/run エントリ)は実行されません — 25 は wasmtime がエクスポートの戻り値を WAVE 構文でレンダリングしたものであり、print の出力ではありません。素の run は代わりにトップレベルのプログラムを実行するため、400 は (print (sumsquared 10 10)) の出力です。
型付きシグネチャ(各整数指定子はそれ自身の WIT 名で — :s32 → s32、:u32 → u32、… — 加えて :float → f64、:bool → bool、:string → string、:s-expr → 印字された S 式テキストを運ぶ string、:returns 省略 → 結果なし)は任意のコンポーネントホストから見え、:as はコア側と同様にコンポーネントエクスポートの名前を変更します。
:string 境界は本物のコンポーネントモデル string として越えます — どちら側にも手動のポインタ処理はありません。ホストは引数のバイト列をリニアメモリへローワリングし、結果を正準 ABI を通じて読み出します。その後モジュールは呼び出しごとの確保を解放する(正準 post-return 関数がバンプアロケータをポップする)ため、常駐インスタンスは繰り返し呼び出しでもフラットに保たれます:
rontolisp greet.lisp --component -o greet.wasm
wasmtime run -W gc=y --invoke 'greet("世界")' greet.wasm
# "Hello, 世界"
デフォルトではエクスポートは同期的にリフトされます。それでも、その中の I/O は通常動作します: 非同期組み込みはホストが即座に受理する限り(標準出力はそうです)ブロックせずに完了し、BLOCKED を報告するホストだけがブロッキング待機を強制します。この待機は同期タスク内では "cannot block a synchronous task" でトラップします。:async t でエクスポートを非同期と宣言すると、代わりに非同期関数型に対してリフトされ — run エントリと同じ非同期型付きリフトです — この残余リスクがなくなります。wasmtime --invoke は非同期エクスポートもまったく同じ方法で呼び出します:
rontolisp status.lisp --component -o status.wasm
wasmtime run -W gc=y -W exceptions=y -S http=y \
--invoke 'fetch-status("https://httpbin.ik.am/status/204")' status.wasm
# "fetching"
# 204
コンポーネントの WIT レベルの契約では、:async t エクスポートは async func です(例えば jco は Promise を返す関数として型付けし、同期エクスポートは普通の関数のままです)。同期と非同期のエクスポートは 1 つのコンポーネント内で自由に混在でき、:async は :string/:s-expr を含むすべての境界型と組み合わせられ、:async エクスポートのないプログラムはバイト単位で同一の出力を生成します。
コンポーネントエクスポートの現在の制限:
- 同期(デフォルト)エクスポートでも I/O は通常動作します(非同期組み込みはホストが即座に受理する限りブロックせずに完了します)。BLOCKED を報告するホストだけがブロッキング待機を "cannot block a synchronous task" でトラップさせます。エクスポートが印字・fetch・その他の I/O を行うときは
:async tにオプトインしてこの残余リスクを除き、純粋計算のエクスポートは同期のままにしてください。 :asyncが意味を持つのはここだけです: Preview 1 /--no-wasiのコアエクスポートは無視し(そこではホストが直接 I/O を提供します)、--no-gc --componentは拒否します(コンパクトなリアクターコンポーネントには非同期アダプタがありません)。- jco(1.25.2)は
:async tエクスポートをトランスパイルして非同期として型付けしますが、まだ呼び出せません — 0.3 非同期 ABI のサポートが上流で未実装です(トランスパイルされたrunを呼べないのと同系統のギャップです)。非同期エクスポートの検証済みパスはwasmtime run --invokeです。同期エクスポートはどちらでも動作します。 - エクスポート名は lower-kebab-case のコンポーネントモデル名(
sum-squared)でなければなりません。その文法から外れる Lisp 名については、コンパイラが:asでの改名を求めます。 - エクスポートの呼び出しはプログラムのトップレベルを先に実行しないため、
defvar/defparameterのグローバルを読むエクスポートは未初期化の値を見ることになります(これは Preview 1 の--invokeの動作と一致します)。後述のリアクター形状はこれを解消します: そのトップレベルはインスタンス化時に実行されます。
純粋計算のエクスポートキットには、コンパクトな --no-gc --component が同じ型付きエクスポート(ただし :s-expr なし)を、wasmtime のフラグを一切必要としない数百バイトのコンポーネントとして出力します。
リアクターコンポーネント(--component --no-wasi)
--no-wasi を追加するとリアクターコンポーネントが出力されます: 何もインポートしないコンポーネントです。WASI 表面が一切なく — wasm-tools component wit はどの --optimize レベルでも import 行を 1 行も表示しません — 任意のコンポーネントホストが空のインポートオブジェクトでインスタンス化でき、エクスポートはリフトされた wasm-export 関数だけです(wasi:cli/run エントリはありません):
$ rontolisp greet-reactor.lisp --component --no-wasi -o greet.wasm
$ wasm-tools component wit greet.wasm
package root:component;
world root {
export greet: func(p0: string) -> string;
}
$ wasmtime run -W gc=y --invoke 'greet("world")' greet.wasm
"hello, world"
他のすべてのコンポーネント形状と異なり、トップレベルフォームはインスタンス化時に実行されます(コアモジュールの start セクション)。そのため上の defparameter は最初のエクスポート呼び出しが届く時点で既に代入済みです — ホスト側の協力は不要です。裏返しとして、トップレベルフォームでのトラップはインスタンス化自体を失敗させます。
I/O の契約は Preview 1 リアクターのものがそのまま適用されます: 出力(print、format t)は破棄され、入力・時刻・random はトラップし、with-open-file/open は捕捉可能なエラーをシグナルします。インポートを持ち込むものは --no-wasi とはコンパイルできません — rontolisp:fetch、rontolisp:http-handler、rontolisp:wait-for、rontolisp:wit-import はそれぞれ衝突を名指しで報告します。
JavaScript エンベッダにとって、これは WASI シムを一切必要としないコンポーネント形状です: jco transpile greet.wasm --instantiation sync が生成するグルーの ImportObject 型は文字通り空({})です。clack:clackup ... :server :rontolisp アプリケーションにも同じことが当てはまります — リアクタービルドの handle-request エクスポート(Clack ガイドを参照)は --component --no-wasi でも動作し、Clack アプリケーションに型付きの handle-request: func(p0: string) -> string コンポーネントエクスポートを与えます。