Movatterモバイル変換


[0]ホーム

URL:


Writing a simple WASM API layer using interface types and Wasmtime

The WebAssembly interface types proposal aims toadd a new common set of interface types to the core specification that woulddescribe abstract, higher level values, and the ability to adapt the interfaceof a module so that different hosts can inter-operate using the higher leveltypes. For a comprehensive explainer of the problems interface types aresupposed to solve,Lin Clark has anexcellent article on the MozillaHacks blog, with a demo of using the same markdownrenderer compiled to WebAssembly, and using native strings from languages likeRust, Python, or #"https://doc.rust-lang.org/book/ch10-02-traits.html">trait: itdefines shared behavior in an abstract way, withoutproviding an actual implementation for how to achieve that behavior.

In this article, we will manually write an interface type for a simplecalculator module, then use Wasmtime tooling tocorrectly implement thatinterface type in Rust, link our implementation, and use it in a module thatrequires 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 findthe complete project on GitHub.

Writing a simple interface type in WITX

The current proposal describes interface types usingWITX, anexperimental file format based on theWebAssembly Text Format, with addedsupport formodule types andannotations. It ishow theWASI API is defined, and if you are familiar with.watfiles,.witx should seem familiar.

The goal is to have a calculator module with a single function that adds twonumbers. Let’s describe this using WITX in a file calledcalculator.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 errortype returned from the function. You can find its definition in the repositoryhere.

The WITX file defines acalculator module with a single function,add, whichtakes two 32-bit signed integers and returns a 32-bit signed integer, or anerror. Now we can usewiggle, a Rust crate that generates Rust codebased on interface types definitions, and get a strongly-typed Rust trait basedon the WITX file above:

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

According to thewiggle documentation, thefrom_witx macrotakes a WITX file and generates a set of public Rust modules based on theinterface type definition. Specifically, it generates atypes module thatcontains all user-defined types, and one module for each WITXmodule definedthat contains Rust traits that have to be implemented by the structure passed as“context”.

This means there is now aCalculator trait with a singleadd method that ourCalculatorCtx 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 theadd method, and at this point,CalculatorCtx can be used asour 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 theadd method usess32, or signedintegers. However,s32 is not a fundamental WebAssembly data type (quickreminder thatthe fundamental data types in WebAssembly are:𝗂𝟥𝟤 | 𝗂𝟨𝟦 | 𝖿𝟥𝟤 | 𝖿𝟨𝟦). This is where theinterface typesproposal defines the additional integer data types:

In addition tostring, the proposal includes the integer typesu8,s8,u16,s16,u32,s32,u64, ands64. […] Since values of thesetypes are proper integers, not bit sequences like core wasmi32 andi64values, there is no additional information needed to interpret their value asa number.

The trait generated bywiggle maps the signed 32-bit integer from theinterface types proposal tothe Rustsigned integer,i32 (not tobe confused with the WebAssemblyi32 data type, which isnot inherentlysigned or unsigned, [its] interpretation is determined by individualoperations). Before actually instantiating the module that will use thisimplementation, the Rusti32 will be mapped to the WebAssemblyi32 datatype.

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

Using Wasmtime to link the calculator library

Now that we have an actual implementation that we know satisfies the interfacedefined in the WITX file, we can use it to instantiate a module that importsthat functionality in a file calledusing_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 anadd function that takes twoi32 parameters and returns ani32. (Note theparameters are fundamental WebAssembly types). Then, we call this function laterin our module’s implementation.

In order to successfully instantiate the module above, we need to satisfy itsimports. This is where our implementation becomes useful - we can use theWasmtime linker to define the implementation for theadd functionrequired by the module by creating an instance of theCalculatorCtx structuredefined earlier, and returning the result of itsadd 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, usestdout andstderr foroutput and error reporting, and link the current WASI implementation. The resultof building the Rust program that instantiates WebAssembly modules usingWasmtime and links our calculator implementation is an executable,wasm-calc,whose complete implementation can be found on GitHub. Now we can usethis binary to instantiate any generic WASI module, as well as modules thatrequire acalculator module. Similarly to how Wasmtime works, the argumentsare the module to run, the name of the function to execute, followed by itsarguments:

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

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

For simplicity, the example above is written in theWebAssembly textformat - but in a real scenario, something similar to that wouldbe generated by a compiler. If compiled using a WebAssembly target(wasm32-unknown-unknown orwasm32-wasi),the following Rustprogram 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’sarticle.

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

While the semantics are different, functionally, the same thing happens: wedeclare the signature of an external function that we expect from a modulecalledcalculator, and we use that function later in the program, and we caninvoke theconsume_add function in the same way from ourwasm-calc binary:

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

Conclusion

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

This doesn’t mean, that the experience cannot be improved - for exampledetermining if an interface type definition satisfies the imports forinstantiating a given module, compiler support for generating interface types,or automatically linking all the exports required by a module given a list ofdependent modules (thewig crate does something similar for WASIimports).

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

The interface types proposal is still inearly stages, but if youare interested in language interoperability and sandboxing, these are incrediblyexciting times.

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


[8]ページ先頭

©2009-2025 Movatter.jp