Rust で WebAssembly (wasm) - Webpack 利用 on Arch Linux (Rust 1.66)
Tags: rust, webassembly, webpack, archlinux
Published: 2023-01-09 08:36:13

## はじめに

[WebAssembly](https://webassembly.org/) (wasm と略されることもあります) は "バイナリの命令形式" です。"仮想のスタックマシン" 上で動作します。コードが直接手で書かれることはありません。代わりに、さまざまなプログラミング言語からコンパイルされます。[C 言語](https://www.iso.org/iso-name-and-logo.html)、[C++](https://isocpp.org/)、[Go 言語](https://go.dev/) そして [Rust](https://www.rust-lang.org/) (rustlang) などからです。付け加えて言うと、原義のアセンブリとはいくつかの点で異なっています。

["WebAssembly Core Specification" - version 1.0](https://www.w3.org/TR/wasm-core-1/) というものが [W3C](https://www.w3.org/) から 2019 年 12 月 5 日に公開されています。WebAssembly の中心となる標準仕様に関して次のように書かれています:

> a safe, portable, low-level code format designed for efficient execution and compact representation.

(私訳: 安全で、携帯性を備えた、低レベルのコード形式。効率的な実行と簡潔な表現を目指して設計されている。)

今日において WebAssembly が登場するのはたいてい Web ブラウザ上です。4 つのモダンなブラウザでサポートされています。具体的には [FireFox](https://www.mozilla.org/firefox)、[Safari](https://www.apple.com/safari/) と [Chrome](https://www.google.com/chrome/) / [Edge](https://www.microsoft.com/edge/) (後二者はいずれも [Chromium](https://www.chromium.org/) がベース) です。(Roadmap は [こちら (英語)](https://webassembly.org/roadmap/) です。)

WebAssembly は速度、実行の効率性、安全性の点で優れています。それゆえ JavaScript ([ECMAScript](https://www.ecma-international.org/technical-committees/tc39/)) と協同して (置き換えて、ではありません)、オープンな Web の世界をよりすばらしいものにすることが期待されています。

さて Rust です。こちらは汎用プログラミング言語で、コードを [WebAssembly にコンパイル](https://developer.mozilla.org/ja-JP/docs/WebAssembly/Rust_to_wasm) することができます。Rust も高速で、効率的で、かつ安全性。さらに開発生産性も高いです。

この記事では Rust のコードから wasm を生成してそれをデプロイする流れを説明します。

### 環境

- OS: [Artix Linux](https://artixlinux.org/) ([Arch Linux](https://archlinux.org/) ベース)
- App: Rust [1.66.0](https://releases.rs/docs/1.66.0/)
- Webassembly: [wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/) 0.2.83 / [wasm-pack](https://rustwasm.github.io/wasm-pack/) 0.10.3
- [Node.js](https://nodejs.org/): [19](https://nodejs.org/en/blog/announcements/v19-release-announce/).3
- [Webpack](https://webpack.js.org/): 4.46.0

## チュートリアル

\* [`doas`](https://man.openbsd.org/doas) の代わりに [`sudo`](https://www.sudo.ws/) を使っても大丈夫です。

### 必要パッケージのインストール

#### Rust

Rust をインストールする方法は 2 つあります。rustup を使うか直接インストールするかです。([こちらの記事](https://scqr.net/ja/blog/2023/01/07/rust-on-arch-linux-getting-started/index.html) が参考になるかもしれません。)

##### rustup を使う (推奨)

```console
$ doas pacman -Sy rustup

$ rustup default stable
```

##### (代わりの方法) 直接インストール

```console
$ doas pacman -Sy rust
```

#### wasm-bindgen (+ Node.js)

wasm-bindgen は、Rust から wasm をビルドする上で、"Wasm モジュール - JavaScript 間の高度なやり取りを支援" してくれます。別の言い方をすると、これが無いと [`console.log()`](https://developer.mozilla.org/en-US/docs/Web/API/Console/log) を呼び出すことすらできません。

community リポジトリで取得できます。見てみましょう:

```console
$ doas pacman -Ss wasm
```

以下のような内容が出力されるでしょう:

```console
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` をインストールします。こちらを実行してください:

```console
$ doas pacman -Sy wasm-bindgen
```

出力は以下の通りでした:

```console
(...)
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 のパッケージをビルドして公開するのに使います。以下を実行してインストールしましょう:

```console
$ doas pacman -Sy wasm-pack
```

出力は以下の通りでした:

```console
(...)
Packages (1) wasm-pack-0.10.3-2
(...)
:: Processing package changes...
(1/1) installing wasm-pack                                         [#####################################] 100%
```

#### [Yarn](https://yarnpkg.com/)

こちらはスキップもできます。`node` タスクで代用できます。

`yarn` を使いたい場合、以下を実行してください:

```console
$ doas pacman -Sy yarn
```

出力は以下の通りでした:

```console
(...)
Packages (1) yarn-1.22.19-1
(...)
:: Processing package changes...
(1/1) installing yarn                                              [#####################################] 100%
```

これで必要なインストールがすべて終わりました !!

### cargo の lib プロジェクトを作成

ライブラリとしてプロジェクトを作成します:

```console
$ cargo new wasm-example --lib
```

出力は以下の通りでした:

```console
     Created library `wasm-example` package
```

実際に生成されたのは以下の内容でした:

```
├─src
├───lib.rs
└─Cargo.toml
```

進みましょう:

```console
$ cd wasm-example
```

### wasm-bindgen を依存パッケージに追加

まずこちらを編集します:

```console
$ nvim Cargo.toml
```

そして以下の行を追加します:

```diff
  [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 のファイルを編集します:

```console
$ nvim src/lib.rs
```

もとの内容を以下のように書き換えてください:

```rust
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`](https://developer.mozilla.org/docs/Web/API/Window/alert) を wasm に引き合わせています。

#### 備考: wasm-bindgen を使わない場合のコード

余談ですが、自動生成されたもとの内容は以下の通りでした:

```rust
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 プロジェクトのビルド

下記を実行します:

```console
$ cargo build
```

出力は以下の通りでした:

```console
    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` を作成してエントリポイントにします:

```console
$ nvim index.js
```

以下のように書き込みましょう:

```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 のための準備をしましょう。

こちらを作成します:

```console
$ nvim package.json
```

中に次の内容を記述してください:

```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"
  }
}
```

さらにこちらを作成します:

```console
$ nvim webpack.config.js
```

内容を以下のようにします:

```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 をインストールしましょう:

```console
$ yarn install
```

出力は以下の通りでした:

```console
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.
```

#### ビルドとデプロイ

ビルドを行い、その内容を公開してみましょう:

```console
$ env NODE_OPTIONS=--openssl-legacy-provider \
      yarn build
```

出力は以下の通りでした:

```console
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` を付けないと)、下記のようなエラーが出るかもしれません:

```console
(...)
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 が動くか見てみましょう !!

```console
$ env NODE_OPTIONS=--openssl-legacy-provider \
      yarn serve
```

出力は以下の通りでした:

```console
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/` にブラウザでアクセスしてください。迎え入れられるはずです ☺

![wasm が正常に動作](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/chxd6mqgthsi5e1qs46p.png)

### 参考

- [Rust 🦀 and WebAssembly 🕸 (英語)](https://rustwasm.github.io/docs/book/)
- [WebAssembly: 「なぜ」と「どうやって」 [翻訳記事]](https://dev.to/nabbisen/webassembly--3385)