Native apps
Checked against 2994ef5 on 2026-11-10 by Capy (review pass: Subagent 124 and fable). The files it covers are unchanged since.
A niso app doesn’t have to be JavaScript. A native app builds its document
with the same DOM calls a page makes (createElement, appendChild,
addEventListener, requestAnimationFrame), written in its own language,
and gets the same native controls, layout and text as a page. The repository
has one such app, a stopwatch with start, stop, lap and reset buttons, a lap
list, a “keep screen on” switch and a progress bar, written five times in
examples/lang:
in C, Rust, Zig, Go and Java.
The contract
Section titled “The contract”An app is a library that exports one function, niso_app_main, and does
everything else through niso.h, the C API generated from niso’s WebIDL. The
host makes the DOM, calls niso_app_main once before the first frame, and
from then on runs the app’s code only in the callbacks it registered:
listeners, timers and animation frames. A JS page is the same contract, with
the JS engine as its app.
/* How a host runs a niso app written in any language. * * An app is a library that exports niso_app_main and does everything else * through niso.h. The host creates the DOM and sets it up as for any page * (viewport, niso_dom_set_control_size, niso_dom_set_button_style), then * calls niso_app_main once, before the first frame. The app builds its * document there: elements, a <style> element with its CSS, listeners for * the events it handles, and timers and animation frame callbacks for work * over time. From then on the app's code runs only in those callbacks, and * the host runs the DOM's event loop exactly as for a JS page: * * on each display frame niso_dom_run(dom, niso_dom_now(dom)); * niso_render_frame(dom, &frame), then apply it * on the user's input niso_dom_tap, niso_dom_control_input, ... * * A JS page is the same contract with the JS binding as its app: the host * evaluates the bundle with niso_js_eval where it would call niso_app_main, * and calls niso_js_run(js, 0) where it would call niso_dom_run. * * The DOM collects between tasks, so a handle the app keeps across callbacks * must be connected to the document or retained (niso_retain). * * The app library leaves the niso_* functions to the host: it is linked * against the library that has them, libniso_capi for a headless run, * libniso_android on Android, or into the host's binary next to niso-app's * static library on iOS. */crates/niso-app/include/niso_app.h, line 1
Two rules follow from it. The DOM collects garbage between tasks, so a handle
the app keeps across callbacks must point at a node in the document or be
retained with niso_retain. And the library doesn’t link the niso_*
functions itself: the host it’s loaded into provides them
(libniso_capi for a headless run, libniso_android on Android, the app’s
own binary on Apple platforms).
The timer in each language
Section titled “The timer in each language”| Language | Source | How it reaches the DOM | Toolchain |
|---|---|---|---|
| C | timer.c |
niso.h directly |
a C11 compiler |
| Rust | src/lib.rs |
the part of niso.h it uses, declared by hand in src/niso.rs |
cargo |
| Zig | timer.zig |
niso.h through zig translate-c |
Zig (the script says 0.17; 0.16.0 also passed) |
| Go | timer.go |
niso.h through cgo |
Go with cgo |
| Java | Timer.java |
the dev.niso.dom classes, generated from the WebIDL |
a JDK 17 |
Here is the C version’s entry point. Each call returns a status, and TRY
returns early on the first one that fails:
int32_t niso_app_main(niso_dom* dom) { app.dom = dom; niso_handle doc = niso_dom_document(dom), head = 0, body = 0, buttons = 0, label = 0; TRY(niso_document_get_head(dom, doc, &head)); TRY(niso_document_get_body(dom, doc, &body)); TRY(append(head, S("style"), css, sizeof css - 1, NULL));
TRY(append(body, S("main"), NULL, 0, &app.main)); TRY(append(app.main, S("h1"), NULL, 0, &app.elapsed)); TRY(niso_element_set_id(dom, app.elapsed, S("elapsed"))); TRY(niso_document_create_text_node(dom, doc, S(""), &app.elapsed_text)); TRY(niso_node_append_child(dom, app.elapsed, app.elapsed_text, &app.elapsed_text)); TRY(append(app.main, S("progress"), NULL, 0, &app.minute)); TRY(niso_element_set_id(dom, app.minute, S("minute"))); TRY(niso_element_set_attribute(dom, app.minute, S("max"), S("60000")));
TRY(append(app.main, S("div"), NULL, 0, &buttons)); TRY(niso_element_set_class_name(dom, buttons, S("buttons"))); TRY(button(buttons, S("Start"), on_start, &app.start)); TRY(button(buttons, S("Stop"), on_stop, &app.stop)); TRY(button(buttons, S("Lap"), on_lap, &app.lap)); TRY(button(buttons, S("Reset"), on_reset, &app.reset)); TRY(niso_element_set_id(dom, app.start, S("start"))); TRY(niso_element_set_id(dom, app.stop, S("stop"))); TRY(niso_element_set_id(dom, app.lap, S("lap"))); TRY(niso_element_set_id(dom, app.reset, S("reset")));
TRY(append(app.main, S("label"), S("Keep screen on"), &label)); TRY(append(label, S("input"), NULL, 0, &app.awake)); TRY(niso_element_set_id(dom, app.awake, S("awake"))); TRY(niso_element_set_attribute(dom, app.awake, S("type"), S("checkbox"))); TRY(niso_element_set_attribute(dom, app.awake, S("switch"), S(""))); TRY(listen(app.awake, S("change"), on_awake));
TRY(append(app.main, S("ol"), NULL, 0, &app.laps)); TRY(niso_element_set_id(dom, app.laps, S("laps"))); TRY(show(0)); return update_buttons();}examples/lang/c/timer.c, line 148
The Java version is the same app over generated classes, so it reads like a page’s script:
public void accept(Dom dom) { doc = dom.getDocument(); window = dom.getWindow(); append(doc.getHead(), "style", CSS);
main = append(doc.getBody(), "main", null); elapsed = (HTMLElement) append(main, "h1", null); elapsed.setId("elapsed"); elapsedText = doc.createTextNode(""); elapsed.appendChild(elapsedText); minute = (HTMLProgressElement) append(main, "progress", null); minute.setId("minute"); minute.setAttribute("max", "60000");
Element buttons = append(main, "div", null); buttons.setClassName("buttons"); start = button(buttons, "start", "Start", this::onStart); stop = button(buttons, "stop", "Stop", this::onStop); lap = button(buttons, "lap", "Lap", this::onLap); reset = button(buttons, "reset", "Reset", this::onReset);
Element label = append(main, "label", "Keep screen on"); awake = (HTMLInputElement) append(label, "input", null); awake.setId("awake"); awake.setAttribute("type", "checkbox"); awake.setAttribute("switch", ""); awake.addEventListener("change", e -> { if (awake.getChecked()) { main.setAttribute("data-keep-awake", ""); } else { main.removeAttribute("data-keep-awake"); } });
laps = append(main, "ol", null); laps.setId("laps"); show(0); updateButtons(); }examples/lang/java/Timer.java, line 96
The language pages walk through each binding: C, Zig and Go, Rust, Java and Kotlin.
Build and test
Section titled “Build and test”Each language has a build script that builds the app as a library and then runs its headless test. The test is a small host: it makes a DOM, calls the app’s entry point, taps the buttons, runs the timers in virtual time, and checks both the document and the render stream a native host would draw. The C API they link has no JavaScript engine, so the C, Zig, Go and Rust apps don’t need the QuickJS-ng submodule.
examples/lang/c/build.shc timer: okexamples/lang/rust/build.shThe Rust app is a workspace member, so cargo test --workspace runs its test
too.
JAVA_HOME=$(/usr/libexec/java_home -v 17) examples/lang/java/build.shjava timer: okjava binding: okjava module: okThat Java run is the macOS form; on Linux, set JAVA_HOME to a JDK 17. Besides
the timer, it runs the binding’s own test and a page that calls a JVM native
module.
examples/lang/zig/build.shZig’s test runner reports on stderr: 1/1 timer_test.test.timer...OK and
All 1 tests passed.
examples/lang/go/build.shThe builds go to target/lang/<language>/: libtimer.so for C, Zig and Go,
the classes for Java.
Run it in a host
Section titled “Run it in a host”The Android and Apple hosts run an app library in place of a page:
- Android:
hosts/android/build.sh --app LIBwith an arm64 shared library linked againstlibniso_android.so, orhosts/android/build.sh --jvm examples/lang/java Timerfor the Java version, built into the app. See Build for Android. - Apple platforms:
hosts/apple/build.sh --app LIBwith a static library for the platform. See Build for Apple platforms.
The desktop host, niso-iced, runs only JS pages for now: it takes a script or
an HTML page, not an app library.