Rustで書いた小さなツール（空間ID生成）をWasmにしてTypeScriptから呼び出してみる！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/d907449e-cd8f-4e26-a530-d2f905f7ce4d.png)
以前こんな記事を書きました！

https://qiita.com/nokonoko_1203/items/c548f24d2e779118ac7c

https://qiita.com/nokonoko_1203/items/3179ea525ccd5fee1ac0

「空間ID」という地球上の場所を一意のキーで特定するための概念があるんですが、緯度経度や標高からこのIDを算出するためのツールをRustで作成しました！

このRustで作ったツールをPythonで動かせるようになったので、次はTypeScriptから呼び出していこうかと思います。

呼び出し方は色々あるのかもしれませんが、RustにはWasmへビルドするコマンドが標準で備わっているため、Wasmに変換してTypeScriptで読んでみましょう。

最終的なコードはここに置いているので、いつでも参考にしてください！
- ロジック本体: `spatial-id-core`
- wasm: `spatial-id-wasm`
- 動作確認用: `examples/typescript`

https://github.com/nokonoko1203/spatial-id-rs/tree/main

## 準備

Rustのインストールは簡単なので、導入されている前提で進めます。
[Install Rust](https://www.rust-lang.org/tools/install)

RustのパッケージをWasmに変換するためには`wasm-pack`が必要なのでインストールしましょう。

```bash
cargo install wasm-pack
```

次に、プロジェクトを作成します。

```bash
cargo new --lib spatial-id-wasm
```

`Cargo.toml`は以下のようにしましょう！

`spatial-id-core = { path = "../spatial-id-core" }`という記述がありますが、今回は上記リポジトリの`spatial-id-core`が親ディレクトリに存在して、それをWasmで利用する、という想定で話を進めていきます。

```toml
[package]
name = "spatial-id-wasm"
version = "0.1.0"
edition = "2024"

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

[dependencies]
wasm-bindgen = "0.2"
spatial-id-core = { path = "../spatial-id-core" }
```

## RustでWebAssembly用のコードを書く

プロジェクトの構造はこんな感じになっている想定です

```
spatial-id-rs/
├─ spatial-id-core/  # Rustのメインロジック
└─ spatial-id-wasm/  # 今回作成するWasm用プロジェクト
```

`spatial-id-wasm/src/lib.rs`に以下のようなコードを書きます。

```rust
use spatial_id_core::cell::zfxy::ZFXY;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct WasmLatLon {
    pub lat: f64,
    pub lon: f64,
}

#[wasm_bindgen]
impl WasmLatLon {
    #[wasm_bindgen(constructor)]
    pub fn new(lat: f64, lon: f64) -> WasmLatLon {
        WasmLatLon { lat, lon }
    }
}

#[wasm_bindgen]
pub fn generate_spatial_id(lat: f64, lon: f64, alt: f64, zoom: u8) -> String {
    let cell = ZFXY::from_lat_lon_alt(lat, lon, alt, zoom);
    cell.to_spatial_id_str()
}
```

コアモジュールから`spatial_id_core::cell::zfxy::ZFXY`（空間ID生成）をインポートし、それを利用して空間IDを生成するコードです。

`#[wasm_bindgen]`を付けることで、この関数がJavaScriptやTypeScriptから呼び出せるようになります。

## WebAssemblyにビルドする

`spatial-id-wasm`フォルダで以下のコマンドを入力し、RustのコードをWasmに変換します。

```bash
wasm-pack build --target bundler --release
```

これで `pkg` フォルダにWasmファイルとTypeScript用のラッパーファイルが生成されます。

## TypeScriptで使う

新しくTypeScriptのプロジェクトを作成します。

```bash
pnpm create vite@latest examples/typescript --template vanilla-ts
cd examples/typescript
pnpm add -D vite-plugin-wasm "file:../../spatial-id-wasm/pkg"
pnpm i
```

`examples/typescript/vite.config.ts`を作成しましょう。

```
import { defineConfig } from "vite";
import wasm from "vite-plugin-wasm";

export default defineConfig({
  plugins: [wasm()],
});
```

プロジェクトの `examples/typescript/src/main.ts` を以下の内容に変更します。
`console.log`で空間IDの生成結果を表示するだけでもいいんですが、面白みがないので動的に生成できるようにしてみましょう。

- `examples/typescript/index.html`
```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Spatial ID Demo</title>
</head>
<body>
    <div id="app">
      <h1>Spatial ID Generator</h1>
      <form id="spatial-form">
        <label>
          緯度 (lat): <input type="number" step="any" name="lat" required value="0">
        </label><br>
        <label>
          経度 (lon): <input type="number" step="any" name="lon" required value="0">
        </label><br>
        <label>
          標高 (alt): <input type="number" step="any" name="alt" required value="10">
        </label><br>
        <label>
          ズームレベル (zoom): <input type="number" name="zoom" min="0" max="26" required value="25">
        </label><br>
        <button type="submit">空間ID生成</button>
      </form>
      <div id="result"></div>
      <div id="error" style="color:red;"></div>
    </div>
    <script type="module" src="/src/main.ts"></script>
</body>
</html>
```

- `examples/typescript/src/main.ts`

```typescript
import { generate_spatial_id } from "spatial-id-wasm";

async function main() {
  const form = document.getElementById("spatial-form") as HTMLFormElement;
  const resultDiv = document.getElementById("result")!;
  const errorDiv = document.getElementById("error")!;

  form.onsubmit = (e) => {
    e.preventDefault();
    errorDiv.textContent = "";
    resultDiv.textContent = "";

    const formData = new FormData(form);
    const lat = Number(formData.get("lat"));
    const lon = Number(formData.get("lon"));
    const alt = Number(formData.get("alt"));
    const zoom = Number(formData.get("zoom"));

    if (
      isNaN(lat) || isNaN(lon) || isNaN(alt) || isNaN(zoom) ||
      zoom < 0 || zoom > 26
    ) {
      errorDiv.textContent = "入力値が不正です。";
      return;
    }

    try {
      const spatialId = generate_spatial_id(lat, lon, alt, zoom);
      resultDiv.textContent = `Spatial ID: ${spatialId}`;
    } catch (err) {
      errorDiv.textContent = `エラー: ${(err as Error).message}`;
    }
  };
}

main().catch((err) => {
  document.querySelector<HTMLDivElement>("#app")!.innerHTML =
    `<div style="color:red;">初期化エラー: ${(err as Error).message}</div>`;
});
```

動作確認してみましょう！

```bash
pnpm run dev
```

ブラウザで `http://localhost:5173` にアクセスして、「Spatial ID」が正しく表示されれば成功です！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203944/d907449e-cd8f-4e26-a530-d2f905f7ce4d.png)

## まとめ

RustのコードをWebAssembly経由でTypeScriptから呼び出す方法を紹介しました。これにより、ブラウザ上でもRustの高速な処理が使えるようになります。

ぜひ色々な機能を追加して試してみてください！