Vue3 × WasmでRustを簡単・爆速・安全に呼び出すことができた話

## Wasmとは
Web Assembly(Wasm)とはウェブブラウザ上で高性能なアプリケーションを実行するためのバイナリ形式のコードを指し、JavaScriptよりも高速で動作する特徴があります。

2019年12月5日、W3CはWebAssemblyが正式にウェブ標準として採用されたことを発表しました。

https://www.w3.org/press-releases/2019/wasm/

## 良いところ
### ネイティブコードをWeb移植しやすくなる
Wasmでは現在多くの言語に対応しており、動作の安定性には課題があるものの盛んにアップデートが行われています。

以下の言語はWasmにおいて盛んに利用されている言語です。
* C
* C++
* C#
* Rust
etc.

リッチなWebアプリケーションを構築するためだけでなく、C#,C++等で書かれているレガシーな社内ツール等をWebベースへ移植する一助になりそうだと感じました。

### 負荷の高い処理を軽量化できる
音声・動画処理や機械学習等計算処理など負荷の高い処理がフロントで高速・軽量化できるようになります。

### ブラウザ対応が進んでいる
W3C標準化というだけあり、WASMは多様なブラウザで対応しています。

https://caniuse.com/wasm

### TypeScriptなら型情報を利用できる
後述しますが、wasm-packのコンパイル時にd.tsファイルを作成するためモジュールの型情報を継承できます。
ただしJavascriptのプリミティブ型として丸められるので厳密な指定ができない箇所があります。

## Vueで試してみる
Next.js(React)での事例はいくつかみられましたが、TypeScriptとの親和性が高いVue3での事例があまりないな？と思ったので試してみました。

Wasmの利用元はRustを用い、簡単なサンプルコードをVueで動かしてみます。

## セットアップ

### Wasm側
1.Rust環境をセットアップ
以下公式のセットアップに従いRustの実行環境を構築します

https://www.rust-lang.org/ja/tools/install

----
以降はMDNのRust to Wasmを参考にしています。

https://developer.mozilla.org/ja/docs/WebAssembly/Rust_to_Wasm#wasm-bindgen_%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6_rust_%E3%81%A8_javascript_%E3%82%92%E5%8D%94%E8%AA%BF%E3%81%95%E3%81%9B%E3%82%8B

----

2.wasm-packをインストール
以下コマンドでRustのソースをWasmへビルドするためのツール「wasm-pack」をインストールします
```shell
cargo install wasm-pack
```

3.Cargo newでプロジェクト作成
```shell
cargo new --lib rust-wasm
```
フォルダ構成はこうなります。
```
├── Cargo.toml
└── src
    └── lib.rs
```

4.lib.rsとCargo.tomlを調整

* lib.rsを修正

以下の通り修正します。
MDNの例に倣い、固定の文字列と引数にとった文字列を結合し、アラートを出す処理を実装しています。
```rust
//lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    pub fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}
```

「なぜRustからjsのalert関数が呼べるのか？」と思った方もいると思います。
これはソース頭に記載しているwasm_bindgenがよしなにjavascriptの機能を提供してくれているからです。
詳細は以下をご参照ください。

https://developer.mozilla.org/ja/docs/WebAssembly/Rust_to_Wasm#wasm-bindgen_%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6_rust_%E3%81%A8_javascript_%E3%82%92%E5%8D%94%E8%AA%BF%E3%81%95%E3%81%9B%E3%82%8B

* Cargo.tomlを修正
以下のように変更します。
```Cargo.toml
[package]
name = "rust-wasm"
version = "0.1.0"
edition = "2021"

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

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

大事なのはlib,dependenciesの記載で、これらによりコンパイルする形式とパッケージの依存関係を指定できます。

5.ビルド
以下コマンドでビルド
```
wasm-pack build --target web
```

### Vue側
1. ViteでVueプロジェクト作成
以下コマンドを実行
```
npm create vite vue-wasm-app --template vue
```

※この時、TypeScriptを有効するとwasmの型情報を参照できます。
パッケージをインストール
```
cd vue-wasm-app
yarn install
yarn dev
```
localhost:5173でアクセスし、Vueの初期テンプレートが起動されたらOK
![スクリーンショット 2024-08-18 13.06.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840428/5f945baa-fbcf-9d23-f04c-f9188908f1db.png)


2.Wasmをインポートできるよう調整
VueでWasmを使うにあたり、特殊な設定は必要ありません。
まずRust側のプロジェクトからコンパイルしたwasmファイルをコピーします。
wasm-packでコンパイルされたコードはpkgというフォルダで出力されるので、丸ごとwasmという名称でvue側へ移動させましょう。
```
mv rust-wasm/pkg vue-wasm-app/src/wasm
```

この時点でvue側のフォルダ構成はこうなっているはずです
```
├── node_modules
├── public
├── src
│   └── assets
│   └── components
│   └── wasm
│       ├── rust_wasm.d.ts
│       ├── rust_wasm.js
│       ├── rust_wasm_bg.wasm
│       ├── rust_wasm_bg.wasm.d.ts
│       └── package.json
│   └── App.vue
│   └── main.ts
│   <省略>
├── index.html
├── README.md
├── package.json
├── vite.config.ts
...

```

一番簡単にwasmを呼び出す方法はimportでrust_wasmを呼び出すモジュールを実装し、vueコンポーネントで呼び出すことです。

wasmの関数を呼び出すには直接呼ぶのではなく、initを実行してから呼ぶようにします。
Promiseで返却されるのでasync/awaitで実行してやります。
```wasmGlue.ts
import init, { greet } from "./wasm/rust_wasm"

export default async() => {
    await init().then(() => {
        greet("Web Assembly in Vue3!")
    })
}
```
型定義ファイルも出力してくれるのでTypescript形式でインポートできます。


init関数さえ先に呼べば良いのでcomposableっぽく書くときはこうなります。
```useWasm.ts
import init, { greet, add } from "../wasm/rust_wasm"

export default async () => {
    //wasmの初期化
    await init()

    return {
        greet,
        add
    }
}
```

Rust側ではgreet関数にstr型を引数に取るように指定していましたが、typescriptでも型が適切に定義されていることが確認できます。
![スクリーンショット 2024-08-18 13.36.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840428/bf71bea5-96ae-8978-b031-d683db78c1e9.png)

number型の引数を入れるとエラーが表示されることを確認できました。
![スクリーンショット 2024-08-18 13.36.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840428/8544d50f-8e97-9006-a2bb-82688d8056f1.png)

コンパイル後も型安全に利用できるのは非常に嬉しいですね...!

次にテンプレートで作成されたHelloWorld.vueでwasmGlue.tsを組み込みます。
```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import wasmGreet from "../wasmGlue.ts"
defineProps<{ msg: string }>()
onMounted(async () => {
  await wasmGreet()
})
const count = ref(0)
</script>
...
```

Composableっぽく使うなら以下のようにインポートします。
```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import useWasm from "../composables/useWasm.ts"
defineProps<{ msg: string }>()
const { greet, add } = await useWasm()

onMounted(() => {
  greet("Web Assemble in Vue3!")
  alert(add(10, 20))
})
const count = ref(0)
</script>
...
```

※子コンポーネントのtop-levelにawaitがある場合、Suspenseで囲ってやる必要があります。
``` App.vue
<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <Suspense>
    <HelloWorld msg="Vite + Vue" />
  </Suspense>
</template>
```


3.動作チェック
yarn devでvueアプリを起動し、localhost:5173にアクセスしたとき以下のメッセージが表示されれば成功です。
![スクリーンショット 2024-08-18 13.39.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840428/8c5c1387-abfe-8c34-bb05-fefe70e54304.png)

yarn buildでビルドしたコードも正常に動作したので本番でも利用できることが確認できました。

## 最終的なコード
最終的なソースコードは以下へ配置しました。
vue側のwasmモジュールをComposableへ書き換えました。

https://github.com/Hal-Spidernight/wasm-with-vue3-test/

## 注意点

### WasmからDOM操作はできない
WASMは低レベルのバイナリコードなのもあり、ブラウザのネイティブAPIへのアクセスはできません。
DOMとWasm処理を組み合わせたい場合はうまくロジックを切り出し、DOM操作をJavaScript側で実装するのが良さそうです。

### OSシステム操作はできない
ブラウザ内での動作を前提としているため、デバイス操作やネイティブな操作はできません。

### 言語の互換性に注意
各言語においてWasmへのコンパイルシステムの開発が盛んに行われていますが、まだ発展途上である側面も大いにあります。
いきなり本番へ実装するのではなく、PoCを経てから導入を進めるのが良さそうです。

### Viteとの噛み合わせでうまくいかないこともある
今回のRust + wasm-packでは問題なく実装できましたが、Wasmの形式によってはViteで読み込めないことがあるようです。

https://zenn.dev/nyatlan/articles/614421064965d5

## 最後に

Webアプリケーションに要求される機能・品質は年々上がりつつもユーザ環境の多様化が進んでおり、むやみに高負荷をかける処理をフロントに出すことはできません。
また、既にネイティブで稼働しているシステムをWeb上で動作させていくには移行コストが高い傾向にあります。

こういった問題を解決する助けになりそうなシステムをVueで簡単に呼び出せることが分かったことは大きな収穫でした！

## 参考資料

https://developer.mozilla.org/ja/docs/WebAssembly/Rust_to_Wasm

https://zenn.dev/tetter/scraps/da36684a96709d