Rust から WebAssembly にコンパイル
Rust のコードがあれば、それを WebAssembly (Wasm) にコンパイルすることができます。このチュートリアルでは、Rust プロジェクトを WebAssembly にコンパイルし、既存のウェブアプリケーションで使用する方法を示します。
Rust と WebAssembly の用途
Rust と WebAssembly には、主に 2 つの用途があります。
- アプリケーション全体を構築する — ウェブアプリ全体を Rust ベースで構築します。
- アプリケーションの一部を構築する — 既存の JavaScript フロントエンドの内部で Rust を使用します。
今のところ、Rust チームは後者のケースに焦点を当てているので、ここではこれについて説明します。前者の場合、yew
のようなプロジェクトをチェックアウトしてください。
このチュートリアルでは、Rust で npm パッケージを構築するためのツールである wasm-pack
を使用して npm パッケージを構築します。このパッケージには WebAssembly と JavaScript のコードしか含まれていないため、パッケージのユーザーは Rust をインストールする必要がありません。WebAssembly で書かれていることにすら気づかないかもしれません。
Rust 開発環境のセットアップ
環境を整えるために必要なすべてのステップを踏んでみましょう。
Rust のインストール
Install Rust ページに行って指示に従い、Rust をインストールしてください。これによって "rustup" と呼ばれる複数のバージョンの Rust を管理できるようにするツールがインストールされます。既定の設定では、通常の Rust 開発で使いたいであろう最新の安定版 Rust リリースをインストールします。rustup は Rust コンパイラーの rustc
や Rust のパッケージマネージャーの cargo
や Rust の標準ライブラリーの rust-std
やいくつかの助けになるドキュメント — rust-docs
をインストールします。
メモ:
インストール後のメモで、cargo の bin
ディレクトリーをシステムの PATH
に追加する必要があるという点に注意してください。これは自動的に追加されるはずですが、有効にするためにターミナルを再起動する必要があります。
wasm-pack
パッケージをビルドするには、wasm-pack
という追加のツールが必要です。これは npm
向けに正しくパッケージングをすることだけでなく、WebAssembly にコードをコンパイルするのにも役立ちます。ダウンロードしてインストールするには、ターミナルに次のコマンドを入力します。
cargo install wasm-pack
WebAssembly パッケージのビルド
セットアップは以上です。 Rust で新しいパッケージを作りましょう。個人的なプロジェクトを置いておく場所へ移動して以下を実行してください。
$ cargo new --lib hello-wasm
Created library `hello-wasm` project
これにより新たなライブラリーが出発に必要なものすべてと一緒に hello-wasm
という名前のサブディレクトリーに作成されます。
├── Cargo.toml └── src └── lib.rs
まず Cargo.toml
があります。これはビルドを設定するためのファイルです。もし Gemfile
を Bundler から使ったり、package.json
を npm から使ったりしたことがあるなら、なじみがあるでしょう。cargo は両者と似たような動作をします。
次に、 Cargo はいくつかの Rust コードを src/lib.rs
に生成してくれています。
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
このチュートリアルでは、このテストコードはまったく使わないので、消してください。
Rust を書いてみよう
代わりに以下のコードを src/lib.rs
に書き込みましょう。
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
これが Rust プロジェクトの中身です。三つの主要な部分があります。順番に説明しましょう。ここでは高水準な説明を行い、細部は省略します。Rust についてもっと学びたいのであれば、無料のオンラインブック The Rust Programming Language (訳注: 和訳もあります) を確認してください。
wasm-bindgen
を使用して Rust と JavaScript を協調させる
最初の部分は以下のようになっています。
use wasm_bindgen::prelude::*;
ライブラリーは Rust では「クレート」と呼ばれます。
理解できましたか? Cargo がクレートを取り入れるのです。
3 行目にはコードをライブラリーから自分のコードにインポートする use
コマンドがあります。この場合、wasm_bindgen::prelude
モジュールにあるものすべてをインポートしています。これらの機能は次の節で使用します。
次の節に移動する前に、もう少し wasm-bindgen
について話しておいたほうがいいでしょう。
wasm-pack
は 別のツールの wasm-bindgen
を利用して、JavaScript と Rust の型を繋いでいます。wasm-bindgen
によって JavaScript が文字列に関する Rust API を呼び出すことや Rust の関数が JavaScript の例外をキャッチすることができるようになります。
パッケージ内で wasm-bindgen
の機能を使うことになるでしょう。実際、次の節で利用します。
JavaScript 内の外部関数を Rust から呼び出す
次の部分は以下のようになっています。
#[wasm_bindgen]
extern {
pub fn alert(s: &str);
}
#[ ]
の内側は「属性」と呼ばれ、次に来る文を何らかの形で修飾します。この場合、その文は外部で定義された関数を呼び出したいことを Rust に伝える extern
です。属性は「wasm-bindgen はこれらの関数を見つける方法を知っている」ということを意味しています。
3 行目は関数の Rust で書かれたシグニチャです。「alert
関数は s
という名前の引数を一つ取る」ということを意味しています。
お察しの通り、これは JavaScript によって提供される alert
関数です。次の節でこの関数を呼び出します。
JavaScript 関数を呼び出したい時はいつでも、このファイルに追加すれば、wasm-bindgen
があらゆるセットアップの世話をしてくれます。まだすべてに対応している訳ではありませんが、作業をしています。何か見つからないものがあればバグを報告してください。
JavaScript が呼び出せる Rust 関数の作成
最後の部分は以下のコードです。
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
再び #[wasm_bindgen]
属性が目に入ります。この場合、extern
ブロックではなく fn
を改変しています。これは JavaScript がこの Rust 関数を呼び出せるようにしてほしいということを意味します。これは extern
とは逆です。自分が必要とする関数ではなく、外の世界に渡す関数なのです。
この関数は greet
という名前で、引数に (&str
と書かれる) 文字列 name
を一つ取ります。そしてそれは上の extern
ブロックで要求した alert 関数を呼び出します。文字列を結合する format!
マクロに呼び出しを渡します。
format!
マクロはこの場合フォーマット文字列とそこに挿入する変数の二つの引数を取ります。フォーマット文字列は "Hello, {}!"
の部分です。それは変数が補完される {}
を含みます。渡している変数は関数の引数 name
なので、greet("Steve")
と呼び出すと "Hello, Steve!"
が見られるはずです。
これは alert() に渡されるので、この関数を呼び出すと "Hello, Steve!" と書かれたアラートボックスが現れるでしょう。
ライブラリーを書いたので、それをビルドしましょう。
コードを WebAssembly にコンパイルする
コードを正しくコンパイルするには、はじめに Cargo.toml
で設定する必要があります。Cargo.toml
を開き、以下のように中身を変更してください。
[package]
name = "hello-wasm"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
repository = "https://github.com/yourgithubusername/hello-wasm"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
自分自身のリポジトリーを記入し、git
が authors
フィールドに使用するものと同じ情報を使用してください。
追加する大部分は下にあるものです。最初の部分 — [lib]
— は Rust にパッケージの cdylib バージョンをビルドするよう伝えます。何を意味するかはこのチュートリアルでは掘り下げません。もっと知るには、Cargo と Rust Linkage のドキュメンテーションを調べてください。
第二の項は [dependencies]
の項です。ここで Cargo にどのバージョンの wasm-bindgen
に依存させるかを知らせます。今回の場合、バージョン 0.2.z
のいずれかのものです (0.3.0
やそれ以上ではありません)。
パッケージのビルド
すべてのセットアップが完了したので、ビルドしましょう。ターミナルに以下のものを入力してください。
wasm-pack build --target web
このコマンドは多くのことをします (そして時間がかかます。特に初めて wasm-pack
を実行したときはそうです)。それらについて詳しく学ぶには、Mozilla Hacks のこのブログ投稿を確認してください。手短に説明すると、wasm-pack build
は次のことをします。
- Rust コードを WebAssembly にコンパイルする。
wasm-bindgen
をその WebAssembly に対して実行し、WebAssembly ファイルを npm が理解できるモジュールにラップする JavaScript ファイルを生成する。pkg
ディレクトリーを作成し、その JavaScript ファイルと WebAssembly コードをそこに移動する。Cargo.toml
を読み、等価なpackage.json
を生成する。- (もし存在するなら)
README.md
をパッケージにコピーする。
最終的な結果は? npm パッケージが pkg
ディレクトリーに生成されます。
コードサイズについての余談
生成された WebAssembly のコードサイズについて確認すると、それはおそらく数百キロバイトでしょう。Rust にはサイズの最適化をまったく指示しておらず、最適化すればサイズを大幅に削減できます。これはこのチュートリアルの脱線ですが、もしもっと学習したいなら、Rust WebAssembly Working Group の.wasm のサイズの縮小を確認してください。
パッケージのウェブでの利用
さて、コンパイルされた Wasm モジュールが入手できたので、ブラウザーで動かしてみましょう。
まず index.html
というファイルをプロジェクトのルートに作成するところから始めましょう。最終的には以下のようなプロジェクト構造になります。
├── Cargo.lock ├── Cargo.toml ├── index.html ├── pkg │ ├── hello_wasm.d.ts │ ├── hello_wasm.js │ ├── hello_wasm_bg.wasm │ ├── hello_wasm_bg.wasm.d.ts │ └── package.json ├── src │ └── lib.rs └── target ├── CACHEDIR.TAG ├── release └── wasm32-unknown-unknown
index.html
ファイルの内容は以下のようなものです。
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>hello-wasm example</title>
</head>
<body>
<script type="module">
import init, { greet } from "./pkg/hello_wasm.js";
init().then(() => {
greet("WebAssembly");
});
</script>
</body>
</html>
このファイルのスクリプトは、js グルーコードをインポートし、Wasm モジュールを初期化し、rust で書いた greet
関数を呼び出します。
プロジェクトのルートディレクトリーに、ローカルのウェブサーバーを用意します(例: python3 -m http.server
)。やり方がよくわからない場合は、シンプルなローカル HTTP サーバーの実行を参考にしてください。
メモ:
必ず application/wasm
という MIME 型に対応している最新のウェブサーバーを使用してください。古いウェブサーバーでは、まだ対応していないかもしれません。
ウェブサーバーから index.html
を読み込む (Python3 の例を使用する場合: http://localhost:8000
)。 画面上にアラートボックスが現れ、 Hello, WebAssembly!
と表示されます。 JavaScript から Rust へ、そして Rust から JavaScript への呼び出しに成功しました。
npm でパッケージが利用できるようにする
WebAssembly モジュールを npm で使用する場合、いくつかの変更が必要です。
まず、Rust を target bundler オプションで再コンパイルすることから始めましょう。
wasm-pack build --target bundler
Node.js と npm のインストール
npmパッケージを構築しているので、Node.jsと npm をインストールしておく必要があります。
Node.js と npm を入手するには、 Get npm! ページへ移動して指示に従ってください。 このチュートリアルでは node 16 をターゲットとしていますので、node のバージョンを切り替えたい場合は、nvm を使用してください。
次に、インストールした他の JavaScript パッケージがこのパッケージを利用できるようにするために、npm link
を使用しましょう。
cd pkg
npm link
Rust で書かれ、 WebAssembly にコンパイルされた npm パッケージができました。JavaScript から利用する準備ができており、ユーザーが Rust をインストールすることを必要としません。コードに含まれているのは WebAssembly コードであり、Rust のソースではないのです。
パッケージのウェブでの利用
この新たなパッケージを利用するウェブサイトを構築しましょう。多くの人が様々なバンドラーツールで npm のパッケージを利用していますが、このチュートリアルではそのうちの一つである webpack
を使用します。これは若干複雑ですが、現実的なユースケースを示します。
pkg
ディレクトリーの外に戻り、新たなディレクトリー site
を作成し、そこでこれを試してみましょう。
cd ..
mkdir site
cd site
npm link hello-wasm
新しいファイル package.json
を作成し、次のコードをそこに書き込んでください。
{
"scripts": {
"serve": "webpack-dev-server"
},
"dependencies": {
"hello-wasm": "^0.1.0"
},
"devDependencies": {
"webpack": "^4.25.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10"
}
}
次に、Webpack を設定する必要があります。webpack.config.js
を作成し、そこに次のことを記入してください。
const path = require("path");
module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "index.js",
},
mode: "development",
};
次に、HTML ファイルで参照される index.js
を作成し、以下の内容を追加してください。
import("./node_modules/hello-wasm/hello_wasm.js").then((js) => {
js.greet("WebAssembly with npm");
});
これは新しいモジュールを node_modules
フォルダーからインポートします。これは最善の方法ではないと思いますが、デモなので、これでいいでしょう。一度そのモジュールが読み込まれると、そこから greet
関数を呼び出し、"WebAssembly"
を文字列として渡します。ここに特別なことはなにもありませんが、Rust コードを呼び出していることに注意してください。JavaScript コードから観察する限り、これはただの普通のモジュールです。
最後に、JavaScript を読み込むための HTML ファイルを追加します。index.html
ファイルを作成し、以下の内容を追加してください。
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>hello-wasm example</title>
</head>
<body>
<script src="./index.js"></script>
</body>
</html>
hello-wasm/site
ディレクトリーは次のようになります。
├── index.html ├── index.js ├── node_modules │ └── hello-wasm -> ../../pkg ├── package.json └── webpack.config.js
ファイルを作りました。これを試してみましょう。
npm install
npm run serve
これでウェブサーバーが起動します。 http://localhost:8080
を読み込んでください。 Hello, WebAssembly!
と書かれたアラートボックスが画面に出てくるはずです。JavaScript からの Rust の呼び出しと Rust からの JavaScript の呼び出しに成功しました。
おわりに
ここでチュートリアルは終わりです。あなたの役に立ったと思われることを望みます。
この領域にはたくさんの進行中の刺激的な仕事があります。もしそれをもっとよくするのを手伝いたいなら、 the Rust WebAssembly Working Group を確認してください。