rontolisp:wit-provide
(rontolisp:wit-provide "wasi:keyvalue/store@0.2.0" #'my-store)
rontolisp:wit-import で取り込んだ WIT
インターフェースの実装を束縛します。インタプリタと JVM
バックエンドには呼び出す先の WASM ホストが存在しないため、インポートされた関数は
すべてそのインターフェースの プロバイダ 経由でディスパッチします。それを供給するのが
この関数です。インターフェース id を返し、そのインターフェースに既に束縛されている
プロバイダを置き換えます。
rontolisp はどのインターフェースについてもプロバイダを一切同梱していません。
同梱しているのはプロバイダの仕組みであって、wasi:keyvalue が — あるいは他のどの
インターフェースが — 何であるかを rontolisp は知りません。WIT
インターフェースの実装は通常の Lisp コードであり、それを渡す手段がこの関数です。
プロバイダは、束縛された関数の Lisp メンバー名 (文字列 — "open"、
"bucket-get"。生の WIT ラベルではなく、束縛が綴られる名前) に続けてその関数の引数を
受け取る、通常の Lisp 呼び出し可能オブジェクトです。以下は完全なプロバイダです —
ハッシュテーブル 1 つのストアで、ストアに必要なのはこれだけです。
これをソースに置けば、wit-import の wasi:keyvalue
プログラムは *rows* を相手にします。呼び出し箇所
((kv:bucket-get b "visits")) も .wit もそのままで、キーと値がどこにあるかを
知っているコードはプログラムの中に 1 行もありません。
引数
- インターフェース id (文字列)。
.witの完全修飾 id ("wasi:keyvalue/store@0.2.0") です。rontolisp:wit-importは:interfaceに どの綴りを渡された場合でも、この正規化された id でディスパッチします ("wasi:keyvalue/store"や裸のstoreも同じインターフェースを指します)。 そのため、このキー 1 つですべての綴りに対してプロバイダが束縛されます。 - プロバイダ:
(member &rest args)の形の任意の Lisp 呼び出し可能オブジェクト —#'name関数、lambda、その他funcallが受け付けるものなら何でも構いません。
プロバイダが尋ねられるメンバー
インターフェースが宣言する関数ごとに 1 つ、綴りは
wit-import が束縛するとおりです:
"open"、"bucket-get"、コンストラクタなら "bucket-new"。加えてリソースごとに
"<resource>-drop" があり、その引数はハンドル 1 つだけです。この最後のメンバーは
どの .wit も宣言していません。リソースの解放はインターフェースの関数ではなく
コンポーネントモデルの canonical な組み込み機能だからです。そこで rontolisp
がこれに
<resource>-drop
という名前を与え、他のメンバーと同じようにプロバイダへディスパッチします。
drop が何を意味するかを決めるのはプロバイダであり、プロバイダだけです。
コアが知っているのは「プログラムがそのハンドルを使い終えた」ことだけで、そのハンドルが
何を指していたかは知りません。ですから、行をハッシュテーブルに持つストアはハンドルを
忘れて行はそのまま残し (上の my-store は nil を返すだけで、それで実装として完全
です)、JDBC 接続や開いたファイルを握っているプロバイダはそれを閉じ、ハンドルに何の
コストもないプロバイダは解放するものが無いので同じく nil を返します。drop
がやってはいけないのは、ハンドルが指していたものを破棄することです。ハンドルは
ストアへの参照であってストアそのものではないので、後から (kv:open "counts")
したときには、drop されたハンドル経由で書かれたキーがすべて見えなければなりません。
メンバーを書き落とすと、drop はプロバイダのフォールバック節に落ちます — 上の
my-store なら rontolisp:wit-error の節です。解放するものが無いプロバイダも、
黙っているのではなく nil を返すようにしてください。
rontolisp はプロバイダを同梱しません
コアが知っているのはプロバイダの仕組みです。wasi:keyvalue
が何であるかをコアは知らず、その実装も — 他のどのインターフェースの実装も —
同梱していません。これは意図的なものです: 新しいホストインターフェースに必要なのは
.wit ファイルであるべきで、コアのコードであるべきではありません。プロバイダが
まだ束縛されていない状態で束縛済みの関数を呼ぶと、何らかの既定値に到達するのではなく
rontolisp:wit-error がシグナルされます。
$ rontolisp counter.lisp
No provider is bound for the WIT interface wasi:keyvalue/store@0.2.0 -- bind one with rontolisp:wit-provide
プロバイダはただの関数なので、偽物と本物は互換であり、プログラムはその違いを
見分けられません。
wit/keyvalue の例
は wasi:keyvalue/store に対して書かれたページビューカウンタで、その背後には 2
つのストアがあります: 可搬なインメモリ Lisp ストアと、JVM では
java: interop を通じて本物の
java.util.LinkedHashMap に支えられたストア (後から束縛されるので、こちらが前者を
置き換えます) です。ストアは変わりますが、(kv:bucket-set b "/index" "3")
という呼び出し箇所は変わらず、カウンタの出力もどちらでも同一です。実際のデプロイでは、
同じ 1 行でその map を Redis や JDBC 接続に差し替えます。代わりに Preview 1 WASM
へコンパイルすればホストがプロバイダになり、これもプログラムの変更なしで済みます。
wit-error コンディション
rontolisp:wit-error は WIT の result<T, E> の error アームがシグナルする
コンディションです。ok アームは関数の戻り値、error アームはコンディション —
すべてのバックエンドでそうなります。プロバイダはマップされた E
をペイロードとしてこれをシグナルし (上の例では wasi:keyvalue の error variant、
すなわちタグ付きリスト)、呼び出し側は (rontolisp:wit-error-payload e)
でペイロードを読み戻します。
;;; The caller of an imported function, in the same program as the wit-import.
(handler-case (kv:bucket-delete *bucket* "visits")
(rontolisp:wit-error (e)
(print (rontolisp:wit-error-payload e)))) ; (:other "read-only store")
これは通常のコンディションクラスです。handler-case、ignore-errors、
unwind-protect がすべて使え、error のサブクラスなので素の
(handler-case ... (error (e) ...)) でも捕捉できます。
バックエンド
| Backend | Effect |
|---|---|
| interpreter | binds the provider; imported functions dispatch to it |
JVM (-o Prog.class) | the same |
Preview 1 WASM (-o prog.wasm) | a top-level form is dropped — the WASM host is the provider |
--component | the same: dropped, the host (or a composed component) is the provider |
--no-gc | it rejects rontolisp:wit-import itself |
エラーにせず捨てるからこそ、1 つのソースがすべてのバックエンドで動きます。
インタプリタでプログラムを支えていた rontolisp:wit-provide
は、ホストが引き継いだ時点で単に無効化されるだけです。
制限事項
- インターフェース id は文字列として照合されます。したがって
"wasi:keyvalue/store"で束縛したプロバイダは"wasi:keyvalue/store@0.2.0"へディスパッチする呼び出しには使われません。wit-importの:interfaceと同じ綴りにしてください。 - プロバイダはグローバルでスコープを持ちません。あるインターフェースについては最後の
rontolisp:wit-provideが、プログラムの残り全体で有効になります。 - これらのバックエンドでは境界で何もマーシャリングも型チェックもされません — プロバイダには Lisp の値がそのまま渡され、その戻り値もそのまま返されます。 WIT 型の表 が、プロバイダを書くときの契約です。
- 手書きの
rontolisp:wasm-importには束縛すべきプロバイダはありません。rontolisp:wit-provideが相手にするのはrontolisp:wit-importが束縛したインターフェースです。