wit-bindgenとjcoでWebAssembly Component Modelに入門する
2023-04-18
WebAssembly Component Model
を追えていなかったのでHello World
的なものを動かし概要を理解したい。
WARNING
この記事で紹介しているwit
の記法は古くなっている可能性があります
目次
WebAssembly Component Model
WebAssembly/component-modelにspec
は定義されており、現在のPhase
はPhase 1 - Feature Proposal (CG)
となっている。
Goals
上記においてGoals
として以下を挙げている。
- ポータブルで効率的なバイナリフォーマットの定義
- ポータブルで仮想化可能なインターフェースのサポート
WebAssembly
のユニークな価値提案の維持Component Model
の段階的定義
MVP における usecase
またMVP
におけるusecase
として以下のようなものを挙げている。
Node.js, CPython
などにおいてComponent
をポータブルでサンドボックス化された代替として使用することで、ネイティブプラグインの移植性とセキュリティの問題の回避- サーバーレスプラットフォームにおいて、固定のスクリプト言語の代わりに
Component
を使用し、wasm
の強力なサンドボックス化と言語中立性を活用 - サーバーレスプラットフォームにおいて、低オーバーヘッドと高速インスタンス化のために
Component
を活用 - 大規模なアプリケーショにおいて、最小権限の原則やモジュール型プログラミングを実践し、アプリケーションを
Component
に分解
まだいくつか挙げられているがざっくり書くと上記のようなものが挙げられている。
PostMVP における usecase
対してPostMVP
には結構野心的なものが挙げられている。
中でもdynamic linking
は期待している。というより個人的には必須の機能だと思っている。
Runtime dynamic linking
Parallelism
Copy Minimization
Component-level multi-threading
Hello Component Model
ここからは実際に試して見る。
今回はGuest
としてRust
を用いHost
はJS
を前提に試していく。
Guest(Rust)
cargo new hello
Cargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", version = "0.4.0" }
wit
ファイルを書くのだがwit
ディレクトリはCargo.toml
と同じ階層に作るようだ。
とりあえずrun
するとHello
というstring
が返ってくるものを想定する。
wit/hello.wit
// wit/hello.wit
default world hello {
export run: func() -> string
}
Rust
は以下のようにmacro
経由でhello.wit
を参照する。
src/lib.rs
wit_bindgen::generate!("hello");
struct Component;
impl Hello for Component {
fn run() -> String {
"Hello".to_string()
}
}
export_hello!(Component);
ちなみにexpand
すると以下のようになる。
展開後はwasm-bindgen
などとやっていることは同じような感じだろうか。
call_run
を見るとforget
してwit
によって隠蔽されたABI
に基づいてpointer
やlength
などを返しているように見える。
またconst
としてwit
自体も埋め込まれている。
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
pub trait Hello {
fn run() -> wit_bindgen::rt::string::String;
}
#[doc(hidden)]
pub unsafe fn call_run<T: Hello>() -> i32 {
#[allow(unused_imports)]
use wit_bindgen::rt::{alloc, vec::Vec, string::String};
let result0 = T::run();
let ptr1 = _RET_AREA.0.as_mut_ptr() as i32;
let vec2 = (result0.into_bytes()).into_boxed_slice();
let ptr2 = vec2.as_ptr() as i32;
let len2 = vec2.len() as i32;
core::mem::forget(vec2);
*((ptr1 + 4) as *mut i32) = len2;
*((ptr1 + 0) as *mut i32) = ptr2;
ptr1
}
#[doc(hidden)]
pub unsafe fn post_return_run<T: Hello>(arg0: i32) {
wit_bindgen::rt::dealloc(
*((arg0 + 0) as *const i32),
(*((arg0 + 4) as *const i32)) as usize,
1,
);
}
#[allow(unused_imports)]
use wit_bindgen::rt::{alloc, vec::Vec, string::String};
#[repr(align(4))]
struct _RetArea([u8; 8]);
static mut _RET_AREA: _RetArea = _RetArea([0; 8]);
const _: &str = "default world hello {\n export run: func() -> string\n}";
struct Component;
impl Hello for Component {
fn run() -> String {
"Hello".to_string()
}
}
const _: () = {
#[doc(hidden)]
#[export_name = "run"]
#[allow(non_snake_case)]
unsafe extern "C" fn __export_hello_run() -> i32 {
call_run::<Component>()
}
#[doc(hidden)]
#[export_name = "cabi_post_run"]
#[allow(non_snake_case)]
unsafe extern "C" fn __post_return_hello_run(arg0: i32) {
post_return_run::<Component>(arg0)
}
};
ここまで来たら普通にbuild
した後wasm-tools
を使用してcomponent
を作成する。
今回、WASI
は使用せずwasm32-unknown-unknown
を使う。
cargo build --release --target wasm32-unknown-unknown
wasm-tools component new target/wasm32-unknown-unknown/release/hello.wasm -o hello-component.wasm
成果物をobjdump
してみるとcomponent
用の情報の存在がわかる。
❯ wasm-tools objdump hello-component.wasm
module | 0xc - 0x1b8310 | 1803012 bytes | 1 count
------ start module 0 -------------
types | 0x16 - 0x75 | 95 bytes | 15 count
functions | 0x77 - 0xdb | 100 bytes | 99 count
tables | 0xdd - 0xe2 | 5 bytes | 1 count
memories | 0xe4 - 0xe7 | 3 bytes | 1 count
globals | 0xe9 - 0x102 | 25 bytes | 3 count
exports | 0x104 - 0x14e | 74 bytes | 6 count
elements | 0x150 - 0x168 | 24 bytes | 1 count
code | 0x16c - 0x4587 | 17435 bytes | 99 count
data | 0x458a - 0x47f0 | 614 bytes | 1 count
custom ".debug_info" | 0x4800 - 0x6745e | 404574 bytes | 1 count
custom ".debug_pubtypes" | 0x67471 - 0x675b5 | 324 bytes | 1 count
custom ".debug_ranges" | 0x675c7 - 0x92547 | 176000 bytes | 1 count
custom ".debug_abbrev" | 0x92558 - 0x935ad | 4181 bytes | 1 count
custom ".debug_line" | 0x935bd - 0xd92e1 | 285988 bytes | 1 count
custom ".debug_str" | 0xd92f0 - 0x178616 | 652070 bytes | 1 count
custom ".debug_pubnames" | 0x17862a - 0x1b69d2 | 254888 bytes | 1 count
custom "name" | 0x1b69da - 0x1b8296 | 6332 bytes | 1 count
custom "producers" | 0x1b82a2 - 0x1b8310 | 110 bytes | 1 count
------ end module 0 -------------
core instances | 0x1b8312 - 0x1b8316 | 4 bytes | 1 count
component alias | 0x1b8318 - 0x1b8335 | 29 bytes | 2 count
component types | 0x1b8337 - 0x1b833c | 5 bytes | 1 count
component alias | 0x1b833e - 0x1b8359 | 27 bytes | 2 count
canonical functions | 0x1b835b - 0x1b8366 | 11 bytes | 1 count
custom "producers" | 0x1b8372 - 0x1b8395 | 35 bytes | 1 count
component exports | 0x1b8397 - 0x1b83a0 | 9 bytes | 1 count
またprint
してみるとcomponent
やcore module
といったものが見えるComponent
化前後のdiff
を取ってみると以下のような差異が見られた。
(module
(type (;0;) (func (param i32 i32)))
(component
(core module (;0;)
...
(data $.rodata (;0;) (i32.const 1048576) "Hello\00\00\00\03\00\00\00\04\00\00\00\04\00\00\00\04\00\00\00\05\00\00\00\06\00\00\00called `Option::unwrap()` on a `None` valuememory allocation of bytes failed\0a\00\00K\00\10\00\15\00\00\00`\00\10\00\0e\00\00\00library/std/src/alloc.rs\80\00\10\00\18\00\00\00U\01\00\00\09\00\00\00library/std/src/panicking.rs\a8\00\10\00\1c\00\00\00>\02\00\00\0f\00\00\00\a8\00\10\00\1c\00\00\00=\02\00\00\0f\00\00\00\07\00\00\00\0c\00\00\00\04\00\00\00\08\00\00\00\03\00\00\00\08\00\00\00\04\00\00\00\09\00\00\00\0a\00\00\00\10\00\00\00\04\00\00\00\0b\00\00\00\0c\00\00\00\03\00\00\00\08\00\00\00\04\00\00\00\0d\00\00\00\0e\00\00\00\03\00\00\00\00\00\00\00\01\00\00\00\0f\00\00\00library/alloc/src/raw_vec.rscapacity overflow\00\00\00X\01\10\00\11\00\00\00<\01\10\00\1c\00\00\00\06\02\00\00\05\00\00\00\11\00\00\00\00\00\00\00\01\00\00\00\12\00\00\0000010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899")
(data (;0;) (i32.const 1048576) "Hello\00\00\00\03\00\00\00\04\00\00\00\04\00\00\00\04\00\00\00\05\00\00\00\06\00\00\00called `Option::unwrap()` on a `None` valuememory allocation of bytes failed\0a\00\00K\00\10\00\15\00\00\00`\00\10\00\0e\00\00\00library/std/src/alloc.rs\80\00\10\00\18\00\00\00U\01\00\00\09\00\00\00library/std/src/panicking.rs\a8\00\10\00\1c\00\00\00>\02\00\00\0f\00\00\00\a8\00\10\00\1c\00\00\00=\02\00\00\0f\00\00\00\07\00\00\00\0c\00\00\00\04\00\00\00\08\00\00\00\03\00\00\00\08\00\00\00\04\00\00\00\09\00\00\00\0a\00\00\00\10\00\00\00\04\00\00\00\0b\00\00\00\0c\00\00\00\03\00\00\00\08\00\00\00\04\00\00\00\0d\00\00\00\0e\00\00\00\03\00\00\00\00\00\00\00\01\00\00\00\0f\00\00\00library/alloc/src/raw_vec.rscapacity overflow\00\00\00X\01\10\00\11\00\00\00<\01\10\00\1c\00\00\00\06\02\00\00\05\00\00\00\11\00\00\00\00\00\00\00\01\00\00\00\12\00\00\0000010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899")
)
(core instance (;0;) (instantiate 0))
(alias core export 0 "memory" (core memory (;0;)))
(alias core export 0 "cabi_realloc" (core func (;0;)))
(type (;0;) (func (result string)))
(alias core export 0 "run" (core func (;1;)))
(alias core export 0 "cabi_post_run" (core func (;2;)))
(func (;0;) (type 0) (canon lift (core func 1) (memory 0) string-encoding=utf8 (post-return 2)))
(export (;1;) "run" (func 0))
Component
が準備できたのでGuest
はここまでだ。
ちなみに現時点wit-bindgen
のREADME
にはRust
,C/C++
,Java
,TinyGo
をGuest
として使用する例が書かれている。
Host(JavaScript)
今回はHost
にはJavaScript
を使用するものとする。wit-bindgen
のREADME
ではHost Runtime
としてRust
,JavaScript
,Python
が紹介されている。
上記を見るとわかるがJavaScript
の場合はbytecodealliance/jcoが紹介されているのでそれを使用していく。
jco
をinstall
後以下を実行することでJavaScript
のコードやd.ts
、wasm
などが生成される。
詳細は追っていないが、恐らくwasm
に埋められたComponent section
などを見てGlue Code
を生成するのだろう。
jco transpile hello-component.wasm -o hello
まずはd.ts
を見ると以下。
export function run(): string;
次にjs
を見ると以下。 この辺もemscripten
やwasm-bindgen
を触ったことある方からしたらよくある感じだと思う。
const instantiateCore = WebAssembly.instantiate;
let dv = new DataView(new ArrayBuffer());
const dataView = (mem) =>
dv.buffer === mem.buffer ? dv : (dv = new DataView(mem.buffer));
const utf8Decoder = new TextDecoder();
const isNode =
typeof process !== "undefined" && process.versions && process.versions.node;
let _fs;
async function fetchCompile(url) {
if (isNode) {
_fs = _fs || (await import("fs/promises"));
return WebAssembly.compile(await _fs.readFile(url));
}
return fetch(url).then(WebAssembly.compileStreaming);
}
let exports0;
let memory0;
let postReturn0;
function run() {
const ret = exports0.run();
const ptr0 = dataView(memory0).getInt32(ret + 0, true);
const len0 = dataView(memory0).getInt32(ret + 4, true);
const result0 = utf8Decoder.decode(
new Uint8Array(memory0.buffer, ptr0, len0)
);
postReturn0(ret);
return result0;
}
export { run };
const $init = (async () => {
const module0 = fetchCompile(
new URL("./hello-component.core.wasm", import.meta.url)
);
({ exports: exports0 } = await instantiateCore(await module0));
memory0 = exports0.memory;
postReturn0 = exports0.cabi_post_run;
})();
await $init;
js
だけmjs
にrename
して以下のようなindex.mjs
を用意。
import { run } from "./hello/wit-component.mjs";
console.log(run());
動いた。
❯ node index.mjs
Hello
Browser
のSupport
については以下のIssue
が立っている。
https://github.com/bytecodealliance/jco/issues/42
まとめ
WebAssembly Component Model
の概要を読んだりHello World
を動かしてみたりした。phase1
だが、WASI
はこの上に構築されるとのことだし、Dynamic Linking
にも必須だと思うので今後の動向を見ていきたい。
以上。