RustのWASMをAngularのサービスとして使う方法

ハイサイ！オースティンやいびーん！

# 概要

Rustとwasm_bindgenを使って、Angularのサービスとして使えるWASMを作る方法をサクサクと紹介します。

## なぜWASMをAngularに？

そもそもなぜWASMをAngularで使いたいのかというところを疑問に思う読者もいるかもしれませんが、その思いはおそらくパフォーマンスを追求するならAngularを使うのはどうかという前提だと仮定してお話しします。

Angularはパフォーマンスが悪いのはその通りです。特にFirst Paint（DOMに最初にレンダーされる）までの時間が悲惨です。読み込んだ後のパフォーマンスはReactよりはいいが素晴らしいわけでもないのです。

しかし、Angularが向いているのは、業務アプリであり、その業務アプリではサクサクと動作することが非常に大事です。以前勤めていた会社では、Angularを使ったアプリ開発をしていたのですが、パフォーマンスが最大の課題でした。Angularを使っていたこと自体が問題だったというより、JavaScriptのメモリ管理の問題に直面していたといった方が正確です。

そのアプリもメモリを用いた様々な計算をサーバーではなく端末で行っていたのですが、これはまさにWASMの出番だったな、と悔しく思います。今筆者がまだその会社にいたらきっとWASMを訴えていたことでしょう（それとバックエンドのマイクロサービス化もしたかったのですが心残りが今も）。

AngularおよびJavaScriptの最大難関であるメモリ管理を突破してくれるのはRust + WASMです。なので、疑問に答えると、**AngularにWASMを使うのは、Angularの弱点を大きく補えるからです**。

# プロジェクトをセットアップ

 AngularのCLIがなければインストールしてください。

https://angular.io/cli

そしたら、新しいAngularプロジェクトを作ります。

```
ng new wasm-ng
```

その中に入ってサービスを作成します。フォルダーに入るようにしましょう。

```
ng g service wasm/wasm
```

さらに`Cargo.toml`を作ります。

```
cd wasm-ng
touch Cargo.toml
```

上記のCargo.tomlにはWorkspaceの設定を入れて複数のWASMサービスが作れるようにします。とりあえず最初のパッケージの場所を入れます。

```toml:Cargo.toml
[workspace]

members = ["src/app/wasm.service/wasm_service"]
```

次、RustのCrateを作成します。もしかしてよりわかりやすい名前にした方がいいのかもしれません。`count_service_wasm`など。

```
cargo new src/app/wasm.service/wasm_service --lib
```

これでとりあえず下準備はOKです。

# `wasm_bindgen`を追加します

Rustでコンパイルしたコードを気持ちよくJavaScriptでWASMとして使えるように、`wasm_bindgen`という素敵なCrateがあります。これを使うとRustの型とJavaScriptの型を変換することが楽になります。RustはRustらしく書いて、JavaScriptではJavaScriptらしくそれを消費する、そのようにさせてくれるのです。

https://github.com/rustwasm/wasm-bindgen

追加するには以下のコマンドを実行します。

```
cargo add -p wasm_service wasm-bindgen 
```

それから`src/app/wasm.service/wasm_service/Cargo.toml`を少し修正します。Rustの`lib`の設定に`cdylib` (C Dynamic Lib)を追加する必要があります。なぜなら、wasm-bindgenがRust以外のライブラリ（WASM）で使われるからです。

```toml
[package]
name = "wasm_service"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2.87"
```

# Rustでサービスのメソッドを実装する

最初に、Rustの実装から入ります。`wasm_bindgen`の`prelude`を含める必要があります。

```rust:src/app/wasm.service/wasm_service/src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct _WasmService {}
```
_\_WasmServiceにしたのには理由がありますが、後ほど説明します_

`#[wasm_bindgen]`のmacroを使うと、`struct`はJavaScriptの`class`に匹敵するものに変換されます。ただ、注意点があります。`new _WasmService()`のようなコンストラクタはこのままだと呼べないのです！

## `new _WasmService()`のコンストラクタが呼べるように

wasm_bindgenのドキュメンテーションを漁ったらやり方がありました！

https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-js-imports/constructor.html

`struct`の一つのメソッドの上に`#[wasm_bindgen(constructor)]`を追加しておけば、`new`がJavaScriptで使えるようにコンパイルしてくれます！

```rust
#[wasm_bindgen]
impl _WasmService {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        Self {}
    }
}
```

## Angularで使いたいメソッドを実装する

Angularのサービスで使いたいメソッドをここで実装していきます。本記事では単純に`greet`という文言を返すメソッドを実装して終わりますが、読者はぜひここで面白いことをお試しください！

注意点としては、ここで作っている`struct`のプロパティ（`self.*`）は、WASMのメモリの中に保管されることになり、WASMの境界線からコピーしたり、動かしたりするような実装をしてしまうと、パフォーマンスの打撃を喰らう点です。ちょっとわかりづらいのですが、WASMのメモリの仕組みを説明するのは本記事のスコープ外なので興味があればぜひWASMのメモリーについて読んでいただければと思います。

https://developer.mozilla.org/en-US/docs/WebAssembly/Using_the_JavaScript_API#memory

本記事では以下の実装でやっていきます！

```rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct _WasmService {}

#[wasm_bindgen]
impl _WasmService {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        Self {}
    }

    pub fn greet(&self) -> String {
        String::from("Hello World")
    }
}
```

# `wasm-pack`をインストールしてRustをWASMにコンパイルする

Rustのコードを`cargo build`でそのままコンパイルするというわけにはいかず、WASMにコンパイルできるように簡略化してくれる`wasm-pack`を使うことをお勧めします。

`--target wasm32-unknown-unknown`ではcargoでもビルドできますがJavaScriptで使うためにはいくつか余計な段階を踏まないといけなくなるので、`wasm-pack`がいいのです！

インストールは[こちら](https://rustwasm.github.io/wasm-pack/installer/)。

インストールすると以下のコマンドを実行してビルドします。

```
cd src/app/wasm.service/wasm_service  
wasm-pack build
```

すると、`pkg`のダイレクトリーにいろいろとコンパイルされます。

![Screenshot 2023-09-20 at 16.39.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/1adf9bfe-90ec-e570-9896-be9138c49567.png)

それらのファイルの中身を見ると面白いのですが、重要なのは`src/app/wasm.service/wasm_service/pkg/wasm_service.d.ts`です。ここで自分らが実装した`pub`のメソッドとプロパティが記載されており、JavaScript/TypeScriptにもその型と使い方がわかるように親切にもしてくれています。

```typescript:src/app/wasm.service/wasm_service/pkg/wasm_service.d.ts
/* tslint:disable */
/* eslint-disable */
/**
*/
export class _WasmService {
  free(): void;
/**
*/
  constructor();
/**
* @returns {string}
*/
  greet(): string;
}
```

よろしい！

# Angularのサービスとしてデカレートする

Angularのサービスとして使えるように`@Injectable`のお馴染みのデコレーターを使いたいのですが、筆者の知る限り、インポートされるクラスにデコレーターを追加することは不可能なはずです。せめてTypeScript型のデコレーターではクラス定義を再度しないといけないはずなので、以下のように`extends`を使って再エクスポートします。このために、Rust側で`_`を追加していました。

```typescript:src/app/wasm.service/wasm.service.ts
import { Injectable } from '@angular/core';
import { _WasmService } from './wasm_service/pkg/wasm_service';

@Injectable({
  providedIn: 'root',
})
export class WasmService extends _WasmService {}
```

JavaScriptのWASMに詳しい方はここで首を傾げるかもしれませんが、なせWASMをESMと同様にインポートできたのか、不思議ではないでしょうか？これは、WebPackがやってくれているのだと思います。その辺を設定しないといけないのかなと最初身構えていたのですが、ダメもとで試してみたら大丈夫でした。最近のWebPackはデフォルトでWASMのESMポリフィルを入れてくれているようです。

WASMをESMとして使えるようにするのは今提案としてまとまっています。

https://github.com/WebAssembly/esm-integration/blob/main/proposals/esm-integration/README.md

# AppComponentで使ってみる

最後の変更として、デフォルトのAppComponentの`title`プロパティを`greet()`にします。

```typescript:src/app/app.component.ts
import { Component, inject } from '@angular/core';
import { WasmService } from './wasm.service/wasm.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  wasmService = inject(WasmService);

  title = this.wasmService.greet();
}
```


# Angularのサーバーを立ち上げて見てみよう

これが緊張するその瞬間ですが、果たしてうまくいくのでしょうか！？

```
ng serve
```

![Screenshot 2023-09-20 at 16.52.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/36798037-e023-a431-4a5a-99c8f8549a2f.png)

バッチリ！

と、全然感動できないデモではあるのですが！:joy:

気になるのはWASMのサイズですが、14kbで大きいなぁ、と思いました。

![Screenshot 2023-09-20 at 16.52.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/33e39ad9-64e9-65fc-d682-74736ccf49c1.png)

これはいろいろと削れるので削りたいところです。`"Hello World`を返すのに`14kb`をダウンロードさせるのはアホらしいので:joy:

# まとめ

以上、AngularのサービスにWASMを使う方法を紹介しましたが、如何でしょうか？

デモが感動できる内容でなくとも、これで広がる可能性を想像すればワクワクしますね。Angularは業務ツール用途にとても適しているフレームワークなので、WASMと一緒に使えればかなりアキレス腱を補助できるのではないでしょうか。

## RxJSとの互換性

RxJSのインタフェースを満たすようなRustの`trait`を実装すれば、そのまま`.subscribe`が呼べるようにできそうです。ちょっと試して見ないと！