Open7
WebAssembly でのメモリの読み方
Moonbit 言語の生成する wat ファイルを理解するために、シンプルな wat を手書きすることで理解したい
最初に簡単なユーティリティを用意した。deno で wat をコンパイルして実効する。
wat2wasm が CLI しか見つからなかったので、 dax でラップしている。
import $ from "jsr:@david/dax";
type Exports = Record<string, (...args: Array<number>) => number>;
const wat = /*s*/ `
(module
(func $i (import "imports" "imported_func") (param i32))
(func (export "exported_func")
i32.const 42
call $i))
`;
async function compile(wat: string) {
const temp = await Deno.makeTempFile({ suffix: ".wat" });
const tempOut = await Deno.makeTempFile({ suffix: ".wasm" });
await Deno.writeTextFile(temp, wat);
await $`wat2wasm ${temp} -o ${tempOut}`;
return Deno.readFile(tempOut);
}
こんな風に使う。
const wat = /*s*/ `
(module
(func $i (import "imports" "imported_func") (param i32))
(func (export "exported_func")
i32.const 42
call $i))
`;
{
const { instance: { exports } } = await WebAssembly.instantiate(
await compile(wat),
{
imports: {
imported_func: (x: number) => console.log(x),
},
}
);
const api = exports as Exports;
const out = api.exported_func(2, 3);
console.log(out);
}
/s/ はvscode 上で S 式としてコンパイルする前提の記法
以降はこのユーティリティがある前提
以下のMDNのドキュメントを読みながらステップバイステップで理解していく。
WebAssembly.Memory の 0 番目を読み出すだけの wat と、そこに 5 を書き込んで渡すだけの ts
// simple read_memory
const wat_read_mem = /*s*/ `
(module
(memory (import "js" "mem") 1)
(func (export "read") (param $ptr i32) (result i32)
(i32.load
(local.get $ptr)
)
)
)
`;
{
const binary = await compile(wat_read_mem);
const mem = new WebAssembly.Memory({ initial: 1 });
const { instance: { exports } } = await WebAssembly.instantiate(
binary,
{ js: { mem } }
);
const api = exports as Exports;
const i32 = new Uint32Array(mem.buffer);
i32[0] = 5;
const out = api.read(0);
console.log(out);
}
(memory (import "js" "mem") 1)
を省略するとエラー。i32.load はメモリがある前提の処理
0 番目に書き込んで、それを JS から読み出す例
// simple read_memory
const wat_read_store = /*s*/ `
(module
(memory (import "js" "mem") 1)
(func (export "write") (param $ptr i32) (param $val i32)
(i32.store
(local.get $ptr) ;; アドレス
(local.get $val) ;; 書き込む値
)
)
)
`;
{
const mem = new WebAssembly.Memory({ initial: 1 });
const { instance: { exports } } = await WebAssembly.instantiate(
await compile(wat_read_store),
{ js: { mem } }
);
const api = exports as Exports;
api.write(0, 3);
const i32 = new Uint32Array(mem.buffer);
console.log(i32[0]);
}
WebAssembly.Table を使う例。値を返す関数をアサインする。
MDNのドキュメントが間違っていてだいぶ迷った。
const wat_tbl = /*s*/ `
(module
(import "js" "tbl" (table 2 funcref))
(func $f42 (result i32) i32.const 42)
(func $f83 (result i32) i32.const 83)
(elem (i32.const 0) $f42 $f83)
)
`;
{
console.log("-------------");
const tbl = new WebAssembly.Table({
initial: 2,
element: "anyfunc"
});
const { instance: { exports: _ } } = await WebAssembly.instantiate(
await compile(wat_tbl),
{ js: { tbl } }
);
console.log(tbl.get(0)?.());
console.log(tbl.get(1)?.());
}
memory.init の使い方の確認
const wat_init_memory = /*s*/ `
(module
(memory (export "mem") 1)
(data $init_data "Hello World")
(func $initialize_memory
(memory.init $init_data (i32.const 0) (i32.const 0) (i32.const 11)) ;; コピーするデータの範囲を明示
)
(func $drop_data
(data.drop $init_data)
)
(export "initialize_memory" (func $initialize_memory))
(export "drop_data" (func $drop_data))
)
`;
{
const { instance: { exports } } = await WebAssembly.instantiate(await compile(wat_init_memory));
const api = exports as Exports & { mem: WebAssembly.Memory; };
api.initialize_memory();
const dataView = new DataView(api.mem.buffer);
const length = 11; // "Hello World" の長さ
let text = '';
for (let i = 0; i < length; i++) {
text += String.fromCharCode(dataView.getUint8(i));
}
console.log(text);
api.drop_data();
// api.initialize_memory(); // 呼ぶとエラー
}