Rust WebAssembly (wasm) with Webpack on Arch Linux (Rust 1.66)
Tags: rust, webassembly, webpack, archlinux
Published: 2023-01-09 07:13:54

## Summary

[WebAssembly](https://webassembly.org/) (wasm in abbreviation) is "a binary instruction format". It works on "a stack-based virtual machine". It is not manually written as code. Instead, it is compiled from various programming languages such as [C (programming language)](https://www.iso.org/iso-name-and-logo.html), [C++](https://isocpp.org/), [Golang](https://go.dev/) and [Rust](https://www.rust-lang.org/) (rustlang). In addition, it is different in some ways from what assembly is originally.

You can find ["WebAssembly Core Specification" - version 1.0](https://www.w3.org/TR/wasm-core-1/), which was published in [W3C](https://www.w3.org/) on 5 Dec. 2019. It says as to the core WebAssembly standard:

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

Where WebAssembly usually acts nowadays is in web browsers. It is supported by four modern browsers, [FireFox](https://www.mozilla.org/firefox), [Safari](https://www.apple.com/safari/) and [Chrome](https://www.google.com/chrome/) / [Edge](https://www.microsoft.com/edge/) (both based on [Chromium](https://www.chromium.org/)). (Here is their [Roadmap](https://webassembly.org/roadmap/).)

WebAssembly has advantage on speed, efficiency and safety, and so it is expected to work (not alternative to but) along with JavaScript ([ECMAScript](https://www.ecma-international.org/technical-committees/tc39/)) to make open web much better.

Well, Rust is a general-purpose programming language whose code can be [compiled to WebAssembly](https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_wasm). Rust is also fast, efficient and safe. Also productive on development.

This post shows how to implement Rust code to generate wasm and deploy it.

### Environment

- OS: [Artix Linux](https://artixlinux.org/) (based on [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

## Tutorial

\* [`doas`](https://man.openbsd.org/doas) can be replaced with [`sudo`](https://www.sudo.ws/).

### Install required packages

#### Rust

Install Rust with rustup or directly. ([This post](https://scqr.net/en/blog/2023/01/07/rust-on-arch-linux-getting-started/index.html) may help you.)

##### Use rustup (recommended)

```console
$ doas pacman -Sy rustup

$ rustup default stable
```

##### (Alternatively) Install directly

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

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

wasm-bindgen helps us by "facilitating high-level interactions between Wasm modules and JavaScript" in building wasm from Rust. In other words, without it, you can't call even [`console.log()`](https://developer.mozilla.org/en-US/docs/Web/API/Console/log).

You can find it in the community repository. Let's check:

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

Will be printed as below:

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

Let's install `wasm-bindgen` above. Run:

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

The output was:

```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%
```

You would see Node.js come together.

#### wasm-pack

It helps us to build WebAssembly packages and publish them. Run to install:

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

The output was:

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

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

This is optional. `node` tasks are available alternatively.

Well, if you prefer `yarn`, run:

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

The output was:

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

Here, all required installation is finished !!

### Create a cargo lib project

Run to create a project as library::

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

The output was:

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

All generated were:

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

Come in:

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

### Add dependency to wasm-bindgen

First, edit:

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

to add the lines below:

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

### Call JavaScript function via wasm-bindgen

Next, edit the core src file:

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

Replace the original with:

```rust
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}
```

Here, wasm_bindgen brings [`window.alert`](https://developer.mozilla.org/docs/Web/API/Window/alert) to wasm.

#### Note: Code without wasm-bindgen

Besides, the original generated was:

```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);
    }
}
```

It works without wasm-bindgen and, however, possibly less functional.

### Build the library

Run:

```console
$ cargo build
```

The output was:

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

### Make the entrypoint

Create `index.js` as the entrypoint:

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

Write in it:

```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);
```

Here, `greet` is called, which is our custom function defined in `src/lib.rs`.

### Install task runner

The goal is nearby. Prepare for Webpack.

Create:

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

Write in it:

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

Then create:

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

Write in it:

```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'
};
```

Ready. Let's install Webpack:

```console
$ yarn install
```

The output was:

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

#### Build and deploy

Run to build and publish:

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

The output was:

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

Done with success. Yay 🙌

#### Troubleshooting: yarn build failed due to ssl provider

When running only `yarn build` (I mean, without `NODE_OPTIONS=--openssl-legacy-provider`), you might meet the error below:

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

This is why `env NODE_OPTIONS=--openssl-legacy-provider` is necessary. It mitigates the error about `ERR_OSSL_EVP_UNSUPPORTED`.

## Conclusion

Let's see our wasm works !!

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

The output was:

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

Connect to `http://localhost:8080/` with you browser, and you will be welcomed ☺

![wasm works](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/chxd6mqgthsi5e1qs46p.png)

### References

- [Rust 🦀 and WebAssembly 🕸](https://rustwasm.github.io/docs/book/)
- [WebAssembly: How and why](https://blog.logrocket.com/webassembly-how-and-why-559b7f96cd71/)