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.
::: 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 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.
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) |
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) |
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.
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