Skip to main content

WASI 0.2.0 and Why It Matters

· 7 min read

stable foundation and community

WASI Preview 2 officially launched! After a vote in the WASI Subgroup of the W3C WebAssembly Community Group, the standard set of interfaces included in the launch of Preview 2, aka WASI 0.2.0, is ready for use by library implementers. We've been closely tracking the different release candidates of WASI 0.2.0 over the last 6 months, and wasmCloud will update its runtime WIT definitions to the pinned versions in just a few days.

If you look in our quickstart now you'll notice that we now provide documentation for building WebAssembly components, starting with Rust and TinyGo. As seen in previous community calls, JavaScript and Python are also supported thanks to the ComponentizeJS and componentize-py projects. I worked on getting examples up-and-running for components, and there are upsides and downsides to the initial approach we're taking. This post will focus on what current component support looks like and what WASI 0.2.0 means for wasmCloud.

The Hello World of WASI:HTTP

A WebAssembly component is a reactive unit of compute that has to be loaded, instantiated, and then a function that the component exports needs to be imported by the host runtime. Like so many applications, the entrypoint for many of our examples is an HTTP endpoint as it provides a common abstraction that's easy to work with and test using curl on the CLI.

The simplest “hello world” example looks like this using the wasi:http interface:

rust
# Rust
wit_bindgen::generate!({
    world: "hello",
    exports: {
        "wasi:http/incoming-handler": HttpServer,
    },
});

use exports::wasi::http::incoming_handler::Guest;
use wasi::http::types::*;

struct HttpServer;

impl Guest for HttpServer {
    fn handle(_request: IncomingRequest, response_out: ResponseOutparam) {
        let response = OutgoingResponse::new(Fields::new());
        response.set_status_code(200).unwrap();
        let response_body = response.body().unwrap();
        response_body
            .write()
            .unwrap()
            .blocking_write_and_flush(b"Hello from Rust!\n")
            .unwrap();
        OutgoingBody::finish(response_body, None).expect("failed to finish response body");
        ResponseOutparam::set(response_out, Ok(response));
    }
}

In our Cargo.toml, the only dependency here is wit-bindgen to generate the Rust types for us to refer to from wasi::http::*. This is lower level than the Smithy module abstraction, which provides a simple model of receiving an HTTP request struct and returning a response struct:

rust
async fn handle_request(&self, _ctx: &Context, _req: &HttpRequest) -> RpcResult<HttpResponse> {
    Ok(HttpResponse::ok("Hello, World!"))
}

At this point, looking at the above examples, you may consider the component experience to be more WebAssembly-specific than code you'd ideally like to write. This is true, as the base level of support for Wasm components starts with using the wit-bindgen generated types and functions to implement business logic. So, what's the path forward?

Why WASI 0.2.0 is Important

Working with the standard set of WASI interfaces isn't meant for application developers. They are fundamentally lower levels of abstraction meant for library developers and implementers. WASI 0.2.0 is important not because it gives application developers the ability to write real things with WebAssembly (we've been doing that for four years!), but because it provides a stable definition of common interfaces for library developers in each language to build on top of.

Taking the example above, we're not working towards a world where developers need to work with WebAssembly interfaces, we're working towards a world where the capabilities of standard libraries of language ecosystems will support WASI interfaces. Developers using language standard libraries and popular libraries will likely not code directly to WASI API's and will instead use idiomatic abstractions. As WASI grows on the 0.2 foundation, higher-level interfaces like wasi-keyvalue may be at the forefront. Let me show you an example of the ideal Wasm hello world application to tie what I'm saying together in a concrete way:

rust
#[macro_use] extern crate rocket;

#[get("/hello/<name>")]
fn hello(name: &str) -> String {
    format!("Hello, {name}!")
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![hello])
}

Note that there are no Wasm interfaces here! This is actually copy/pasted from the Rocket homepage. The ideal experience is that developers can use idiomatic community and standard libraries to write their code, then simply compile to a Wasm binary. We're not quite at this point yet, but WASI 0.2.0 is the first step in this direction because it provides a common standard set of interfaces for library developers to build on top of.

WASI Preview 2 contains the following APIs:

ProposalVersions
https://github.com/WebAssembly/wasi-io0.2.0
https://github.com/WebAssembly/wasi-clocks0.2.0
https://github.com/WebAssembly/wasi-random0.2.0
https://github.com/WebAssembly/wasi-filesystem0.2.0
https://github.com/WebAssembly/wasi-sockets0.2.0
https://github.com/WebAssembly/wasi-cli0.2.0
https://github.com/WebAssembly/wasi-http0.2.0

What to Expect

If you take a look at our examples in the wasmCloud/wasmCloud repository you'll see we've added Rust, TinyGo and TypeScript/JavaScript examples, with Python coming soon. The above snippet of code, where a developer can use just a language library out-of-the-box and compile to WASI, is likely a few months away. The important thing to keep in mind is that WASI 0.2.0 enables this in the first place and, looking forward, here's what you can expect from the wasmCloud developer experience.

As language toolchains evolve to better and better support WASI, what you can expect to see is Wasm-first frameworks for developing applications using standard interfaces. One of these such frameworks will be the wasmcloud-actor crate, which will provide both access to a stable set of WASI interfaces and nicer abstractions around specific functionality. Take the above HTTP request handler code and how we respond with an HTTP response of “Hello from Rust”:

rust
let response = OutgoingResponse::new(Fields::new());
response.set_status_code(200).unwrap();
let response_body = response.body().unwrap();
response_body
    .write()
    .unwrap()
    .blocking_write_and_flush(b"Hello from Rust!\n")
    .unwrap();
OutgoingBody::finish(response_body, None).expect("failed to finish response body");
ResponseOutparam::set(response_out, Ok(response));

The code I personally would want to write is something like the original abstraction. Avoiding modifying traits and the standard way to define exports, we may have an abstraction like:

rust
use std::collections::HashMap();
use wasmcloud_actor::http::set_response;

impl Guest for HttpServer {
    fn handle(_request: IncomingRequest, response_out: ResponseOutparam) {
        set_response(HashMap::new(), 200, b"Hello from Rust!\n", response_out).expect("should be able to set HTTP response");
    }
}

//
// extern::crate::wasmcloud_actor::http
//
pub fn set_response(headers: HashMap<String, String>, status_code: u8, body: &[u8], response_out: ResponseOutparam) -> anyhow::Result<bool> {
    let response = OutgoingResponse::new(headers);
    response.set_status_code(status_code)?;
    let response_body = response.body()?;
    response_body
        .write()?
        .blocking_write_and_flush(body)?;
    OutgoingBody::finish(response_body, None).context("failed to finish response body")?;
    ResponseOutparam::set(response_out, Ok(response));
}

Check it out! A much nicer abstraction around returning an HTTP request, and all we had to do was pull the generated WASI structures behind a more idiomatic API. The above is just sample code I wrote for this blog, so our actual implementation might be different at the end of the day (and even better). This should, however, gives a sense of how library developers will use WASI APIs when the target architecture is Wasm. Even better, the wasmcloud-actor crate will still work out-of-the-box with tools like wasmtime-serve, as there are zero wasmCloud specific APIs used. That being said, this kind of library belongs in an upstream location like the Bytecode Alliance or in the language itself, and this is an intermediate solution unless we see evidence that we should keep the library around for good.

We will see libraries emerge in each language that demonstrates good Wasm component support, and I expect to see something like this for Rust, TinyGo, Python, JavaScript, C/C++, and C# in the near future. We may even see this support built into the standard libraries of these languages. Go for, example, just resurfaced support for the wasmexport directive, enabling libraries like net/http to have WASI implementations.

Conclusion

We are, arguably, reaching the most exciting period of the wasmCloud project to-date. The stabilization of WASI 0.2.0, alongside native support for components in wasmCloud, lets us support every Wasm component language that uses the same WIT definitions. Even as WASI proposals continue to evolve, we now have a stable base to adapt those interfaces to ensure future stability. I firmly believe that this enables the community to truly collaborate on furthering language support, and writing real applications with WebAssembly, and I look forward to being at the forefront of that.

If you have any questions or concerns, please drop by our Slack and let's have a discussion. While the new interfaces look different from what a seasoned wasmCloud developer is used to, they enable us to truly benefit from and contribute to WebAssembly standards, and the best experience is just within arm's reach.