ハイサイ!オースティンやいびーん!
概要
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がなければインストールしてください。
そしたら、新しいAngularプロジェクトを作ります。
ng new wasm-ng
その中に入ってサービスを作成します。フォルダーに入るようにしましょう。
ng g service wasm/wasm
さらにCargo.toml
を作ります。
cd wasm-ng
touch Cargo.toml
上記のCargo.tomlにはWorkspaceの設定を入れて複数のWASMサービスが作れるようにします。とりあえず最初のパッケージの場所を入れます。
[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らしくそれを消費する、そのようにさせてくれるのです。
追加するには以下のコマンドを実行します。
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)で使われるからです。
[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
を含める必要があります。
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct _WasmService {}
_WasmServiceにしたのには理由がありますが、後ほど説明します
#[wasm_bindgen]
のmacroを使うと、struct
はJavaScriptのclass
に匹敵するものに変換されます。ただ、注意点があります。new _WasmService()
のようなコンストラクタはこのままだと呼べないのです!
new _WasmService()
のコンストラクタが呼べるように
wasm_bindgenのドキュメンテーションを漁ったらやり方がありました!
struct
の一つのメソッドの上に#[wasm_bindgen(constructor)]
を追加しておけば、new
がJavaScriptで使えるようにコンパイルしてくれます!
#[wasm_bindgen]
impl _WasmService {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {}
}
}
Angularで使いたいメソッドを実装する
Angularのサービスで使いたいメソッドをここで実装していきます。本記事では単純にgreet
という文言を返すメソッドを実装して終わりますが、読者はぜひここで面白いことをお試しください!
注意点としては、ここで作っているstruct
のプロパティ(self.*
)は、WASMのメモリの中に保管されることになり、WASMの境界線からコピーしたり、動かしたりするような実装をしてしまうと、パフォーマンスの打撃を喰らう点です。ちょっとわかりづらいのですが、WASMのメモリの仕組みを説明するのは本記事のスコープ外なので興味があればぜひWASMのメモリーについて読んでいただければと思います。
本記事では以下の実装でやっていきます!
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
がいいのです!
インストールはこちら。
インストールすると以下のコマンドを実行してビルドします。
cd src/app/wasm.service/wasm_service
wasm-pack build
すると、pkg
のダイレクトリーにいろいろとコンパイルされます。
それらのファイルの中身を見ると面白いのですが、重要なのはsrc/app/wasm.service/wasm_service/pkg/wasm_service.d.ts
です。ここで自分らが実装したpub
のメソッドとプロパティが記載されており、JavaScript/TypeScriptにもその型と使い方がわかるように親切にもしてくれています。
/* tslint:disable */
/* eslint-disable */
/**
*/
export class _WasmService {
free(): void;
/**
*/
constructor();
/**
* @returns {string}
*/
greet(): string;
}
よろしい!
Angularのサービスとしてデカレートする
Angularのサービスとして使えるように@Injectable
のお馴染みのデコレーターを使いたいのですが、筆者の知る限り、インポートされるクラスにデコレーターを追加することは不可能なはずです。せめてTypeScript型のデコレーターではクラス定義を再度しないといけないはずなので、以下のようにextends
を使って再エクスポートします。このために、Rust側で_
を追加していました。
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として使えるようにするのは今提案としてまとまっています。
AppComponentで使ってみる
最後の変更として、デフォルトのAppComponentのtitle
プロパティをgreet()
にします。
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
バッチリ!
と、全然感動できないデモではあるのですが!
気になるのはWASMのサイズですが、14kbで大きいなぁ、と思いました。
これはいろいろと削れるので削りたいところです。"Hello World
を返すのに14kb
をダウンロードさせるのはアホらしいので
まとめ
以上、AngularのサービスにWASMを使う方法を紹介しましたが、如何でしょうか?
デモが感動できる内容でなくとも、これで広がる可能性を想像すればワクワクしますね。Angularは業務ツール用途にとても適しているフレームワークなので、WASMと一緒に使えればかなりアキレス腱を補助できるのではないでしょうか。
RxJSとの互換性
RxJSのインタフェースを満たすようなRustのtrait
を実装すれば、そのまま.subscribe
が呼べるようにできそうです。ちょっと試して見ないと!