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 thewasm32-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:
- 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 - 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
, andplugin_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
- 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
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)
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 tofn name() -> String
-
exports_repl_api_plugin_man()
corresponds tofn man() -> String
-
exports_repl_api_plugin_run()
corresponds tofn 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; }
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:
- the plugin-echo C implementation and the plugin_api.h header file
- PR#6 - Add C Language Plugin Support
Top comments (0)