wit-bindgen 是通过 wit 生成 wasm 绑定函数的库。wit 是 wasm 的类型抽象描述文件,替换原来的 witxwat 格式。wit-bindgen 生成的绑定函数,可以直接在 Rust 代码中调用,不需要手动编写 wasm 代码。

wasmtime 是一个 wasm runtime,可以直接运行 wasm 文件,也可以通过 wasmtime 的 API 在 Rust 代码中调用 wasm 函数。wasm 在 serverless runtime 中的应用,可以通过 wasmtime 来运行能够处理 HTTP 请求的 wasm 文件的。这里以处理 HTTP 请求的能力为例,介绍如何使用 wit-bindgenwasmtime 开发 WebAssembly 应用。

定义 wit 接口并生成代码

首先,我们需要定义一个 wit 接口文件 leaf-http.wit,用于描述 wasm 文件的能力。这里我们定义一个处理 HTTP 请求的接口,接口定义如下:

interface exports {
    // status code
    type status = u16
    // headers as key value pairs
    type headers = list<tuple<string, string>>
    // request uri
    type uri = string
    // body as bytes
    type body = list<u8>
    // request method
    type method = string
    // request object
    record request{
        method: method,
        uri: uri,
        headers: headers,
        body: option<body>,
    }
    // response object
    record response{
        status: status,
        headers: headers,
        body: option<body>,
    }
    // handle request function
    handle-request: func(req: request) -> response
}

// exports the interface
world leaf-http {
  default export exports
}

这里定义了一个 handle-request 函数,接收一个 request 对象,返回一个 response 对象。request 对象包含了请求的方法、uri、headers、body 等信息。response 对象包含了响应的状态码、headers、body 等信息。export 的接口名为 leaf-http

这里使用 wit_bindgen_gen_guest_rust 生成 rust 代码,生成的代码如下 https://github.com/fuxiaohei/leaf-wasm/blob/0f62fc90ae044edd7861053e1373cdb9dd76bf4d/wit/leaf-http.rs。使用代码生成而不是宏,为了更直观的看到需要编译为 rust 的 wasm 文件的内容。

如果使用宏的姿势,可以参考:

wit_bindgen_guest_rust::generate!("leaf-http.wit");

实现 wit 接口,编写 wasm 文件

通过宏生成代码后,就可以在 rust 代码中直接实现 handle-request 函数了。这里我们实现一个简单的处理 HTTP 请求的 wasm 文件,代码如下:

wit_bindgen_guest_rust::generate!("../../wit/leaf-http.wit");

struct HttpImpl;

use leaf_http::Request;
use leaf_http::Response;

/// HttpImpl implements the leaf-http interface
impl leaf_http::LeafHttp for HttpImpl {
    fn handle_request(req: Request) -> Response {
        let url = req.uri;
        let method = req.method.to_uppercase();
        let mut headers = req.headers;
        headers.push(("X-Request-Method".to_string(), method));
        headers.push(("X-Request-Url".to_string(), url));
        Response {
            status: 200,
            headers,
            body: req.body,
        }
    }
}

/// export the leaf-http interface,
/// so that it can be called by the host runtime
export_leaf_http!(HttpImpl);

这里实现了一个 HttpImpl 结构体,实现了 leaf_http::LeafHttp 接口,实现了 handle-request 函数。这里我们简单的将请求的 uri 和 method 作为响应的 headers 返回。编译为 wasm 文件后,就可以调用 wasmtime 运行了。

cargo build --target wasm32-unknown-unknown --release

这里生成的 wasm 文件在 target/wasm32-unknown-unknown/release/leaf_http.wasm。wasm 文件是 Wasm Module 的二进制格式。但是 wit-bindgen 是按照 component model 来定义的,所以我们需要将 wasm 文件转换为 component model,这里使用 wit-component 来转换。

fn convert_rust_component(path: &str) {
    let file_bytes = std::fs::read(path).expect("Read wasm file error");

    let component = ComponentEncoder::default()
        .module(file_bytes.as_slice())
        .expect("Pull custom sections from module")
        .validate(true)
        .encode()
        .expect("Encode component");

    std::fs::write(path, component).expect("Write component file error");
    info!("Convert wasm module to component success")
}

这样就可以生成一个 component 文件,之后就可以使用 wasmtime 来运行了。

运行 wasm 文件

这里我们使用 wasmtime 来运行 wasm 文件,然后调用 handle-request 函数。首先我们利用 wit 生成 host 调用的代码。

wasmtime::component::bindgen!({
    path: "leaf-http.wit",
    async: true,
});

use wasmtime::{
    component::{Component, Instance, Linker},
    Config, Engine, Store,
};

fn create_wasmtime_config() -> Config {
    let mut config = Config::new();
    /// enable component model
    config.wasm_component_model(true);
    /// enable async support
    config.async_support(true);
    config
}

pub struct Worker {
    pub engine: Engine,
    pub component: Component,
    pub instance: Instance,
    pub store: Store<()>,
    pub exports: LeafHttp,
}

impl Worker {
    pub async fn new(path: &str) -> Result<Self, Error> {
        let config = create_wasmtime_config();
        let engine = Engine::new(&config).map_err(Error::InitEngine)?;
        let component = Component::from_file(&engine, path)
            .map_err(|e| Error::ReadWasmComponent(e, String::from(path)))?;

        let mut store = Store::new(&engine, ());
        let linker: Linker<()> = Linker::new(&engine);
        let (exports, instance) =
            LeafHttp::instantiate_async(&mut store, &component, &linker)
                .await
                .map_err(Error::InstantiateWasmComponent)?;

        Ok(Self {
            engine,
            component,
            instance,
            store,
            exports,
        })
    }
}

调用 LeafHttp::instantiate_async 来实例化 wasm 文件,然后就可以调用 handle-request 函数了。

let mut worker = Worker::new(wasm_file).await.unwrap();
let headers: Vec<(&str, &str)> = vec![];
let req = Request {
        method: "GET",
        uri: "/abc",
        headers: &headers,
        body: Some("xxxyyy".as_bytes()),
    };

let resp = worker
    .exports
    .handle_request(&mut worker.store, req)
    .await
    .unwrap();

assert_eq!(resp.status, 200);
assert_eq!(resp.body, Some("xxxyyy".as_bytes().to_vec()));

wasmtime::component::bindgen 生成的代码中暴露了 LeafHttp 接口和 handle-request 函数。这样就非常友好的调用了 wasm 文件中暴露的接口函数。

总结

这里我们使用 wit-bindgen 来生成 wasm 文件的 component model,然后使用 wasmtime 来运行 wasm 文件。这样就可以在 rust 中调用 wasm 文件中 wit 定义并暴露的接口函数。

以此为基础,我们可以在 rust 中实现一个 http server,然后将 wasm 按照不同的业务规则加载进来,这样就可以实现一个高性能自然租户隔离的多用户 http 请求触发的客户代码运行环境,也就是我们常说的 serverless 中边缘函数的运行环境。

最近在学习 rust,这里就简单的实现了一个例子,代码在 github 上,欢迎大家参考。

参考

[1] wasmtime

[2] wit-bindgen