Skip to content

WIT Primer

WIT (WebAssembly Interface Types) is the interface definition language of the WebAssembly Component Model. Think of it as the equivalent of Protocol Buffers for WebAssembly: it describes the functions, types, and error shapes that two sides of a boundary exchange, without prescribing the implementation language on either side.

For Torchsnap, WIT is the single source of truth for the host-gadget boundary. The host uses it to generate the Rust code that calls into gadgets. Gadgets use it (through the SDK or directly) to generate the code that implements those calls and reaches back into the host. Both sides agree on the same types, the same function signatures, and the same error variants because they are generated from the same file.

This page covers the WIT syntax itself and how bindings are generated from the contract. If you are already familiar with WIT and want to jump straight to the available APIs, see the Imports and Exports reference pages, which document every interface with its WIT definition, SDK helpers, and code examples.

WIT organizes contracts into worlds. A world declares which interfaces the component imports (calls into) and which it exports (implements). The Torchsnap gadget contract defines a single world called gadget:

world gadget {
// Host capabilities the gadget can call
import logging;
import clipboard;
import sql-storage;
// ... (14 imports total)
// Interfaces the gadget must implement
export lifecycle;
export search;
export messaging;
export tasks;
}

The import and export lines reference named interfaces defined elsewhere in the same file. What each interface contains is covered in the sections below.

An interface groups related functions and types. Functions are declared with func, listing their parameters and return type:

interface clipboard {
write-text: func(text: string) -> result<_, clipboard-error>;
}

This declares a clipboard interface with one function, write-text, that takes a string and returns either success or a clipboard-error.

A record is a named struct with typed fields:

record action {
id: action-id,
label: string,
}

A variant is a tagged union. Each case can optionally carry data:

variant entry-icon {
hero-icon(string),
data-url(string),
asset-icon(string),
emoji(string),
}

Each case names the kind of icon and carries the value (a Heroicons name, a base64 data URL, a bundled asset path, or an emoji character).

An enum is a variant where no case carries data:

enum post-action { nothing, dismiss, keep-open }

A resource is an opaque handle with methods. The host creates it, the gadget holds a reference and calls methods on it:

resource sql-handle {
execute: func(sql: string, params: list<sql-value>) -> result<u64, string>;
query: func(sql: string, params: list<sql-value>) -> result<list<list<sql-value>>, string>;
}

The gadget obtains an sql-handle by calling connection() and then uses execute and query on it. The host manages the underlying database connection behind the handle.

WIT provides a set of generic types used throughout the contract:

  • option<T>: a value that may be absent (like Rust’s Option or TypeScript’s T | undefined)
  • result<T, E>: success or failure (like Rust’s Result)
  • list<T>: a variable-length sequence
  • tuple<T, U>: a fixed-size pair (or triple, etc.)
  • string, bool, u8, u16, u32, u64, s32, s64, f64: scalar types

The use keyword imports types from another interface so they can be shared:

interface search {
use types.{entry-icon, action-id, action};
// ...
}

This allows search to reference entry-icon, action-id, and action without redefining them. The types interface exists specifically to hold shared type definitions used by both imports and exports.

The WIT file is language-agnostic. Any language that can compile to wasm32-wasip2 and produce a valid WebAssembly component can implement the gadget contract. What differs between languages is how you turn the WIT definitions into callable code.

For Rust, the wit-bindgen project provides a procedural macro that reads the WIT file at compile time and generates Rust traits, structs, and function stubs matching the contract. The torchsnap-gadget-sdk crate runs this generation once:

// Inside torchsnap-gadget-sdk (gadgets/gadget-sdk/src/lib.rs)
wit_bindgen::generate!({
path: "wit",
world: "gadget",
pub_export_macro: true,
default_bindings_module: "::torchsnap_gadget_sdk",
});

This produces the four guest traits (LifecycleGuest, SearchGuest, MessagingGuest, TasksGuest), all the WIT record and variant types (CatalogEntry, ScoredEntry, SearchResponse, EntryIcon, etc.), and the host import modules (clipboard, http, opener, and so on). The SDK re-exports everything through its prelude, so your gadget code only needs:

use torchsnap_gadget_sdk::prelude::*;

On top of the raw generated bindings, the SDK adds convenience helpers like typed settings access, SQL row parsing, and structured logging. These are covered in the Imports reference alongside the underlying WIT interfaces.

You do not need to run wit-bindgen yourself or reference the WIT file directly when using the SDK. The bindings are pre-generated inside the crate.

The complete interface definition for the gadget world. This is the source of truth for what gadgets must implement and what the host provides. The canonical copy lives at gadgets/gadget-sdk/wit/torchsnap-gadget.wit in the Torchsnap repository.