Building WASI Preview 2 Components from C
This page answers one task: you have C code — a library or a small service — and want to ship it as a WebAssembly component with a typed WIT interface, runnable in Wasmtime or any Component Model host, rather than as a core module with raw pointer-and-length exports.
Prerequisites
- [ ] A recent wasi-sdk (version 22 or later), which includes the
wasm32-wasip2target. - [ ]
wit-bindgenCLI andwasm-toolsinstalled (cargo install wit-bindgen-cli wasm-tools). - [ ] Wasmtime for running components.
Core modules versus components
A core WebAssembly module exports functions on numbers; strings, lists and records travel as pointers into linear memory by private convention. A
component wraps one or more core modules with a typed interface described in WIT (WebAssembly Interface Types): functions take string, list<u8>,
records, variants and results, and the canonical ABI defines exactly how those values are lowered into and lifted out of linear memory. Hosts and other
components call the component through that interface without knowing its language or memory layout.
WASI Preview 2 is defined in WIT too: wasi:cli, wasi:filesystem, wasi:clocks, wasi:random, wasi:sockets, wasi:http. A component targeting
Preview 2 imports these interfaces instead of the older wasi_snapshot_preview1 functions. For C, the toolchain pieces are: wasi-sdk to compile, wit-bindgen
to generate C glue for your WIT world, and wasm-tools to turn the core module into a component (wasi-sdk’s wasm32-wasip2 target runs this step for you
through its linker wrapper).
Step 1 — describe the world in WIT
// wit/world.wit
package acme:textstats@0.1.0;
interface stats {
record summary {
words: u32,
lines: u32,
longest-word: string,
}
analyse: func(text: string) -> summary;
}
world textstats {
export stats;
}
The world says what the component exports (here, the stats interface) and imports (nothing beyond what the WASI adapter needs). Keep the WIT small and
typed: it is the contract other components and hosts compile against.
Step 2 — generate C bindings
wit-bindgen c wit/ --out-dir gen/
ls gen/
# textstats.c textstats.h textstats_component_type.o
The header declares the C types for WIT types — textstats_string_t with a pointer and length, exports_acme_textstats_stats_summary_t for the record —
and the prototype of each export you must implement. The generated .c contains the canonical ABI glue: lifting arguments from memory, calling your
function, lowering results, and the cabi_realloc function hosts use to allocate memory inside the component. The _component_type.o object embeds the
WIT description in a custom section so the linker can produce a component.
Step 3 — implement the exports
#include "gen/textstats.h"
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
void exports_acme_textstats_stats_analyse(textstats_string_t *text,
exports_acme_textstats_stats_summary_t *ret) {
uint32_t words = 0, lines = 0, best = 0, cur = 0;
const uint8_t *s = text->ptr, *best_start = s, *cur_start = s;
for (size_t i = 0; i < text->len; i++) {
if (s[i] == '\n') lines++;
if (isspace(s[i])) {
if (cur > best) { best = cur; best_start = cur_start; }
cur = 0;
} else {
if (cur == 0) { words++; cur_start = s + i; }
cur++;
}
}
if (cur > best) { best = cur; best_start = cur_start; }
ret->words = words;
ret->lines = lines + (text->len > 0);
textstats_string_dup_n(&ret->longest_word, (const char *)best_start, best);
}
Strings arrive as UTF-8 bytes with a length — not NUL-terminated. Ownership follows the generated header’s comments: the argument string is owned by your
function and should be freed with the generated free helper when you are done with it (omitted above for brevity in a short-lived call); returned strings
must be allocated with malloc (the _dup helpers do this), because the glue frees them after lowering.
Step 4 — compile and link to a component
export WASI_SDK=/opt/wasi-sdk
$WASI_SDK/bin/clang --target=wasm32-wasip2 -O2 -mexec-model=reactor \
src/stats.c gen/textstats.c gen/textstats_component_type.o \
-o textstats.wasm
wasm-tools component wit textstats.wasm # prints the component's world
wasm-tools validate textstats.wasm
With the wasm32-wasip2 target, wasi-sdk’s linker wrapper (wasm-component-ld) links the core module and wraps it as a component in one step.
-mexec-model=reactor builds a library-style component without a main. On an older wasi-sdk with only wasm32-wasip1, compile to a core module and
run wasm-tools component new core.wasm --adapt wasi_snapshot_preview1.reactor.wasm -o textstats.wasm, using the adapter that translates Preview 1 calls
to Preview 2 imports.
Step 5 — call it from a host
A Rust host with Wasmtime can generate bindings from the same WIT and call the component with native types:
wasmtime::component::bindgen!({ world: "textstats", path: "wit" });
let engine = Engine::default();
let component = Component::from_file(&engine, "textstats.wasm")?;
let mut linker = Linker::<MyState>::new(&engine);
wasmtime_wasi::add_to_linker_sync(&mut linker)?;
let mut store = Store::new(&engine, MyState::new());
let bindings = Textstats::instantiate(&mut store, &component, &linker)?;
let s = bindings.acme_textstats_stats().call_analyse(&mut store, "hello wide world")?;
println!("{} words, longest: {}", s.words, s.longest_word);
In JavaScript, jco transpile textstats.wasm produces an ES module that runs the component in browsers and Node, with analyse taking and returning
ordinary JavaScript values; see
generating JavaScript bindings with jco.
Building a command component instead
For a program with main — a CLI tool — compile without -mexec-model=reactor. The result exports wasi:cli/run, and wasmtime run tool.wasm args…
executes it, with standard input and output and preopened directories working through Preview 2 interfaces. Existing C programs that use stdio and the
file system often compile unchanged for wasm32-wasip2, because wasi-libc implements POSIX-like functions on top of WASI. Sockets are available through
wasi:sockets in recent wasi-libc for programs that need them, with runtime permission flags.
Memory ownership rules to get right
The canonical ABI is precise about who frees what, and C has no compiler help. Arguments of type string or list passed into your export are
allocated by the glue in your memory and become yours; free them with the generated *_free helpers when you are done. Results you return must be heap
allocated with the same allocator, because the glue’s post-return function frees them after the host has copied them out. Returning a pointer into a
static buffer or a stack array crashes or corrupts memory in the post-return step. When in doubt, read the comments in the generated header, which state
ownership for every function and type.
Comparing with a Rust component
The same world implemented in Rust with wit-bindgen’s Rust macros needs no manual ownership handling, and cargo component or the wasm32-wasip2 Rust
target produce components directly. C makes sense when the logic already exists in C or when the smallest possible component matters — a C component
of this size is around 30 KB, dominated by wasi-libc and the glue. Both produce components that hosts cannot tell apart.
Versioning the interface
The package acme:textstats@0.1.0 line is part of the contract. Hosts and composed components refer to interfaces by package name and version, so a
change to a record’s fields or a function’s signature is a new interface version, not a silent edit. Follow semantic-versioning rules for WIT
packages: adding a new function in a new interface is compatible; changing an existing function’s parameters is breaking and needs a new major version
(or a new minor version during 0.x, which tooling treats as incompatible). Keep the WIT files in a separate directory or repository that both the
component and its hosts depend on, and publish them to a registry such as a wkg-compatible OCI registry when several teams consume them. Generated
bindings should always be regenerated from the pinned WIT version in the build rather than committed by hand, so the C glue cannot drift from the
interface it claims to implement.
Debugging a C component
When a call fails, start outside the component: wasm-tools component wit confirms the world is what the host expects, and wasm-tools validate --features component-model catches malformed output. Inside, the core module can be extracted with wasm-tools component unbundle or inspected with
wasm-tools print. Compile with -g to keep DWARF, and Wasmtime’s -D debug-info option lets a native debugger such as LLDB step through the C source
while the host runs the component. Traps in the generated glue — usually a bad pointer returned from your function — show up with the glue’s function
names in the backtrace, which points directly at the ownership rules above.
Expected output
wasm-tools component wit textstats.wasm prints the textstats world exporting acme:textstats/stats; validation passes; the Rust host prints
3 words, longest: hello; and the component is 31 KB, or 12 KB after wasm-opt -Oz on the inner core module.
Gotchas
- Treating WIT strings as NUL-terminated. They carry a length. Never call
strlenon them. - Returning static or stack memory. The glue frees results. Allocate them with
malloc. - Forgetting
-mexec-model=reactorfor libraries. The component expectsmain. - Mismatched wit-bindgen and runtime versions. Interface versions must match. Pin both.
- Old wasi-sdk without
wasm32-wasip2. Use a core module plus the Preview 1 adapter.
Performance note
Calling analyse on a 10 KB string through the component boundary in Wasmtime took 6.1 µs, of which about 0.8 µs was the canonical ABI copying the string
in and the result out; the rest was the C loop.
Frequently Asked Questions
Can C++ be used the same way?
Yes — wit-bindgen’s C output works from C++, and wasi-sdk compiles C++ for wasm32-wasip2.
Do I need the adapter with a new wasi-sdk?
No — the wasm32-wasip2 target links against Preview 2 directly.
Can the browser run this component?
Through jco transpile, which converts it into core modules plus JavaScript glue.
How do I import host functions?
Declare an import in the world; wit-bindgen generates C prototypes you call.
How do I debug a crash inside the component?
Build with -g, run Wasmtime with debug info enabled, and attach LLDB; traps in the generated glue usually mean a returned pointer broke ownership rules.
Related
- Building C code with the WASI SDK — the Preview 1 basics.
- WASI Preview 1 vs Preview 2 differences — what changed.
- Embedding Wasmtime in a Rust application — the host side.
- Using sockets from WASI — networking in Preview 2.
← Back to WASI Target Builds & Runtimes