Konubinix' opinionated web of thoughts

Trying Rust in Webassembly

Fleeting

rust wasm, wasm in rust

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