How to use the wit bindgen

# Background

Wit-bindgen is a language bindings generator for wit. And wit is the abbreviation of WebAssembly Interface Types. Here's an excellent article explaining what is WebAssembly Interface Types (opens new window). In short, wit enables wasm components to work with each other better, with rich apis and complex types.

In this post, I will try to introudce how to use wit-bindgen. The source code used in this post can be found in this repository (opens new window).

# What can wit bindgen do?

wit-bindgen generate bindings for various languages from *.wit files, which defines the general interface of the wasm module.

With the interface defined in *.wit, we can implement that interface in one language, say Rust, and compile it into WASM.

With the same wit file, we can import the compiled WASM module into other languages like python, js, etc. And it just works. We don't need to concern about the glue code, they are generated by wit-bindgen for us.

# A quick demo.

# Define the wit file

First, we create a file markdown.wit like this:

interface exports {
  render: func(input: string) -> string
}
world markdown {
  default export exports
}

In this wit file, we define a function that renders a markdown string to html string.

A world is more like a component which can import and export functions. This defines the API for this component.

# Implement that in Rust

In the lib file, we use pulldown_cmark to parse the markdown string.

wit_bindgen_guest_rust::generate!("./wits/markdown.wit");
use pulldown_cmark::{html, Parser};

struct Markdown;
export_markdown!(Markdown);

impl markdown::Markdown for Markdown {
    fn render(input: String) -> String {
        let parser = Parser::new(&input);
        let mut html_output = String::new();
        html::push_html(&mut html_output, parser);
        return html_output;
    }
}

Then compile it to wasm with cargo b --release --target wasm32-unknown-unknown.

Note that export_markdown!(Markdown); is necessary, otherwise you will get a error, complaining that render is not exported.

# Use the component in Rust

Then we can use this component in Rust, following the code below.

// generate bindings.
wit_bindgen_host_wasmtime_rust::generate!("./wits/markdown.wit");
use wit_bindgen_host_wasmtime_rust::anyhow::{self, Result};
use wit_bindgen_host_wasmtime_rust::wasmtime::{
    self,
    component::{Component, Linker},
    Config, Engine, Store,
};
// Acutally, when you run `cargo b` in the last step, you will get a wasm module,
// not a wasm component, so we need to use this ComponentEncoder to transform the 
// wasm module to component.
use wit_component::ComponentEncoder;

fn main() -> Result<()> {
    let mut config = Config::new();
    // Enable component here.
    config.wasm_component_model(true);
    let engine = Engine::new(&config)?;
    let mut store = Store::new(&engine, 0);
    let linker = Linker::new(&engine);
    // we first read the bytes of the wasm module.
    let module = std::fs::read("./target/wasm32-unknown-unknown/release/app.wasm")?;
    // then we transform module to compoennt.
    // remember to get wasi_snapshot_preview1.wasm first.
    let component = ComponentEncoder::default()
        .module(module.as_slice())?
        .validate(true)
        .adapter_file(std::path::Path::new("./wits/wasi_snapshot_preview1.wasm"))?
        .encode()?;
    let component = Component::from_binary(&engine, &component)?;
    // after getting the component, we can instantiate a markdown instance.
    let (markdown, _instance) = Markdown::instantiate(&mut store, &component, &linker)?;
    let res = markdown.render(&mut store, "# hello")?;
    assert_eq!(res, "<h1>hello</h1>\n");
    Ok(())
}

# Use the Compoennt in Python

Wait, how can I use the wasm compoennt in Python? It is not that straightforward.

First, we need to install wasmtime for python.

Since python support in wit-bindgen-cli has been moved to wasmtimepy (opens new window), we don't need to install wit-bindgen-cli anymore.

# Install wasmtime for python.
pip3 install wasmtime

Then, we can generate the bindings, from the component.wasm, not component.wit anymore.

So let's save the component.wasm using rust first.

// we first read the bytes of the wasm module.
let module = std::fs::read("./target/wasm32-unknown-unknown/release/app.wasm")?;
// then we transform module to compoennt.
// remember to get wasi_snapshot_preview1.wasm first.
let component = ComponentEncoder::default()
    .module(module.as_slice())?
    .validate(true)
    .adapter_file(std::path::Path::new("./wits/wasi_snapshot_preview1.wasm"))?
    .encode()?;
std::fs::write("./target/component.wasm", &component)?;
# generate bindings 
python -m wasmtime.bindgen ./target/component.wasm --out-dir python/markdown/

After all of these, we can create a python file to use the component.

from wasmtime import Engine, Store, Config
from markdown import Component

if __name__ == "__main__":
    text = "# head"

    config = Config()
    engine = Engine(config)
    store = Store(engine)
    component = Component(store)
    result = component.render(store, text)
    assert result == "<h1>head</h1>\n"
    print(result)

That's it! Using python is eaiser, but you have to deal with other things. I prefer to use rust, cause it's a end to end solution.

# Where to get wasi_snapshot_preview1.wasm?

The source code of wasi_snapshot_preview1 is located in the repo of wit-bindgen. Just clone the source code and build it.

The following script may help with this.

fn main() {
    let mut cmd = std::process::Command::new("cargo");
    cmd.arg("build")
        .arg("--release")
        .current_dir("../wasi_snapshot_preview1")
        .arg("--target=wasm32-unknown-unknown")
        .env("CARGO_TARGET_DIR", "./target")
        .env(
            "RUSTFLAGS",
            "-Clink-args=--import-memory -Clink-args=-zstack-size=0",
        )
        .env_remove("CARGO_ENCODED_RUSTFLAGS");
    let status = cmd.status().unwrap();
    assert!(status.success());
}

wasi_snapshot_preview1.wasm will be generated at wit-bindgen/crates/wasi_snapshot_preview1/wasm32-unknown-unknown/release.

# References

  1. Introduction of WIT (opens new window).
  2. Great talk about wit-bindgen (opens new window).
  3. Source code of wit-bindgen (opens new window).