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)