はじめに
(この記事では、AssemblyScriptのインストールや基本的な使い方については説明を割愛します)
WebAssemblyとJavaScriptの間には「モジュール境界(module boundaries)」があり、文字列・配列・オブジェクトなどの複雑なデータ型を、そのままやりとりすることはできない、ということが説明されます。しかし、配列について言うならば、「コピー渡し」ではない、別記事(「小ネタ:AssemblyScript内で生成したオブジェクトの「不透明な参照渡し」の型には、anyを指定するしかない」)で説明したような「不透明な参照渡し」でもない、もっと直接的な形でモジュール境界を超えてデータを共有するための、低レベルな方法が存在します。それが、memory.bufferを利用するという方法です。
AssemblyScript側のモジュール
class WasmUint32Array{ // exportはつけない
private arrayContent: Uint32Array; // Wasm側の配列
constructor(arrayLength: number){
// 指定の長さの空の配列で初期化
this.arrayContent = new Uint32Array(arrayLength);
}
}
// AssemblyScript側で管理するメモリ空間にUint32Arrayを保持するオブジェクトを作成
export createUint32Array(arrayLength: i32): WasmUint32Array {
return new WasmUint32Array(arrayLength);
}
// AssemblyScript側で管理するメモリ空間上のUint32Array配列のアドレスを返す
export getUint32Array(target: WasmUint32Array): usize {
return target.arrayContent.dataStart;
}
// AssemblyScript側で管理するメモリ空間上のUint32Array配列の内容を更新
export reverseUint32Array(target: WasmUint32Array): void {
// reverse target.arrayContent
for(var i = 0; i < target.arrayContent.length / 2; i++) {
const j = target.arrayContent.length - 1 - i;
const tmp = target.arrayContent[i];
target.arrayContent[i] = target.arrayContent[j];
target.arrayContent[j] = tmp;
}
}
TypeScript側のモジュール
import * as WasmArrays from '../../build/as/WasmArrays.release';
class WasmUint32ArrayProxy {
// Wasm側モジュールにおけるUint32Arrayを保持するオブジェクトへの不透明参照
private wasmUint32Array: any;
// Wasm側モジュールとJavaScript側モジュールの双方で共有するUint32Array配列
arrayContent: Uint32Array;
// Wasm側モジュールとJavaScript側モジュールの双方で共有するUint32Array配列の長さ
arrayContentLength: number;
// コンストラクタ、指定の長さのUint32Array配列を、Wasm側とJavaScript側の双方に共有した形で作成する
constructor(arrayContentLength: number) {
this.arrayContentLength = arrayContentLength;
this.wasmUint32Array = WasmArrays.createUint32Array(arrayContentLength);
this.arrayContent = this.createUint32ArrayContent(this.wasmUint32Array);
}
// 内部的に使用するメソッド、Wasm側と共有したバッファもとにUint32Arrayを作成する
private createUint32ArrayContent(wasmUint32Array: any): Uint32Array {
return new Uint32Array(
WasmArrays.memory.buffer, // WasmArraysモジュールのmemoryを参照
WasmArrays.getUint32Array(wasmUint32Array), // WasmArraysモジュールの管理するメモリ上の配列のバッファの開始位置
this.arrayContentLength // 配列の長さ
);
}
// Wasm側と共有したUint32Array配列に、引数で与えたUint32Array配列の内容をセット
set(arrayContent: Uint32Array): void {
if (this.arrayContent.byteLength === 0) {
// もしこの配列の長さがゼロ=バッファが再配置されているならば、
// バッファの最新の位置をもとに配列を作り直す
this.arrayContent = this.createUint32ArrayContent(this.wasmUint32Array, this.arrayContentLength);
}
this.arrayContent.set(arrayContent);
}
// Wasm側で配列の内容を逆にする処理を実行
reverseWithWasm(): void {
WasmArrays.reverseUint32Array(this.wasmUint32Array);
}
// JavaScript側で配列の内容を逆にする処理を実行
reverseWithJS(): void {
for(let i = 0; i < this.arrayContentLength / 2; i++) {
const j = this.arrayContentLength - 1 - i;
const tmp = this.arrayContent[i];
this.arrayContent[i] = this.arrayContent[j];
this.arrayContent[j] = tmp;
}
}
// 配列の中身をconsole.logで表示
showContent(): void {
console.log(this.arrayContent);
}
}
// 長さ5のUint32Arrayを、Wasm側とJavaScript側の双方から直接操作可能なものとして作成する
const arr1 = new WasmUint32ArrayProxy(5);
// この配列に[1,2,3,4,5]の内容をセットする
arr1.set(new Uint32Array([1,2,3,4,5]));
arr1.reverseWithWasm(); // Wasm側で配列内容を逆順にする処理を行う
arr1.showContent(); // 配列を逆にした内容が表示される [5,4,3,2,1]
arr1.reverseWithJS(); // JavaScript側で配列内容を逆順にする処理を行う
arr1.showContent(); // 配列をさらに逆にした(もとに戻した)内容が表示される [1,2,3,4,5]
コード内のコメントで示していることの繰り返しになりますが、ここでは、AssemblyScript側で作成した配列のバッファの位置をもとに、TypeScript側でその参照を共有して、配列のビューを作成しています。この配列は、AssemblyScript側での配列の更新、TypeScript側での配列の更新の、どちらからも可能なものになります。
モジュール境界をまたいだ形での関数呼び出しで、大きなサイズの配列を引数で渡すようなことをすると、処理が冗長になり、性能が低下してしまいます。そうした場合には、ここで示したような、モジュール境界のどちら側からも直接的に配列操作できるしくみを利用することができます。
ただし、こうしたしくみを使う際に、ランタイムのメモリ管理のしくみによって、メモリ上でバッファがいつのまにか再配置されて、配列の長さがゼロになったように見えることがあります。その場合には、バッファの最新の位置をもとに配列(のビュー)を作り直せばOKです。バッファの再配置が発生したことを検出するには、配列のbyteLengthが0になっているかどうかをチェックします。
まとめ
この記事では、AssemblyScriptでモジュール境界を超えて、Wasm側とJavaScript側の双方から配列を直接的に操作できるようにする方法を示しました。処理内容によっては、AssemblyScriptの導入はたいへん大きな効果があるので、ぜひ試してみてください。
ただし、こういうグルーコードを書かなければいけないというのは、正直、ちょっと面倒くさいですよね!
TypeScript側のコードに付与したアノテーションをもとに、ascコマンドがこうしたグルーコードを自動生成してくれるようになったりすると、とても幸せな感じになると思いました。
そのうち、時間ができたら、仕様を整理して、必要な開発をして、本家にプルリクを送って貢献したいなぁなどと思っています。あるいは、AssemblyScriptは、まだまだ発展途上なので、私なんかでなくても、誰かがいずれやってくれそうな気もしています。
ひとまず以上です。