社内でwasmを触っているプロジェクトの人と話して、羨ましかったので入門してみる。

wasmとは

ブラウザ上で高速に動作するバイナリファイルのことです。

WebAssemblyとは?何ができるのか?できないことやユースケースをわかりやすく解説 |パーソルクロステクノロジー

主にC++やRustで実装されています。

2023-08-07のwasmの対応状況はこんな感じです。

safariやモバイル版operaを除いて、ほぼほぼ対応していますね。

WebAssembly | Can I use... Support tables for HTML5, CSS3, etc

Cargoプロジェクトを作成する

とにかく実装したかったので、Rustで実装してみます。

公式にチュートリアルが用意してあったので、これをなぞる形でやっていこうと思います。

チュートリアル - Rust and WebAssembly

チュートリアルを実施するに際して、以下の3つのツールが必要なのでインストールしておきます。

私の場合はcargo-generateとwasm-packがなかったのでインストールしておきました。

  • wasm-pack
  • cargo-generate
  • npm

cargo-generateを使用して、以下のgithubリポジトリをベースにパッケージを作成します。

cargo generate --git https://github.com/rustwasm/wasm-pack-template

名前は以下のようにしておきます。

wasm-game-of-life

これでwasmのチュートリアルパッケージが生成されました。

Cargo.tomlの中身は以下のようになっています。

[package]
name = "wasm-game-of-life"
version = "0.1.0"
edition = "2018"

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

[features]
default = ["console_error_panic_hook"]

[dependencies]
wasm-bindgen = "0.2.84"

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }

[dev-dependencies]
wasm-bindgen-test = "0.3.34"

[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"

見てみると、crate-typeというセクションがありますね。

一体なんなのかよくわからなかったので調べてみました。

  • cdylib : 動的なシステムライブラリを生成します。他言語からロードするために使用されます。
  • rlib : “Rust library"を生成します。静的なRustライブラリとも言え、中間生成物として使用されます。

Rust のライブラリ crate_type をまとめてみた #Rust - Qiita

要はFFI(foreign function interface)を使用して他の言語でRustを動かすための設定と言った感じでとりあえず理解しておけばよいでしょう。

ビルドは先程インストールしたwasm-pack CLIを使用して行います。

wasm-pack build

これを実行すると、以下のような処理を実行してくれます。

  • Rust 1.30以上とwasm32-unknown-unknownターゲットをrustupを通してインストールしていることを確認します。
  • cargoを使用してRustのソースをWebAssemblyの.wasmバイナリにコンパイルします。
  • wasm-bindgenを使ってRustで生成したWebAssemblyを使用するためのJavaScript APIを生成します。

Hello, World! - Rust and WebAssembly

本来であれば、ビルドするためにwasm32-unknown-unknownというターゲットが必要なので、rustupで追加して

rustup target add wasm32-unknown-unknown

以下のようにターゲット指定をしてビルドしなければいけないです。

cargo build --target=wasm32-unknown-unknown

他にもwasmファイル生成のためにコマンドを実行する必要がありますが、それらをすべて包括的に行ってくれるコマンドがwasm-pack buildということですね。便利。

ビルドを行うとpkgというディレクトリが出来上がります。

pkg
├── README.md # パッケージと全く同じREADME.md
├── package.json # 動かすのに必要な依存関係や設定が書かれた状態になっている
├── wasm_game_of_life.d.ts # wasm_game_of_life.jsに対するTypeScriptの型チェックに使用する
├── wasm_game_of_life.js # Rustとの間で値のやり取りを行う際に利用する
├── wasm_game_of_life_bg.js # wasm_game_of_life.jsで参照されるファイル
├── wasm_game_of_life_bg.wasm # wasmの本体
└── wasm_game_of_life_bg.wasm.d.ts # wasmのTypeScript型チェックに使用する

ちなみに、これらのファイルの_bgbindgenの略称で、wasm-bindgenを使用して作成されたファイルであることを表しているそうです。

“bg” stands for “bindgen” here, and they’re intended to be internal files generated by wasm-bindgen for your project.

和訳:ここでの「bg」は「bindgen」を表しており、プロジェクト用に wasm-bindgen によって生成される内部ファイルであることを目的としています。

What the suffix meaning (bg) with _bg.js and _bg.d.ts when we built into pkg folder? · Issue #2290 · rustwasm/wasm-bindgen · GitHub

npmパッケージの作成

つづけて、このwasmを静的サイトで動かすためのnpmパッケージを作成します。

npm init wasm-app www

実行すると、wwwというディレクトリが作成されます。

ちなみに、wasm-appコマンドのベースはこの実装↓

GitHub - rustwasm/create-wasm-app: npm init template for consuming rustwasm pkgs

www
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── bootstrap.js # webpackでエントリーモジュールとして使用するjs
├── index.html # メインHTML
├── index.js # メインモジュール。hello-wasm-packから関数を実行
├── package-lock.json
├── package.json # hello-wasm-pack(wasmのhello worldプロジェクト)やwebpackへの依存を持つ
└── webpack.config.js

早速このディレクトリに移動して依存関係のインストールを行いましょう。

% npm install
npm WARN old lockfile 
npm WARN old lockfile The package-lock.json file was created with an old version of npm,
npm WARN old lockfile so supplemental metadata must be fetched from the registry.
npm WARN old lockfile 
npm WARN old lockfile This is a one-time fix-up, please be patient...
npm WARN old lockfile 
npm WARN deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
npm WARN deprecated resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
npm WARN deprecated uuid@3.3.2: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
npm WARN deprecated source-map-resolve@0.5.2: See https://github.com/lydell/source-map-resolve#deprecated
npm WARN deprecated ini@1.3.5: Please update to ini >=1.3.6 to avoid a prototype pollution issue
npm WARN deprecated source-map-url@0.4.0: See https://github.com/lydell/source-map-url#deprecated
npm WARN deprecated mkdirp@0.5.1: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)
npm WARN deprecated chokidar@2.1.8: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies
npm WARN deprecated debug@4.1.1: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)
npm WARN deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
npm WARN deprecated querystring@0.2.0: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
npm WARN deprecated debug@3.2.6: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)
npm WARN deprecated debug@3.2.6: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)
npm WARN deprecated debug@3.2.6: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)

added 587 packages, and audited 588 packages in 32s

18 packages are looking for funding
  run `npm fund` for details

38 vulnerabilities (1 low, 4 moderate, 27 high, 6 critical)

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

なんかいっぱいWARNが出ていますが、とりあえずインストールはできたようです。

とりあえず起動してみます。

% npm start  

> create-wasm-app@0.1.0 start
> webpack-dev-server

(node:192465) [DEP0111] DeprecationWarning: Access to process.binding('http_parser') is deprecated.
(Use `node --trace-deprecation ...` to show where the warning was created)
ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /home/username/Project/personal/wasm-game-of-life/www
node:internal/crypto/hash:67
  this[kHandle] = new _Hash(algorithm, xofLen);
                  ^

Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:67:19)
    at Object.createHash (node:crypto:135:10)
    at module.exports (/home/username/Project/personal/wasm-game-of-life/www/node_modules/webpack/lib/util/createHash.js:135:53)
    at NormalModule._initBuildHash (/home/username/Project/personal/wasm-game-of-life/www/node_modules/webpack/lib/NormalModule.js:417:16)
    at handleParseError (/home/username/Project/personal/wasm-game-of-life/www/node_modules/webpack/lib/NormalModule.js:471:10)
    at /home/username/Project/personal/wasm-game-of-life/www/node_modules/webpack/lib/NormalModule.js:503:5
    at /home/username/Project/personal/wasm-game-of-life/www/node_modules/webpack/lib/NormalModule.js:358:12
    at /home/username/Project/personal/wasm-game-of-life/www/node_modules/loader-runner/lib/LoaderRunner.js:373:3
    at iterateNormalLoaders (/home/username/Project/personal/wasm-game-of-life/www/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
    at Array.<anonymous> (/home/username/Project/personal/wasm-game-of-life/www/node_modules/loader-runner/lib/LoaderRunner.js:205:4)
    at Storage.finished (/home/username/Project/personal/wasm-game-of-life/www/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:43:16)
    at /home/username/Project/personal/wasm-game-of-life/www/node_modules/enhanced-resolve/lib/CachedInputFileSystem.js:79:9
    at /home/username/Project/personal/wasm-game-of-life/www/node_modules/graceful-fs/graceful-fs.js:78: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 v17.9.0

なんかエラーがいっぱい出ちゃいました。

調べてみたら、バージョンが原因っぽい?です。

Nodejsのバージョンを上げたら`error:0308010C:digital envelope routines::unsupported`が出てしまう #JavaScript - Qiita

nodenv使っていたので、これで一つメジャーバージョンを下げます。

nodenv install 16.20.1
nodenv local 16.20.1

再度起動してみます。

% npm start           

> create-wasm-app@0.1.0 start
> webpack-dev-server

(node:193763) [DEP0111] DeprecationWarning: Access to process.binding('http_parser') is deprecated.
(Use `node --trace-deprecation ...` to show where the warning was created)
ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /home/username/Project/personal/wasm-game-of-life/www
ℹ 「wdm」: Hash: c2abf06051812e05e0b9
Version: webpack 4.43.0
Time: 666ms
Built at: 2023/08/08 8:59:44
                           Asset       Size  Chunks                         Chunk Names
                  0.bootstrap.js    3.4 KiB       0  [emitted]              
8e8fa9289c240ac706a1.module.wasm  872 bytes       0  [emitted] [immutable]  
                    bootstrap.js    369 KiB    main  [emitted]              main
                      index.html  297 bytes          [emitted]              
Entrypoint main = bootstrap.js
[0] multi (webpack)-dev-server/client?http://localhost:8080 ./bootstrap.js 40 bytes {main} [built]
[./bootstrap.js] 279 bytes {main} [built]
[./index.js] 56 bytes {0} [built]
[./node_modules/ansi-html/index.js] 4.16 KiB {main} [built]
[./node_modules/ansi-regex/index.js] 135 bytes {main} [built]
[./node_modules/hello-wasm-pack/hello_wasm_pack.js] 698 bytes {0} [built]
[./node_modules/strip-ansi/index.js] 161 bytes {main} [built]
[./node_modules/webpack-dev-server/client/index.js?http://localhost:8080] (webpack)-dev-server/client?http://localhost:8080 4.29 KiB {main} [built]
[./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.51 KiB {main} [built]
[./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.53 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/createSocketUrl.js] (webpack)-dev-server/client/utils/createSocketUrl.js 2.91 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/log.js] (webpack)-dev-server/client/utils/log.js 964 bytes {main} [built]
[./node_modules/webpack-dev-server/client/utils/reloadApp.js] (webpack)-dev-server/client/utils/reloadApp.js 1.59 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/sendMessage.js] (webpack)-dev-server/client/utils/sendMessage.js 402 bytes {main} [built]
[./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built]
    + 21 hidden modules
ℹ 「wdm」: Compiled successfully.

うまくいきました。やったね!

ブラウザ側でも表示してみます。

ちゃんと動いていますね。

OpenSSLの問題らしいので、package.jsonのscriptsを以下のように変えてあげても動きます。

{
  "scripts": {
    "build": "NODE_OPTIONS=--openssl-legacy-provider webpack --config webpack.config.js",
    "start": "NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server"
  },
}

はやく対応してほしい…

まとめ

長くなってきたので一旦終わります。

続きはまたすぐ更新すると思うので、乞うご期待!