3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebAssembly / WasmAdvent Calendar 2024

Day 14

小ネタ:AssemblyScriptでモジュール境界を超えてArray内容を共有する

Last updated at Posted at 2024-12-04

はじめに

(この記事では、AssemblyScriptのインストールや基本的な使い方については説明を割愛します)

WebAssemblyとJavaScriptの間には「モジュール境界(module boundaries)」があり、文字列・配列・オブジェクトなどの複雑なデータ型を、そのままやりとりすることはできない、ということが説明されます。しかし、配列について言うならば、「コピー渡し」ではない、別記事(「小ネタ:AssemblyScript内で生成したオブジェクトの「不透明な参照渡し」の型には、anyを指定するしかない」)で説明したような「不透明な参照渡し」でもない、もっと直接的な形でモジュール境界を超えてデータを共有するための、低レベルな方法が存在します。それが、memory.bufferを利用するという方法です。

AssemblyScript側のモジュール

WasmArrays.ts
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側のモジュール

WasmUint32ArrayProxy.ts
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は、まだまだ発展途上なので、私なんかでなくても、誰かがいずれやってくれそうな気もしています。

ひとまず以上です。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?