1. 始めに
タイトルの通り、Angularで実装した処理からWebAssemblyで実装した関数を呼び出してみました。
AngularはTypeScriptでの開発が推奨されているため、WebAssemblyのホームページに記載されているサンプルコードをそのままコピペしただけでは動かず、ちょっと細工が必要でした。
今回はその一連の作業をまとめてみました。
2. Angularのサンプルアプリを作成
まずはAngularの環境を構築します。Angular CLIのインストールは既に完了しているものとして、コマンドラインで以下を実行してワークスペースを作成します。
ng new Application
コマンドラインでApplicationフォルダに移動し、以下のコマンドを実行してアプリが起動することを確認します。この「Hello, Application」と書かれている部分を、WebAssemblyで実装した関数から取得した値で書き換えます。
ng serve --open
ちなみに、Application/src/app/app.component.tsを見てみると以下のようになっています。
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'Application';
}
3. WebAssemblyのバイナリファイル作成
WebAssemblyの技術を利用する際はwasmと呼ばれるバイナリファイルを作成し、それを読み込んでから関数の呼び出しを行います。このwasmの作成方法は色々あるのですが、一番よく知られているであろう、Emscriptenを使ってC++のコードから作成していきます。
3.1 ビルド環境構築
公式サイトを読んで、コマンドラインでセットアップを行います。
3.2 WASM作成の元となるソースコードを準備
C++で書いた、以下のソースファイルを準備します。
#include <emscripten/emscripten.h>
int main() {
return 0;
}
extern "C" {
EMSCRIPTEN_KEEPALIVE
int add(int x, int y) {
return x + y;
}
}
ここで、以下の点に注意する必要があります。
- main関数は必ず作成します
- 呼び出す関数の前に、EMSCRIPTEN_KEEPALIVEを書いておきます
- 呼び出す関数にはCリンケージを指定します。こうしないとC++の機能で名前マングリングが行われて、TypeScriptのソースコードでアクセスするときに関数名(今回の場合は「add」という文字列)から関数オブジェクトを取得できません
呼び出す関数のexport方法については、こちらの記事を参考にしました。
Emscriptenを使ったC/C++の関数のエキスポート方法まとめ
3.3 Emscriptenを使ってwasmへコンパイル
コマンドラインから以下を実行します。その結果、test.cppと同じフォルダにtest.wasmが生成されます。このtest.wasmを読み込み、TypeScriptからadd関数を呼び出します。
なお、EmscriptenではラッパーとなるJavaScriptファイルも同時に生成することができますが、言語やコンパイラによってはラッパーを生成しないものがあると思いますので、ラッパーを使わず直接wasmを参照した実装にします。
em++ test.cpp -o test.wasm -sWASM=1
オプションの指定については、こちらの記事を参考にしました。
Emscriptenを使ってブラウザでWebAssemblyを動かす
4. AngularアプリからWebAssemblyの関数を呼び出す
ここからが本番です。
4.1 wasmファイルを配置
生成したwasmファイルをAngularアプリが読み込める場所に配置する必要があるのですが、最初に「ng new Application」を実行してフォルダを作成した時にApplication/publicフォルダが生成されており、angular.jsonではここがアセットとして指定されています。他に適当な場所が思いつかなかったので、ここにtest.wasmを配置します。
4.2 wasm読み込みクラスを作成
まずはWebAssemblyのバイナリ読み込み用にクラスを作成し、このファイルをApplication/src/wasmに配置します(wasmフォルダは新規に作成しました)。
type ListenerFunction = () => void;
export class WASM {
// WebAssemblyモジュールの読み込み結果
private wasm: WebAssembly.WebAssemblyInstantiatedSource | undefined = undefined;
// モジュール読み込み完了を通知するリスナー
private listenerList: ListenerFunction[] = [];
// コンストラクタ
constructor() {
const importObject = {
wasi_snapshot_preview1: {
proc_exit: () => console.log("proc_exit")
},
};
WebAssembly.instantiateStreaming(fetch('test.wasm'), importObject)
.then(obj => {
this.wasm = obj;
this.listenerList.forEach(func => func());
this.listenerList = [];
});
}
// WebAssemblyのモジュール読み込みが完了しているかどうか
public isReady(): boolean {
return this.wasm !== undefined;
}
// WebAssemblyのモジュール読み込みが完了時に呼び出すリスナーを登録
public addListener(func: ListenerFunction): void {
this.listenerList.push(func);
}
// WebAssemblyで実装した関数
public add(x: number, y: number): number {
let result: number = 0;
let func = this.wasm?.instance.exports['add'];
if (typeof(func) === 'function') {
result = func(x, y);
}
return result;
}
}
注意点は以下の通りです。
- instantiateStreaming関数の第2引数は必須です。渡さないとランタイムで読み込み失敗のエラーが出ます。また、importObject.wasi_snapshot_preview1.proc_exitも必須です
- this.wasm?.instance.exports['add']で関数オブジェクトを取得しています
- WASMの読み込みは非同期で実行するため、読み込みが完了する前にadd関数が呼ばれると正常に動作しません。そのため読み込みが完了しているか判定する関数と、読み込み完了を通知するリスナーを登録する仕組みを作成しました
4.3 WebAssemblyの関数を呼び出す
上記で作成したwasm.tsを使用し、WebAssemblyの関数を呼び出します。
import { WASM } from '../wasm/wasm'
// 省略
export class AppComponent {
title: string;
wasm: WASM;
constructor() {
this.title = 'Application';
this.wasm = new WASM();
this.wasm.addListener(() => {
// インスタンス生成直後はまだ読み込みが完了していないので、
// 完了後に呼び出されるリスナーを登録する
this.title = '1 + 2 = ' + this.wasm.add(1, 2);
});
}
}
4.4 アプリ実行
この状態でアプリを実行すると、以下のように表示されます。
「Hello, Application」が「Hello, 1 + 2 = 3」に変わっていることが分かります。
最後に
というわけで、AngularからWebAssemblyで実装した関数を呼び出してみました。
どちらもモダンなWeb技術なので、使いこなせるようになっていきたいです。