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

nabbisen - Jan 9 '23 - - Dev Community

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

Use rustup (recommended)
$ doas pacman -Sy rustup

$ rustup default stable
Enter fullscreen mode Exit fullscreen mode
(Alternatively) Install directly
$ doas pacman -Sy rust
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let's install wasm-bindgen above. Run:

$ doas pacman -Sy wasm-bindgen
Enter fullscreen mode Exit fullscreen mode

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%
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The output was:

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

Yarn

This is optional. node tasks are available alternatively.

Well, if you prefer yarn, run:

$ doas pacman -Sy yarn
Enter fullscreen mode Exit fullscreen mode

The output was:

(...)
Packages (1) yarn-1.22.19-1
(...)
:: Processing package changes...
(1/1) installing yarn                                              [#####################################] 100%
Enter fullscreen mode Exit fullscreen mode

Here, all required installation is finished !!

Create a cargo lib project

Run to create a project as library::

$ cargo new wasm-example --lib
Enter fullscreen mode Exit fullscreen mode

The output was:

     Created library `wasm-example` package
Enter fullscreen mode Exit fullscreen mode

All generated were:

├─src
├───lib.rs
└─Cargo.toml
Enter fullscreen mode Exit fullscreen mode

Come in:

$ cd wasm-example
Enter fullscreen mode Exit fullscreen mode

Add dependency to wasm-bindgen

First, edit:

$ nvim Cargo.toml
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Call JavaScript function via wasm-bindgen

Next, edit the core src file:

$ nvim src/lib.rs
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Build the library

Run:

$ cargo build
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Make the entrypoint

Create index.js as the entrypoint:

$ nvim index.js
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Then create:

$ nvim webpack.config.js
Enter fullscreen mode Exit fullscreen mode

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'
};
Enter fullscreen mode Exit fullscreen mode

Ready. Let's install Webpack:

$ yarn install
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Build and deploy

Run to build and publish:

$ env NODE_OPTIONS=--openssl-legacy-provider \
      yarn build
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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

wasm works

References

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .