radu's blog

Writing a simple WASM API layer using interface types and Wasmtime

· Radu Matei

The WebAssembly interface types proposal aims to add a new common set of interface types to the core specification that would describe abstract, higher level values, and the ability to adapt the interface of a module so that different hosts can inter-operate using the higher level types. For a comprehensive explainer of the problems interface types are supposed to solve, Lin Clark has an excellent article on the Mozilla Hacks blog, with a demo of using the same markdown renderer compiled to WebAssembly, and using native strings from languages like Rust, Python, or JavaScript:

The goal is for compiler toolchains to automatically generate interface types when compiling a module, as well as to read interface types and adapt the types needed in the module to the types used by the host. For the purpose of this article, however, you can think of an interface type as a regular interface or trait: it defines shared behavior in an abstract way, without providing an actual implementation for how to achieve that behavior.

In this article, we will manually write an interface type for a simple calculator module, then use Wasmtime tooling to correctly implement that interface type in Rust, link our implementation, and use it in a module that requires a calculator library. The examples used will be purposefully simple, and the goal is to show how to currently use this set of tools.

You can find the complete project on GitHub.

Writing a simple interface type in WITX

The current proposal describes interface types using WITX, an experimental file format based on the WebAssembly Text Format, with added support for module types and annotations. It is how the WASI API is defined, and if you are familiar with .wat files, .witx should seem familiar.

The goal is to have a calculator module with a single function that adds two numbers. Let’s describe this using WITX in a file called calculator.witx:

(use "errno.witx")

;;; Add two integers
(module $calculator
  (@interface func (export "add")
    (param $lh s32)
    (param $rh s32)
    (result $error $errno)
    (result $res s32)
  )
)

errno.witx is another WITX file that describes a custom, user-defined error type returned from the function. You can find its definition in the repository here.

The WITX file defines a calculator module with a single function, add, which takes two 32-bit signed integers and returns a 32-bit signed integer, or an error. Now we can use wiggle, a Rust crate that generates Rust code based on interface types definitions, and get a strongly-typed Rust trait based on the WITX file above:

wiggle::from_witx!({
    witx: ["examples/calculator.witx"],
    ctx: CalculatorCtx,
});

pub struct CalculatorCtx {}

According to the wiggle documentation, the from_witx macro takes a WITX file and generates a set of public Rust modules based on the interface type definition. Specifically, it generates a types module that contains all user-defined types, and one module for each WITX module defined that contains Rust traits that have to be implemented by the structure passed as “context”.

This means there is now a Calculator trait with a single add method that our CalculatorCtx structure has to satisfy:

  ::: src/calculator.rs:6:1
   |
6  | pub struct CalculatorCtx {}
   | ------------------------ method `add` not found for this
   |
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following traits define an item `add`, perhaps you need to implement one of them:
           candidate #1: `calculator::calculator::Calculator`
           candidate #2: `std::ops::Add`

error[E0277]: the trait bound `calculator::CalculatorCtx: calculator::calculator::Calculator` is not satisfied
 --> src/calculator.rs:1:1
  |
1 |   wiggle::from_witx!({
  |   ^------------------
  |   |
  |  _required by `calculator::calculator::Calculator::add`
  | |
2 | |     witx: ["examples/calculator.witx"],
3 | |     ctx: CalculatorCtx,
4 | | });
  | |___^ the trait `calculator::calculator::Calculator` is not implemented for `calculator::CalculatorCtx`

calculator.rs(12, 1): implement the missing item:
fn add(&self, _: i32, _: i32) -> std::result::Result<i32, calculator::types::Errno> { todo!() }

We implement the add method, and at this point, CalculatorCtx can be used as our implementation for the calculator interface.

impl calculator::Calculator for CalculatorCtx {
    fn add(&self, lh: i32, rh: i32) -> Result<i32, types::Errno> {
        Ok(lh + rh)
    }
}

The interface type definition for the add method uses s32, or signed integers. However, s32 is not a fundamental WebAssembly data type (quick reminder that the fundamental data types in WebAssembly are: 𝗂πŸ₯𝟀 | π—‚πŸ¨πŸ¦ | 𝖿πŸ₯𝟀 | π–ΏπŸ¨πŸ¦). This is where the interface types proposal defines the additional integer data types:

In addition to string, the proposal includes the integer types u8, s8, u16, s16, u32, s32, u64, and s64. […] Since values of these types are proper integers, not bit sequences like core wasm i32 and i64 values, there is no additional information needed to interpret their value as a number.

The trait generated by wiggle maps the signed 32-bit integer from the interface types proposal to the Rust signed integer, i32 (not to be confused with the WebAssembly i32 data type, which is not inherently signed or unsigned, [its] interpretation is determined by individual operations). Before actually instantiating the module that will use this implementation, the Rust i32 will be mapped to the WebAssembly i32 data type.

Note that at this point, the Rust implementation above can also be compiled as a standalone WebAssembly module and instantiated separately.

Now that we have an actual implementation that we know satisfies the interface defined in the WITX file, we can use it to instantiate a module that imports that functionality in a file called using_add.wat:

(module
  (import "calculator" "add" (func $calc_add (param i32 i32) (result i32)))

  (func $consume_add (param $lhs i32) (param $rhs i32) (result i32)
    local.get $lhs
    local.get $rhs
    call $calc_add)
  (export "consume_add" (func $consume_add))
)

There is nothing special about the WAT file above: it defines an import for an add function that takes two i32 parameters and returns an i32. (Note the parameters are fundamental WebAssembly types). Then, we call this function later in our module’s implementation.

In order to successfully instantiate the module above, we need to satisfy its imports. This is where our implementation becomes useful - we can use the Wasmtime linker to define the implementation for the add function required by the module by creating an instance of the CalculatorCtx structure defined earlier, and returning the result of its add method.

let mut linker = Linker::new(store);
linker.func("calculator", "add", |x: i32, y: i32| {
    let ctx = calculator::CalculatorCtx {};
    ctx.add(x, y).unwrap()
})?;

linker.instantiate(&module)

Then, we continue to use Wasmtime to instantiate, use stdout and stderr for output and error reporting, and link the current WASI implementation. The result of building the Rust program that instantiates WebAssembly modules using Wasmtime and links our calculator implementation is an executable, wasm-calc, whose complete implementation can be found on GitHub. Now we can use this binary to instantiate any generic WASI module, as well as modules that require a calculator module. Similarly to how Wasmtime works, the arguments are the module to run, the name of the function to execute, followed by its arguments:

Building the project and executing our .wat file, we see that our add implementation is properly linked, the module correctly instantiated, and the consume_add function from the module successfully executed:

$ ./target/debug/wasm-calc examples/using_add.wat consume_add 1 2
3

For simplicity, the example above is written in the WebAssembly text format - but in a real scenario, something similar to that would be generated by a compiler. If compiled using a WebAssembly target (wasm32-unknown-unknown or wasm32-wasi), the following Rust program generates an equivalent module:

#[link(wasm_import_module = "calculator")]
extern "C" {
    fn add(lh: i32, rh: i32) -> i32;
}

#[no_mangle]
pub unsafe extern "C" fn consume_add(lh: i32, rh: i32) -> i32 {
    add(lh, rh)
}

For a guide on manually linking WASI imports in Rust, check out @kubkon’s article.

For an example of using .NET to instantiate WebAssembly modules using Wasmtime, check out @peterhuene’s article on the Mozilla Hacks blog.

While the semantics are different, functionally, the same thing happens: we declare the signature of an external function that we expect from a module called calculator, and we use that function later in the program, and we can invoke the consume_add function in the same way from our wasm-calc binary:

$ ./target/debug/wasm-calc examples/using_add/target/wasm32-wasi/debug/using_add.wasm consume_add 1 99
100

Conclusion

The big advantage here is that if either the interface type definition, or the implementation changes, we get a Rust compilation error because the calculator::Calculator trait is no longer satisfied by CalculatorCtx, ensuring that the interface and implementation are always in sync.

This doesn’t mean, that the experience cannot be improved - for example determining if an interface type definition satisfies the imports for instantiating a given module, compiler support for generating interface types, or automatically linking all the exports required by a module given a list of dependent modules (the wig crate does something similar for WASI imports).

In this article we explored a very narrow use case by manually writing an interface type file. But as more and more compiler toolchains start supporting WebAssembly, that is probably not how most people will end up using interface types, and in an ideal scenario, most consumers of modules would not be aware of interface types, but benefit from tools that automatically generate them and code based on them (wasm-bindgen is an excellent example of tooling that generates both Rust bindings based on interface types, as well as interface types based on exported members in Rust code).

The interface types proposal is still in early stages, but if you are interested in language interoperability and sandboxing, these are incredibly exciting times.

Special thanks to @peterhuene, @kubkon, and @ppog_penguin for reviewing this article.