This repository provides WASI component APIs for GPIO and CAN, Wasmtime host implementations for Linux and Raspberry Pi, and native/Wasm programs that exercise and benchmark both interfaces. Each host uses a TOML policy to decide which physical devices a component may open.
| Path | Contents |
|---|---|
lib/host/wasmtime-wasi-can |
WASIp2 CAN WIT interfaces and their Wasmtime host implementation |
lib/host/wasmtime-wasi-gpio |
WASIp2 and WASIp3 GPIO WIT interfaces and their Wasmtime host implementations |
lib/host/sysfs-pwm-embedded-hal |
embedded-hal adapter for Linux sysfs PWM |
lib/guest/wasi-embedded-hal |
Guest adapters from the WIT APIs to embedded-hal, embedded-hal-async, and embedded-can |
host |
WASIp2 host using the Linux GPIO character-device API, including async edge waits |
host-rppal |
WASIp2 host using rppal for synchronous Raspberry Pi GPIO access |
host-p3 |
Async Wasmtime host used by the WASIp3 GPIO demo |
gpio-demos |
Native and Wasm GPIO input/output examples |
can-demos |
Native and Wasm blocking/non-blocking CAN examples |
benchmarking |
Native and Wasm GPIO/CAN round-trip-time benchmarks and echo programs |
The entire repository is one Cargo workspace. Its Cargo.toml, Cargo.lock,
Makefile, and deploy.sh are all used from the repository root.
The supplied programs are intended for Raspberry Pi 5 boards running 64-bit Linux. They have not been validated on other systems. The programs assume these device names:
- GPIO output:
GPIO3 - GPIO input:
GPIO4(the async native programs specifically use/dev/gpiochip0, line offset 4) - CAN interface:
can0
To run all examples you need:
- One Raspberry Pi 5 for the standalone demos, or two Raspberry Pi 5 boards for the physical GPIO and CAN round-trip benchmarks.
- A 3.3 V-safe way to drive or observe GPIO3 and GPIO4. Never connect a 5 V signal directly to a Raspberry Pi GPIO.
- For physical CAN, a CAN controller/transceiver for each participating Pi (for example an MCP2515-based board with suitable 3.3 V/5 V level shifting) and correct bus termination.
- For the GPIO benchmark, two boards with common ground. Connect board A GPIO3 through a current-limiting resistor (the test setup uses 523 ohms) to board B GPIO4, and board B GPIO3 through another resistor to board A GPIO4. The echo program runs on one board and the benchmark on the other.
The standalone GPIO demos can instead use an LED or logic analyzer on GPIO3 and a 3.3 V input source on GPIO4. CAN send/receive demos may run in two terminals on one configured host or on two nodes on the same bus.
On the build machine:
- A stable Rust toolchain with Rust 2024 edition support
- The
wasm32-wasip2Rust target crossand its Docker or Podman backend- GNU Make
- SSH and
scpif using the deployment scripts
Install the Rust-specific prerequisites with:
rustup target add wasm32-wasip2
cargo install crossOn each Raspberry Pi 5:
- 64-bit Linux with the GPIO character-device and SocketCAN interfaces
- A driver that creates the required
/dev/gpiochip*and/orcan0device ipfrom iproute2 (and optionallycan-utils) to configure and inspect CAN- Permission to access the GPIO devices and CAN interface
Bring up both physical CAN nodes at the bitrate compiled into the benchmark (125 kbit/s by default):
sudo ip link set can0 down 2>/dev/null || true
sudo ip link set can0 up type can bitrate 125000
ip -details link show can0For local CAN experiments, create a virtual interface instead:
sudo modprobe vcan
sudo ip link add vcan0 type vcan
sudo ip link set vcan0 upThe demos are hard-coded to can0; using vcan0 for them requires changing
that constant and rebuilding. The benchmark suite provides a separate
echo-vcan0 helper.
Run make at the repository root to build every host, demo, and benchmark:
makeThe root Makefile also exposes smaller build targets:
make hosts # host, host-p3, and host-rppal
make host-rppal # one host only; host and host-p3 work the same way
make gpio-demos
make can-demos
make benchmarkingcross produces all AArch64 native binaries under the root
target/cross/aarch64-unknown-linux-gnu/release/ directory. Its separate cache
prevents container-built host tools from conflicting with the local Cargo
cache. Cargo produces all Wasm components under the root
target/wasm32-wasip2/release/ directory.
After building a group, use the root deployment script to copy its release
artifacts to ~/tests/ on one or more SSH targets. The first argument selects
the artifact group; every remaining argument is an SSH target:
./deploy.sh all pi@board-a pi@board-b
./deploy.sh host pi@board-a
./deploy.sh host-rppal pi@board-a
./deploy.sh host-p3 pi@board-a
./deploy.sh gpio-demos pi@board-a pi@board-b
./deploy.sh can-demos pi@board-a pi@board-b
./deploy.sh benchmarking pi@board-a pi@board-bThe all group deploys every host, demo, and benchmark artifact. Policies are
always deployed as host-policy.toml, host-p3-policy.toml, or
host-rppal-policy.toml, including when deploying an individual host.
On every target, create the directory that the host preopens for benchmark output:
mkdir -p ~/tests/results
cd ~/testsAll commands below assume ~/tests is the current directory.
The WASI hosts take exactly two positional arguments:
./host <component.wasm> <policy.toml>
./host-rppal <component.wasm> <policy.toml>
./host-p3 <component.wasm> <policy.toml>
Choose the host according to the component and GPIO operation:
| Executable | Component model | GPIO backend and support |
|---|---|---|
host |
WASIp2 | Linux GPIO character-device backend; synchronous pins and async input waits |
host-rppal |
WASIp2 | Raspberry Pi rppal backend; synchronous input/output pins only |
host-p3 |
WASIp3 | Linux GPIO character-device backend; used for the WASIp3 async-input demo |
All three hosts expose CAN. host-rppal interprets policy pin names such as
GPIO3 as BCM GPIO numbers; the generic hosts look up those names through the
Linux GPIO character-device API. PWM continues to use the Linux sysfs PWM
backend in every host.
The policy maps guest-visible capability names to host devices. A guest that
calls open("can") or open-input("pin4") receives only the resource with that
name; devices omitted from the policy cannot be opened.
The checked-in host/policy.toml and host-p3/policy.toml enable blocking
can0/vcan0, stateful output GPIO3, and async input GPIO4.
host-rppal/policy.toml instead enables synchronous input GPIO4, because the
RPPAL host does not implement input-wait-pin. Deploy the policy belonging to
the selected host and enable only the resource types needed by the component:
[can]
debug = false
[[can.blocking]]
ifname = "can0" # Linux SocketCAN interface
name = "can" # name visible to the component
# Use this entry instead for the non-blocking CAN demos.
# [[can.nonblocking]]
# ifname = "can0"
# name = "can"
[gpio]
debug = false
# Do not expose the same GPIO line through multiple resource entries
# simultaneously.
# Required by wasm-toggle.
# [[gpio.output-pin]]
# pin-name = "GPIO3" # Linux GPIO line name
# name = "pin3" # name visible to the component
# initial-state = "low"
# Required by wasm-toggle-stateful and the GPIO Wasm benchmark.
[[gpio.stateful-output-pin]]
pin-name = "GPIO3"
name = "pin3"
initial-state = "low"
# Required by wasm-read. This is enabled in host-rppal/policy.toml.
# [[gpio.input-pin]]
# pin-name = "GPIO4"
# name = "pin4"
# Required by the async input demos and GPIO Wasm benchmark. Not supported by
# host-rppal.
[[gpio.input-wait-pin]]
pin-name = "GPIO4"
name = "pin4"
# [[gpio.pwm-pin]]
# chip = 0
# channel = 0
# period-ns = 20_000_000
# name = "pwm"initial-state accepts low or high. Setting debug = true preserves more
detailed host-side error text in the WIT other error value; leave it disabled
when details about the underlying device should not be revealed to a guest.
A GPIO line cannot normally be claimed by two resource entries simultaneously.
For example, comment out stateful-output-pin before enabling output-pin for
the same physical line.
The native programs access Linux devices directly. The Wasm programs must run
through the matching host and policy. The table uses host-rppal for
synchronous WASIp2 GPIO operations; host can run those same components when
its corresponding synchronous policy resource is enabled.
| Demo | Policy resource | Command |
|---|---|---|
| Poll GPIO4 once per second (native) | None | ./native-read |
| Report GPIO4 edges (native async) | None | ./native-async-read |
| Poll GPIO4 once per second (WASIp2) | gpio.input-pin named pin4 |
./host-rppal ./wasm-read.wasm ./host-rppal-policy.toml |
| Report GPIO4 edges (WASIp2) | gpio.input-wait-pin named pin4 |
./host ./wasm-async-read.wasm ./host-policy.toml |
| Report GPIO4 edges (WASIp3) | gpio.input-wait-pin named pin4 |
./host-p3 ./wasm-async-read-p3.wasm ./host-p3-policy.toml |
| Toggle GPIO3 every two seconds (native) | None | ./native-toggle |
| Toggle GPIO3 after each Enter press (native) | None | ./native-toggle-stateful |
| Toggle GPIO3 every two seconds (WASIp2) | gpio.output-pin named pin3 |
./host-rppal ./wasm-toggle.wasm ./host-rppal-policy.toml |
| Toggle GPIO3 after each Enter press (WASIp2) | gpio.stateful-output-pin named pin3 |
./host-rppal ./wasm-toggle-stateful.wasm ./host-rppal-policy.toml |
Stop a continuous demo with Ctrl-C.
Start a receiver first, then run the matching sender in another terminal or on
another node. Each sender transmits one standard-ID frame with an eight-byte
payload; receivers continue until interrupted. The table uses host-rppal;
the generic host may be used instead with its matching deployed policy.
| API | Receiver | Sender | Policy for Wasm |
|---|---|---|---|
| Blocking, native | ./blocking-native-recv |
./blocking-native-send |
None |
| Blocking, WASIp2 | ./host-rppal ./blocking-wasm-recv.wasm ./host-rppal-policy.toml |
./host-rppal ./blocking-wasm-send.wasm ./host-rppal-policy.toml |
can.blocking named can |
| Non-blocking, native | ./nb-native-recv |
./nb-native-send |
None |
| Non-blocking, WASIp2 | ./host-rppal ./nb-wasm-recv.wasm ./host-rppal-policy.toml |
./host-rppal ./nb-wasm-send.wasm ./host-rppal-policy.toml |
can.nonblocking named can |
The two Wasm processes may share the same policy file. For the non-blocking pair,
replace the [[can.blocking]] entry with [[can.nonblocking]] before starting
the host processes.
Both benchmark clients perform 50,000 round trips per case, wait 100
microseconds between samples, and write one rtt_ns value per CSV row in
results/. These values are compile-time constants in the benchmark sources.
The CAN benchmark tests payload lengths 1 through 8 at the bitrate recorded by
the BITRATE constant (125 kbit/s by default). It requires an echo process that
receives each frame and sends it back.
On the echo node:
./echo-can0On the benchmark node, run the native and Wasm variants separately:
./can-native-benchmarking
./host-rppal ./can-wasm-benchmarking.wasm ./host-rppal-policy.tomlThe Wasm run needs can.blocking named can; the generic host may be used in
place of host-rppal. Output files are named like
results/native-can0-bitrate125-len8.csv and
results/wasm-can0-bitrate125-len8.csv.
For a virtual CAN echo loop, run ./echo-vcan0. To benchmark vcan0, also
change the native benchmark's INTERFACE and the Wasm benchmark's CAN_NAME,
INTERFACE, and matching policy entry, then rebuild.
Wire the two boards as described in Hardware. On the echo board, run:
./gpio-echoOn the benchmark board, run the native and Wasm variants separately:
./gpio-native-benchmarking
./host ./gpio-wasm-benchmarking.wasm ./host-policy.tomlThe Wasm run needs gpio.stateful-output-pin named pin3 and
gpio.input-wait-pin named pin4, so it requires the generic host rather than
host-rppal. Results are written to
results/native-gpio-rtt.csv and results/wasm-gpio-rtt.csv.
Do not run the native and Wasm benchmark clients concurrently: they claim the same devices and use distinct sequential runs for a meaningful comparison.
See LICENSE.