Prompt and context
A platform must load plugins compiled from different languages and compose them in multiple runtimes. Explain how to define interfaces in WIT, compose components, handle strings and resources, and test Canonical ABI, errors, and version evolution.
What the interviewer is testing
- Distinguishing the WebAssembly core-module layer from the Component Model interface layer.
- Understanding WIT as a language-neutral interface description and the Canonical ABI as the cross-component representation of high-level types.
- Explaining resource ownership, borrowing, result errors, and asynchronous lifetimes.
- Accounting for component versions, capability isolation, adapters, runtime differences, and compatibility tests.
Clarifying questions to ask
- Which types cross the plugin boundary: large files, streams, handles, or long-lived resources?
- Which WASI capabilities does the runtime grant, and must plugins be isolated from files, networks, or keys?
- Are calls synchronous or asynchronous, and should errors recover, retry, or terminate a component?
- How are WIT versions published, can an old component compose with a new host, and who owns the compatibility matrix?
A 30-second answer
I would write the smallest cross-language contract in WIT and generate bindings for each language. Components exchange strings, lists, results, and resources through Component Model interfaces and the Canonical ABI rather than exposing a language's memory layout. The runtime grants only required file or network capabilities; the host owns cancellation, deadlines, error mapping, and cleanup. Version evolution uses additive fields and compatibility tests, while core-module ABI details remain implementation-specific.
Step-by-step deep dive
1. Separate core modules from the component interface
Core modules expose linear memory, functions, and numeric types. The Component Model builds higher-level interfaces, dependencies, and composition on top. A cross-language API should stop at the component interface, not expose a compiler's memory address, struct layout, or exported function name as a public contract.
2. Express language-neutral types in WIT
WIT describes interfaces, worlds, resources, and result errors; tools generate bindings for Rust, Go, and other languages. This example shows a resource handle; validate the exact syntax with the target toolchain:
package example:plugin;
interface store {
resource session;
open: func(name: string) -> result<session, string>;
read: func(s: borrow<session>) -> result<list<u8>, string>;
}The interface exposes business semantics while storage, threading, and allocation remain implementation details.
3. Understand the Canonical ABI and resource lifetimes
The Canonical ABI specifies how strings, lists, results, and other high-level values cross a component boundary using core WebAssembly representations. The component model manages ownership and borrowing of resources; the host must define closing, cancellation, and cleanup. A numeric handle is not an immortal pointer. For large data, evaluate copying, streaming, and backpressure costs.
4. Design errors, capabilities, and versions
Model expected business failures as result values or explicit error enums, distinguishing retryable errors from authorization failures. Grant only required WASI capabilities and bound execution time, memory, concurrency, and output size. Evolve WIT additively where possible, preserve compatibility tests for old worlds, and use adapters for version conversion without hiding semantic changes.
Model answer
I would define the smallest language-neutral world interface in WIT and generate bindings. Components compose through the Component Model, while the Canonical ABI represents strings, lists, results, and resources at the boundary; core-module memory and exports remain implementation details. Resources use borrowing and explicit close semantics, with the host controlling cancellation, deadlines, capabilities, and cleanup. Errors use distinguishable result types and capabilities follow least privilege. Every WIT change runs a cross-language compatibility matrix, with adapters handling version conversion so old components are not silently bound to new semantics.
Common mistakes
- Sharing core-module linear memory or language structs directly and bypassing the component interface.
- Treating WIT as one language's header file and ignoring generated bindings and type semantics.
- Treating a resource handle as a raw pointer without borrow, close, cancellation, and cleanup rules.
- Testing only the happy path and omitting result errors, backpressure, deadlines, and denied capabilities.
- Granting a plugin the entire filesystem or network instead of a least-privilege boundary.
- Compiling only the host after a WIT change and skipping old-component/new-host compatibility tests.
Follow-up questions and responses
When would you use a stream instead of a byte list?
Use a stream when data can be large, must be processed incrementally, or needs a bounded memory peak. Define backpressure, completion, and cancellation. A small bounded configuration or result can use a list after validating size and lifetime.
How would you stop a malicious plugin consuming resources?
Limit capabilities, memory, execution time, concurrency, and output size at the runtime, and provide host cancellation and isolation. Monitor call duration, error rate, and quotas; terminate and clean up on a breach.
How do WIT changes remain compatible?
Prefer optional interfaces or fields and preserve the meaning of existing types. Keep old world versions and cross-language tests, using adapters when needed. Removing or changing semantics requires a new version and a migration deadline.