rokdocs
v0.1

1.8. WASM component model - ffi.wit

The WebAssembly Component Model (bytecodealliance.org; Preview 2 stabilized in January 2024) is the foundation for defining typed cross-language interfaces. It replaces the raw pointer + length model that traditional C ABI FFI (1.7) relies on with rich types: record / variant / resource.

1.8.1. WIT - WebAssembly Interface Types

WIT is a language-neutral interface definition language.

package example:fs;

interface fs {
    record stat {
        size: u64,
        mtime: u64,
        is-dir: bool,
    }

    variant error {
        not-found,
        permission-denied,
        io-error(string),
    }

    resource file {
        constructor(path: string);
        read: func(buf-size: u32) -> result<list<u8>, error>;
        close: func();
    }

    open: func(path: string) -> result<file, error>;
}

An interface defined this way can be imported or exported from any language: Rust, Go, Python, rok and so on.

1.8.2. Binding form on the rok side

The import side uses an external component. Here wit/fs.wit holds the 1.8.1 interface with package example:fs; at its top.

ffi::wit::import { "wit/fs.wit" }       // relative to this source file

use example::fs::fs::{File, Error};

let f = File::new("data.txt");
let bytes = try f.read(1024);           // throws (Error) -> List<u8>
f.close();

The export side provides a component; calculator.wit declares add: func(a: s32, b: s32) -> s32 in a world.

ffi::wit::export { "calculator.wit" }

#[wit::export]
fn add(a: i32, b: i32) -> i32 { a + b }

Paths and placement

  • A path is relative to the source file. It names a .wit file or a package directory, whose deps/ holds its dependencies.
  • The item is allowed in the program's package and in dependencies. Its bindings go in the module that holds it (in a dependency, that dependency's module), under ns::pkg::iface (wasi::filesystem::types).
  • WIT types become PascalCase rok types (descriptor-type → DescriptorType, file → File); functions and fields become snake_case.

Worlds

A world in a named file is the component. Otherwise the keyword decides: ffi::wit::import imports the interfaces, ffi::wit::export exports them. include ... with { a as b } renames the included items.

Type mapping

WITrok
recordstruct
variant, enumenum
flagsstruct of bool fields with Default (at most 32 flags)
option<T>T? (nested options keep their layers: option<option<T>> → T??)
result<T, E>Result<T, E>; a function returning it is throws (E) -> T
tuple<..>tuple
list<T>List<T>
list<T, N>[T; N]
stringstr (UTF-8)
char, bool, f32, f64the same
sN / uNiN / uN
future<T>AsyncFuture<T>
stream<T>rok::core::channel::Receiver<T> (closing the sender ends it)
error-contextrok::bare::ffi::wit::ErrorContext
imported resourcea handle struct, dropped with using / Drop
exported resourcethe user's #[wit::export] pub struct

Async

An async func is an async fn, both imported and exported. A synchronous task (a preview 2 main, a synchronous export) cannot wait for the component model; doing so is a runtime panic, so async imports are awaited inside async exports. A preview 3 command exports wasi:cli/[email protected] as #[wit::export] async fn run() -> Result<(), ()>. Async export calls run concurrently, each a component model task; a call returns its result in its own turn. Inside an async export, sockets and timers are waited on between the host's events; with component model events also pending they are checked at least every 10 ms (a preview 2 pollable and the component model's events cannot be waited on at once). An async export that waits for what nothing delivers waits; another call may still deliver it.

The async canonical ABI the generated bindings call lives in rok::std::wit: it runs on the executor, and stream<T> maps to a core type. Memory helpers, blocks, handles and ErrorContext stay in rok::bare::ffi::wit.

Exports

An exported function is a #[wit::export] fn in the item's module, matched to the WIT function by its snake_case name. An exported resource is a #[wit::export] pub struct whose impl gives new (the constructor), the methods and the static functions; a fallible constructor (result<r, ..>) is a throwing new, and the resource's destructor runs its Drop. A missing export is a compile error.

A package that exports WIT and whose entry file has neither fn main nor top-level statements is a reactor: it exports no wasi:cli/run (4.16.1).

Targets

WIT needs wasm32-wasi (a WASI preview 2 component); on any other target it is a compile error. Exports may be called before main: the runtime starts on the first export call.

A prebuilt dependency carries the .wit files its ffi::wit items read in its ROKHIR (refs-lang 9.4.7); a dependent does not read them from disk.

Restrictions

An imported interface cannot use types of an interface the component exports; that is a compile error.

1.8.3. Differences between C FFI and WIT

The two approaches differ clearly along several axes.

AspectC FFI (1.7)WIT (this chapter)
Type systemC primitives + user structsrecord / variant / resource
Safetylow (raw ptrs managed by hand)high (typed)
Memory managementmalloc/freeresource (automatic destructor)
Error handlingerrno / return value conventionfirst-class result<T, E>
Stringnull-terminatedlength-prefixed UTF-8
List(ptr, len) manuallist<T> automatic
Call costalmost none (direct call)trampoline (~ns)
Scopeevery native targetwasm32-wasi (preview 2 component)
unsafe required✓✗

1.8.4. Selection criteria

The criteria are simple: they depend on what is being called and where it is deployed.

ScenarioRecommendation
Calling a traditional C libraryC FFI (1.7)
System calls (libc, Win32)C FFI
GPU / SIMD intrinsicsC FFI
Safe interface with other languagesWIT
Deployment to WASM environmentsWIT
Plugin / sandbox isolationWIT
Multi-language (Modular / Mojo etc.)WIT

The main significance of this chapter is that rok supports both traditional C FFI and WIT. Users choose either according to their safety, performance and target environment constraints.

1.8.5. References

For further reading:

Edit this page on GitHub