C API #

The WebUI FFI (Foreign Function Interface) handler exposes the loaded-protocol rendering pipeline as a C-compatible shared library. Any language with C interop, Go, Python, Ruby, PHP, Lua, and more, can render compiled WebUI applications without a JavaScript runtime. .NET applications should prefer the managed Microsoft.WebUI NuGet package, which restores native runtime packages transitively.

Building the Shared Library #

cargo build -p microsoft-webui-ffi            # debug
cargo build -p microsoft-webui-ffi --release  # release

This produces a shared library:

PlatformLibrary file
macOStarget/release/libwebui_ffi.dylib
Linuxtarget/release/libwebui_ffi.so
Windowstarget/release/webui_ffi.dll

The generated C header is at crates/webui-ffi/include/webui_ffi.h.

Protocol Rendering #

Decode and index protocol.bin once with webui_protocol_create, then use the protocol handle for every operation:

void *handler = webui_handler_create_with_plugin("webui");
webui_handler_set_nonce(handler, "Ep7tTOr+HyRkByAPXxZ9ag==");

uint8_t *data = load_file("dist/protocol.bin", &len);
webui_protocol_t *protocol = webui_protocol_create(data, len);
if (protocol == NULL) {
    fprintf(stderr, "Protocol error: %s\n", webui_last_error());
    webui_handler_destroy(handler);
    return;
}

char *html = webui_handler_render(
    handler, protocol, state_json, "index.html", request_path
);
if (html) {
    webui_free(html);
}

webui_protocol_destroy(protocol);
webui_handler_destroy(handler);

Loaded protocol handles are thread-safe. Handler instances are safe for concurrent renders as long as configuration such as the nonce is not mutated concurrently.

C API Reference #

The generated C header is at crates/webui-ffi/include/webui_ffi.h.

webui_free #

void webui_free(char *string_ptr);

Free a string returned by a WebUI protocol operation such as webui_handler_render. Passing NULL is a safe no-op.

webui_last_error #

const char *webui_last_error();

Return the last error message for the current thread, or NULL if no error has occurred. Call this after any function returns NULL to get a human-readable diagnostic.

  • The returned pointer is owned by the library. Do not free it.
  • The pointer is valid until the next FFI call on the same thread.
  • Each thread has its own independent error state.

webui_handler_create #

void *webui_handler_create();

Create a reusable handler instance. Returns an opaque pointer that must eventually be freed with webui_handler_destroy. Use this with webui_handler_render when rendering pre-compiled protobuf protocols.

webui_handler_create_with_plugin #

void *webui_handler_create_with_plugin(const char *plugin_id);

Create a reusable handler instance with a named plugin. Pass NULL for no plugin (equivalent to webui_handler_create). See Plugins for the available identifiers.

  • plugin_id, null-terminated UTF-8 string identifying the plugin, or NULL.
  • Returns an opaque pointer on success, or NULL on error (call webui_last_error() for details).
  • The caller must free the returned pointer with webui_handler_destroy().

webui_handler_destroy #

void webui_handler_destroy(void *handler_ptr);

Destroy a handler instance created by webui_handler_create. Passing NULL is a safe no-op.

webui_handler_set_nonce #

void webui_handler_set_nonce(void *handler_ptr, const char *nonce);

Set the CSP nonce for inline tags on a handler instance. When set, all subsequent renders include nonce="VALUE" on every inline <script> tag emitted during SSR (including the <script type="importmap"> tags that register Module-strategy CSS), and emit a <meta name="webui-nonce" content="VALUE"> tag in the <head>.

  • handler_ptr, pointer returned by webui_handler_create.
  • nonce, null-terminated UTF-8 string (typically a base64-encoded random value), or NULL to clear a previously set nonce.

The nonce is written verbatim โ€” pass the raw base64 string without any encoding. The same value should appear in your Content-Security-Policy header.

::: warning Thread Safety Concurrent render calls are supported after configuration. Do not call webui_handler_set_nonce or webui_handler_destroy while another operation is using the same handler. :::

webui_protocol_create / webui_protocol_destroy #

typedef void webui_protocol_t;

webui_protocol_t *webui_protocol_create(const uint8_t *protocol_data,
                                        uintptr_t protocol_len);
void webui_protocol_destroy(webui_protocol_t *protocol_ptr);

Decode protobuf bytes and build reusable component and route indices. The returned handle is thread-safe and can be shared across requests. Destroy it after every render using it has completed. Passing NULL to webui_protocol_destroy is a safe no-op.

webui_handler_render #

char *webui_handler_render(void *handler_ptr,
                           const webui_protocol_t *protocol_ptr,
                           const char *data_json,
                           const char *entry_id,
                           const char *request_path);

Render a protocol handle created by webui_protocol_create with JSON state data.

  • handler_ptr, pointer returned by webui_handler_create.
  • protocol_ptr, pointer returned by webui_protocol_create.
  • data_json, null-terminated UTF-8 JSON string with the render state.
  • entry_id, null-terminated UTF-8 string identifying the entry fragment (e.g., "index.html").
  • request_path, null-terminated UTF-8 string with the request path for route matching (e.g., "/users/42").
  • Returns a heap-allocated string on success, or NULL on error.
  • The caller must free the returned string with webui_free().

Partial, component-template, and token helpers #

FunctionResult
webui_protocol_render_partial(...)Complete JSON partial response containing active-route projected state, templates, inventory, path, and route chain
webui_protocol_render_component_templates(...)Requested component template payloads and updated inventory
webui_protocol_tokens(...)Newline-delimited CSS token names

These functions all accept a protocol handle from webui_protocol_create. The partial function validates state_json, skips unselected values without materializing them, and copies only raw values required by authored components on the active route.

The explicit create/destroy pair is the C representation of the normal Protocol object lifecycle. C cannot safely infer ownership from a raw (pointer, length) input: callers may mutate or free the bytes, pointer identity is not content identity, and hashing or copying on every request would erase the startup-only performance model.

Error Handling #

The FFI uses thread-local error storage following the POSIX dlerror() pattern:

  1. Any function that can fail returns NULL on error.
  2. Call webui_last_error() immediately after to get a human-readable message.
  3. The error pointer is valid until the next FFI call on the same thread.
  4. Each thread has independent error state, safe for concurrent use.
char *result = webui_handler_render(
    handler, protocol, json, "index.html", request_path
);
if (result == NULL) {
    const char *err = webui_last_error();  // valid until next FFI call
    fprintf(stderr, "Render failed: %s\n", err);
    // do NOT free err
}

Memory Management #

Two rules to remember:

  1. Free what you receive. Every non-NULL string returned by a render or protocol operation is heap-allocated. You must free it with webui_free().
  2. Don't free error strings. The pointer from webui_last_error() is owned by the library. It remains valid until your next FFI call on the same thread.
Pointer sourceWho frees it?How?
webui_handler_renderCallerwebui_free(ptr)
Partial, component-template, and token stringsCallerwebui_free(ptr)
webui_last_errorLibrary (do not free)Replaced on next call
webui_handler_createCallerwebui_handler_destroy(ptr)
webui_handler_create_with_pluginCallerwebui_handler_destroy(ptr)
webui_protocol_createCallerwebui_protocol_destroy(ptr)

Using Plugins #

Pass a plugin identifier string to webui_handler_create_with_plugin:

// Create handler with a hydration plugin
void *handler = webui_handler_create_with_plugin("webui");
if (handler == NULL) {
    printf("Error: %s\n", webui_last_error());
    return 1;
}

// Render, output includes hydration markers
void *protocol = webui_protocol_create(protocol_data, protocol_len);
char *html = webui_handler_render(
    handler, protocol, state_json, "index.html", "/"
);

webui_free(html);
webui_protocol_destroy(protocol);
webui_handler_destroy(handler);

Pass NULL for no plugin (equivalent to webui_handler_create). See Plugins for the available identifiers.

Python #

Python's built-in ctypes module can load the shared library directly. No pip packages needed.

import ctypes
from pathlib import Path
from ctypes import c_char_p, c_size_t, c_uint8, c_void_p, POINTER

lib = ctypes.cdll.LoadLibrary("./target/debug/libwebui_ffi.dylib")  # or .so / .dll

lib.webui_protocol_create.argtypes = [POINTER(c_uint8), c_size_t]
lib.webui_protocol_create.restype = c_void_p
lib.webui_protocol_destroy.argtypes = [c_void_p]
lib.webui_handler_create.restype = c_void_p
lib.webui_handler_destroy.argtypes = [c_void_p]
lib.webui_handler_render.argtypes = [
    c_void_p, c_void_p, c_char_p, c_char_p, c_char_p
]
lib.webui_handler_render.restype = c_void_p
lib.webui_free.argtypes = [c_void_p]
lib.webui_last_error.restype = c_char_p

protocol_bytes = Path("dist/protocol.bin").read_bytes()
buffer = (c_uint8 * len(protocol_bytes)).from_buffer_copy(protocol_bytes)
protocol = lib.webui_protocol_create(buffer, len(protocol_bytes))
handler = lib.webui_handler_create()

ptr = lib.webui_handler_render(
    handler,
    protocol,
    b'{"title":"Groceries"}',
    b"index.html",
    b"/",
)

if ptr is None or ptr == 0:
    print("Error:", lib.webui_last_error().decode("utf-8"))
else:
    result = ctypes.cast(ptr, c_char_p).value.decode("utf-8")
    lib.webui_free(ptr)
    print(result)

lib.webui_protocol_destroy(protocol)
lib.webui_handler_destroy(handler)

Why c_void_p? Using c_void_p as the return type instead of c_char_p prevents ctypes from automatically converting the pointer to a Python bytes object. This lets you copy the string first, then explicitly free the original pointer with webui_free().

Go #

Go's cgo lets you call C functions directly. Link against libwebui_ffi and use C strings with standard lifecycle management.

package main

// #cgo LDFLAGS: -L./target/debug -lwebui_ffi
// #include <stdlib.h>
// #include "webui_ffi.h"
import "C"
import (
	"fmt"
	"os"
	"unsafe"
)

func render(protocol *C.webui_protocol_t, handler unsafe.Pointer, dataJSON string) (string, error) {
	cJSON := C.CString(dataJSON)
	defer C.free(unsafe.Pointer(cJSON))
	cEntry := C.CString("index.html")
	defer C.free(unsafe.Pointer(cEntry))
	cPath := C.CString("/")
	defer C.free(unsafe.Pointer(cPath))

	ptr := C.webui_handler_render(handler, protocol, cJSON, cEntry, cPath)
	if ptr == nil {
		return "", fmt.Errorf("render failed: %s", C.GoString(C.webui_last_error()))
	}
	defer C.webui_free(ptr)

	return C.GoString(ptr), nil
}

func main() {
	bytes, err := os.ReadFile("dist/protocol.bin")
	if err != nil || len(bytes) == 0 {
		panic("protocol.bin is missing or empty")
	}
	protocol := C.webui_protocol_create(
		(*C.uint8_t)(unsafe.Pointer(&bytes[0])),
		C.uintptr_t(len(bytes)),
	)
	defer C.webui_protocol_destroy(protocol)
	handler := C.webui_handler_create()
	defer C.webui_handler_destroy(handler)

	result, err := render(protocol, handler, `{"title":"Groceries"}`)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(result)
}

Memory note: C.GoString(ptr) copies the string into Go-managed memory, so it's safe to call webui_free immediately after.

C# #

For .NET applications, prefer the managed Microsoft.WebUI NuGet package. It wraps the same opaque native handles in SafeHandle types:

using var protocol = new Protocol(File.ReadAllBytes("dist/protocol.bin"));
using var handler = new WebUIHandler("webui");
string html = handler.Render(
    protocol,
    """{"title":"Groceries"}""",
    "index.html",
    "/");

Custom P/Invoke bindings should mirror this lifecycle and receive returned strings as IntPtr, copy them with Marshal.PtrToStringUTF8, then release them with webui_free.

Other Languages #

Any language with C FFI support can use WebUI. The pattern is always the same:

  1. Load the shared library (libwebui_ffi.dylib / .so / .dll).
  2. Declare the functions you need. For a server, use webui_protocol_create, webui_handler_render, webui_protocol_destroy, webui_free, and webui_last_error.
  3. Pass UTF-8 null-terminated strings for html and data_json.
  4. Check the return value, NULL means an error occurred.
  5. Copy the returned string into your language's managed memory, then call webui_free.

Next Steps #

  • Plugins, Plugin system and built-in plugin reference
  • CLI Reference, Building protocols with webui build