Skip to main content

Modeling WebAssembly Components with Wit

If you've ever defined a service interface using protocol buffers or you've written schemas using Avro, or any of a dozen other service and schema definition DSLs (like Amazon's Smithy or JSON Schema), then you should feel relatively at ease writing component specifications in wit.

As you go through the examples here, keep in mind that all identifiers in wit are lowercase kebab-cased. Whether or not we like kebab casing, it does provide for a level of consistency and predictability that many other DSLs don't provide. Once you get used to it it's actually pretty intuitive.

Records

Records in wit serve the same purpose as structs in other languages and tools like wit-bindgen actually convert wit records into those structs in target languages like Rust. Records are defined using the record keyword and contain a block of field definitions.

record position {
  latitude: f64,
  longitude: f64,
  altitude: f64,
}

record customer {
  id: string,
  name: string,
  address: string,
  phone: string,
}

A good rule of thumb when you're writing wit that helps with remembering kebab case is that you only ever need to use the shift key for special characters like parentheses, colons, and brackets.

Enumerated Types

Many interface and schema definition languages have little to no support for enums and variant types. This is why it's such a pleasant surprise that wit has incredibly robust support for enums. Enums in wit come in two varieties: variant, enum.

enum sensor-type {
  temperature
  humidity
  pressure
}

variant sensor-reading {
    none,
    temperature(list<f64>),
    humidity(f64),
    altitude(f64),
    accelerometer(accelerometer-data)
}

(Bit) Flags

The flags type, also known as a "bag of booleans", comes in handy when you need to keep track of a number of boolean flags and you want to use a nice compact memory footprint that packs booleans into the bit values of a single integer.

flags quest-completion {
    has-sword
    has-shield
    has-boots
    has-armor
    is-hero
    did-turn-knob
}

It's worth pointing out that none of the booleans in a flags type are related to each other. They can all be set/unset independently.

Functions

Functions can be defined inside interfaces, but they can also be included directly in world definitions via the import or export keyword. Whether you import/export individual functions in a world or whole interfaces is up to you and your own modeling preferences.

interface amazing {
    fib: func(n: u32) -> u32
    do-a-thing: func() -> result<string>
    // we can even return multiple values from a function if they're named:
    fancy: func(input: string) -> (output: string, acknowledged: bool)
    // of course we can use records as well
    fancy-record: func(input: fancy-data) -> result<not-so-fancy-data>
}

For those of us familiar with languages where keywords like fun, func, def, or fn precede the function name, wit's name-first notation can take a little getting used to.

Type Aliases

Type aliases are used to make reading and writing your wit files a little easier. You can provide an alias (so long as it's a valid identifier) for any other data type: primitive or otherwise.

type okless-result = result<_, errno>            // no "ok" type
type errless-result = result<string>              // no "err" type
type char-result = result<char, errno>         // both types specified
type row-list = list<row-data>
type why-would-we-do-this = result<option<list<string>>, option<list<bool>>>

Next let's take a look at how we can turn these wit definitions into code!