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)