iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

React・Node.js・CloudflareWorkersでRustから作成したWASMを動かす

こんにちは!iimonでCTOをしているもりごです。
本記事はiimon Advent Calendar 2024最終日の記事となります!

今年はふるさと納税で沖縄に沢山寄付をしたので、沖縄の名産品を食べるのが毎日の楽しみとなっています。

今回、フロントエンド・サーバーサイド・エッジコンピューティングの3つの環境でWASM(WebAssembly)を活用し、同じ処理を別の環境で動かす方法を試してみました。複数の環境で共通の処理を使う事ができれば、同じコードを複数の環境に記述する必要がなくなり、管理の煩雑さを軽減できると考えて試してみました。

例えばフォームのバリデーションのように、フロントエンドとサーバーサイドの両方で同じ処理が必要になるケースはよくあると思っています。弊社は不動産領域を扱っており、住所関連の処理で色々な場所で同じ処理が必要になることが多々ありました。これまでは、なるべく1箇所にまとめて実装を行う様にしてはいるものの、どうしても別々の場所で重複して実装しなければいけないことがあり、片方だけに変更を反映してしまうといったことがありました。そのため、こうした共通の処理はなるべく1箇所にまとめて出来ると良いなと思っていたので今回WASMで試してみました。

RustでWASMを作る

コンパイルの準備

まずはCargoコマンドでテンプレートを作成します。

cargo new wasm-anywhere

これを実行すると

├── Cargo.toml
└── src
    └── main.rs

この様なファイル構成になるのでまずはCargo.tomlを

[package]
name = "wasm-anywhere"
version = "0.1.0"
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2.99"
regex = "1.11.1"

この様に書き換えます。次にsrc/main.rssrc/lib.rsに書き換えます。

mv src/main.rs src/lib.rs

次にコンパイルする際に使用するwasm-packというツールを導入します。
インストール方法は色々な方法があるのですが今回は

cargo install wasm-pack

この様にインストールします。

ここまでで一応コンパイル出来るようになったので試しにコンパイルしてみます。

wasm-pack build --target web

ファイル構成はこの様になったかと思います

├── Cargo.lock
├── Cargo.toml
├── pkg
│   ├── package.json
│   ├── wasm_anywhere.d.ts
│   ├── wasm_anywhere.js
│   ├── wasm_anywhere_bg.wasm
│   └── wasm_anywhere_bg.wasm.d.ts
├── src
│   └── lib.rs
└── target
    ├── CACHEDIR.TAG
    ├── release
    │   ├── build
 ⋮

Rustのコード作成

次にRustのコードをsrc/lib.rsに作成していきます。
今回共通で呼び出したい処理は住所を分割する処理とし、関数名はsplit_addressとします。
*住所を分割する処理は実際は複雑ですが今回趣旨と外れるので簡略化しています

split_addressという関数をRustで書いていきます!
都、道、府、県のどれかが出たら分割、その後で市、区、町、村のどれかが出たら分割という処理をします。引数に住所の文字列を受け取り、返り値には分割された住所の文字列の配列を返します。
例えば、"東京都中央区新川1丁目21-2"の文字列の場合、返り値は["東京都", "中央区", "新川1丁目21-2"] です。

今回wasm-bindgenとwasm-packを使いRustからWASMへコンパイルを行います、このため外部から使用したい関数名の前には#[wasm_bindgen]という属性を付けます。

use wasm_bindgen::prelude::*;
use regex::Regex;

#[wasm_bindgen]
pub fn split_address(address: &str) -> Vec<String> {
    let re_pref = Regex::new(r"[都道府県]").unwrap();
    let re_cdtv = Regex::new(r"[市区町村]").unwrap();
    let mut split_address:Vec<String> = vec![];
    let mut start_pos = 0;

    match re_pref.find(address) {
        Some(m) => {
            split_address.push((&address[0..m.end()]).to_string());
            start_pos = m.end();
        },
        None => split_address.push(String::new()),
    }

    match re_cdtv.find(address) {
        Some(m) => {
            split_address.push((&address[start_pos..m.end()]).to_string());
            split_address.push((&address[m.end()..]).to_string());
        },
        None => split_address.push(String::new()),
    }

    split_address
}

src/lib.rsの元のmain関数は削除してこのコードに置き換えます。これで住所を分割するメソッドの完成です!

ReactからWASMを呼び出す

Reactの準備

ReactからWASMを呼び出すためにReactの準備をします。

npx create-react-app react_wasm

としてReactのテンプレートを作成した上で、react_wasm/src/App.jsを下記の様に書き換えます。

import './App.css';
import React, { useEffect, useState } from 'react';
import init, { split_address } from './pkg/wasm_anywhere';

function App() {
  const [result, setResult] = useState(null);
  const [address, setAddress] = useState("");

  useEffect(() => {
    init();
  }, []);

  const handleSplitAddress = (e) => {
    const result = split_address(e.target.value).join(" / ");
    setAddress(e.target.value);
    setResult(result);
  };

  return (
    <div>
      <form>
        <label>
          Address:
          <input
            value={address}
            onChange={handleSplitAddress}
          />
        </label>
      </form>
      <p>Result: {result}</p>
    </div>
  );
}

export default App;

wasm-packを使用してコンパイルを行うとWASMの作成だけではなく、読み込むために必要なjsファイルも一緒に生成してpkgディレクトリにはき出してくれます。buildオプションにwebを指定すると、ES Modulesで書き出してくれるため、フロントで使用する事が出来ます。

関数を直接インポート出来るようになっているため、Rustのコードのwasm_bindgenで指定した関数名を指定してインポートします。

Reactからではなく、フロントのjsのコードとして使用する場合には、この様に使うことも出来ます。

(async () => {
    await init();
    const splitAddr = split_address("東京都中央区新川1丁目21-2");
    console.log(splitAddr);
})();

また、今回reportWebVitalsは使用しないので下記の様にsrc/index.jsから削除しておきます。

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

WASMの準備

web用にWASMをコンパイルします。

wasm-pack build --target web

コンパイルしたpkgディレクトリをReactから参照出来るようにreact_wasm/src/以下にコピーします。

cp -r pkg react_wasm/src/

Reactで動かしてみる

WASMの準備が整ったので早速動かしてみます。

npm start

Addressのフォームに住所を入力するとResultに分割した結果が無事に表示されました🎉

Node.jsからWASMを呼び出す

Node.jsからWASMを呼び出すのは簡単です!

まずはnodejsというディレクトリを作成し、その下に下記のコードを記載したapp.jsというファイルを作成します。

const wasm = require('../pkg/wasm_anywhere');

const result = wasm.split_address("東京都中央区新川1丁目21-2");
console.log(result);

というファイルを作成した上でbuildオプションにnodejsと指定してCommonJSをはき出させます。

wasm-pack build --target nodejs

nodejsディレクトリに移動して実行してみます。

node app.js

[ '東京都', '中央区', '新川1丁目21-2' ]と出ました!🎉
*手元で確認したところNode.jsのバージョンが17以上であれば対応していました

Cloudflare Workers上でWASMを動かす

CloudflareWorkersでWASMを呼び出すことが出来るということで試してみました。
*Cloudflareのアカウント作成などは省略します

wranglerの準備

CloudflareのCLIツールであるwranglerを使用するため入っていない場合はインストールします。

npm install -g wrangler

wrangler.tomlというファイルを作成し下記の設定をします。

ame = "wasm-anywhere"
type = "javascript"
main = "src/worker.js"
account_id = "xxxxx"
compatibility_date = "2024-12-23"

[observability.logs]
enabled = true

account_idは自分のアカウントのIDを記入します。

コードの作成

srcディレクトリ以下にworker.jsというファイルを下記の内容で作成します。

import initSync, { split_address } from '../pkg/wasm_anywhere.js';
import wasmModule from '../pkg/wasm_anywhere_bg.wasm';

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const address = url.searchParams.get("address");
    if(!address){
      return new Response(JSON.stringify({detail: "No address parameter."}), {
        status: 400,
        headers: { "Content-Type": "application/json" },
      });
    }

    try {
      await initSync(wasmModule);

      const res = {
          splitAddress: split_address(address)
      }

      return new Response(JSON.stringify(res), {
        status: 200,
        headers: { "Content-Type": "application/json" },
      });
    } catch (err) {
      return new Response(`Error: ${err.message}`, { status: 500 });
    }
  },
};

addressパラメータで受け取った住所の文字列を分割して、JSON形式で返すAPIです。

WASMの準備

buildオプションをwebにしてコンパイルします。

wasm-pack build --target web

Cloudflareへデプロイ

wrangler deploy

これだけでデプロイ完了です。
デプロイしたURLへアクセスします。

https://wasm-anywhere.<アカウント名>.workers.dev/?address=東京都中央区新川1丁目21-2

レスポンスはこの様になっていました!🎉(読みやすく整形しています)

{
  "splitAddress": [
    "東京都",
    "中央区",
    "新川1丁目21-2"
  ]
}

まとめ

今回、フロント・サーバー・エッジコンピューティングと3箇所での呼び出しをしてみましたが、PythonやGoなどからの呼び出しもできる様にしたいです。
実はPythonではwasmer、wasmtime、pywasm、pythonmonkeyなどで試したのですが上手く動かず、、もう少し勉強が必要だと感じました。
一方でJavaScriptから呼び出す際はwasm-packとwasm-bindgenを使うと結構簡単に外部から使用することが出来るので使い所もあるかもと思いました。

参考

おわりに

今年もアドベントカレンダー終わりということでもう年末だなという感じですね。

最後になりますが、現在弊社ではエンジニアを募集しています。
この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!

iimon採用サイト / Wantedly / Green

最後まで読んでいただきありがとうございました!