RustのコードをWebAssemblyにコンパイルしてみて、自分なりにわかったことをこのページにまとめます。
※Rust(1.3.0以上推奨)がインストールされていることが前提。
※またブラウザからJavaScriptでWebAssembly(Wasm)を使うことを想定しています。
参考にしたページ
Rust から WebAssembly にコンパイルする - WebAssembly | MDN
Introduction - The wasm-bindgen Guide
使用した環境
WSL2 Ubuntu - 20.04
Rust 1.66.1
wasm-pack 0.10.3
wasm-bindgen 0.2.83
wasm-packとwasm-bindgen
このページではRustをWasmにコンパイルするまでに2つのものを使います。
「wasm-pack」というツールと、「wasm-bindgen」というクレートです。
- wasm-pack
- npm向けにパッケージ化し、RustのコードをWebAssemblyにコンパイル。
コンパイル後のファイルサイズを小さく抑えられる。
- npm向けにパッケージ化し、RustのコードをWebAssemblyにコンパイル。
- wasm_bindgen
- RustとJavaScriptの架け橋となるクレート。
RustからJavaScriptの機能を使いたいとき、またその逆で使用します。
- RustとJavaScriptの架け橋となるクレート。
これらがなくてもコンパイルは可能でしょうが、難易度が上がりそうだったため、使うことにしました。
インストール
wasm-packをcargoからインストールします。
cargo install wasm-pack
wasm-bindgenはこれから作成するパッケージの「Cargo.toml」内、「dependencies」に追記してインストールします。
バージョンは現時点(2023/1)では「0.2」系であれば動きます。
[dependencies]
wasm-bindgen = "0.2"
また「Rust 1.62.0」以降ならcargoのaddコマンドでも追加できます。
cargo add wasm-bindgen
Wasm用のパッケージ作成
では実際に簡単なものを作っていきます。
$ cargo new --lib first-wasm Created library `first-wasm` package $ cd first-wasm/ $ tree . . ├── Cargo.toml └── src └── lib.rs 1 directory, 2 files
「--lib」をつけてライブラリクレートを作成するようにし、パッケージ名を「first-wasm」にしました。
$ cargo add wasm-bindgen
wasm-bindgenを依存関係に追加。
「Cargo.toml」ファイルを開きます。
[package]
name = "first-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.83"
コンパイルの際に必要な[lib]の項目を上記のように追加してください。
”cdylib”は、他の言語で読めるような動的なライブラリにコンパイルします。
テストを行いたい場合は"rlib"も合わせて加えるといいかもしれません。
RustとJavaScriptの橋渡し
RustとJavaScriptの連携をwasm-bidgenで行います。
実際に「lib.rs」を書いてみます。
use wasm_bindgen::prelude::*;
// Rustで使いたいJavaScriptAPI
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
fn prompt(s: &str) -> String;
}
// これ以降の関数はJavaScriptから呼ばれる
#[wasm_bindgen]
pub fn alert_from_wasm() {
alert("Rustから呼ばれました");
}
#[wasm_bindgen]
pub fn return_your_name() -> String {
let name = prompt("あなたの名前を教えてください");
name
}
#[wasm_bindgen]
pub fn sum(num1: u32, num2: u32) -> u32 {
num1 + num2
}
冒頭でwasm-bindgenを使えるようにpreludeのすべてを読み込みます。
Rustで使いたいJavaScriptの機能にはアトリビュート#[wasm_bindgen]をつけ、「extern "C"」キーワードの波括弧内にAPIを列挙します。
引数と戻り値がある場合、型を指定してください。
「extern "C"」の"C"の部分はなくても動いたのですが、wasm-bindgenの公式ドキュメントでは書かれています。
またない場合「cargo fmt」をかけると自動で”C”が挿入されるので、私も書くことにしました。
上記では、JavaScriptの「alert」と「prompt」を書きました。
自分で作ったJavaScriptのモジュールも呼ぶことができます。
Importing functions from JS - The wasm-bindgen Guide
JavaScriptで使いたいRustの機能には同じくアトリビュートをつけ、その下で通常通り定義を行います。
戻り値として返せるのは、数値やString、boolなどwasm-bindgenが対応している型です。
Supported Types - The wasm-bindgen Guide
コンパイル
コンパイルしてWasmを作ります。
カレントディレクトリが「first-wasm」であることを確認し、以下のコマンドを打ちます。
wasm-pack build --target web
ブラウザから使うつもりなので、「--target web」を付けて実行。
$ wasm-pack build --target web [INFO]: Checking for the Wasm target... [INFO]: Compiling to Wasm... Compiling first-wasm v0.1.0 (/home/fujino/rust/first-wasm) Finished release [optimized] target(s) in 0.65s [WARN]: :-) origin crate has no README [INFO]: Installing wasm-bindgen... [INFO]: Optimizing wasm binaries with `wasm-opt`... [INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended [INFO]: :-) Done in 1.15s [INFO]: :-) Your wasm pkg is ready to publish at /home/fujino/rust/first-wasm/pkg.
新たに「./pkg」が追加され、中に作成物が入っています。
$ tree pkg pkg ├── first_wasm_bg.wasm ├── first_wasm_bg.wasm.d.ts ├── first_wasm.d.ts ├── first_wasm.js └── package.json 0 directories, 5 files
「first_wasm_bg.wasm」が目的のもので、サイズは8.8KBでした。
同じく作成された「first_wasm.js」は、「.wasm」を読み込み、中のもの――今回の場合、alert_from_wasmなどの関数――を呼べるようにしてくれるプログラムです。
後ほどこのファイルを使います。
WebAssemblyを使ってみる
ローカルサーバーを起動して実行
「first-wasm」ディレクトリ内に「index.html」を作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>first-wasm</title>
</head>
<body>
<script type="module">
import init, {alert_from_wasm, return_your_name, sum} from "./pkg/first_wasm.js";
init()
.then(() => {
alert_from_wasm();
const name = return_your_name();
console.log(name);
console.log(sum(26, 43));
})
</script>
</body>
</html>
「first_wasm.js」のinit関数は、デフォルトエクスポートなので波括弧外で行い、名前はそのまま使いました。
初期化が終わったら、戻り値がPromiseオブジェクトのため、thenメソッドで関数を呼び出しています。
モジュールを使用するため、ローカルではサーバーを起動しなければ動きません。
私はVSCodeの拡張機能「Live Server」を使いましたが、Pythonであれば「python3 -m http.server」でも簡単にローカルサーバーを立てることができます。
自身の環境に合うものであればOKです。
ローカルサーバーを起動後、「index.html」を読み込み。
ブラウザ(Chrome)の様子は次のようになりました。



alertやpromptはWasm(Rust)からの呼び出しです。
Wasmからの戻り値は、デベロッパーツールのコンソールで表示。
問題なく動作しました。
Webサイトでの利用
せっかくなので、このページ内でも体験できるようにしたいと思います。
サーバーに「first_wasm_bg.wasm」と「first_wasm.js」、そして「main.js」を作成し、アップロードしました。
ちなみに使用しているのはエックスサーバー
import init, {alert_from_wasm, return_your_name, sum} from "./first_wasm.js";
const alertButton = document.getElementById("alert-button");
const promptButton = document.getElementById("prompt-button");
const sumButton = document.getElementById("sum-button");
const sumResult = document.getElementById("sum-result");
init().then(()=> {
alertButton.addEventListener("click", alert_from_wasm, false);
promptButton.addEventListener("click", () => {
const name = return_your_name();
alert(`${name}さん。\nこれはwasmから返ってきたあなたの名前です。`);
}, false);
sumButton.addEventListener("click", sumButtonClicked, false);
});
function sumButtonClicked() {
let num1 = document.getElementById("value1").value;
let num2 = document.getElementById("value2").value;
const reg = new RegExp(/[^0-9]/g);
if (reg.test(num1) || reg.test(num2)) {
sumResult.innerHTML = "「入力値が不正」"
return;
}
num1 = Number(num1);
num2 = Number(num2);
const resultByRust = sum(num1, num2);
sumResult.innerHTML = resultByRust;
}
ページ下部で「script type="module"」にしてこの「main.js」を読み込んでいます。
下にあるのがHTMLの抜粋と、実際の機能です。
<div class="first-wasm">
<div class="button-wrap">
<button id="alert-button">アラートを呼ぶ</button><br>
<button id="prompt-button">プロンプトを呼ぶ</button>
</div>
<label for="value1"></label>
<input type="text" id="value1" name="value1" maxlength="6">
<span>+</span>
<label for="value2"></label>
<input type="text" id="value2" name="value2" maxlength="6">
<button id="sum-button">計算する</button>
<p>こたえは<span id="sum-result">0</span>です。</p>
</div>
こたえは0です。
ちょっと込み入ったものが作りたくて、迷路の自動生成を以下のWebページに組込みました。
説明は行いませんが、内部の処理をWasmで行っています。
MIMEタイプ
これは余談ですが、最初、問題ないように見えたものの、コンソールを確認すると、次のようなwarningメッセージが出ていました。
WebAssembly.instantiateStreaming failed because your server does not serve wasm with application/wasm MIME type. Falling back to WebAssembly.instantiate which is slower. Original error:
TypeError: Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'.
サーバーがMIMEタイプ「application/wasm」のwasmを提供していないことが原因のようです。
各サーバーごとにやり方は異なるでしょうが、エックスサーバーであれば、以下を参考にしてMIMEタイプ「application/wasm」、拡張子「.wasm」を追加し、解消しました。