wit-bindgen が生成する moonbit のコードとそこから生成されるWATを眺めて、wit-bindgen の実装を学ぶ
(https://www.moonbitlang.com/blog/component-model とは少しやり方が変わっている)
moonbit + WASIp2 で hello
を出力するやつ作った。これの moon build --target wasm
で生成されるwasmコードと、wasm-tools
の出力とを眺めて wasm-tools component
の文脈で WASIp2 対応するために必要なことを調べていく。
Repositoryに書いているけど、やっていることまとめ
アプリケーションのWIT worldを書く。
$ cat wit/world.wit
package tanishiking:hello-moon-wasip2@0.0.1;
world run {
import wasi:cli/stdout@0.2.2;
export wasi:cli/run@0.2.2;
}
実行時のエンドポイントのため export wasi:cli/run@0.2.2;
と 標準出力から記述するために import wasi:cli/stdout@0.2.2
依存する WIT ファイルを wasi.dev から取得
https://github.com/bytecodealliance/wasm-pkg-tools の fetch
$ wkg wit fetch
wit-bindgen で moonbit binding 生成
$ wit-bindgen moonbit wit --derive-show --derive-eq --out-dir .
gen/interface の cli/run/stub に実装
moon.pkg.json
を編集して stdout
package と streams
package を使えるように
$ cat gen/interface/wasi/cli/run/moon.pkg.json
{
"import": [
{
"path": "tanishiking/hello-moon-wasip2/interface/wasi/cli/stdout",
"alias": "stdout"
},
{
"path" : "tanishiking/hello-moon-wasip2/interface/wasi/io/streams",
"alias" : "streams"
}
]
}
実装
$ cat gen/interface/wasi/cli/run/stub.mbt
// Generated by `wit-bindgen` 0.35.0.
/// Run the program.
pub fn run() -> Result[Unit, Unit] {
let stdout = @stdout.get_stdout()
let hello : Bytes = b"hello"
let _ = stdout.blocking_write_and_flush(hello)
Ok(())
}
stdout
って使った後に drop
しなくていいのかな?
/// Drops a resource handle.
pub fn OutputStream::drop(self : OutputStream) -> Unit {
let OutputStream(resource) = self
wasmImportResourceDropOutputStream(resource)
}
run
$ moon build --target wasm
# Not sure why this is needed
$ wasm-tools component embed wit target/wasm/release/build/gen/gen.wasm -o target/wasm/release/build/gen/gen.wasm --encoding utf16
$ wasm-tools component new target/wasm/release/build/gen/gen.wasm -o target/wasm/release/build/gen/gen.wasm
$ wasmtime target/wasm/release/build/gen/gen.wasm
hello%
$ moon build --target wasm
で生成した wasm ファイルの中身を$ wasm-tools print target/wasm/release/build/gen/gen.wasm
で見る。
全部 https://gist.github.com/tanishiking/d45473891a01ec241641ada2e2fb02cd
import
(type (;0;) (func (param i32 i32 i32 i32)))
(type (;1;) (func (result i32)))
(import "wasi:io/streams@0.2.2" "[method]output-stream.blocking-write-and-flush" (func (;0;) (type 0)))
(import "wasi:cli/stdout@0.2.2" "get-stdout" (func (;1;) (type 1)))
wasm component 文脈での import export で使う import name は このあたり で定義されていて
The plainname production captures several language-neutral syntactic hints that allow bindings generators to produce more idiomatic bindings in their target language. At the top-level, a plainname allows functions to be annotated as being a constructor, method or static function of a preceding resource. In each of these cases, the first label is the name of the resource and the second label is the logical field name of the function.
またWITのpackage nameはここにかかれているように は foo:bar:baz/quux@x.y.z
というような形式 (foo:bar:baz
package の、 quux
interface で、 バージョン x.y.z
)
Package names are used to generate the names of imports and exports in the Component Model's representation of interfaces and worlds as described below
なので
(type (;1;) (func (result i32)))
(import "wasi:cli/stdout@0.2.2" "get-stdout" (func (;1;) (type 1)))
は、wasi:cli
package の stdout
interface 内で定義されている get-stdout
関数を import する
@since(version = 0.2.0)
interface stdout {
@since(version = 0.2.0)
use wasi:io/streams@0.2.2.{output-stream};
@since(version = 0.2.0)
get-stdout: func() -> output-stream;
}
;; https://github.com/WebAssembly/WASI/blob/5120d70557cc499b98ee21b19f6066c29f1400a4/wasip2/cli/stdio.wit#L10-L17
func() -> output-stream
は (result i32)
に変換。なぜなら canonical ABI によると resource 型(own/borrow)を lower すると i32
https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flattening
(type (;0;) (func (param i32 i32 i32 i32)))
(import "wasi:io/streams@0.2.2" "[method]output-stream.blocking-write-and-flush" (func (;0;) (type 0)))
こちらも同様に output-stream resource の blocking-write-and-flush 関数 を import する
@since(version = 0.2.0)
blocking-write-and-flush: func(
contents: list<u8>
) -> result<_, stream-error>;
ん〜なんでこのinterface型に対して (type (;0;) (func (param i32 i32 i32 i32)))
になるんだ? そもそも result 型はどこにいった
ああ、最初の3つのi32はそれぞれ、resource (i32), 可変長リスト(i32, i32), そして最後のi32はresultだ。
https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#flattening にあるように result 型が複数の core wasm type を保つ場合は linear memory を経由してデータをやり取りして、パラメータにi32のポインタを置くんだ
When there are too many flat values, in general, a single i32 pointer can be passed instead (pointing to a tuple in linear memory). When lowering into linear memory, this requires the Canonical ABI to call realloc (in lower below) to allocate space to put the tuple.
生成された moonbit のコードを見ると、
pub fn get_stdout() -> @streams.OutputStream {
let result : Int = wasmImportGetStdout();
return @streams.OutputStream::OutputStream(result)
}
// interface/wasi/io/streams/top.mbt
pub type OutputStream Int derive(Show, Eq)
// interface/wasi/cli/stdout/ffi.mbt
// Generated by `wit-bindgen` 0.35.0. DO NOT EDIT!
fn wasmImportGetStdout() -> Int = "wasi:cli/stdout@0.2.2" "get-stdout"
(import "wasi:cli/stdout@0.2.2" "get-stdout")
になるのか
そして blocking_write_and_flush
はこちら。
pub fn OutputStream::blocking_write_and_flush(self : OutputStream, contents : Bytes) -> Result[Unit, StreamError] {
let OutputStream(handle) = self
let return_area = @ffi.malloc(12)
wasmImportMethodOutputStreamBlockingWriteAndFlush(handle, @ffi.bytes2ptr(contents), contents.length(), return_area);
let lifted6 = match (@ffi.load8_u((return_area) + 0)) {
0 => {
Result::Ok(())
}
1 => {
let lifted = match (@ffi.load8_u((return_area) + 4)) {
0 => {
StreamError::LastOperationFailed(@error.Error_::Error_(@ffi.load32((return_area) + 8)))
}
1 => {
StreamError::Closed
}
_ => panic()
}
Result::Err(lifted)
}
_ => panic()
}
ignore(contents)
@ffi.free(return_area)
return lifted6
}
return_area のために 12 bytes を malloc している。
Result 型は variant の特殊型なので
# result<_, stream-error> は
(variant
(case "ok" )
(case "error" stream-error)
stream-error
も variant
で error は resource型
@since(version = 0.2.0)
variant stream-error {
last-operation-failed(error),
closed
}
stream-errorについては、
- discriminant_type は u8 (これは core wasm では i32 にlower)
- payload は
last-operation-failed(error)
が最長なので i32
よってstream-error
は[i32, i32]
だし、alignmentは4
result<_, stream-error>
- discriminant_type は u8(これは core wasm では i32 にlower)
- payloadはstream-error (
[i32, i32]
) が最長
よってresult<_, stream-error>
は[i32, i32, i32]
alignment は4なので padding も不要
なので result<_, stream-error>
は12bytes必要。
-
@ffi.load8_u((return_area) + 0
でresult
のcase indexを読む。- 0ならokでデータ無しなので
Ok(())
を返す - 1なら
stream-error
を返す- stream-errorもvariantなので最初の4バイトからcase indexを読み、0か1によってLastOperationFailedかClosedに対応するエラーを返す
- 0ならokでデータ無しなので
これ、wit-bindgenがcanonicalABIでどういうデータ型になるかは計算してくれるんだろうか???
例えばcomponent-level functionの型をcore wasmに変換する部分は wit-parser
に実装されていて
- ところで Scala.js ではメモリに関するAPIを表に出さない(つまりIRにも出現しない)設計にしたい
- そうなると
wit-bindgen
は単に Interface Type の対応する Scala のデータ構造に変換されるだけ - CanonicalABI相当のflattening処理はScala.jsのリンカーバックエンドが担うことになる
- 多分型の変換だけで済む気がしているんだけど、moonbitとかrustのwit-bindgen、実装を見ていると思ったよりいろんなことをしているように見える? のでちょっと不安
- ところで
realloc
って export されていることを期待するのかな?
これを実装する、emit
で渡してくる Instruction ってやつに対応するゲスト言語の実装を書いて core wasm からデータを読み出してbindingが生成したデータ型に変換するやつを実装してね♥って感じの世界観らしい。
こうですか
export編
componentからimportした関数をcore wasmで利用する方法はわかった。じゃあexportは?
core wasm で export されているものは以下
(export "memory" (memory 0))
(export "wasi:cli/run@0.2.2#run" (func 41))
(export "cabi_realloc" (func 42))
;; ...
(type (;1;) (func (result i32)))
(func (;41;) (type 1) (result i32)
wasi:cli/runのrun は run: func() -> result
なので (func (result i32))
になるのはOK
wasi:cli/run@0.2.2#run
って名前は convention で決まっているのか?
最終的にはこの export された wasi:cli/run@0.2.2#run
は以下のような感じに扱われるので名前は関係ないんだけど。
(alias core export 3 "wasi:cli/run@0.2.2#run" (core func (;4;)))
(func (;2;) (type 6) (canon lift (core func 4)))
(instance (;3;) (instantiate 0
(with "import-func-run" (func 2))
)
)
つまり wasm-tools
が WIT をベースに component を作るタイミングでどうにかして、どの export が wasi:cli/run@0.2.2 の run
実装かを知っているべき
う〜ん custom section が怪しそう
わかんなかったので質問した
現状の wasm-tools
の実装はこのへん
だけど、この <interface>#<function>
という名前はwasm-toolsの実装がそうなっているだけの "legacy standard" で
Add BuildTargets.md by lukewagner · Pull Request #378 · WebAssembly/component-modelで component model の文脈での core wasm module がどういう形になっているべきかを定めるドキュメントをspecに追加する予定らしい。
ところでComponent Modelのresource使って、WasmGCのstructを外側にexportするときってGCどうなる?
今resource rep i32しかないけどrefも使えるようになったとして、resourceのdestructorでrefを破棄するようにすれば、外部からの参照が消えたときにrefも破棄されてそのうちGC対象になる?