Angularで作ったアプリからWebAssemblyで実装した関数を呼び出す

## 1. 始めに
タイトルの通り、Angularで実装した処理からWebAssemblyで実装した関数を呼び出してみました。
AngularはTypeScriptでの開発が推奨されているため、WebAssemblyのホームページに記載されているサンプルコードをそのままコピペしただけでは動かず、ちょっと細工が必要でした。
今回はその一連の作業をまとめてみました。

## 2. Angularのサンプルアプリを作成
まずはAngularの環境を構築します。Angular CLIのインストールは既に完了しているものとして、コマンドラインで以下を実行してワークスペースを作成します。
```
ng new Application
```
コマンドラインでApplicationフォルダに移動し、以下のコマンドを実行してアプリが起動することを確認します。この「Hello, Application」と書かれている部分を、WebAssemblyで実装した関数から取得した値で書き換えます。
```
ng serve --open
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501246/2ba5c33e-422a-4f80-82ed-87b2a4e55d61.png)

ちなみに、Application/src/app/app.component.tsを見てみると以下のようになっています。
```typescript: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 ビルド環境構築
[公式サイト](https://emscripten.org/docs/getting_started/downloads.html)を読んで、コマンドラインでセットアップを行います。

### 3.2 WASM作成の元となるソースコードを準備
C++で書いた、以下のソースファイルを準備します。
```cpp:test.cpp
#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++の関数のエキスポート方法まとめ](https://qiita.com/chikoski/items/462b34db61daf13a7897)

### 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を動かす](https://qiita.com/t_katsumura/items/beeb2c09afdf80a30f37)

## 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フォルダは新規に作成しました）。
```typescript:wasm.ts
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の関数を呼び出します。
```typescript:app.component.ts
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」に変わっていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501246/2d0458e5-bb70-4782-981e-df66f82f8d08.png)

## 最後に
というわけで、AngularからWebAssemblyで実装した関数を呼び出してみました。
どちらもモダンなWeb技術なので、使いこなせるようになっていきたいです。