Skip to content
On this page

wit-bindgenとjcoでWebAssembly Component Modelに入門する

2023-04-18

WebAssembly Component Modelを追えていなかったのでHello World的なものを動かし概要を理解したい。

WARNING

この記事で紹介しているwitの記法は古くなっている可能性があります

目次

WebAssembly Component Model

WebAssembly/component-modelspecは定義されており、現在のPhasePhase 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を用いHostJSを前提に試していく。

Guest(Rust)

sh
cargo new hello
  • Cargo.toml
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
// wit/hello.wit
default world hello {
    export run: func() -> string
}

Rustは以下のようにmacro経由でhello.witを参照する。

  • src/lib.rs
rust
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に基づいてpointerlengthなどを返しているように見える。

またconstとしてwit自体も埋め込まれている。

rust
#![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を使う。

sh
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してみるとcomponentcore moduleといったものが見えるComponent化前後のdiffを取ってみると以下のような差異が見られた。

wasm
(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-bindgenREADMEにはRust,C/C++,Java,TinyGoGuestとして使用する例が書かれている。

Host(JavaScript)

今回はHostにはJavaScriptを使用するものとする。
wit-bindgenREADMEではHost RuntimeとしてRust,JavaScript,Pythonが紹介されている。

上記を見るとわかるがJavaScriptの場合はbytecodealliance/jcoが紹介されているのでそれを使用していく。

jcoinstall後以下を実行することでJavaScriptのコードやd.tswasmなどが生成される。

詳細は追っていないが、恐らくwasmに埋められたComponent sectionなどを見てGlue Codeを生成するのだろう。

sh
jco transpile hello-component.wasm -o hello

まずはd.tsを見ると以下。

typescript
export function run(): string;

次にjsを見ると以下。 この辺もemscriptenwasm-bindgenを触ったことある方からしたらよくある感じだと思う。

typescript
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だけmjsrenameして以下のようなindex.mjsを用意。

JavaScript
import { run } from "./hello/wit-component.mjs";

console.log(run());

動いた。

sh
 node index.mjs
Hello

BrowserSupportについては以下のIssueが立っている。

https://github.com/bytecodealliance/jco/issues/42

まとめ

WebAssembly Component Modelの概要を読んだりHello Worldを動かしてみたりした。
phase1だが、WASIはこの上に構築されるとのことだし、Dynamic Linkingにも必須だと思うので今後の動向を見ていきたい。

以上。