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:
| Platform | Library file |
|---|---|
| macOS | target/release/libwebui_ffi.dylib |
| Linux | target/release/libwebui_ffi.so |
| Windows | target/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, orNULL.- Returns an opaque pointer on success, or
NULLon error (callwebui_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 bywebui_handler_create.nonce, null-terminated UTF-8 string (typically a base64-encoded random value), orNULLto 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 bywebui_handler_create.protocol_ptr, pointer returned bywebui_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
NULLon error. - The caller must free the returned string with
webui_free().
Partial, component-template, and token helpers #
| Function | Result |
|---|---|
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);
| Function | Result |
|---|---|
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:
- Any function that can fail returns
NULLon error. - Call
webui_last_error()immediately after to get a human-readable message. - The error pointer is valid until the next FFI call on the same thread.
- 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:
- Free what you receive. Every non-
NULLstring returned by a render or protocol operation is heap-allocated. You must free it withwebui_free(). - 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 source | Who frees it? | How? |
|---|---|---|
webui_handler_render | Caller | webui_free(ptr) |
| Partial, component-template, and token strings | Caller | webui_free(ptr) |
| Streaming session chunks | Caller | webui_free(ptr) |
webui_last_error | Library (do not free) | Replaced on next call |
webui_handler_create | Caller | webui_handler_destroy(ptr) |
webui_handler_create_with_plugin | Caller | webui_handler_destroy(ptr) |
webui_protocol_create | Caller | webui_protocol_destroy(ptr) |
webui_streaming_session_create | Caller | webui_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? Usingc_void_pas the return type instead ofc_char_ppreventsctypesfrom automatically converting the pointer to a Pythonbytesobject. This lets you copy the string first, then explicitly free the original pointer withwebui_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 callwebui_freeimmediately 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:
- Load the shared library (
libwebui_ffi.dylib/.so/.dll). - Declare the functions you need. For a server, use
webui_protocol_create,webui_handler_render,webui_protocol_destroy,webui_free, andwebui_last_error. - Pass UTF-8 null-terminated strings for
htmlanddata_json. - Check the return value,
NULLmeans an error occurred. - 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