Introduction

This project defines WASI APIs for embedded devices with the aim of providing a common language and platform independent runtime for embedded use, simplifying targetting a variety of embedded 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 IoT applications.

Core to the project are witx specifications describing these APIS, alongside runtime bindings providing the APIs and HAL libraries to expose these to consumers in different languages. In the future it is expected that we'll be able to generate runtime and language bindings using these specifications, however, as witx has not yet standardised we're writing boilerplate for now.

To get started using embedded-wasm, check out the Getting Started section.

Project Layout

  • spec/ contains API specifications using witx and other mechanisms
  • hal_rs/ contains wasm-embedded-hal, an rust embedded-hal implementation using these APIs, including test applications
  • hal_as/ contains wasm-embedded-hal, an AssemblyScript implementation using these APIs, including test applications
  • rt/ contains wasm-embedded-rt, a rust/wasmtime based runtime for linux, including mocking and wasm-embedded-lib bindings for development purposes.
  • lib/ contains [wasm-embedded-lib], a c/wasm3 based runtime for embedding in C projects
  • tests/ contains a set of test specifications for end-to-end testing of HALs and runtimes
  • docs/ contains these docs

Getting Started

To get started using embedded-wasm you need to find a suitable platform or runtime and the appropriate library for your language of choice.

If your language or platform isn't supported, check out the porting documentation. For more information on building/testing embedded-wasm components, see contributing

Runtimes:

wasm-embedded-rt

A rust wasmtime (or wasm3) based runtime for execution on linux / macOS / windows. Supports mocking on all platforms, with physical hardware access only on linux (for now?).

Crates.io Docs.rs

You can install this with cargo install wasm-embedded-rt or grab a binary from the releases page.

wasm-embedded-lib

A C/wasm3 based runtime designed for embedding, see the Library section for usage.

Crates.io Docs.rs

Typically you'll want to embed this library in your project, either as a git submodule or by copying out the lib directory.

Language Bindings / HALs

Rust

Rust bindings based on embedded-hal.

Crates.io Docs.rs

AssemblyScript

Bindings for AssemblyScript, compiled with asc.

npm

Tools

wasm-embedded-cli

A command line interface for interacting with embedded-wasm capable devices.

Crates.io Docs.rs

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!

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, if you run into any roadblocks please open an issue!

Adding the specification

  • Add a witx specification for the protocol to the specs

Updating the runtime (rust/wasmtime)

The rust runtime uses wiggle to automatically generate traits for binding. We provide an abstraction using these traits to hide the WASM implementation details, then implement these abstractions for viable platforms.

  • Update the list of specs in rt/src/api/mod.rs
  • Create a new API wrapper in rt/src/api/ to translate from wiggle generated traits to basic rust types, see rt/src/api/i2c_api.rs for an example
  • Add a mock implementation to rt/src/mock/ for mock execution, see rt/src/mock/i2c.rs for an example
  • Add a linux implementation to rt/src/linux/ for runtime use, see rt/src/linux/i2c.rs for an example

Because this is (currently) the only auto-generated component, we treat this runtime as the source-of-truth for testing other components.

To build/test the runtime you can run make runtime, or change into the rt directory and use cargo check or cargo watch -x check.

Updating the library (C/wasm3)

The wasme C library is designed to simplify porting and embedding. A simple Object Oriented C / VTable style object is provided for each API, hiding the internal wasm3 implementation from the user and supporting dependency injection and other useful testing tricks.

To add an API:

  • Create new source and header files in lib/src/ and lib/inc/wasme/
  • Add the new source file to lib/CMakeLists.txt to add it to the build
  • Add the new header file to lib/build.rs with appropriate allow-listing to support rust binding generation
  • 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 directory
  • cmake .. to setup the project
  • make 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 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

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

Updating the tests

Because there are many points at which the API specification / interpretation / execution can be incorrect, we run tests across both the runtime and library for every API.

  • Create an expectation file for the test in tests/, see tests/i2c.toml for an example
    • This should be named API.toml to work with existing makefiles
  • Create a test application in the Rust HAL to exercise the new API
    • Place the source in hal_rs/tests/
    • Add a [[bin]] section to hal_rs/Cargo.toml to build the test
    • This should be named test-API.rs to work with existing makefiles
  • Create a test application in the AssemblyScript HAL to exercise the new API

You can then build the tests with make tests which invokes a cargo build with the output in target/. Once the runtime has been updated (and built with make runtime) you can execute a test with make test-rt-API to execute this using the runtime (or make test-lib-API to use the runtime with wasm3 if supported).

Build and test commands

The project uses a top-level makefile to help simplify the collection of underlying commands.

Hints

  • All APIs use integer handles for each device/peripheral to avoid passing around opaque objects
    • On initialisation a positive handle should be returned
    • 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> becomes fn do(a, &mut b) -> c in WASM)
  • The makefile re-maps a bunch of generated file paths to approximate using a workspace, this is helpful because workspaces do not support multiple targets

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.

Low Level APIs

High Level APIs

I2C API

Specification

The I2C API specification is defined in spec/i2c.witx:

(use "common.witx")

(module $i2c
  (@interface func (export "init")
    ;; I2C device to init 
    (param $port u32)
    ;; Baud rate
    (param $baud u32)

    (param $sda s32)
    (param $scl s32)

    ;; Returns a device handle or error
    (result $res (expected $dev (error $errno)))
  )

  (@interface func (export "deinit")
    ;; I2C device handle to deinit 
    (param $handle s32)
    ;; Result
    (result $res (expected (error $errno)))
  )

  (@interface func (export "write")
    ;; I2C device handle for write
    (param $handle s32)
    ;; Peripheral address
    (param $addr u16)
    ;; Data to write
    (param $data $rbytes)
    ;; Result
    (result $res (expected (error $errno)))
  )

  (@interface func (export "read")
    ;; I2C device handle for transfer 
    (param $handle s32)
    ;; Peripheral address
    (param $addr u16)
    ;; Buffer to read into
    (param $buff $wbytes)
    ;; Result
    (result $res (expected (error $errno)))
  )

  (@interface func (export "write_read")
    ;; I2C device handle for exec
    (param $handle s32)
    ;; Peripheral address
    (param $addr u16)
    ;; Data to write
    (param $data $rbytes)
    ;; Buffer to read into
    (param $buff $wbytes)
    ;; Result
    (result $res (expected (error $errno)))
  )
)

SPI API

Specification

The SPI API specification is defined in spec/spi.witx:

(use "common.witx")

;;; Operation kinds for transactional SPI
(typename $op_kind
   (enum (@witx tag u32)
    $transfer
    $read
    $write
   )
)

;;; Operation for transactional SPI
(typename $op
   (record
    (field $kind $op_kind)
    (field $ptr (@witx pointer u8))
    (field $len u32)
   )
)

(typename $op_array (list $op))

(typename $cfg
  (record
    (field $freq u32)
    (field $mosi s32)
    (field $miso s32)
    (field $sck  s32)
    (field $cs   s32)
  )
)

(module $spi
  (@interface func (export "init")
    ;; SPI device to init 
    (param $port u32)
    ;; Baud rate
    (param $baud u32)

    (param $mosi s32)
    (param $miso s32)
    (param $sck  s32)
    (param $cs   s32)
    ;; Returns a device handle or error
    (result $res (expected $dev (error $errno)))
  )

  (@interface func (export "deinit")
    ;; SPI device to deinit 
    (param $handle s32)
    ;; Result
    (result $res (expected (error $errno)))
  )

  (@interface func (export "write")
    ;; SPI device for write
    (param $handle s32)
    ;; Data to write
    (param $data $wbytes)
    ;; Result
    (result $res (expected (error $errno)))
  )

  (@interface func (export "transfer")
    ;; SPI device for transfer 
    (param $handle s32)
    ;; Data to transfer (write and read)
    (param $data $rbytes)
    ;; Result
    (result $res (expected (error $errno)))
  )

  (@interface func (export "exec")
    ;; SPI device for exec
    (param $handle s32)
    ;; List of operations to execute
    (param $data $op_array)
    ;; Result
    (result $res (expected (error $errno)))
  )
)

UART

GPIO

LED

Display

Pub/Sub

HALs

Rust

WASM Embedded AssemblyScript HAL

testing 1243

Runtime

The runtime provides an implementation of the wasm-embedded APIs using wasmtime (or wasm3 on some platforms), with support for physical devices on linux and mocking on all platforms.

You can install this with cargo install wasm-embedded-rt or grab a binary from the releases page.

TODO: how to use runtime

Library

The library provides a simple API for embedding and using the wasm-embedded runtime, based on WASM3. At it's core this provides a set of functions to setup a runtime and to execute tasks.

For each API a separate module is provided defining a driver that can be attached to the runtime instance.

From lib/int/wasme/wasme.h:

{{#include ../lib/inc/wasme/wasme.h:core_api}}

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.