Skip to main content
Kevin Hoffman
Kevin Hoffman
Kevin Hoffman
||7 min read

For the Wit! My First Day with Components

Some thoughts and reflection on my first day using Wasm components for something more involved than hello world.

Disclaimer

Everything in this post is based on completely unreleased and highly volatile tooling and specification.

I've been evangelizing WebAssembly for what feels like the last 100 years (it's only been 4!). I've introduced enough people and teams to Wasm to have noticed a common journey to adoption that starts with what might feel like stages of grief. As we peel away the layers of shiny demos and things that only work under a blue moon at the stroke of midnight in a thunderstorm, reality sets in that the core Wasm standard is just, as my colleague Bailey Hayes says, “three ints in a trenchcoat.”

WebAssembly, at its core, is only able to exchange integers and floats. Anything more complicated than that has to be stuffed into linear memory and then pulled out again, hoping that both sides of that exchange know how to talk to each other and can survive iterative changes. Today's Wasm ecosystem is absolutely rife with custom interfaces that only work in one specific scenario. That's not necessarily a bad thing, but for a technology that promises the kind of portability we've always dreamed of, it's a hard pill to swallow that these portable modules only work with specific, bespoke hosts.

But don't despair, because that shiny ideal we've all been promised may be coming sooner than we thought, and the component model might help usher it in. As a note of caution, everything I'm talking about in this post is highly volatile and subject to rapid breaking changes. This is definitely not 1.0.

Inspired by Bailey's recent talk at WASM I/O, I wanted to roll up my sleeves and get my hands dirty with Wasm components and, specifically, with the interface definition language (IDL) for describing components: wit. Wit stands for Wasm Interface Type and has two main purposes:

  • Describing interfaces made up of functions and types
  • Describing a larger ecosystem of worlds where interfaces are stitched together through imports and exports.

While it's certainly nice to be able to describe functions, IDLs are hardly a recent innovation. What I'm really excited about is the latter: worlds. Before I explain why, let's go through some fundamentals.

Let's see what a simple “hello world” interface might look like:

interface hello {
  greeting: func(name: string) -> string
}

This interface refers to a single function called greeting that takes a string and returns a string in the canonical example form. What is blissfully missing from this definition is how that exchange will happen. We don't particularly care about the details of how the guest and host agree to exchange those strings. Tools like wit-bindgen will generate the functions needed to do that marshaling for us. As I write this, wit-bindgen supports generating in Rust, C/C++, Java, and TinyGo, and rumors are that Python is on the way.

Next, we can define a world. A world is (I'm oversimplifying a bit) a logically associated named pair of imports and exports.

default world hello-world {
    import greet: self.hello
    export stuff: func()
}

A world can define the necessary imports and exports for a Wasm component (which is then used by code generators), but it can also define what is supported by a given runtime host. It's entirely conceivable that near-future tooling will allow us to match components with target runtimes (very similar to how wasmCloud's lattice auctions already do this with actor components). It makes me think of a matchmaking service that discovers overlapping and supporting worlds within disparate infrastructure, shuffling components about automatically.

For a bank account example that I've been building for our event sourcing framework, I decided to see what it would look like to write the interfaces in wit rather than in framework-proprietary definitions.

Here's a world definition for my bank account aggregate:

default interface bankaccount-agg {
    use pkg.types.{aggregate-state, account-created-event}
    apply-account-created:
        func(state: aggregate-state,
             event: account-created-event) -> result<aggregate-state, string>
}

default world bankaccount-aggregate {
    export account-aggregate: self.bankaccount-agg
}

Don't worry too much about terminology like aggregates and event sourcing, I'll be talking at KubeCon about that as well as writing many blog posts. Through the "magic" of wit-bindgen, I can use a simple Rust macro to parse the preceding wit file and generate the necessary binding code, letting me focus solely on my business logic:

wit_bindgen::generate!({
    path: "../wit",
    world: "bankaccount-aggregate"
});

use crate::bankaccount_types::AccountCreatedEvent;
use account_aggregate::{AccountAggregate, AggregateState};

struct BankAccount;

impl AccountAggregate for BankAccount {
    fn apply_account_created(
        _state: AggregateState,
        event: AccountCreatedEvent,
    ) -> Result<AggregateState, String> {
       Ok(AggregateState::new(event.account_number, event.amount))
    }
}

export_bankaccount_aggregate!(BankAccount);

This is it! This is all I need to write! The event sourcing framework takes care of all the plumbing, and all I need to do is write code that conforms to one of these interfaces.

This might not look like much, but remember that with wit (and friends), we're no longer bound to one-off contracts between the guest and host. We're free to write components that do just what we want them to do and other components are free to import and export our interfaces as they see fit.

There is currently an effort underway to define the most commonly used cloud services like key-value stores, blob stores, SQL, message brokers, clocks and random numbers; all as wit worlds. When multiple host runtimes embrace these standards, we'll actually be able to see some of the portability we've been promised. You'll be able to take a component written against one of these interfaces and shift it from a test harness to wasmCloud to Cosmonic (or other implementations) without having to redesign or even recompile your code.

Further, and possibly even more importantly, the number and shape of intervening components is completely opaque to your component. This means that in one environment, your component could go through authorization, monitoring, tracing, and usage limiting middleware while in another less constrained environment (like your laptop), the component could be communicating directly with the host that provides the implementation of that interface.

This enables some possibilities for new paradigms that we might not immediately notice. For example, we could write a component in Rust that tackles some stuff that we might find difficult in another language's Wasm support. We could then import that component interface and produce a new component out of our business logic and the reusable Rust component. While this isn't going to make language choice completely irrelevant, it will potentially let us make language choices based on suitability for a particular use case rather than solely based on availability of ecosystem libraries.

It's an exciting time in the evolution of Wasm. WASI Preview 2 is in draft and we expect this to be GA in the fall time frame. For more details on WASI Preview 2 and what the roadmap looks like, check out Dan Gohman's talk at WASM I/0.

On the community front, as wasmCloud maintainers, we'll continue to participate in, contribute to, and consume WASI standards and the Component Model. We will provide experimental support for many of these incoming proposals to enable us, and our community, to iterate on upstream. This is the best way to achieve production-ready, language-agnostic, vendor-neutral, portable, and widely adopted standards - the future we want to see.

Want to get started building with components?

Get in touch for a demo
Book Now

Keep up to date

Subscribe to Cosmonic for occasional communication straight to your inbox.