WebAssembly を作るため、Rust に入門しました。
とりあえず簡単に作る方法まで、公式が非常に親切なので触っていきます。
簡単な実行と WASM の作成まで試してみました。
注意:後から追記がありますが、今回の用途なら wasm-pack は不要。wasm-bindgen の利用で十分。
参考
実行環境用意
実行環境を Docker で用意していきます。
以下の docker-compose.yml と Dockerfile を用意しました。
docker-compose.yml1 2 3 4 5 6 7 8 9 10
| version: "3" services: app: build: context: . dockerfile: Dockerfile volumes: - .:/app:cached tty: true
|
Dockerfile1 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 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.rs1 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"
[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.rs1 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.rs1 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.rs1 2 3 4 5 6 7 8
| #[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] pub fn increment(num: i32) -> i32 { num + 1 }
|
Cargo.toml の編集
サンプルを見るとメールアドレスなどの入力を想定されていますが、自前で使いたいだけなので最小限の記述にしました。
test-wasm/Cargo.toml1 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
| $ wasm-pack build --target web
|
すると、pkg/ 以下に型定義ファイルなどがまとめて作成されました。
これら作成されたものの中で test_wasm_bg.wasm
が WASM 本体なので、こちらを deno で呼び出してみます。
deno で使ってみる
それでは、wasm を TypeScript で使ってみます。
use_wasm.ts1 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 }
#[wasm_bindgen] extern { #[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.ts1 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.js1 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.ts1 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.ts1 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.toml1 2 3 4 5 6 7 8 9 10 11 12
| [package] name = "test-wasm-no-pack" version = "0.1.0" edition = "2018"
[dependencies]
[lib] crate-type = ["cdylib", "rlib"]
|
src/lib.rs を改めて編集し、3 乗する関数を Rust で実装します。
src/lib.rs1 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
|
Deno で読み込みしてみます。
use_wasm4.ts1 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追記]