はじめに
WebAssembly (wasm と略されることもあります) は "バイナリの命令形式" です。"仮想のスタックマシン" 上で動作します。コードが直接手で書かれることはありません。代わりに、さまざまなプログラミング言語からコンパイルされます。C 言語、C++、Go 言語 そして Rust (rustlang) などからです。付け加えて言うと、原義のアセンブリとはいくつかの点で異なっています。
"WebAssembly Core Specification" - version 1.0 というものが W3C から 2019 年 12 月 5 日に公開されています。WebAssembly の中心となる標準仕様に関して次のように書かれています:
a safe, portable, low-level code format designed for efficient execution and compact representation.
(私訳: 安全で、携帯性を備えた、低レベルのコード形式。効率的な実行と簡潔な表現を目指して設計されている。)
今日において WebAssembly が登場するのはたいてい Web ブラウザ上です。4 つのモダンなブラウザでサポートされています。具体的には FireFox、Safari と Chrome / Edge (後二者はいずれも Chromium がベース) です。(Roadmap は こちら (英語) です。)
WebAssembly は速度、実行の効率性、安全性の点で優れています。それゆえ JavaScript (ECMAScript) と協同して (置き換えて、ではありません)、オープンな Web の世界をよりすばらしいものにすることが期待されています。
さて Rust です。こちらは汎用プログラミング言語で、コードを WebAssembly にコンパイル することができます。Rust も高速で、効率的で、かつ安全性。さらに開発生産性も高いです。
この記事では Rust のコードから wasm を生成してそれをデプロイする流れを説明します。
環境
- OS: Artix Linux (Arch Linux ベース)
- App: Rust 1.66.0
- Webassembly: wasm-bindgen 0.2.83 / wasm-pack 0.10.3
- Node.js: 19.3
- Webpack: 4.46.0
チュートリアル
必要パッケージのインストール
Rust
Rust をインストールする方法は 2 つあります。rustup を使うか直接インストールするかです。(こちらの記事 が参考になるかもしれません。)
rustup を使う (推奨)
$ doas pacman -Sy rustup
$ rustup default stable
(代わりの方法) 直接インストール
$ doas pacman -Sy rust
wasm-bindgen (+ Node.js)
wasm-bindgen は、Rust から wasm をビルドする上で、"Wasm モジュール - JavaScript 間の高度なやり取りを支援" してくれます。別の言い方をすると、これが無いと console.log()
を呼び出すことすらできません。
community リポジトリで取得できます。見てみましょう:
$ doas pacman -Ss wasm
以下のような内容が出力されるでしょう:
world/rust-wasm 1:1.66.0-1
WebAssembly targets for Rust
galaxy/rustup 1.25.1-2 [installed]
The Rust toolchain installer
extra/rust-wasm 1:1.66.0-1
WebAssembly targets for Rust
community/rustup 1.25.1-2 [installed]
The Rust toolchain installer
community/wasm-bindgen 0.2.83-1
Interoperating JS and Rust code
community/wasm-pack 0.10.3-2
Your favorite rust -> wasm workflow tool!
community/wasmer 3.1.0-2
Universal Binaries Powered by WebAssembly
community/wasmtime 4.0.0-1
Standalone JIT-style runtime for WebAssembly, using Cranelift
上の wasm-bindgen
をインストールします。こちらを実行してください:
$ doas pacman -Sy wasm-bindgen
出力は以下の通りでした:
(...)
Packages (3) c-ares-1.18.1-1 nodejs-19.3.0-1 wasm-bindgen-0.2.83-1
(...)
:: Processing package changes...
(1/3) installing c-ares [#####################################] 100%
(2/3) installing nodejs [#####################################] 100%
Optional dependencies for nodejs
npm: nodejs package manager
(3/3) installing wasm-bindgen [#####################################] 100%
Node.js が一緒に来ていますね。
wasm-pack
WebAssembly のパッケージをビルドして公開するのに使います。以下を実行してインストールしましょう:
$ doas pacman -Sy wasm-pack
出力は以下の通りでした:
(...)
Packages (1) wasm-pack-0.10.3-2
(...)
:: Processing package changes...
(1/1) installing wasm-pack [#####################################] 100%
Yarn
こちらはスキップもできます。node
タスクで代用できます。
yarn
を使いたい場合、以下を実行してください:
$ doas pacman -Sy yarn
出力は以下の通りでした:
(...)
Packages (1) yarn-1.22.19-1
(...)
:: Processing package changes...
(1/1) installing yarn [#####################################] 100%
これで必要なインストールがすべて終わりました !!
cargo の lib プロジェクトを作成
ライブラリとしてプロジェクトを作成します:
$ cargo new wasm-example --lib
出力は以下の通りでした:
Created library `wasm-example` package
実際に生成されたのは以下の内容でした:
├─src
├───lib.rs
└─Cargo.toml
進みましょう:
$ cd wasm-example
wasm-bindgen を依存パッケージに追加
まずこちらを編集します:
$ nvim Cargo.toml
そして以下の行を追加します:
[package]
name = "wasm-example"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+ [lib]
+ crate-type = ["cdylib"]
+
[dependencies]
+ wasm-bindgen = "0.2.83"
JavaScript の関数を wasm-bindgen 経由で呼び出す
次に中心となる src のファイルを編集します:
$ nvim src/lib.rs
もとの内容を以下のように書き換えてください:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
ここで wasm_bindgen は window.alert
を wasm に引き合わせています。
備考: wasm-bindgen を使わない場合のコード
余談ですが、自動生成されたもとの内容は以下の通りでした:
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
wasm-bindgen 無しでも動きますが、機能面で不足を感じることがあるかもしれません。
lib プロジェクトのビルド
下記を実行します:
$ cargo build
出力は以下の通りでした:
Updating crates.io index
(...)
Downloaded wasm-bindgen v0.2.83
(...)
Downloaded 13 crates (742.7 KB) in 0.87s
Compiling proc-macro2 v1.0.49
Compiling quote v1.0.23
Compiling unicode-ident v1.0.6
Compiling syn v1.0.107
Compiling log v0.4.17
Compiling wasm-bindgen-shared v0.2.83
Compiling cfg-if v1.0.0
Compiling bumpalo v3.11.1
Compiling once_cell v1.17.0
Compiling wasm-bindgen v0.2.83
Compiling wasm-bindgen-backend v0.2.83
Compiling wasm-bindgen-macro-support v0.2.83
Compiling wasm-bindgen-macro v0.2.83
Compiling wasm-example v0.1.0 (/(...)/wasm-example)
Finished dev [unoptimized + debuginfo] target(s) in 23.41s
エントリポイントの作成
index.js
を作成してエントリポイントにします:
$ nvim index.js
以下のように書き込みましょう:
// Note that a dynamic `import` statement here is required due to
// webpack/webpack#6615, but in theory `import { greet } from './pkg';`
// will work here one day as well!
const rust = import('./pkg');
rust
.then(m => m.greet('World!'))
.catch(console.error);
ここで greet
が呼ばれていることに注目してください。これは今回作成した関数です。src/lib.rs
で定義しています。
タスクランナーのインストール
ゴールは近くまで来ています。Webpack のための準備をしましょう。
こちらを作成します:
$ nvim package.json
中に次の内容を記述してください:
{
"name": "<your-project-name>",
"version": "<project-version>",
"author": "<author>",
"email": "<email>",
"license": "<your-license>",
"scripts": {
"build": "webpack",
"serve": "webpack-dev-server"
},
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "1.0.1",
"text-encoding": "^0.7.0",
"html-webpack-plugin": "^3.2.0",
"webpack": "^4.29.4",
"webpack-cli": "^3.1.1",
"webpack-dev-server": "^3.1.0"
}
}
さらにこちらを作成します:
$ nvim webpack.config.js
内容を以下のようにします:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
plugins: [
new HtmlWebpackPlugin(),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, ".")
}),
// Have this example work in Edge which doesn't ship `TextEncoder` or
// `TextDecoder` at this time.
new webpack.ProvidePlugin({
TextDecoder: ['text-encoding', 'TextDecoder'],
TextEncoder: ['text-encoding', 'TextEncoder']
})
],
mode: 'development'
};
準備完了です。Webpack をインストールしましょう:
$ yarn install
出力は以下の通りでした:
yarn install v1.22.19
(...)
info No lockfile found.
(...)
[1/4] Resolving packages...
(...)
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 21.75s.
ビルドとデプロイ
ビルドを行い、その内容を公開してみましょう:
$ env NODE_OPTIONS=--openssl-legacy-provider \
yarn build
出力は以下の通りでした:
yarn run v1.22.19
$ webpack
🧐 Checking for wasm-pack...
✅ wasm-pack is installed.
ℹ️ Compiling your crate in development mode...
(...)
✅ Your crate has been correctly compiled
(...)
Version: webpack 4.46.0
(...)
Entrypoint main = index.js
(...)
Done in 1.01s.
成功です。やりました 🙌
トラブルシューティング: yarn build が ssl プロバイダ起因で失敗
yarn build
だけを実行すると (つまり NODE_OPTIONS=--openssl-legacy-provider
を付けないと)、下記のようなエラーが出るかもしれません:
(...)
node:internal/crypto/hash:71
this[kHandle] = new _Hash(algorithm, xofLen);
^
Error: error:0308010C:digital envelope routines::unsupported
at new Hash (node:internal/crypto/hash:71:19)
at Object.createHash (node:crypto:140:10)
at module.exports (/(...)/wasm-example/node_modules/webpack/lib/util/createHash.js:135:53)
at NormalModule._initBuildHash (/(...)/wasm-example/node_modules/webpack/lib/NormalModule.js:417:16)
at handleParseError (/(...)/wasm-example/node_modules/webpack/lib/NormalModule.js:471:10)
at /(...)/wasm-example/node_modules/webpack/lib/NormalModule.js:503:5
at /(...)/wasm-example/node_modules/webpack/lib/NormalModule.js:358:12
at /(...)/wasm-example/node_modules/loader-runner/lib/LoaderRunner.js:373:3
at iterateNormalLoaders (/(...)/wasm-example/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
at Array.<anonymous> (/(...)/wasm-example/node_modules/loader-runner/lib/LoaderRunner.js:205:4)
at Storage.finished (/(...)/wasm-example/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:55:16)
at /(...)/wasm-example/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:91:9
at /(...)/wasm-example/node_modules/graceful-fs/graceful-fs.js:123:16
at FSReqCallback.readFileAfterClose [as oncomplete] (node:internal/fs/read_file_context:68:3) {
opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ],
library: 'digital envelope routines',
reason: 'unsupported',
code: 'ERR_OSSL_EVP_UNSUPPORTED'
}
Node.js v19.3.0
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
これが env NODE_OPTIONS=--openssl-legacy-provider
を付けた理由です。ERR_OSSL_EVP_UNSUPPORTED
関連のエラーを抑制してくれます。
おわりに
それでは wasm が動くか見てみましょう !!
$ env NODE_OPTIONS=--openssl-legacy-provider \
yarn serve
出力は以下の通りでした:
yarn run v1.22.19
$ webpack-dev-server
🧐 Checking for wasm-pack...
✅ wasm-pack is installed.
ℹ️ Compiling your crate in development mode...
ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /(...)/wasm-example
[INFO]: Checking for the Wasm target...
[INFO]: Compiling to Wasm...
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
[WARN]: :-) origin crate has no README
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: :-) Done in 0.11s
[INFO]: :-) Your wasm pkg is ready to publish at /(...)/wasm-example/pkg.
✅ Your crate has been correctly compiled
ℹ 「wdm」: Hash: 192d2af568ea3f4244a1
Version: webpack 4.46.0
Time: 688ms
Built at: 01/07/2023 3:17:27 PM
Asset Size Chunks Chunk Names
0.index.js 623 KiB 0 [emitted]
1.index.js 6.82 KiB 1 [emitted]
446639ea4b6743dab47f.module.wasm 58.7 KiB 1 [emitted] [immutable]
index.html 181 bytes [emitted]
index.js 339 KiB main [emitted] main
Entrypoint main = index.js
(...)
ℹ 「wdm」: Compiled successfully.
http://localhost:8080/
にブラウザでアクセスしてください。迎え入れられるはずです ☺