Hello, WebAssembly!の謎(by 調査班)
いきなり、箱庭さんからバトンタッチされましたが、、
私の専門はPacketなのですが、どうも箱庭さんは細かいことは全てこちらに回す癖がついてるようですね。「Packetに乗ってやってくる、WebAssembly興味ない?」とか言われて、こんな画像が送られてきました。
つい。やってしまいました。 ま、アプリ層(L7)界隈の話ということで、箱庭さんに聞きながら調査してみましょう。
まずはじっくりソースを見ていきましょう。
はて?なんで、いきなり「Hello, WebAssembly!」が開いた?
こういう時は、ソースを順番に解剖していきましょう。
index.htmlは、
<!doctype html>
<html lang="ja-JP">
<head>
<meta charset="utf-8" />
<title>Hello Wasm</title>
</head>
<body>
<script type="module">
import init, { greet } from "./pkg/hello_wasm.js";
init().then(() => {
greet("WebAssembly");
});
</script>
</body>
</html>
このようになっていました。順番に関係しそうなここをみていきます。
<script type="module">
import init, { greet } from "./pkg/hello_wasm.js";
init().then(() => {
greet("WebAssembly");
});
</script>
scirptのタイプがmodule
になっていますね。scriptのtypeは色々あるようです。
type=moduleは、非同期でimport/exportが使えるmoduleスコープで動くよ。ということです。
今回、WebAssemblyとの橋渡し役である、hello_wasm.js
からimportしますのでこのtype=moduleを使うということですね。
まずhello_wasm.jsからinit関数とgreet関数をimportしています。
import init, { greet } from "./pkg/hello_wasm.js";
あれ?でも、hello_wasm.jsを見ると、initという関数が見あたらないのですが、、
hello_wasm.js
の最後の行を見てください。
...
...
export default __wbg_init;
とあります。デフォルトでエクスポートされている関数は、読み込む側で名前を勝手に決めれるらしいです。
実際にhello_wasm.jsで、エクスポートされているのは、__wbg_init
という関数です。
読み込む側で、慣習的にinit
という名前にしているだけですね。
そして、index.htmlの方に戻って、
init().then(() => {
greet("WebAssembly");
});
init()=__wbg_init()関数を呼び出し、返ってきたら、greet関数を呼び出す。
という流れです。
なぜここでthenで待っているのでしょう?
init()が何をやってるのか?を見た方が良さそうですね。
実際には、hello_wasm.js
の中の__wbg_init()
という関数でしたね。見てましょう。
1: async function __wbg_init(module_or_path) {
2: if (wasm !== undefined) return wasm;
3:
4:
5: if (typeof module_or_path !== 'undefined') {
6: if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
7: ({module_or_path} = module_or_path)
8: } else {
9: console.warn('using deprecated parameters for the initialization function; pass a single object instead')
10: }
11: }
12:
13: if (typeof module_or_path === 'undefined') {
14: module_or_path = new URL('hello_wasm_bg.wasm', import.meta.url);
15: }
16: const imports = __wbg_get_imports();
17:
18: if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
19: module_or_path = fetch(module_or_path);
20: }
21:
22: __wbg_init_memory(imports);
23:
24: const { instance, module } = await __wbg_load(await module_or_path, imports);
25:
26: return __wbg_finalize_init(instance, module);
27: }
この中でやってることを順番に見ていきましょう。2行目で、
2: if (wasm !== undefined) return wasm;
これは、hello_wasm.js
の一番最初の行で、
let wasm;
...
...
と宣言されていて、wasm=WebAssemblyのインスタンスを持つということですね。 なので、この行では、既に初期化済みのがあったら、それを返す。 まだ何もしていないので、ないです。
次に、5行目〜11行目のブロックでは、
5: if (typeof module_or_path !== 'undefined') {
6: if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
7: ({module_or_path} = module_or_path)
8: } else {
9: console.warn('using deprecated parameters for the initialization function; pass a single object instead')
10: }
11: }
ここですが、module_or_path
という変数は、関数のパラメーターとして渡されてくるものですが、
1: async function __wbg_init(module_or_path) {
今回、init()
という風に何もパラメーターを指定しないで呼んでいますので、WebAssemblyのモジュールのパスが指定されていた場合は、それを使うということでしょう。
そしてそこで指定されているのがオブジェクトリテラル(例:{ module_or_path: 'path/to/module' }のような形式かどうかをチェックしていますね。
OKだったら、その値をmodule_or_path
に入れています。
ない場合=今回の場合は、次のこの部分になります。
13: if (typeof module_or_path === 'undefined') {
14: module_or_path = new URL('hello_wasm_bg.wasm', import.meta.url);
15: }
デフォルトの、hello_wasm_bg.wasm
=WebAssemblyバイナリ本体を使用するってことですね。
つまり__wbg_init()
関数の前半部分では、WebAssemblyのモジュールを読み込むパスの判断をしているということになりますね。
判明してきたことをコメントで入れていきましょう。
1: async function __wbg_init(module_or_path) {
// 既に初期化済みの場合はそのwasmのインスタンスを返す
2: if (wasm !== undefined) return wasm;
3:
4:
// 引数としてmodule_or_pathが入ってきていたら、その値を使って、module_or_pathを初期化
// (module_or_pathはobjectで、例えば{ module_or_path: 'path/to/module' }の形であること)
5: if (typeof module_or_path !== 'undefined') {
6: if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
7: ({module_or_path} = module_or_path)
8: } else {
9: console.warn('using deprecated parameters for the initialization function; pass a single object instead')
10: }
11: }
12:
// 引数としてmodule_or_pathが指定されていなかった場合、今回の場合、デフォルトのhello_wasm_bg.wasmを使用する。
13: if (typeof module_or_path === 'undefined') {
14: module_or_path = new URL('hello_wasm_bg.wasm', import.meta.url);
15: }
16: const imports = __wbg_get_imports();
17:
18: if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
19: module_or_path = fetch(module_or_path);
20: }
21:
22: __wbg_init_memory(imports);
23:
24: const { instance, module } = await __wbg_load(await module_or_path, imports);
25:
26: return __wbg_finalize_init(instance, module);
27: }
そして、次に16行目。
16: const imports = __wbg_get_imports();
次の行のこの行は、hello_wasm.js
の中のこの関数を呼んでいます。中身を見てみると、
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_alert_8755b7883b6ce0ef = function(arg0, arg1) {
alert(getStringFromWasm0(arg0, arg1));
};
return imports;
}
ここをよくみると、alert関数
が書かれていますね。
そういえば今回、WebAssembly(Rust)側から呼び出すJavascriptの関数として、Rustのソースに
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
と定義されていましたね!これは、JavaScript側で実装された関数をWebAssemblyモジュールが呼び出せるように設定するためのものかも!?ということで、
__wbg_get_imports
関数を詳細に見ていくと、
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_alert_8755b7883b6ce0ef = function(arg0, arg1) {
alert(getStringFromWasm0(arg0, arg1));
};
return imports;
}
まず、importsオブジェクト
に、wbg
というオブジェクトを作っていますね。
imports.wbg = {};
このwbg
がなんなのか?を調べたところ、wasm-bindgen
(=Rust側でWebAssemblyを作る際に使ったクレート)の命名プレフィックスのようです。
WebAssembly側から呼ばれるJavaScript関数を登録するための名前空間ということのようでした。つまりWebAssemblyから呼び出せるJavascript側の関数をあそこでわざわざ定義していたのは、wbg
という名前空間を作ってそこにある関数しか呼べないようセキュリティ的に固めていたのですね。
なんでも呼べちゃうようになっていると危険ですからね。
imports.wbg.__wbg_alert_8755b7883b6ce0ef = function(arg0, arg1) {
alert(getStringFromWasm0(arg0, arg1));
};
そして、imports.wbg
オブジェクトに、__wbg_alert_8755b7883b6ce0ef
というプロパティが追加されています。
このプロパティ名は、RustのWebAssemblyバイナリが生成される際(=コンパイル時)に自動的に付けられた名前で、WebAssembly側からは__wbg_alert_8755b7883b6ce0ef
という名前でalert
が呼ばれるようにコンパイルされたようです。
そのプロパティの中身である実体は、
function(arg0, arg1) { alert(getStringFromWasm0(arg0, arg1)); }
となっており、WebAssembly側からalert()
関数が呼ばれたときにはこの部分が実行されることになります。
getStringFromWasm0(arg0, arg1)
を呼び出しています。これはhello_wasm.js
で定義されています。
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
今回の調査でわかったのは、これが重要なポイントのようです。
メインのお話しに入る前に、ptr = ptr >>> 0;
をやっつけておきますね。
これは、JavaScriptの>>>演算子には「符号なし右シフト演算子」という名前がついています。これは、数値を右にシフトし、符号ビットに関係なく
ゼロで埋めるため、常に非負(正の)数を返すという特徴があります。このビットシフトを使うと、正の整数に変換してくれるということです。
負の値が入ってきたら、正の値に変換するためにやっているようです。それは、このあとのメインのお話しで出てくるWebAssemblyとJavascriptでやりとりするメモリの形に関係してきます。
「なぜ正の値でないといけないのか?」
を頭に置きながら読み進めてください。
この関数をみると、ptr
とlen
が渡されています。
普通alert関数だとalert(文字列)
となりますが、このgetStringFromWasm0
で、文字列を取り出しているようです。
これは、何をしているのでしょう? WebAssemblyでは低レベルのメモリ管理を行うため、Rustでの文字列をそのままJavaScriptで直接扱えません。この関数を使うことで、メモリ上の文字列データをJavaScript側で復元しているようです。
WebAssemblyのメモリ管理では、線形メモリ(linear memory)
を使用しています。
単純な1方向のメモリで、バイト配列
だと考えるとわかりやすいです。
WebAssembly側とJavascript側のやり取りは、このバイト配列を介してやり取りされています。
そして、WebAssembly側からalert
を呼び出す時には、まず文字列データを、そのメモリ領域に書き込み、その時の開始位置(=ポインター)とサイズ(=len)を渡しています。
Javascript側ではその情報を受け取って、メモリ領域から、その位置からサイズ分だけ取り出す。ということでやり取りしているようです。
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
と、1行で書かれていますが、
分解してみていきましょう。最初にgetUint8ArrayMemory0()
関数は、hello_wasm.js
に定義されていました。
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
getUint8ArrayMemory0()でWebAssemblyのメモリ=バイト配列を取得しているということですね。
wasm.memory.buffer
が実際のバイト配列を表す変数です。それをUnit8Array型にしてる。ということになります。
WebAssemblyのメモリは、最初に設定されたサイズで開始され、ページ単位(64KiB)で拡張可能です。
ただし、メモリのサイズに関しては、WebAssemblyの実行環境や設定による制限があり、初期サイズ、最大サイズを指定して実行します。wasm.memory.grow(1)のようにして1ページ単位で拡張していきます。
設定するのは、Rust側でWebAssemblyにするときに指定します。今回は何も指定していなかったので、デフォルトで最少の1で始まることになります。
そして、元のソースに戻って、
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
次に、ptrからptr + lenまでのバイト範囲をsubarray()で取得。
そのバイト列をcachedTextDecoder.decode()を使い、Javascriptで扱える文字列データにするために、UTF-8エンコードのバイトデータを文字列としてデコード。となります。
この図のようなやり取りをして、データをやりとりしていることになりますね。
なるほど。
さて、なぜ最初に、ptr = ptr >>> 0;
をしているのか?わかりましたか?
WebAssemblyと、Javascriptのやり取りは、線形メモリであるバイト配列を介してお互いやり取りしている。ということがわかりましたが、バイト配列なので、いきなり−1とかマイナスの方向にアクセスしようとしてもそれはメモリーエラーを引き起こしてしまいますから、このように、正の値に変更しているんですね。
スッキリしましたか?
さて、戻りましょう。
えっ?今なんの話だったっけ?となってますか?今は、どうして、index.htmlを開くといきなり、'Hello, WebAssembly!'というalertが表示されたのか?を追いかけていたのですよ。
それで、index.htmlから呼ばれている、__wbg_init()
関数を見ていました。
こういう関数ですね。謎が判明したところはコメント文を入れていきましょう。
1: async function __wbg_init(module_or_path) {
// 既に初期化済みの場合はそのwasmのインスタンスを返す
2: if (wasm !== undefined) return wasm;
3:
4:
// 引数としてmodule_or_pathが入ってきていたら、その値を使って、module_or_pathを初期化
// (module_or_pathはobjectで、例えば{ module_or_path: 'path/to/module' }の形であること)
5: if (typeof module_or_path !== 'undefined') {
6: if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
7: ({module_or_path} = module_or_path)
8: } else {
9: console.warn('using deprecated parameters for the initialization function; pass a single object instead')
10: }
11: }
12:
// 引数としてmodule_or_pathが指定されていなかった場合、今回の場合、デフォルトのhello_wasm_bg.wasmを使用する。
13: if (typeof module_or_path === 'undefined') {
14: module_or_path = new URL('hello_wasm_bg.wasm', import.meta.url);
15: }
// WebAssembly側からJavascript側で定義した関数を呼び出すための定義をimportsに入れておく。
16: const imports = __wbg_get_imports();
17:
// 今ここ
18: if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
19: module_or_path = fetch(module_or_path);
20: }
21:
22: __wbg_init_memory(imports);
23:
24: const { instance, module } = await __wbg_load(await module_or_path, imports);
25:
26: return __wbg_finalize_init(instance, module);
27: }
ということで、importsで、すごい回り道しましたが、1つ謎が解けてよかったです。
次は18行目~20行目のブロックですね。
18: if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
19: module_or_path = fetch(module_or_path);
20: }
これは、module_or_path
で指定されているWebAssemblyファイルをfetchして取得するということになります。
その際に、module_or_path
で指定できる形として、
・ただの文字列かどうかをチェック(typeof module_or_path === 'string')
・Requestオブジェクトかどうかをチェック(typeof Request === 'function' && module_or_path instanceof Request)
・URLオブジェクトかどうかをチェック(typeof URL === 'function' && module_or_path instanceof URL)
というチェックを行なっているので、この3タイプならOKということになります。
そして、22行目
22: __wbg_init_memory(imports);
これは、hello_wasm.js
で定義されているので見てみたのですが、空っぽでした。名前からするとメモリ関連の初期処理で初期サイズや最大サイズを決めるのかな?と思ったのですが、
どうやら今回のプログラムでは、毎回呼ばれるたびに、
// メモリ取得関数
function getUint8ArrayMemory0() {
// キャッシュが空(null)または長さが0の場合、新規にメモリアクセスする
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
ここにいき、その都度、Unit8Arrayにして取得しているようです。
無駄にメモリを確保し続けない。ということですね。
そして、次の行で、モジュールのロードを行っています。
24: const { instance, module } = await __wbg_load(await module_or_path, imports);
__wbg_load
関数はhello_wasm.js
で定義されていたので見てみましたが、
つまり、先ほど、19行目でfetch
したモジュールが読み込まれているのを待って、インスタンス化するためにここで呼ばれているようです。
19: module_or_path = fetch(module_or_path);
↓19行目でfetchした結果を待って、__wbg_load()関数で実際にデータを取り出し、インスタンス化しようとしている
async function __wbg_load(module, imports) {
//fetchした結果がResponseオブジェクト形式なら、
if (typeof Response === 'function' && module instanceof Response) {
//可能ならストリーミングでフェッチしながら、インスタンス化しようと試みる。ダメなら、従来の方式で。
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
// 従来の方法:arrayBufferを取得してからインスタンス化
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
// モジュールが直接渡された場合
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
ストリーミングでのロードを試みますが、ダメな場合、従来の方法でロードという感じになっています。 そして、
24: const { instance, module } = await __wbg_load(await module_or_path, imports);
と結果を受け取る部分の、instanceとmoduleの意味ついて調べてみました。
module
は、WebAssemblyモジュールそのものを指します。モジュールは、まだ実行可能な状態ではないWebAssemblyバイナリのコードです。
instance
は、WebAssemblyインスタンスを指します。これは、先ほどの__wbg_load()
関数で、WebAssembly.instantiate()またはWebAssembly.instantiateStreaming()によって作成された、実行可能なWebAssemblyモジュールのインスタンスです。
インスタンスは、WebAssemblyのエクスポート関数を利用できる状態=Javascriptから呼び出して実行できる状態のこと。を言います。
ClassとObjectの関係みたいなものですね。
ですので、こういうこともできます。
// 同じmoduleから複数のインスタンスを作成できる
const module = new WebAssembly.Module(wasmBytes);
const instance1 = new WebAssembly.Instance(module, imports);
const instance2 = new WebAssembly.Instance(module, imports);
そして、最後の26行目
26: return __wbg_finalize_init(instance, module);
こちらも、hello_wasm.js
で定義されていたので、じっくりみてみましょう。
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedUint8ArrayMemory0 = null;
return wasm;
}
wasm = instance.exports;
は、WebAssemblyのインスタンスのexports
をグローバル変数wasmに入れてますね。
つまり、Rustで作成した、greet
関数などJavascript側から呼び出すWebAssembly側の関数をwasm
に入れてるということで、これでJavascript側から呼び出せるようになるということですね。
__wbg_init.__wbindgen_wasm_module = module;
これは、先ほどのmoduleを__wbg_initのプロパティとして入れてます。
箱庭さんに聞いてみたら、「後で同じmoduleから新しいinstanceを作る時のために保存とかしてるんじゃない」とのことでした。
どんな時だろう?開発中ならホットリロードとかに使われてそうだけど。
cachedUint8ArrayMemory0 = null;
最後の行で、WebAssemblyとデータのやり取りをするためのメモリをnullにしています。
これは先ほどお話ししたように、次回アクセスがあったら、その際に新規にメモリを見る感じで、常にメモリを保持しておかないための工夫でしょう。
ということで、このようにして、init()
部分が終わりました。
最後の__wbg_finalize_init
関数のところでexportsされた関数がセットされたので、
それで、Rust側で作った関数greet
が呼べるようになったんですね。
<script type="module">
import init, { greet } from "./pkg/hello_wasm.js";
init().then(() => {
greet("WebAssembly");
});
</script>
WebAssemblyのgreetが呼ばれると、中でJavascript側で定義されているalert関数を呼ぶ。 という風になっていますね。
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
フムフム。
謎は全てまるっと分かりましたね!解明した謎をまとめてみていきましょう。
1. init()されることで、
- WebAssembly側からJavascript側で定義した関数を呼び出すための定義を
imports
に入れておく。 - Javascript側からWebAssemblyのモジュール(hello_wasm_bg.wasm)がロードされ、インスタンス化。このとき、
imports
がWebAssemblyのinstanceに渡されファンクションマップが作られることにより、WebAssembly側からJavascript側の関数を呼び出すとき、このファンクションマップを見て呼び出しが可能となる。 - Javascript側からWebAssembly側で定義した関数を呼び出すための定義をexportsに入れておく。
- メモリ初期化。
- WebAssemblyのインスタンスの
exports
をwasm変数に入れて、インスタンスを返す。
Javascript側から呼び出す際は、exports
として定義されている関数を呼び出すことができるようになる。
WebAssembly側からはimports
として定義されている関数を呼び出すことができるようになる。
<script type="module">
import init, { greet } from "./pkg/hello_wasm.js";
init().then(() => {
greet("WebAssembly");
});
</script>
なので、ここで、init()が終わり、WebAssemblyがインスタンス化され、imports/exportsがマッピングされて、やり取りするためのメモリーが準備されるまでthen()で待っていたのですね。
2. initが完了したら、インスタンスができ、WebAssembly側で定義されている関数が呼び出せるようになるので、gree('WebAssmbly')が呼ばれる
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
Rustで、greetの中で、Hello, WebAssembly
という文字列を作り出し、Javascript側で定義されているalert関数が呼ばれる。
3. Hello, WebAssembly
という文字列データがWebAssembly用のメモリ領域(バイト配列)に入れられ
4. WebAssembly側から、__wbg_alert_8755b7883b6ce0ef
が呼ばれる
imports.wbg.__wbg_alert_8755b7883b6ce0ef = function(arg0, arg1) {
alert(getStringFromWasm0(arg0, arg1));
};
5. Javascript側で、alert(getStringFromWasm0(arg0, arg1));
が実行され、
6. getStringFromWasm0
関数で、WebAssembly用メモリから文字列データを取り出し、
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
7. alert('Hello, WebAssembly');が実行される!
というこいった流れですね。
スッキリしました。
箱庭さんからの無茶振りもありましたが、やってみると確かに、私むきの調査でした。 単純なバイト配列な線形メモリという形で、プロトコルをきちんと決めてやりとりする。気持ちいいですね。
では、次回は、いよいよ「シミュレーター作り その1」だそうです。
って、「その0」でいきなりこんなに長くなったのに大丈夫かな。。。
なんとなく、本誌 Network Magazine「Packet Pilot」で実現できなかったのがわかった気がします。 これだとネットワークの雑誌の誌面が、ほぼプログラミングに占領されてしまいますもんね。
今回はJSとWebAssemblyのやりとりについて調査しましたが、 本誌の方では、ネットワークの謎、Packetの謎について調査しています。 Vol.1では、はじまりのパケットからARPまでということで興味そそられる謎を解いていってますので、ぜひ読んでみてください。
今ならKindle Unlimited入ってる方は、無料で読めます。ではでは。
このドキュメントの新しい内容がアップされたら、Xでお知らせいたしますので、フォローよろしくお願いいたします。 @PacketPilot_web 公式アカウントへ