Rust で WebAssembly を作ってみる

WebAssembly を作るため、Rust に入門しました。
とりあえず簡単に作る方法まで、公式が非常に親切なので触っていきます。

簡単な実行と WASM の作成まで試してみました。

注意:後から追記がありますが、今回の用途なら wasm-pack は不要。wasm-bindgen の利用で十分。

参考

実行環境用意

実行環境を Docker で用意していきます。

以下の docker-compose.yml と Dockerfile を用意しました。

docker-compose.yml
1
2
3
4
5
6
7
8
9
10
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
# :cached を付与して、高速化
- .:/app:cached
tty: true
Dockerfile
1
2
3
4
5
FROM rust:latest

# 作業ディレクトリ作成、設定
RUN mkdir /app
WORKDIR /app

起動コマンドは次の通り。

1
2
3
docker-compose build
docker-compose up -d
docker-compose exec app bash

初めての Rust 実行

1
2
3
4
5
6
7
8
9
10
11
# Cargo プロジェクトの作成
$ cargo new test-app
Created binary (application) `test-app` package
$ cd test-app

# 実行
$ cargo run
Compiling test-app v0.1.0 (/app/test-app)
Finished dev [unoptimized + debuginfo] target(s) in 1.40s
Running `target/debug/test-app`
Hello, world!

この時作成されている test-app/src/main.rs の内容は以下の通り。

test-app/src/main.rs
1
2
3
fn main() {
println!("Hello, world!");
}

とりあえず、初めての実行ができました。
現在のディレクトリで、開始するには以下の通りでした。

1
2
3
4
5
6
7
$ cargo init
Created binary (application) package
$ cargo run
Compiling app v0.1.0 (/app)
Finished dev [unoptimized + debuginfo] target(s) in 1.35s
Running `target/debug/app`
Hello, world!

クレートを使ってみる

Rust のではパッケージのことをクレートと呼ぶそうです。
クレートを使ってみます。

crates.ioで、クレートが公開されています。
(ロゴが、そこはかとなくマイクラ感があります。)

今回は、rand を使ってみます。(乱数生成機能が外部ライブラリなんですね)

1
2
3
4
5
6
7
8
9
[package]
name = "test-app"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.0" # <= 追記

追記したらなら、以下コマンドで、インストール。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cargo build
Updating crates.io index
Downloaded cfg-if v1.0.0
Downloaded rand_chacha v0.3.1
Downloaded rand_core v0.6.3
Downloaded ppv-lite86 v0.2.10
Downloaded rand v0.8.4
Downloaded getrandom v0.2.3
Downloaded libc v0.2.98
Downloaded 7 crates (701.8 KB) in 0.78s
Compiling libc v0.2.98
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling getrandom v0.2.3
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.4
Compiling test-app v0.1.0 (/app/test-app)
Finished dev [unoptimized + debuginfo] target(s) in 23.97s

Cargo.lock を見ると、上でインストールをしたパッケージ群について記述されている。

The Rust Rand Book - Getting Startedを参照して、main.rs を修正する。

test-app/src/main.rs
1
2
3
4
5
6
use rand::prelude::*;

fn main() {
let r: u8 = random();
println!("{}", r);
}

実行結果は次の通りです。実行都度乱数を出力できています。

1
2
3
4
5
6
7
8
9
$ cargo run
Compiling test-app v0.1.0 (/app/test-app)
Finished dev [unoptimized + debuginfo] target(s) in 2.79s
Running `target/debug/test-app`
29
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.16s
Running `target/debug/test-app`
1

main.rs は、次の記述もできました。

test-app/src/main.rs
1
2
3
4
5
6
7
use rand::{thread_rng, Rng};

fn main() {
let mut rng = thread_rng();
let r: u8 = rng.gen();
println!("{}", r);
}

公開されているクレートを使用できました。

WASM を作る

wasm-pack のインストール

1
$ cargo install wasm-pack

WASM 用ライブラリの作成

WASM の開発においては、作成するのが、ライブラリを作ればよいようです。
参考サイトの例をもとに、進めます。

1
2
$ cargo new --lib test-wasm
Created library `test-wasm` package

こちらが自動で作成されますが、全く使わないので削除するそうです。

test-wasm/src/lib.rs
1
2
3
4
5
6
7
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

編集後は以下のようにしました。

test-wasm/src/lib.rs[編集後]
1
2
3
4
5
6
7
8
9
10
11
use wasm_bindgen::prelude::*;

// #[wasm_bindgen] を入れることで、
// JSから呼び出し可能なものとして指定できる
#[wasm_bindgen]
pub fn increment(num: i32) -> i32 {
num + 1
// もしくは
// let tmp: i32 = num + 1;
// return tmp;
}

Cargo.toml の編集

サンプルを見るとメールアドレスなどの入力を想定されていますが、自前で使いたいだけなので最小限の記述にしました。

test-wasm/Cargo.toml
1
2
3
4
5
6
7
8
9
10
[package]
edition = "2018"
name = "test-wasm"
version = "0.1.0"

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

[dependencies]
wasm-bindgen = "0.2"

WASM をコンパイル

1
2
# --target は、bundler, nodejs, web, no-modules を選択できるが、一旦サンプルに倣って webで
$ wasm-pack build --target web

すると、pkg/ 以下に型定義ファイルなどがまとめて作成されました。
これら作成されたものの中で test_wasm_bg.wasm が WASM 本体なので、こちらを deno で呼び出してみます。

deno で使ってみる

それでは、wasm を TypeScript で使ってみます。

use_wasm.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const url = new URL("test_wasm_bg.wasm", import.meta.url);

const wasmBinary = await Deno.readFile(url);

const wasmModule = new WebAssembly.Module(wasmBinary);

console.log(wasmModule);

const wasmInstance = new WebAssembly.Instance(wasmModule);

console.log(wasmInstance);

const increment = wasmInstance.exports.increment as CallableFunction;
console.log(increment(1));

実行すると次のようになります。

1
2
3
4
5
6
$ deno run --allow-read use_wasm.ts
Check file:///usr/src/app/use_wasm.ts
WebAssembly.Module {}
WebAssembly.Instance {}
1->2
10->11

WASM で計算した結果を TS 側で扱えました。

WASM 側から JavaScript を呼び出す

ここまで JavaScript -> Rust の方向で呼び出しましたが、逆の Rust -> JavaScript の呼び出しを試します。
src/lib.rs を書き換えます。

test-wasm/src/lib.rs[再編集後]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn increment(num: i32) {
num + 1
}

// extern を入れることで、
// 呼び出すJS関数を定義できる
#[wasm_bindgen]
extern {
// グローバル空間に直接公開されていない関数の場合には、
// #[wasm_bindgen(js_namespace = console)] のように namespace の設定が必要
#[wasm_bindgen(js_namespace = console)]
pub fn log(s: &str);
}

#[wasm_bindgen]
pub fn square(num: i32) -> i32 {
log(&format!("receive [{}]", num));
let tmp: i32 = num * num;
return tmp;
}

改めてコンパイルします。

改めて Deno で使おうとして悩む

use_wasm2.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const url = new URL("test_wasm_bg.wasm", import.meta.url);

const wasmBinary = await Deno.readFile(url);
const wasmModule = new WebAssembly.Module(wasmBinary);

console.log(wasmModule);

const wasmInstance = new WebAssembly.Instance(wasmModule);

console.log(wasmInstance);

const square = wasmInstance.exports.square as CallableFunction;
console.log(`1->${square(1)}`);
console.log(`2->${square(2)}`);
console.log(`10->${square(10)}`);

としたんですが、動作しません。

実は wasm_pack にて、 Rust -> JavaScript の方向の呼び出しの際に、WebAssembly.Instance の第二引数として importObject が必要でした。
MDN の資料など見ると比較的シンプルに記載されているのですが、WASM と一緒に吐き出されている pkg/test_wasm.js を見ると内容が非常に複雑なことがわかります。
数段階変換をしているように見えます。
複雑なので、pkg 以下に作成された以下のファイルをすべて pkg ディレクトリとして deno 側に持ち込みます。

1
2
3
4
5
pkg/package.json
pkg/test_wasm_bg.wasm
pkg/test_wasm_bg.wasm.d.ts
pkg/test_wasm.d.ts
pkg/test_wasm.js
use_wasm2.ts[編集後]
1
2
3
4
5
6
7
import init, { square } from "./pkg/test_wasm.js";

await init();

console.log(`1->${square(1)}`);
console.log(`2->${square(2)}`);
console.log(`10->${square(10)}`);

WASM を使っている感が消失しますね。

実はこれも動作しません。

./pkg/test_wasm.js 内で、WASM ファイルを fetch で取得しようとしているので、エラーになります。
ここで、ビルド方法を思い出して、wasm-pack build --target nodejs とすると、各関数が CommonJS 形式で exports されるようになりました。

そうじゃない、そうじゃないんだ。

ここで、再度調べて、以下のように対応。

コンパイルは、wasm-pack build --target web に再度修正。

pkg/test_wasm.js  を書き換え。

pkg/test_wasm.js
1
2
3
4
5
// 以下を削除
input = fetch(input);

// 次の内容に書き換え
input = new WebAssembly.Module(await Deno.readFile(new URL(input).pathname));

この書き換えを行うと、呼び出し側は以下の内容で記述できました。

use_wasm2.ts[編集後]
1
2
3
4
5
6
import init, { square } from "./pkg/test_wasm.js";

await init();
console.log(`1->${square(1)}`);
console.log(`2->${square(2)}`);
console.log(`10->${square(10)}`);

実行すると次のようになりました。

1
2
3
4
5
6
7
8
$ deno run --allow-read use_wasm2.ts
Check file:///usr/src/app/use_wasm2.ts
receive [1]
1->1
receive [2]
2->4
receive [10]
10->100

とりあえず表示できました。

もう少し頑張ってみる

表面的には、JavaScript の呼び出しにしか見えないコードになってしまったわけですが、改めて直接読み込みを試みます。

use_wasm3.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const url = new URL("test_wasm_bg.wasm", import.meta.url);

const wasmBinary = await Deno.readFile(url);
const wasmModule = new WebAssembly.Module(wasmBinary);

console.log(wasmModule);

const imports = {
wbg: {
__wbg_log_2983b8b52db50312: function (arg: any) {
console.log(arg);
},
},
};

const wasmInstance = new WebAssembly.Instance(wasmModule, imports);

console.log(wasmInstance);

const square = wasmInstance.exports.square as CallableFunction;
console.log(`1->${square(1)}`);
console.log(`2->${square(2)}`);
console.log(`10->${square(10)}`);

test_wasm.js を見ると、Rust で記述した log 関数と、console.log の関連付けについて次のように記述があります。

1
2
3
4
5
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_log_2983b8b52db50312 = function (arg0, arg1) {
console.log(getStringFromWasm0(arg0, arg1));
};

こちらを踏まえて、use_wasm3.ts を以下のようにしています。

use_wasm3.ts
1
2
3
4
5
6
7
const imports = {
wbg: {
__wbg_log_2983b8b52db50312: function (arg: any) {
console.log(arg);
},
},
};

動作させると、次の通りです。

1
2
3
4
5
6
7
8
9
$ deno run --allow-read use_wasm3.ts
WebAssembly.Module {}
WebAssembly.Instance {}
1114120
1->1
1114120
2->4
1114120
10->100

文字列がまともに表示されませんねー。
test_wasm.js の getStringFromWasm0 の関数がポイントでした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let cachedTextDecoder = new TextDecoder("utf-8", {
ignoreBOM: true,
fatal: true,
});

cachedTextDecoder.decode();

let cachegetUint8Memory0 = null;
function getUint8Memory0() {
if (
cachegetUint8Memory0 === null ||
cachegetUint8Memory0.buffer !== wasm.memory.buffer
) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory0;
}

function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}

これを自分で実装するの苦しいので、素直にラップされた js を読み込む方が良さそうですね。
wasm-pack のドキュメントを見ると canvas の取り扱いなどもあり、見れば見るほど自前でやろうとは思わなくなってしまいました。

wasm-pack を使わないで WASM が作れないものだろうか?

さらに調べて見ると、wasm-pack を使わないパターンを見つけたので、試してみます。

1
2
$ cargo new test-wasm-no-pack --lib
$ cd test-wasm-no-pack/

Cargo.toml を編集。

Cargo.toml
1
2
3
4
5
6
7
8
9
10
11
12
[package]
name = "test-wasm-no-pack"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

# [lib] 以下を追記
[lib]
crate-type = ["cdylib", "rlib"]

src/lib.rs を改めて編集し、3 乗する関数を Rust で実装します。

src/lib.rs
1
2
3
4
#[no_mangle]
pub fn two_cubed(num: i32) -> i32 {
num * num * num
}
1
2
3
$ rustup target add wasm32-unknown-unknown
$ cargo build --target wasm32-unknown-unknown
# => target/wasm32-unknown-unknown/debug/test_wasm_no_pack.wasm が作成されます。

Deno で読み込みしてみます。

use_wasm4.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const url = new URL("test_wasm_no_pack.wasm", import.meta.url);

const wasmBinary = await Deno.readFile(url);
const wasmModule = new WebAssembly.Module(wasmBinary);

console.log(wasmModule);

const wasmInstance = new WebAssembly.Instance(wasmModule);

console.log(wasmInstance);

const twoCubed = wasmInstance.exports.two_cubed as CallableFunction;
console.log(`1->${twoCubed(1)}`);
console.log(`2->${twoCubed(2)}`);
console.log(`10->${twoCubed(10)}`);

動作させると、次のようになります。

1
2
3
4
5
6
7
$ deno run --allow-read use_wasm4.ts
Check file:///usr/src/app/use_wasm4.ts
WebAssembly.Module {}
WebAssembly.Instance {}
1->1
2->8
10->1000

引数に基づいて 3 乗の値を返すことができました。
wasmpack を使わずとも wasm のコンパイルと実行ができました。


今回は Rust に入門し、WebAssembly の実装を試してみました。

シンプルに JavaScript から呼び出して結果を受け取るだけであれば、wasm-pack は未使用。
複雑なことをするときには、wasm-pack を使う。
みたいなところが落としどころになりそうです。

Rust は数値だけでもいくつも型があったりと、この感じ C++触ってた頃を思い出しました。

ではでは。

[7/23追記]