ComponentizeJS を使うと WebAssembly コンポーネントモデルを JavaScript で実装できます。
Rust で実装する場合と比べるとファイルサイズが大きくなる等のデメリットはありそうですが、手軽に実装できるのは大きなメリットだと思います。
インストール
インストールは @bytecodealliance/componentize-js
を npm install するだけです。
$ npm install @bytecodealliance/componentize-js
これで componentize-js コマンドを使えるようになり、下記のようにして JavaScript ファイルから WebAssembly コンポーネントを作成します。
componentize-js --wit <WITディレクトリ> -o <出力ファイル> <JavaScriptファイル>
サンプル実装
WIT ファイルはこのように定義してみました。
cart は empty
と active
の 2種類の状態を表現するため variant を使っています。
ついでに、動作確認で実行するために wasi:cli/run
を export しています。
package example:cart;
interface types {
type cart-id = string;
type item-id = string;
type quantity = u32;
record cart-item {
item: item-id,
qty: quantity,
}
variant cart {
empty(cart-id),
active(tuple<cart-id, list<cart-item>>),
}
create: func(id: cart-id) -> cart;
change-qty: func(state: cart, item: item-id, qty: quantity) -> option<cart>;
}
world cart {
export types;
export wasi:cli/run@0.2.3;
}
componentize-js コマンドは今のところ WIT の依存関係を解決してくれないようなので、wit-deps を使って wasi:cli への依存関係を処理しておきます。
cli = "https://github.com/WebAssembly/wasi-cli/archive/refs/tags/v0.2.3.tar.gz"
$ wit-deps
JavaScript による実装は例えばこのようになります。
WIT 側でインターフェースを export している場合、JavaScript 側は export const <インターフェース名> = { 関数, ・・・ }
のように実装します。1
// types の実装
export const types = {
// create 関数の実装
create(id) {
return [ id ]
},
// change-qty 関数の実装
changeQty(state, item, qty) {
if (!state || !item || qty < 0) {
return null
}
const [ id, items ] = state
if (items) { // active 状態時の処理
const [ newItems, exists ] = items.reduce(
(acc, x) => {
if (x.item !== item) {
return [ [ ...acc[0], x ], acc[1] ]
}
return (qty > 0) ?
[ [ ...acc[0], { item, qty } ], true ] :
[ acc[0], true ]
},
[[], false]
)
if (!exists && qty > 0) {
newItems.push({ item, qty })
}
if (newItems.length > 0) {
return [ id, newItems ] // active 状態
}
return [ id ] // empty 状態
}
else if (qty > 0) { // empty 状態時の処理
return [ state[0], [{ item, qty }] ]
}
return null
},
}
// wasi:cli/run の実装
export const run = {
run() {
const s1 = types.create('test-cart')
console.log(s1)
const s2 = types.changeQty(s1, 'item-A', 2)
console.log(s2)
const s3 = types.changeQty(s2, 'item-B', 1)
console.log(s3)
const s4 = types.changeQty(s3, 'item-A', 3)
console.log(s4)
const s5 = types.changeQty(s4, 'item-A', 0)
console.log(s5)
const s6 = types.changeQty(s5, 'item-C', 4)
console.log(s6)
const s7 = types.changeQty(s6, 'item-C', 0)
console.log(s7)
const s8 = types.changeQty(s7, 'item-B', 0)
console.log(s8)
const e1 = types.changeQty(s1, 'item-A', 0)
console.log(e1)
const e2 = types.changeQty(s1, 'item-A', -1)
console.log(e2)
}
}
ビルド
次のコマンドを実行して WebAssembly コンポーネントを作成します。
$ npx componentize-js --wit wit -o component.wasm source.js
実行
この component.wasm
を wasmtime で実行した結果はこのようになります。
$ wasmtime run -S http component.wasm
["test-cart"]
["test-cart", [{ item: "item-A", qty: 2 }]]
["test-cart", [{ item: "item-A", qty: 2 }, { item: "item-B", qty: 1 }]]
["test-cart", [{ item: "item-A", qty: 3 }, { item: "item-B", qty: 1 }]]
["test-cart", [{ item: "item-B", qty: 1 }]]
["test-cart", [{ item: "item-B", qty: 1 }, { item: "item-C", qty: 4 }]]
["test-cart", [{ item: "item-B", qty: 1 }]]
["test-cart"]
null
null
なお、今回の WIT では wasi:http を import していませんが、component.wasm 内で wasi:http 等を import するようになっているので2、wasmtime で実行する際に -S http
(もしくは --wasi http
)オプションを指定しておかないと実行に失敗します。