An Overview of WIT

The WIT (Wasm Interface Type) language is used to define Component Model interfaces and worlds. WIT isn't a general-purpose programming language and doesn't define behaviour; it defines only contracts between components.

To define a new component, you will need to define worlds and interfaces by writing code in the Wasm Interface Type (WIT) language. WIT also serves as documentation for existing components that you may wish to use.

This topic provides an overview of key elements of the WIT language. The official WIT specification and history can be found in the WebAssembly/component-model repository.

Structure of a WIT file

A WIT file contains one or more interfaces or worlds. An interface or world can define types and/or functions.

Types and functions can't be defined outside of interfaces or worlds.

A file may optionally start with a package declaration.

Comments

WIT comment syntax is similar to the one used by the C++ family of languages:

  • Everything from // to end of line is a comment.
  • Any text enclosed in /* ... */ is a comment.
    • Unlike the C++ family, block comments can be nested, e.g. /* blah /* rabbit */ rhubarb */.

Documentation

WIT defines special comment formats for documentation:

  • Everything from /// to end of line is documentation for the following item.
  • Any text enclosed in /** ... */ is documentation for the following item.

For example:

/// Prints "hello".
print-hello: func();

/**
Prints "hello".
*/
print-hello: func();

Identifiers

Identifiers are names for variables, functions, types, interfaces, and worlds. WIT identifiers have a slightly different set of rules from what you might be familiar with in languages like C, Rust, and Java. These rules apply to all names, except for packages. Package identifiers are a little more complex and will be covered in the Packages section.

  • Identifiers are restricted to ASCII kebab-case: sequences of words, separated by single hyphens.
    • Double hyphens (--) are not allowed.
    • Hyphens aren't allowed at the beginning or end of the sequence, only between words.
  • An identifier may be preceded by a single % sign.
    • This is required if the identifier would otherwise be a WIT keyword. For example, interface is not a legal identifier, but %interface is legal.
  • Each word in the sequence must begin with an ASCII letter, and may contain only ASCII letters and digits.
    • A word cannot begin with a digit.
    • A word cannot contain a non-ASCII Unicode character.
    • A word cannot contain punctuation, underscores, etc.
  • Each word must be either all lowercase or all UPPERCASE.
    • Different words in the identifier may have different cases. For example, WIT-demo is allowed.
  • An identifier cannot be a WIT keyword such as interface (unless preceded by a % sign).

Built-in types

The types in this section are defined by the WIT language itself.

Primitive types

WIT defines the following primitive types:

IdentifierDescription
boolBoolean value true or false.
s8, s16, s32, s64Signed integers of the appropriate width. For example, s32 is a signed 32-bit integer.
u8, u16, u32, u64Unsigned integers of the appropriate width. For example, u32 is an unsigned 32-bit integer.
f32, f64Floating-point numbers of the appropriate width. For example, f64 is a 64-bit (double precision) floating-point number. See the note on NaNs below.
charUnicode character. (Specifically, a Unicode scalar value.)
stringA Unicode string: that is, a finite sequence of characters.

The f32 and f64 types support the usual set of IEEE 754 single and double-precision values, except that they logically only have a single nan value. The exact bit-level representation of an IEEE 754 NaN is not guaranteed to be preserved when values pass through WIT interfaces as the singular WIT nan value.

Lists

list<T> for any type T denotes an ordered sequence of values of type T. T can be any type, built-in or user-defined:

list<u8>       // byte buffer
list<customer> // a list of customers

This is similar to Rust Vec, or Java List.

Options

option<T> for any type T may contain a value of type T, or may contain no value. T can be any type, built-in or user-defined. For example, a lookup function might return an option in order to allow for the possibility that the lookup key wasn't found:

option<customer>

This is similar to Rust Option, C++ std::optional, or Haskell Maybe.

This is a special case of a variant type. WIT defines it so that there is a common way of expressing it, so that you don't need to create a variant type for every value type, and to enable it to be mapped idiomatically into languages with option types.

Results

result<T, E> for any types T and E may contain a value of type T or a value of type E (but not both). For example, a HTTP request function might return a result, with the success case (the T type) representing a HTTP response, and the error case (the E type) representing the various kinds of error that might occur:

result<http-response, http-error>

This is similar to Rust Result, or Haskell Either.

This is a special case of a variant type. WIT defines the result type so that there is a common way of expressing this behavior, so that developers don't need to create variant types for every combination of value and error types, and to enable it to be mapped idiomatically into languages with result or "either" types.

Sometimes there is no data associated with one or both of the cases. For example, a print function could return an error code if it fails, but has nothing to return if it succeeds. In this case, you can omit the corresponding type as follows:

result<u32>     // no data associated with the error case
result<_, u32>  // no data associated with the success case
result          // no data associated with either case

The underscore _ stands in "no data" and is generally represented as the unit type in a target language (e.g. () in Rust, null in JavaScript).

Tuples

A tuple type is an ordered fixed-length sequence of values of specified types. It is similar to a record, except that the fields are identified by indices instead of by names.

tuple<u64, string>      // An integer and a string
tuple<u64, string, u64> // An integer, then a string, then an integer

This is similar to tuples in Rust or OCaml.

User-defined types

New domain-specific types can be defined within an interface or world.

Records

A record type declares a set of named fields, each of the form name: type, separated by commas. A record instance contains a value for every field. Field types can be built-in or user-defined. The syntax is as follows:

record customer {
    id: u64,
    name: string,
    picture: option<list<u8>>,
    account-manager: employee,
}

Records are similar to C or Rust structs.

User-defined records can't be generic (that is, parameterised by type). Only built-in types can be generic.

Variants

A variant type represents data whose structure varies. The declaration defines a list of cases; each case has a name and, optionally, a type of data associated with that case. An instance of a variant type matches exactly one case. Cases are separated by commas. The syntax is as follows:

variant allowed-destinations {
    none,
    any,
    restricted(list<address>),
}

This can be read as "an allowed destination is either none, any, or restricted to a particular list of addresses".

Variants are similar to Rust enums or OCaml discriminated unions. The closest C equivalent is a tagged union, but variants in WIT both take care of the "tag" (the case) and enforce the correct data shape for each tag.

User-defined variants can't be generic (that is, parameterised by type). Only built-in types can be generic.

Enums

An enum type is a variant type where none of the cases have associated data:

enum color {
    hot-pink,
    lime-green,
    navy-blue,
}

This can provide a simpler representation in languages without discriminated unions. For example, a WIT enum can translate directly to a C/C++ enum.

Resources

A resource is a handle to some entity that exists outside of the component. Resources describe entities that can't or shouldn't be copied: entities that should be passed by reference rather than by value. Components can pass resources to each other via a handle. They can pass ownership of resources, or pass non-owned references to resources.

If you're not familiar with the concepts of borrowing and ownership for references, see the Rust documentation.

Unlike other WIT types, which are simply plain data, resources only expose behavior through methods. Resources can be thought of as objects that implement an interface. ("Interface" here is used in the object-oriented programming sense, not in the sense of a WIT interface.)

For example, we could model a blob (binary large object) as a resource. The following WIT defines the blob resource type, which contains a constructor, two methods, and a static function:

resource blob {
    constructor(init: list<u8>);
    write: func(bytes: list<u8>);
    read: func(n: u32) -> list<u8>;
    merge: static func(lhs: blob, rhs: blob) -> blob;
}

As shown in the blob example, a resource can contain:

  • methods: functions that implicitly take a self (AKA this) parameter that is a handle. (Some programming languages use the this keyword instead of self.) read and write are methods.
  • static functions: functions which do not have an implicit self parameter but are meant to be nested in the scope of the resource type, similarly to static functions in C++ or Java. merge is a static function.
  • at most one constructor: a function that is syntactic sugar for a function returning a handle of the containing resource type. The constructor is declared with constructor.

A method can be rewritten to be a function with a borrowed self parameter, and a constructor can be rewritten to a function that returns a value owned by the caller. For example, the blob resource above could be approximated as:

resource blob;
blob-constructor: func(bytes: list<u8>) -> blob;
blob-write: func(self: borrow<blob>, bytes: list<u8>);
blob-read: func(self: borrow<blob>, n: u32) -> list<u8>;
blob-merge: static func(lhs: blob, rhs: blob) -> blob;

When a resource type name is wrapped with borrow<...>, it stands for a "borrowed" resource. A borrowed resource represents a temporary loan of a resource from the caller to the callee for the duration of the call. In contrast, when the owner of an owned resource drops that resource, the resource is destroyed. (Dropping the resource means either explicitly dropping it if the underlying programming language supports that, or returning without transferring ownership to another function.)

More precisely, these are borrowed or owned handles of the resource. Learn more about handles in the upstream component model specification.

Flags

A flags type is a set of named booleans.

flags allowed-methods {
    get,
    post,
    put,
    delete,
}

A flags type is logically equivalent to a record type where each field is of type bool, but it is represented more efficiently (as a bitfield) at the binary level.

Type aliases

You can define a new type alias using type ... = .... Type aliases are useful for giving shorter or more meaningful names to types:

type buffer = list<u8>;
type http-result = result<http-response, http-error>;

Functions

A function is defined by a name and a function type. As with record fields, the name is separated from the type by a colon:

do-nothing: func();

The function type is the keyword func, followed by a parenthesised, comma-separated list of parameters (names and types). If the function returns a value, this is expressed as an arrow symbol (->) followed by the return type:

// This function does not return a value
print: func(message: string);

// These functions return values
add: func(a: u64, b: u64) -> u64;
lookup: func(store: kv-store, key: string) -> option<string>;

To express a function that returns multiple values, you can use any compound type (such as tuples or records).

get-customers-paged: func(cont: continuation-token) -> tuple<list<customer>, continuation-token>;

A function can be declared inside an interface, or can be declared as an import or export in a world.

Interfaces

An interface is a named set of types and functions, enclosed in braces and introduced with the interface keyword:

interface canvas {
    type canvas-id = u64;

    record point {
        x: u32,
        y: u32,
    }

    draw-line: func(canvas: canvas-id, from: point, to: point);
}

Notice that types and functions in an interface are not comma-separated.

Using definitions from elsewhere

An interface can reuse types declared in another interface via a use directive. The use directive must give the interface where the types are declared, then a dot, then a braced list of the types to be reused. The interface can then refer to the types named in the use.

interface types {
    type dimension = u32;
    record point {
        x: dimension,
        y: dimension,
    }
}

interface canvas {
    use types.{dimension, point};
    type canvas-id = u64;
    draw-line: func(canvas: canvas-id, from: point, to: point, thickness: dimension);
}

The canvas interface uses the types dimension and point declared in the types interface.

Even if you are only using one type, it must still be enclosed in braces. For example, use types.{dimension} is legal but use types.dimension is not.

This works across files as long as the files are in the same package (effectively, in the same directory). For information about using definitions from other packages, see the specification.

Worlds

Roughly, a world describes the contract of a component. A world describes a set of imports and exports, enclosed in braces and introduced with the world keyword. Imports and exports may be interfaces or specific functions. Exports describe the interfaces or functions provided by a component. Imports describe the interfaces or functions that a component depends on.

interface printer {
    print: func(text: string);
}

interface error-reporter {
    report-error: func(error-message: string);
}

world multi-function-device {
    // The component implements the `printer` interface
    export printer;

    // The component implements the `scan` function
    export scan: func() -> list<u8>;

    // The component needs to be supplied with an `error-reporter`
    import error-reporter;
}

This code defines a world called multi-function device, with two exports, a printer interface and a scan function. The exported printer interface is defined in the same file. The imported error-reporter interface is also defined in the same file. From looking at the error-reporter interface, you can see that When a world imports an interface, the full interface with types and function declarations needs to be provided, not just the name of the interface.

Interfaces from other packages

To import and export interfaces defined in other packages, you can use package/name syntax:

world http-proxy {
    export wasi:http/incoming-handler;
    import wasi:http/outgoing-handler;
}

As this example shows, import and export apply at the interface level, not the package level. You can import one interface defined in a package, while exporting another interface defined in the same package. A package groups definitions together; it doesn't describe a coherent set of behaviours.

WIT does not define how packages are resolved; different tools may resolve them in different ways.

Inline interfaces

Interfaces can be declared inline in a world:

world toy {
    export example: interface {
        do-nothing: func();
    }
}

Including other worlds

You can include another world. This causes your world to export all that world's exports, and import all that world's imports.

world glow-in-the-dark-multi-function-device {
    // The component provides all the same exports, and depends on
    // all the same imports, as a `multi-function-device`...
    include multi-function-device;

    // ...but also exports a function to make it glow in the dark
    export glow: func(brightness: u8);
}

As with use directives, you can include worlds from other packages.

Packages

A package is a set of interfaces and worlds, potentially defined across multiple files. To declare a package, use the package directive to specify the package ID. A package ID must include a namespace and name, separated by a colon, and may optionally include a semver-compliant version number:

package documentation:example;
package documentation:example@1.0.1;

All files must have the .wit extension and must be in the same directory. If a package spans multiple files, only one file needs to contain a package declaration, but if multiple files contain package declarations, the package IDs must all match each other. For example, the following documentation:http package is spread across four files:

// types.wit
interface types {
    record request { /* ... */ }
    record response { /* ... */ }
}

// incoming.wit
interface incoming-handler {
    use types.{request, response};
    // ...
}

// outgoing.wit
interface outgoing-handler {
    use types.{request, response};
    // ...
}

// http.wit
package documentation:http@1.0.0;

world proxy {
    export incoming-handler;
    import outgoing-handler;
}

This package defines request and response types in types.wit, an incoming handler interface in incoming wit, an outgoing handler interface in outgoing.wit, and declares the package and defines a world that uses these interfaces in http.wit.

For a more formal definition of the WIT language, take a look at the WIT specification.