Rust WebAssembly (wasm) with Webpack on Arch Linux (Rust 1.66)

created
( modified )
@nabbisen

Summary

WebAssembly (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), C++, Golang and Rust (rustlang). In addition, it is different in some ways from what assembly is originally.

You can find “WebAssembly Core Specification” - version 1.0, which was published in W3C 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, Safari and Chrome / Edge (both based on Chromium). (Here is their Roadmap.)

WebAssembly has advantage on speed, efficiency and safety, and so it is expected to work (not alternative to but) along with JavaScript (ECMAScript) to make open web much better.

Well, Rust is a general-purpose programming language whose code can be compiled to WebAssembly. 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

Tutorial

* doas can be replaced with sudo.

Install required packages

Rust

Install Rust with rustup or directly. (This post may help you.)

$ doas pacman -Sy rustup

$ rustup default stable
(Alternatively) Install directly
$ 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().

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

$ doas pacman -Ss wasm

Will be printed as below:

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:

$ doas pacman -Sy wasm-bindgen

The output was:

(...)
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:

$ doas pacman -Sy wasm-pack

The output was:

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

Yarn

This is optional. node tasks are available alternatively.

Well, if you prefer yarn, run:

$ doas pacman -Sy yarn

The output was:

(...)
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:

$ cargo new wasm-example --lib

The output was:

     Created library `wasm-example` package

All generated were:

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

Come in:

$ cd wasm-example

Add dependency to wasm-bindgen

First, edit:

$ nvim Cargo.toml

to add the lines below:

  [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:

$ nvim src/lib.rs

Replace the original with:

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 to wasm.

Note: Code without wasm-bindgen

Besides, the original generated was:

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:

$ cargo build

The output was:

    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:

$ nvim index.js

Write in it:

// 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:

$ nvim package.json

Write in it:

{
  "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:

$ nvim webpack.config.js

Write in it:

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:

$ yarn install

The output was:

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:

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

The output was:

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:

(...)
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 !!

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

The output was:

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

References

Series

Rust WebAssembly
  1. Rust WebAssembly (wasm) with Webpack on Arch Linux (Rust 1.66)

Comments or feedbacks are welcomed and appreciated.