C, Zig and Go
Checked against 2994ef5 on 2026-11-10 by Capy (review pass: Subagent 124 and fable). The files it covers are unchanged since.
C, Zig and Go use niso through its C API, niso.h: one function per DOM
member, generated from the same WebIDL as the JS binding, plus
niso_runtime.h for what a host does (making a DOM, running its event loop,
rendering frames, input). crates/niso-capi builds both into a static and a
dynamic library, libniso_capi. It has no JavaScript engine: niso-capi
depends only on niso-core, niso-host and niso-xpath. The full reference is generated: C API.
How the C API looks
Section titled “How the C API looks”Each member of a WebIDL interface is a function named after the interface
and the member, in snake case:
document.createElement is niso_document_create_element,
node.textContent = … is niso_node_set_text_content, and an attribute’s
getter is niso_<interface>_get_<attribute>. Every call takes the DOM first
and returns an int32_t status, NISO_OK (0) or the DOMException it would
throw (NISO_HIERARCHY_REQUEST_ERROR and so on), with the message in
niso_dom_last_error(dom). Results come back through out-parameters.
- Nodes and other DOM objects are
niso_handles, 64-bit ids. The DOM collects between tasks, so keep a handle across callbacks only while its node is in the document, or retain it withniso_retain. - Strings go in as UTF-8 pointer and length, and come back as
niso_str(also pointer and length, not NUL-terminated). - Callbacks (listeners, timers, animation frames) are a function pointer, a
void* userdata, and an optional drop function the DOM calls once when it lets go of the userdata.
The types are in
types.h
and the statuses in
status.h.
A first program in C
Section titled “A first program in C”This program is its own host: it makes a DOM, adds a heading and prints the body’s HTML.
#include <stdio.h>#include "niso.h"
#define S(lit) lit, sizeof(lit) - 1
int main(void) { niso_dom* dom = niso_dom_new(800, 600, 1.0f); niso_handle doc = niso_dom_document(dom), body = 0, h1 = 0, added = 0; if (niso_document_get_body(dom, doc, &body) || niso_document_create_element(dom, doc, S("h1"), &h1) || niso_node_set_text_content(dom, h1, S("Hello from C")) || niso_node_append_child(dom, body, h1, &added)) { niso_str e = niso_dom_last_error(dom); fprintf(stderr, "niso: %.*s\n", (int)e.len, e.ptr); return 1; } niso_str html; niso_element_get_outer_html(dom, body, &html); printf("%.*s\n", (int)html.len, html.ptr); niso_dom_free(dom); return 0;}The headers are generated into cargo’s build output for niso-capi, so build
it first and find them there. This is what
examples/lang/capi.sh
does for the examples’ build scripts:
cargo build -q -p niso-capiinclude=$(dirname "$(ls -t target/debug/build/niso-capi-*/out/include/niso.h | head -1)")cc -std=c11 -Wall -I"$include" examples/lang/c/hello.c -Ltarget/debug -lniso_capi -lm \ -Wl,-rpath,"$PWD/target/debug" -o target/hellotarget/hello<body><h1>Hello from C</h1></body>The timer app
Section titled “The timer app”A real app doesn’t make its own DOM: it’s a library that exports
niso_app_main, and a host (the iOS, Android or a headless test) makes the
DOM and calls it (Native apps). Each language’s timer
builds that library and a headless test that drives it.
examples/lang/c/timer.c
is written against niso_app.h, which includes niso.h. A listener and its
registration:
static int32_t on_lap(void* userdata, niso_handle event) { (void)userdata; if (!app.running) return 0; char label[32], time[32]; int label_len = snprintf(label, sizeof label, "Lap %d", ++app.lap_count); int time_len = format(time, sizeof time, elapsed_at(time_stamp(event))); niso_handle li = 0; TRY(append(app.laps, S("li"), NULL, 0, &li)); TRY(append(li, S("span"), label, (size_t)label_len, NULL)); return append(li, S("span"), time, (size_t)time_len, NULL);}examples/lang/c/timer.c, line 105
static int32_t listen(niso_handle target, const char* type, size_t len, niso_event_listener_fn fn) { niso_add_event_listener_options options = niso_add_event_listener_options_default(); return niso_event_target_add_event_listener(app.dom, target, type, len, fn, NULL, NULL, &options);}examples/lang/c/timer.c, line 138
examples/lang/c/build.shc timer: okThe script builds target/lang/c/libtimer.so (the app) and
target/lang/c/timer_test (the host,
timer_test.c),
and runs the test.
Zig reads the C headers itself: the build script turns niso_app.h into a
Zig module with zig translate-c, and
timer.zig
imports it as niso. Errors become a Zig error set:
export fn niso_app_main(dom: ?*c.niso_dom) i32 { return result(build(dom));}examples/lang/zig/timer.zig, line 239
examples/lang/zig/build.shZig’s test runner reports on stderr: 1/1 timer_test.test.timer...OK and
All 1 tests passed.
The script’s comment asks for Zig 0.17; the run above used Zig 0.16.0.
Go calls the C API through cgo.
timer.go
declares its callbacks in the cgo preamble and exports them with
//export, and builds as a c-shared library:
//export onLapfunc onLap(userdata unsafe.Pointer, event C.niso_handle) C.int32_t { if !app.running { return 0 } app.lapCount++ li, err := appendChild(app.laps, "li", "") if err == nil { _, err = appendChild(li, "span", fmt.Sprintf("Lap %d", app.lapCount)) } if err == nil { _, err = appendChild(li, "span", format(elapsedAt(timeStamp(event)))) } return result(err)}examples/lang/go/timer.go, line 180
examples/lang/go/build.shThe script needs Go with cgo (CGO_ENABLED=1, which it sets). It wasn’t run
for this page.