Introduction
This project defines WASI APIs for embedded devices with the aim of providing common language and platform independent runtimes for embedded use, allowing applications to be abstract from platforms, supporting dynamic discovery and hot reloading of applications, and making it easy to design / share / mess with embedded things.
The WASI APIs are intended to span from basic peripheral drivers like SPI and I2C, to more complex functionality like driving LEDs or displays and publishing or subscribing to data. Everything one could need to write unreasonably portable embedded applications.
The project provides an API specification with runtimes to support the execution of this and Hardware Abstraction Layers (HALs) for application development. To get started using embedded-wasm, grab the relevant components and/or check out the Getting Started section.
You might also be interested in the chat and project meta issue.
Components
Specification
The embedded-wasm/spec project provides the witx
API specification as well as helper abstractions for platform implementations and standard tests for HAL/runtime interoperability.
Runtimes
wasm-embedded-rt
An embedded-wasm runtime for execution on linux / macOS / windows, wrapping specific runtime implementations to provide a ready-to-go binary. This supports mocking on all platforms, with physical hardware access only on linux (for now?).
You can install this with cargo install wasm-embedded-rt
or grab a binary from the releases page.
wasm-embedded-rt-wasmtime
A Rust/wasmtime based engine for application use, built into wasm-embedded-rt
with the rt_wasmtime
feature.
Typically you'll want to embed this library in your project, either as a git submodule or by copying out the lib
directory.
wasm-embedded-rt-wasm3
A C/wasm3 based engine designed for embedding, built into wasm-embedded-rt-wasm3
with the rt_wasm3
feature.
Typically you'll want to embed this library in your project, either as a cargo dependency, git submodule or by copying out the relevant directories (note for C use you will also need headers from embedded-wasm/spec
).
Hardware Abstraction Layers (HALs)
Rust
Rust bindings based on embedded-hal.
Add this to your project with cargo add wasm-embedded-hal
.
AssemblyScript
Bindings for AssemblyScript, compiled with asc
.
Add this to your project with npm install --save wasm-embedded-hal
.
Tools
wasm-embedded-cli
WIP: A command line interface for interacting with embedded-wasm capable devices.
Getting Started
To get started using embedded-wasm
you need to find a suitable runtime for your platform, and the appropriate Hardware Abstraction Layer (HAL) to write applications in your language of choice.
If your language or platform isn't supported, check out the porting documentation, and for more information on building/testing embedded-wasm
components, see contributing
For the purposes of this guide, we're going to use the wasm-embedded-rt
the linux runtime on a Raspberry Pi, as it's a common platform with useful physical interfaces, and the rust HAL.
You're going to need rust installed, and may find it useful to set $PROJECT
as an environmental variable while following along.
Please note this is very much a work in progress, expect some hickups / bugs / sharp edges that are yet-to-be resolved
Installing the runtime
First we need to install the runtime. The most straightfoward approach is to fetch a precompiled binary from the releases page.
For aarch64 (64-bit):
wget https://github.com/embedded-wasm/rt/releases/download/v0.1.2/wasm-embedded-rt-aarch64-unknown-linux-gnu.tgz
tar -xvf wasm-embedded-rt-aarch64-unknown-linux-gnu.tgz
sudo cp wasm-embedded-rt /usr/local/bin
For armv7 (32-bit), note this only supports the wasm3
engine:
wget https://github.com/embedded-wasm/rt/releases/download/latest/wasm-embedded-rt-armv7-unknown-linux-gnueabihf.tgz
tar -xvf wasm-embedded-rt-armv7-unknown-linux-gnueabihf.tgz
sudo cp wasm-embedded-rt /usr/local/bin
Alternately you can use:
cargo binstall wasm-embedded-rt
to install the precompiled binary viacargo-binstall
cargo install wasm-embedded-rt
to build from source (note you may need to set features appropriate to your platform)
Once you have this installed you should be able to invoke wasm-embedded-rt
:
> wasm-embedded-rt --help
wasm-embedded-rt 0.1.2
USAGE:
wasm-embedded-rt [OPTIONS] <bin>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
--config <config> Configuration file (toml)
--log-level <log-level> Configure app logging levels (warn, info, debug, trace) [default: info]
--mode <mode> Operating mode [default: dynamic]
--runtime <runtime> Runtime [default: wasmtime]
ARGS:
<bin> WASM binary to execute
Building an application
To get started with the rust HAL you will need to setup a new cargo binary project (where $PROJECT
is your project name). You can do this on the Raspberry Pi, or another machine (though you will need to copy binaries to the RPi).
cargo new --bin $PROJECT && cd $PROJECT
to create the project and change to the new directoryrustup target add wasm32-wasi
to add thewasm32-wasi
targetmkdir .cargo && echo '[build]\r\ntarget = "wasm32-wasi"' > .cargo/config
to set the default build target
- note this can also be set using
forced-target = "wasm32-wasi"
inCargo.toml
cargo add wasm-embedded-hal
to add thewasm-embedded-hal
dependency (viacargo-edit
)cargo build
to build the application
Once you've setup your project you can use the provided APIs to talk to physical peripherals, add the following to your src/main.rs
and build with cargo build
.
//! An I2C detect example using wasm-embedded-hal //! // Copyright 2020 Ryan Kurte use embedded_hal::i2c::blocking::*; use wasm_embedded_hal::i2c::I2c; /// Default I2C bus to poll (must be enabled via `raspi-config`) const BUS: u32 = 1; fn main() { // Connect to I2C device let mut i2c = match I2c::init(BUS, 0, -1, -1) { Ok(v) => v, Err(_e) => return, }; println!("Scanning addresses on bus: {}", BUS); // For each possible address for i in 0..128 { // Print the address every line if i % 16 == 0 { print!("0x{:02x}: ", i); } // Attempt a read let mut d = [0u8; 1]; match i2c.read(i, &mut d) { Ok(_) => print!("{:02x} ", d), Err(_) => print!("-- "), } // Line break every 16 addresses if i % 16 == 15 { print!("\r\n"); } } // Shutdown the I2C device i2c.deinit(); return; }
For more examples check out hal_rs/examples
.
Running your application
If you're working remotely, copy your new binary to the RPi with scp target/debug/$PROJECT.wasm pi@raspberrypi
(replacing raspberrypi
with a different hostname if required).
You can then run your new application on the RPi:
> wasm-embedded-rt --mode linux --log-level error --runtime wasm3 $PROJECT.wasm
Loading WebAssembly (mod: wasme, p: 0x7fb90bb010, 1950651 bytes)...
Initialising I2C device: 1
Received I2C handle: 0
Scanning addresses on bus: 1
0x00: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
0x10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
0x20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
0x30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
0x40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
0x50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
0x60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
0x70: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
Note we're using --runtime wasm3
here due to a bug in the wasmtime
runtime error codes, this will be removed when resolved
References
Resources
- Rust WASM Book
- Wasmtime Guide
- WITX format
- jedisct1/witx-codegen
- WASI WITX specification
- ryankurte/rust-wasm-hal
- ryankurte/esp32-wasm-base
- h1romas4/m5stack-wasm3-testing
- WASM-bindgen
- https://radu-matei.com/blog/practical-guide-to-wasm-memory/
- https://demille.github.io/wasm-ffi/docs/assemblyscript/
Contributing
First make sure you've got the tools installed per Getting Started.
Heads up that there are a lot of moving parts, and it's definitely not a simple process (sorry!). We hope that in future more of this will be generated / automated, but we're waiting for the WITX specification to stabilise before this is likely. If you have any ideas for simplifying the process please do let us know!
Please note these instructions were moved from a prior monorepo version of this project, the concept is the same but the links and components are yet to be updated. You can see the original docs here
Updating the docs
The embedded-wasm/book project contains these docs, please feel free to open a PR!
Proposing an API
So you recon we're missing a useful API? (you're probably right). Before going down the implementation path you may wish to open an issue for discussion.
Once you're ready to implement, there are a few steps to the process. You'll need to be familiar with building rust and C projects, and you'll need to setup a workspace to coordinate these changes between components. If you run into any roadblocks, again, please open an issue or PR!
Setting up your workspace
First you'll need to setup a workspace containing the subprojects, we suggest using bootstrap.sh
which will create an embedded-wasm
directory and check out the subprojects into this.
curl https://embedded-wasm.github.io/book/assets/bootstrap.sh | bash
Updating the specification
- Add a witx specification for the protocol to the
witx
folder (see witx/spi.witx for an example) - Update the list of specs in
lib/api.rs
to tellwiggle
where to find the document - Add an abstract trait to
src/
(see src/spi.rs for an example) - Add an abstract C object to
lib/
(see inc/wasm_embedded/spi.h for an example) - Add a test definition to
tests/
for qualification of runtimes / HALs - Check with
cargo check --all-features
Updating the runtimes
You will first need to implement your API for the each of the underlying rt-wasm3
or rt-wasmtime
engines, then in wasm-embedded-rt
.
Updating rt-wasmtime
TODO: write this
Updating rt-wasm3
The rt-wasm3
C library is designed to simplify porting and embedding. A simple Object Oriented C / VTable style object is defined in the spec for each API, hiding the runtime implementation from the user and supporting dependency injection and other useful testing tricks.
To add an API:
- Create new source and header files for your API
- Add the new source file to CMakeLists.txt to add it to the build
- Add the new header file to build.rs with appropriate allow-listing to support rust binding generation
- Ensure you block generation of driver types from the
spec
package (eg,spi_drv_t
) to avoid generating conflicting incompatible symbols
- Ensure you block generation of driver types from the
- Create C function declarations for the new methods and a container object (vtable-esque) to hold these
- Add m3 calls for each new method, deferring to the container object
- Add a helper function to bind an instance of this API to the wasme runtime (see
WASME_bind_i2c
). - Add C bindings to the rust runtime, see rt/src/wasm3/ for examples.
Explaining all of this is more difficult than showing so, see lib/src/i2c.c and lib/inc/wasme/i2c.h for an example.
When working with the library you can build with make lib
, or use the classic CMake approach from lib/
of:
mkdir build && cd build
to create and switch to a build directorycmake ..
to setup the projectmake
to perform a build
When the runtime is built with --features=wasm3
the ewasm
library will also be included. You can use this instead however, the logs exposed when building under cargo leave a lot to be desired.
Updating rt
- Add a mock implementation to
src/mock/
for mock execution, see src/mock/i2c.rs for an example - Add a linux implementation to
src/linux/
for runtime use, see src/linux/i2c.rs for an example
Updating the HAL (rust)
This HAL exposes the API to rust users, providing an implementation of embedded-hal.
- Create a new source file in hal_rs/src/ for the new API
- Create an API module with
extern
definitions for the WASI interface - Create a wrapper type for the API object, using the handle and
extern
functions, see hal_rs/src/i2c.rs for an example - Update the tests list in
.github/workflows/ci.yml
Updating the HAL (AssemblyScript)
This HAL exposes the API to AssemblyScript users.
- Create a new source file in hal_rs/src/ for the new API
- Create an API module with
extern
definitions for the WASI interface - Create a wrapper type for the API object, using the handle and
extern
functions, see hal_rs/src/i2c.rs for an example - Update the tests list in
.github/workflows/ci.yml
Testing your changes
TODO
Hints
- All APIs use integer handles for each device/peripheral managed by the platform to avoid the need to pass opaque objects
- On initialisation a positive handle should be returned, on error a negative code
- These handles are managed by the runtime and should be closed or will be cleaned-up on exit
- Remember that the WASM runtime has it's own address space
- Function calls with objects will resolve to an integer address that must be translated before access
- If an object contains a pointer you will also need to translate this prior to accessing containing data
- The WASM call ABI is not yet stable / widely supported
- WITX allows multiple returns, in practice this may resolve to an extra argument in the function call (eg.
fn do(a) -> Result<b, c>
becomesfn do(a, &mut b) -> c
in WASM)
- WITX allows multiple returns, in practice this may resolve to an extra argument in the function call (eg.
- A bug with
wiggle
meanswitx
path resolution breaks when using workspaces, resulting in anerror: proc macro panicked
and a bunch oferror[E0432]: unresolved import
s. Patchwasm-embedded-spec
using a folder outside the workspace or a git version until this is resolved
APIs
This project provides a set of platform APIs to support embedded applications, designed to be platform, language, and runtime, independent. APIs are designed to be simple, avoiding the transfer of complex objects over the WASM boundary and leaving the construction and management of objects to the runtime and library.
Runtime abstractions and libraries mean that most users should not need to interact with these directly so, unless you're planning to implement a runtime or library you may choose to skip this section.
For more information (and the actual specifications), see embedded-wasm/spec.
Low Level APIs
High Level APIs
I2C API
Specification
The I2C API specification is defined in spec/i2c.witx:
TODO: demonstrate here
SPI API
Specification
The SPI API specification is defined in spec/spi.witx:
TODO: describe / demonstrate here
UART
GPIO
LED
Display
Pub/Sub
HALs
TODO: document + link to https://github.com/embedded-wasm/hal_rs
TODO: document + link to https://github.com/embedded-wasm/hal_as
Runtime
Wasm3
Wasmtime
Management APIs
A set of management APIs are defined to support consistent interaction with wasm-embedded capable devices.
Discovery
Network devices SHOULD support discovery via mDNS with the service type _ewasm
.
Configuration
Configuration values
Execution
Logging
Logging is based on the syslog protocol with either a UDP or WebSocket connection.