Trying Rust in Webassembly
FleetingThe outcome I want to achieve is to have a rust library exposing a function that fetches some content on the web that I can use in plain javascript, using node or a browser.
rust already build natively in WASI and wasm (I assume the former does not do what I want, it is more about running a “program” rather than importing a library).
using the wasmtime python library to load the wasm content
pdm init -n
Creating a pyproject.toml for PDM...
Project is initialized successfully
INFO: PDM 2.12.3 is installed, while 2.12.4 is available.
Please run `pipx upgrade pdm` to upgrade.
Run `pdm config check_update false` to disable the check.
pdm add wasmtime
Adding packages to default dependencies: wasmtime
STATUS: Resolving dependencies
STATUS: Resolving: new pin python>=3.11,<3.12
STATUS: Resolving: new pin wasmtime 18.0.0
STATUS: Fetching hashes for resolved packages...
🔒 Lock successful
Changes are written to pyproject.toml.
STATUS: Resolving packages from lockfile...
All packages are synced to date, nothing to do.
🎉 All complete!
INFO: PDM 2.12.3 is installed, while 2.12.4 is available.
Please run `pipx upgrade pdm` to upgrade.
Run `pdm config check_update false` to disable the check.
first step, the add function
Let’s copy the add function from the examples of wasm-bindgen
cd ~/test
# git clone https://github.com/rustwasm/wasm-bindgen
# cp -r ~/test/wasm-bindgen/examples/add add
cd add
trying without wasm-bindgen
Just for the sake of having a reference.
cp src/lib.rs{,-old}
cat<<EOF>src/lib.rs
pub fn add(a: u32, b: u32) -> u32 {
a + b
}
EOF
cargo build --target wasm32-unknown-unknown
Compiling add v0.1.0 (/home/sam/test/add)
Finished dev [unoptimized + debuginfo] target(s) in 0.34s
Let’s see its exported symbols
wasm-objdump -x ./target/wasm32-unknown-unknown/debug/add.wasm |gi -A3 export
Export[3]:
- memory[0] -> "memory"
- global[1] -> "__data_end"
- global[2] -> "__heap_base"
There is no add exported symbol. So there is no use of going further.
trying with wasi
cargo build --target wasm32-wasi
Compiling wasm-bindgen v0.2.91
Compiling cfg-if v1.0.0
Compiling add v0.1.0 (/home/sam/test/add)
Finished dev [unoptimized + debuginfo] target(s) in 0.96s
Let’s see its export symbols
wasm-objdump -x ./target/wasm32-wasi/debug/add.wasm |gi -A3 export
Export[1]:
- memory[0] -> "memory"
Code[2]:
- func[0] size=2 <dummy>
No more luck
putting back the old content
let’s get the wasm bindgen example original content
# mv src/lib.rs{-old,}
cat src/lib.rs
trying with a raw build
cargo build --target wasm32-unknown-unknown
Compiling add v0.1.0 (/home/sam/test/add)
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
Let’s see if it contains the appropriate symbols
wasm-objdump -x ./target/wasm32-unknown-unknown/debug/add.wasm |gi -A3 export
Export[13]:
- memory[0] -> "memory"
- func[9] <add> -> "add"
- func[10] <__wbindgen_describe_add> -> "__wbindgen_describe_add"
Great, let’s use it.
pushd "./target/wasm32-unknown-unknown/debug/" > /dev/null
{
pdm run python -c 'import wasmtime.loader;import add;print(add.add(1, 2))'
}
popd > /dev/null
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/sam/.local/pipx/venvs/clk/lib/python3.11/site-packages/wasmtime/loader.py", line 73, in exec_module
imported_module = importlib.import_module(module_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ModuleNotFoundError: No module named '__wbindgen_placeholder__'
It does not work out of the box
using wasm-bindgen
the code built using wasm-bindgen is meant to be used with some glue code.
cargo build --target wasm32-unknown-unknown
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
wasm-bindgen --out-dir pkg --target web ./target/wasm32-unknown-unknown/debug/add.wasm
It creates a new wasm file, much smaller.
wasm-objdump -x ./pkg/add_bg.wasm |gi -A3 export
Export[1]:
- memory[0] -> "memory"
Custom:
- name: "producers"
This one does not export the add function anymore. That’s strange.
cargo build --target wasm32-unknown-unknown --release
wasm-bindgen --out-dir pkg --target web ./target/wasm32-unknown-unknown/release/add.wasm
wasm-objdump -x ./pkg/add_bg.wasm |gi -A3 export
Finished release [optimized] target(s) in 0.03s
Export[2]:
- memory[0] -> "memory"
- func[0] <add> -> "add"
Code[1]:
Hmm. It looks like it works only in release mode.
Also, because it provides some js glue code, it can only be run with a js host.
Worse than that, it provides several targets depending on the js host it runs into. The “build once, run everywhere” moto is not yet here.
testing with deno
wasm-bindgen --out-dir pkg --target deno ./target/wasm32-unknown-unknown/release/add.wasm
import {add} from "./pkg/add.js"
console.log(add(1, 2))
trying the nodejs target
It actually uses CommonJS export.
wasm-bindgen --out-dir pkg --target nodejs ./target/wasm32-unknown-unknown/release/add.wasm
wasm = require("/home/sam/test/add/pkg/add.js")
return wasm.add(3, 2)
5
trying the web target
esm
rm -rf pkg
wasm-bindgen --out-dir pkg --target web ./target/wasm32-unknown-unknown/release/add.wasm
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://konubinix.eu/ipfs/bafybeihp5kzlgqt56dmy5l4z7kpymfc4kn3fnehrrtr7cid7cn7ra36yha?orig=https://cdn.tailwindcss.com/3.4.3"></script>
<script defer type="module">
import init, { add } from './add.js';
await init()
window.rust = {
add: add
}
import alpinejs from 'https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/+esm'
window.Alpine = alpinejs
alpinejs.start();
</script>
</head>
<body>
<body x-data>
<div x-data="{n1: 1, n2: 2}">
N1: <input type="number" x-model="n1"/>
N2: <input type="number" x-model="n2"/>
RES: <span x-text="window.rust ? rust.add(n1, n2) : 'NA'"></span>
</div>
</body>
</html>
does it work in nodejs?
rm -rf pkg
wasm-bindgen --out-dir pkg --target web ./target/wasm32-unknown-unknown/release/add.wasm
mv pkg/add.{,m}js
-
reading the file from the command line
fs = require("fs") async function run() { a = await import("/home/sam/test/add/pkg/add.mjs") wasm = a.initSync(fs.readFileSync("/home/sam/test/add/pkg/add_bg.wasm")) console.log(wasm.add(1, 3)) } run()
4
-
reading the file using fetch
python3 -m http.server 9904 --directory pkg
async function run() { a = await import("/home/sam/test/add/pkg/add.mjs") wasm = await a.default("http://127.0.0.1:9904/add_bg.wasm") console.log(wasm.add(1, 3)) } run()
4
plain script mode (only meant for browser)
rm -rf pkg
wasm-bindgen --out-dir pkg --target no-modules ./target/wasm32-unknown-unknown/release/add.wasm
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://konubinix.eu/ipfs/bafybeihp5kzlgqt56dmy5l4z7kpymfc4kn3fnehrrtr7cid7cn7ra36yha?orig=https://cdn.tailwindcss.com/3.4.3"></script>
<script defer src="https://konubinix.eu/ipfs/bafkreic33rowgvvugzgzajagkuwidfnit2dyqyn465iygfs67agsukk24i?orig=https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="./add.js"></script>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('app', () => ({
message: "",
wasm: null,
async init () {
this.wasm = await wasm_bindgen()
},
}))
})
</script>
</head>
<body>
<body x-data="app">
<div x-data="{n1: 1, n2: 2}">
N1: <input type="number" x-model="n1"/>
N2: <input type="number" x-model="n2"/>
RES: <span x-text="wasm ? wasm.add(n1, n2) : 'na'"></span>
</div>
</body>
</html>
trying wasm-pack
Wasm-pack is supposed to wrap the whole things together to make it easier.
wasm-pack build --target web
[INFO]: 🎯 Checking for the Wasm target...
[INFO]: 🌀 Compiling to Wasm...
Finished release [optimized] target(s) in 0.01s
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨ Done in 0.23s
[INFO]: 📦 Your wasm pkg is ready to publish at /home/sam/test/add/pkg.
wasm-objdump -x ./pkg/add_bg.wasm |gi -A3 export
Export[2]:
- memory[0] -> "memory"
- func[0] <add> -> "add"
Code[1]:
pushd "./pkg" > /dev/null
{
pdm run python -c 'import wasmtime.loader;import add_bg;print(add_bg.add(1, 2))'
}
popd > /dev/null
3
That’s interesting, wasm-pack did something with the wasm file so that I don’t need the wasm-bindgen magic anymore.
adding some web features
Let’s try to do the same thing with the fetch example
The add example worked out of the box in wasmtime using wasm-pack, let’s try it here.
with wasm-pack
cd ~/test
cp -r ~/test/wasm-bindgen/examples/fetch fetch
cd fetch
wasm-pack build --target web
[INFO]: 🎯 Checking for the Wasm target...
[INFO]: 🌀 Compiling to Wasm...
Compiling proc-macro2 v1.0.78
Compiling unicode-ident v1.0.12
Compiling wasm-bindgen-shared v0.2.92
Compiling bumpalo v3.15.3
Compiling log v0.4.21
Compiling once_cell v1.19.0
Compiling wasm-bindgen v0.2.92
Compiling cfg-if v1.0.0
Compiling quote v1.0.35
Compiling syn v2.0.52
Compiling wasm-bindgen-backend v0.2.92
Compiling wasm-bindgen-macro-support v0.2.92
Compiling wasm-bindgen-macro v0.2.92
Compiling js-sys v0.3.69
Compiling web-sys v0.3.69
Compiling wasm-bindgen-futures v0.4.42
Compiling fetch v0.1.0 (/home/sam/test/fetch)
Finished release [optimized] target(s) in 11.61s
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨ Done in 12.18s
[INFO]: 📦 Your wasm pkg is ready to publish at /home/sam/test/fetch/pkg.
wasm-objdump -x ./pkg/fetch_bg.wasm |gi -A3 export
Export[8]:
- memory[0] -> "memory"
- func[70] <run> -> "run"
- func[80] <__wbindgen_malloc> -> "__wbindgen_malloc"
--
- table[0] -> "__wbindgen_export_2"
- func[99] <_dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf541efccf9213a9f> -> "_dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf541efccf9213a9f"
- func[111] <__wbindgen_exn_store> -> "__wbindgen_exn_store"
- func[96] <wasm_bindgen__convert__closures__invoke2_mut__h20266e39809c0e2f> -> "wasm_bindgen__convert__closures__invoke2_mut__h20266e39809c0e2f"
pdm init -n
Creating a pyproject.toml for PDM...
Project is initialized successfully
INFO: PDM 2.12.3 is installed, while 2.12.4 is available.
Please run `pipx upgrade pdm` to upgrade.
Run `pdm config check_update false` to disable the check.
pushd "./pkg" > /dev/null
{
pdm run python -c 'import wasmtime.loader;import fetch_bg;print(fetch_bg.run("rustwasm/wasm-bindgen"))'
}
popd > /dev/null
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/home/sam/.local/pipx/venvs/clk/lib/python3.11/site-packages/wasmtime/loader.py", line 73, in exec_module
imported_module = importlib.import_module(module_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ModuleNotFoundError: No module named 'wbg'
No luck.
back to wasm-bindgen
cargo build --target wasm32-unknown-unknown --release
Finished release [optimized] target(s) in 0.09s
with deno
wasm-bindgen --out-dir pkg --target deno ./target/wasm32-unknown-unknown/release/fetch.wasm
import {run} from "./pkg/fetch.js"
var data = await run("rustwasm/wasm-bindgen")
console.log("The latest commit to the wasm-bindgen %s branch is:", data.name);
console.log("%s, authored by %s <%s>", data.commit.sha, data.commit.commit.author.name, data.commit.commit.author.email);
It works well.
esm
wasm-bindgen --out-dir pkg --target web ./target/wasm32-unknown-unknown/release/fetch.wasm
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://konubinix.eu/ipfs/bafybeihp5kzlgqt56dmy5l4z7kpymfc4kn3fnehrrtr7cid7cn7ra36yha?orig=https://cdn.tailwindcss.com/3.4.3"></script>
<script defer type="module">
import init, { run } from './fetch.js';
await init()
window.wasm = {
run: run
}
import alpinejs from 'https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/+esm'
window.Alpine = alpinejs
alpinejs.start();
</script>
</head>
<body>
<body>
<div x-data="{data: null, async init() {this.data = await wasm.run('rustwasm/wasm-bindgen') }}">
<template x-if="data">
<div>
<span>The latest commit to the wasm-bindgen <span x-text="data.name"></span> branch is:</span>
<span>("<span x-text="data.commit.sha"></span>, authored by <span x-text="data.commit.commit.author.name"></span> <<span x-text="data.commit.commit.author.email"></span>>)</span>
</div>
</template>
</div>
</body>
</html>