RustでWebAssembly体験

Rust

RustのコードをWebAssemblyにコンパイルしてみて、自分なりにわかったことをこのページにまとめます。

※Rust(1.3.0以上推奨)がインストールされていることが前提。
※またブラウザからJavaScriptでWebAssembly(Wasm)を使うことを想定しています。

Rust をインストール - Rustプログラミング言語

参考にしたページ
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

 Androidアプリを作成しました。
 感情用のメモ帳です。

スポンサーリンク
スポンサーリンク

wasm-packとwasm-bindgen

このページではRustをWasmにコンパイルするまでに2つのものを使います。

wasm-pack」というツールと、「wasm-bindgen」というクレートです。

  • wasm-pack
    • npm向けにパッケージ化し、RustのコードをWebAssemblyにコンパイル。
      コンパイル後のファイルサイズを小さく抑えられる。
  • wasm_bindgen
    • RustとJavaScriptの架け橋となるクレート。
      RustからJavaScriptの機能を使いたいとき、またその逆で使用します。

これらがなくてもコンパイルは可能でしょうが、難易度が上がりそうだったため、使うことにしました。

インストール

wasm-packをcargoからインストールします。

cargo install wasm-pack

私の別環境であるWindows10で上記コマンドを実行すると、エラーが出ました。

インストーラーだと問題なくできたので参考までに。
インストーラーのダウンロードページ

wasm-bindgenはこれから作成するパッケージの「Cargo.toml」内、「dependencies」に追記してインストールします。
バージョンは現時点(2023/1)では「0.2」系であれば動きます。

[dependencies]
wasm-bindgen = "0.2"

また「Rust 1.62.0」以降ならcargoaddコマンドでも追加できます。

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"も合わせて加えるといいかもしれません。

Linkage - The Rust Reference

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)の様子は次のようになりました。

アラート画面。「Rustから呼ばれました」
prompt画面。「あなたの名前を教えてください」。「fujino」と入力。
コンソールに「fujino」と「69」。

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」を追加し、解消しました。

MIME設定 | エックスサーバー

タイトルとURLをコピーしました