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.

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.

Reserved $webui state channel #

A top-level $webui object in the render state JSON passed to webui_handler_render (or a streaming session) may carry headEnd, bodyStart, and bodyEnd strings, each emitted raw at the matching structural boundary (before </head>, after <body>, before </body>):

{"$webui": {"headEnd": "<meta name=\"x\">", "bodyEnd": "<script src=\"/a.js\"></script>"}}

Members that are missing, null, empty, or not strings are ignored rather than an error. The $webui key is stripped from the client hydration payload, so it never reaches the DOM. No extra API call is needed โ€” it travels on the state JSON hosts already send.

Safety. The values are written verbatim with no escaping, exactly like the Rust head_inject / body_inject options. Never let untrusted request input reach the $webui key.

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.

Progressive streaming sessions #

A streaming session lets a C host render one response in chunks it writes itself. Every chunk function returns a heap byte pointer plus its length; WebUI never touches your socket, so backpressure and cancellation stay yours.

webui_streaming_session_t *session = webui_streaming_session_create(
    handler, protocol, "index.html", "/", NULL, NULL, NULL);

uint32_t rows = 0;
if (!webui_streaming_session_boundary(session, "rows", &rows)) {
    fprintf(stderr, "%s\n", webui_last_error());  /* lists the valid names */
}

size_t len = 0;
uint8_t *chunk = webui_streaming_session_write_shell(session, "{}", &len);
if (chunk == NULL) {
    fprintf(stderr, "%s\n", webui_last_error());
} else {
    send_all(socket, chunk, len);
    webui_free(chunk);
}

/* ... write_boundary / update ... then: */
chunk = webui_streaming_session_finish(session, "{}", &len);
/* send + free */
webui_streaming_session_destroy(session);
FunctionResult
webui_streaming_session_create(handler, protocol, entry_id, request_path, nonce, head_inject, body_inject)Session handle, or NULL. The last three arguments accept NULL.
webui_streaming_session_destroy(session)Releases the session. NULL is a safe no-op.
webui_streaming_session_boundary(session, name, out_id)true plus the integer handle, or false and an error listing the valid names
webui_streaming_session_boundary_count(session)Boundaries declared by the entry
webui_streaming_session_is_finished(session)Whether the terminal record was written
webui_streaming_session_write_shell(session, state_json, out_len)Document prefix through the first semantic flush
webui_streaming_session_write_boundary(session, id, state_json, mode, out_len)One boundary's markup and checkpoint. mode is 0 final, 1 updatable.
webui_streaming_session_update(session, id, state_json, out_len)Projected state patch for an updatable boundary
webui_streaming_session_finish(session, state_json, out_len)Tail checkpoint, terminal record, and document suffix

Chunks are binary-safe. Always use *out_len. Chunks are not NUL-terminated, and a checkpoint payload may legitimately contain a zero byte. Free every non-NULL chunk with webui_free.

The session clones its own references to the handler and protocol, so you may destroy them in any order. A rejected call returns NULL but leaves the session usable, so bad state input does not cost you the response. See Streaming Boundaries for the authoring side and the ordering rules.

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)
Streaming session chunksCallerwebui_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)
webui_streaming_session_createCallerwebui_streaming_session_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.

The package also wraps the streaming session, so an ASP.NET endpoint can pace a progressive response without touching the native ABI:

using var session = handler.StreamResponse(protocol, "index.html", "/");
uint rows = session.Boundary("rows");

Response.ContentType = "text/html; charset=utf-8";
await Response.Body.WriteAsync(session.WriteShell("{}"));
await Response.Body.FlushAsync();

await Response.Body.WriteAsync(session.WriteBoundary(rows, await LoadRowsAsync()));
await Response.Body.WriteAsync(session.Finish("{}"));

Each call returns a byte[], so HttpResponse.Body keeps its own write and flush semantics. Failures throw WebUIException carrying the same diagnostic webui_last_error() would report.

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