DEV Community

Cover image for Writing components in C with WASI SDK - WebAssembly Component Model
Tophe
Tophe

Posted on

Writing components in C with WASI SDK - WebAssembly Component Model

What is WASI-SDK?

Unlike Rust with cargo-component, C/C++ lacks an integrated toolchain for building WebAssembly components. The WASI SDK provides the essential tooling needed to compile C code to WebAssembly.

The WASI SDK includes:

  • clang compiler configured with a WASI sysroot (complete set of target platform headers and libraries) for the wasm32-wasi target
  • WASI-enabled C standard library (libc) that implements WASI interfaces
  • Cross-platform support for different operating systems and architectures
  • Preview 2 compatibility for building modern WebAssembly components

This allows you to write C plugins that can access filesystem, networking, and other system resources through WASI interfaces, just like Rust plugins.

WASI-SDK Setup

The project uses a custom script just dl-wasi-sdk that acts like a package manager, automatically downloading and extracting the correct version of the WASI SDK for your OS/architecture into c_deps/ (which acts like a node_modules for C dependencies).

How to write a C plugin

Build Process

C plugins are built using a two-step process:

  1. Generate bindings: wit-bindgen c ./crates/pluginlab/wit --world plugin-api --out-dir ./c_modules/plugin-name creates the C bindings from your WIT interface
  2. Compile and convert: Use the WASI SDK's clang to compile C code to a WebAssembly module (P1), then convert it to a P2 component

The build process:

  • Compiles component.c, plugin_api.c, and plugin_api_component_type.o to a WebAssembly module with -mexec-model=reactor:
 ./c_deps/wasi-sdk/bin/clang component.c plugin_api.c plugin_api_component_type.o \ -o plugin-name-c.module.p1.wasm -mexec-model=reactor 
Enter fullscreen mode Exit fullscreen mode
  • Converts the P1 module to a P2 component using wasm-tools component new:
 wasm-tools component new plugin-name-c.module.p1.wasm -o plugin-name-c.wasm 
Enter fullscreen mode Exit fullscreen mode

File Structure

The C plugins follow this structure in the repo:

c_deps/ # WASI SDK installation c_modules/ plugin-echo/ # Plugin directory component.c # Your plugin implementation plugin_api.c # Generated bindings (from wit-bindgen) plugin_api.h # Generated header (from wit-bindgen) plugin_api_component_type.o # Generated object file (from wit-bindgen) plugin-echo-c.module.p1.wasm # Compiled WebAssembly module (P1) plugin-echo-c.wasm # Final WebAssembly component (P2) 
Enter fullscreen mode Exit fullscreen mode

Plugin Implementation

The C plugin implements the same interface as the Rust version, with function signatures generated from the WIT interface by wit-bindgen:

  • exports_repl_api_plugin_name() corresponds to fn name() -> String
  • exports_repl_api_plugin_man() corresponds to fn man() -> String
  • exports_repl_api_plugin_run() corresponds to fn run(payload: String) -> Result<PluginResponse, ()>

Here's the key implementation details - plugin-echo/component.c:

#include "plugin_api.h" #include <string.h> #include <stdlib.h>  void exports_repl_api_plugin_name(plugin_api_string_t *ret) { // Populate ret with "echoc" as the plugin name // plugin_api_string_dup() allocates new memory and copies the string plugin_api_string_dup(ret, "echoc"); } void exports_repl_api_plugin_man(plugin_api_string_t *ret) { // Populate ret with the manual text for the echo command // plugin_api_string_dup() allocates new memory and copies the string const char *man_text = "some man text ...\n"; plugin_api_string_dup(ret, man_text); } bool exports_repl_api_plugin_run(plugin_api_string_t *payload, exports_repl_api_plugin_plugin_response_t *ret) { // Set status to success (0 = success, 1 = error) ret->status = REPL_API_TRANSPORT_REPL_STATUS_SUCCESS; // Set stdout to contain the payload // is_some = true means the optional string has a value ret->stdout.is_some = true; // Create a properly null-terminated string from the payload // The payload has ptr and len, we need to ensure it's null-terminated char *temp_str = malloc(payload->len + 1); if (temp_str == NULL) { // Handle allocation failure ret->stdout.is_some = false; ret->stderr.is_some = false; return false; } // Copy the payload data and null-terminate it memcpy(temp_str, payload->ptr, payload->len); temp_str[payload->len] = '\0'; // Use plugin_api_string_dup to create the output string plugin_api_string_dup(&ret->stdout.val, temp_str); // Free our temporary string free(temp_str); // Set stderr to none (no error output) ret->stderr.is_some = false; // Return true for success (false would indicate an error) // This corresponds to Ok(response) in the Rust Result<T, ()> pattern return true; } 
Enter fullscreen mode Exit fullscreen mode

Memory Management Notes:

  • Input parameters (like payload) are owned by the runtime - they MUST NOT be freed by the plugin
  • Output parameters (like ret) are populated by the plugin, freed by the runtime
  • plugin_api_string_dup() allocates new memory for string copies
  • The generated _free functions handle cleanup automatically

Key Differences from Rust:

  • Manual memory management for temporary strings
  • Explicit handling of string length vs null termination
  • Boolean return values instead of Rust's Result<T, ()> pattern
  • Direct manipulation of the generated C structs

📎 Here are links to:

Top comments (0)