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
- OS: Artix Linux (based on Arch Linux)
- App: Rust 1.66.0
- Webassembly: wasm-bindgen 0.2.83 / wasm-pack 0.10.3
- Node.js: 19.3
- Webpack: 4.46.0
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
(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 ☺