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 coding language and doesn't define behaviour; it defines only contracts between components. 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.
- An Overview of WIT
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 */
.
- Unlike the C++ family, block comments can be nested, e.g.
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
WIT identifiers have a slightly different set of rules from what you might be familiar with from, say, C, Rust, or Java. These rules apply to all names - types, functions, interfaces, and worlds. (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.
- Double hyphens (
- 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.
- This is required if the identifier would otherwise be a WIT keyword. For example,
- 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 allUPPERCASE
.- Different words in the identifier may have different cases. For example,
WIT-demo
is allowed.
- Different words in the identifier may have different cases. For example,
- 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:
Identifier | Description |
---|---|
bool | Boolean value true or false . |
s8 , s16 , s32 , s64 | Signed integers of the appropriate width. For example, s32 is a signed 32-bit integer. |
u8 , u16 , u32 , u64 | Unsigned integers of the appropriate width. For example, u32 is an unsigned 32-bit integer. |
f32 , f64 | Floating-point numbers of the appropriate width. For example, f64 is a 64-bit (double precision) floating-point number. See the note on NaN s below. |
char | Unicode character. (Specifically, a Unicode scalar value.) |
string | A Unicode string - that is, a finite sequence of characters. |
The
f32
andf64
types support the usual set of IEEE 754 single and double-precision values, except that they logically only have a singlenan
value. The exact bit-level representation of an IEEE 754NaN
is not guaranteed to be preserved when values pass through WIT interfaces as the singular WITnan
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, allowing 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). This is typically used for "value or error" situations; 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 it so that there is a common way of expressing it, so that you don't need to create a variant type 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
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 their order 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
You can define your own types within an interface
or world
. WIT offers several ways of defining new types.
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 struct
s.
User-defined records can't be generic (that is, parameterised by type). Only built-in types can be generic.
Variants
A variant
type declares one or more cases. Each case has a name and, optionally, a type of data associated with that case. A variant instance contains exactly one case. Cases are separated by commas. The syntax is as follows:
variant allowed-destinations {
none,
any,
restricted(list<address>),
}
Variants are similar to Rust enum
s or OCaml discriminated unions. The closest C equivalent is a tagged union, but WIT both takes care of the "tag" (the case) and enforces 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++ enum
.
Resources
Resources are handles to some entity that lives outside of the component. They describe things that can't or shouldn't be copied by value; instead, their ownership or reference can be passed between two components via a handle. 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.
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
(often calledthis
in many languages) parameter that is a handle - static functions: functions which do not have an implicit
self
parameter but are meant to be nested in the scope of the resource type - at most one constructor: a function that is syntactic sugar for a function returning a handle of the containing resource type
Methods always desugar to a borrowed self
parameter whereas constructors
always desugar to an owned return value. 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.
More precisely, these are borrowed or owned
handles
of the resource. Learn more abouthandles
in the upstream component model specification.
Flags
A flags
type is a set of named booleans. In an instance of the type, each flag will be either true
or false
.
flags allowed-methods {
get,
post,
put,
delete,
}
A
flags
type is logically equivalent to a record type where each field is of typebool
, but it is represented more efficiently (as a bitfield) at the binary level.
Type aliases
You can define a new named type using type ... = ...
. This can be 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. Like in record fields, the name is separated from the type by a colon:
do-nothing: func();
The function type is the word 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>;
A function can have multiple return values. In this case the return values must be named, similar to the parameter list. All return values must be populated (in the same way as tuple or record fields).
get-customers-paged: func(cont: continuation-token) -> (customers: list<customer>, cont: continuation-token);
A function can be declared as part of 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 items 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);
}
Even if you are only using one type, it must still be enclosed in braces. For example,
use types.{dimension}
is legal butuse 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
A world describes a set of imports and exports, enclosed in braces and introduced with the world
keyword. Roughly, a world describes the contract of a component. Exports are provided by the component, and define what consumers of the component may call; imports are things the component may call. The imports and exports may be interfaces or individual functions.
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;
}
Interfaces from other packages
You can import and export interfaces defined in other packages. This can be done using 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. Packages group definitions; they don't represent behaviour.
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. This must include a namespace and name, separated by a colon, and may optionally include a semver-compliant version:
package documentation:example;
package documentation:example@1.0.1;
If a package spans multiple files, only one file needs to contain a package declaration (but if multiple files contain declarations then they must all be the same). All files must have the .wit
extension and must be in the same directory. 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;
}
For a more formal definition of the WIT language, take a look at the WIT specification.