WIT By Example
This section includes two examples to introduce WIT: a simpler "clocks" example and a more complicated "filesystems" example. For a full WIT reference, see the next section.
Clocks
The following is a simplified version of the world defined in the wasi:clocks package.
Suppose we want to write a component that provides clock functionality.
This component will represent a "wall clock", which can be reset
(the clock is not monotonic).
(The real wasi:clocks
package provides two interfaces,
one for a wall clock and one for a monotonic clock.)
Declaring a world
We declare a world that imports one interface:
package wasi-example:clocks;
/// The following is a simplified copy of a world from wasi:clocks.
/// For the full version, see https://github.com/WebAssembly/wasi-clocks/tree/main/wit
world imports {
import wall-clock;
}
For exposition, version numbers have been removed.
This file contains a package declaration, which declares that
this world is in the clocks
package in the wasi
namespace.
The world is declared using the keyword world
, followed by
the name imports
.
World declarations must begin with world
, but the name imports
is an arbitrary choice.
What follows is a list of import
declarations enclosed in curly braces,
each of which consists of the import
keyword
followed by the name of an interface.
Each declaration is followed by a semicolon.
Declaring an interface: wall-clock
package wasi-example:clocks;
/// The following is a simplified copy of an interface from wasi:clocks.
/// For the full version, see https://github.com/WebAssembly/wasi-clocks/tree/main/wit
interface wall-clock {
record datetime {
seconds: u64,
nanoseconds: u32,
}
now: func() -> datetime;
}
Like a world, an interface is declared with a keyword (interface
) in this case,
followed by a name, followed by a semicolon-separated list of declarations enclosed
in curly braces.
In this case, declarations are type declarations or function declarations.
Type declarations
Record types are one of the possible types that can be declared in WIT.
record datetime {
seconds: u64,
nanoseconds: u32,
}
The record
keyword is followed by a name, then by a list of
field declarations separated by commas.
Each field declaration is a field name (a string), followed by
a colon, followed by a type name.
A record is analogous to a struct
in C or Rust,
in that it groups together named fields.
It is also analogous to a JavaScript object, except
that it has no methods or prototype.
In short, the datetime
type is a record with two fields:
seconds
, an unsigned 64-bit integer, and nanoseconds
,
an unsigned 32-bit integer.
Function declarations
The following declares a function named now
:
now: func() -> instant;
The empty parentheses ()
indicate that the function has no arguments.
The return type is the type after the final arrow (->
),
which is instant
.
Putting it together: now()
is a nullary function that returns an instant.
Summing up
The imports
world contains an interface for wall clocks.
(Real worlds usually contain multiple interfaces.)
The wall clock world defines a record type that represents a time value
in terms of seconds and nanoseconds,
as well as a function to get the current time.
WIT By Example: Filesystems
That was just a warm-up; let's look at an example that uses more of WIT's built-in and user-defined types.
The following is a very simplified version of the main interface defined in the wasi-filesystem package. Much of the functionality has been removed. Here, a file descriptor supports just two operations:
open-at()
: Open a file.read()
: Read from a file, starting at a particular offset.
package wasi-example:filesystem;
/// The following is a simplified copy of an interface from wasi:filesystems.
/// For the full version, see https://github.com/WebAssembly/wasi-filesystem/tree/main/wit
interface types {
enum error-code {
access,
bad-descriptor,
}
resource descriptor {
read: func(
length: filesize,
offset: filesize,
) -> result<tuple<list<u8>, bool>, error-code>;
open-at: func(
path: string,
) -> result<descriptor, error-code>;
}
}
Let's look at some WIT features used in this interface.
Enums
enum error-code {
access,
bad-descriptor,
}
This declaration defines an enumeration type named error-code
with two alternatives: access
and bad-descriptor
.
The contents of the curly brackets is just a list of comma-separated names.
Enum types are similar to enums in C, and are useful for
expressing types that have a known, small set of values.
This declaration expresses the possible error codes
that filesystem operations can return.
In reality, there are many more possible errors,
which would be expressed by adding more alternatives to the enumeration.
Resources
A resource describes an interface for objects.
This is not the same kind of "interface" as a WIT interface;
a WIT interface can contain many different resource
declarations.
The declaration of the descriptor
resource says that
a descriptor
is an object that implements two methods:
read
and open-at
.
Let's look at the method declarations one at a time:
Reading from files
read: func(
length: filesize,
offset: filesize,
) -> result<tuple<list<u8>, bool>, error-code>;
Method declarations use the same syntax as regular function declarations,
like the ones we already saw in the clocks example.
This declaration says that the read()
method has two arguments,
length
and offset
, both of which have type filesize
.
The return type of read
is a result
.
result
is another parameterized type, like option
.
Let's look at the parameters before we look at the entire type:
list
is also a parameterized type; in this case, it's applied tou8
(unsigned 8-bit integer), solist<u8>
can be read as "list of bytes".tuple
is like a list with a known size, whose elements can have different types.tuple<list<u8>, bool>
represents a 2-tuple (pair) of a list of bytes and a boolean.error-code
was defined as anenum
type.
If a
and b
are both types, then result<a, b>
represents
a type that can be either a
or b
.
Often, but not always, b
is a type that represents an error,
like in this case.
So the type result<tuple<list<u8>, bool>, error-code>
means
"either a tuple of a list of bytes and a bool; or an error code".
This makes sense for the read()
function because it takes a
number of bytes to read and an offset within a file to start at;
and the result is either an error, or a list of bytes containing
the data read from the file,
paired with a boolean indicating whether the end of the file was
reached.
Opening files
The open-at()
method is a constructor, which we know because
it returns a descriptor
when it doesn't fail (remember that
these methods are attached to the resource type descriptor
):
open-at: func(
path: string,
) -> result<descriptor, error-code>;
open-at()
returns a new descriptor, given a path string and flags.
Further reading
We've seen how using rich types, WIT can encode a multitude of ideas about how functions interrelate, which are not available in the type system of core WebAssembly.
For more WIT examples, see the tutorial section. The next section, WIT Reference, covers WIT syntax more thoroughly.