Skip to content

Tour of the code

Checked against 54d0f5c on 2026-11-10 by Capy (boundaries page). The files it covers are unchanged since.

This page is for people working on niso itself. The repository is the README, which maps every directory; this page walks the same tree in the order a frame goes through it.

  1. The API: idl/*.webidl declares what niso implements. crates/niso-idl reads it and generates niso.h, the JS binding’s host functions and the JVM classes. The hand-written part of the C API is the header sections in crates/*/include/, composed into niso_runtime.h by crates/niso-compose (C API reference).
  2. Script: a page’s JavaScript runs in an engine bound in bindings/js (niso_js.c, the glue in bindings/js/glue and each crate’s glue/), with the engine itself behind niso_engine.h: crates/niso-quickjs, crates/niso-porffor, crates/niso-napi. Native apps skip this and call niso.h directly (examples/lang), or the JVM classes (bindings/jvm, crates/niso-jvm).
  3. The document: crates/niso-core wraps blitz-dom: the tree and handles, events, style through stylo, layout, input, loading, history and the render stream (crates/niso-core/src/render.rs). Web APIs live in their own crates where they’ve been split out (niso-storage, niso-crypto, niso-xpath, niso-xml, niso-canvas, niso-structured-clone) and in crates/niso-core/src/api where they haven’t yet.
  4. The event loop: crates/niso-sched runs tasks, timers and animation frames for every language; the host calls it once per display frame.
  5. The host: hosts/apple (SwiftUI), hosts/android (Compose, with crates/niso-android for JNI) and hosts/iced apply the render stream to native views and send input back. They link the core differently: the Apple host links crates/niso-app (one static library with QuickJS), the Android host crates/niso-android (which brings in niso-jvm and niso-quickjs), and the iced host depends on niso-quickjs directly.

The counter example (examples/counter/App.svelte) has a button whose onclick does count++ and pushes an item onto a list. Here is everything that happens when you tap it on an iPhone, in order, with the code that does it.

  1. SwiftUI sees the tap. A SpatialTapGesture on the page (hosts/apple/Sources/Platform/Input.swift) calls engine.tap(location) with the point in the page’s coordinates, which are CSS pixels.
  2. The host calls the C API. Engine.tap (hosts/apple/Sources/Engine.swift) calls niso_dom_tap(dom, x, y, &target).
  3. The call enters the bracket. niso_dom_tap in crates/niso-capi/src/lib.rs runs Dom::tap inside ffi::call_reentrant (crates/niso-core/src/ffi.rs): panics are caught, and the DOM is borrowed only between callbacks, because this call will run script.
  4. The core finds the target. Dom::tap (crates/niso-core/src/events.rs) starts a new turn and hit-tests the laid-out boxes (Dom::hit, crates/niso-core/src/input.rs). The button is a native control, but a button’s taps go to the page like any other (a switch’s wouldn’t: the native switch handles its own touch). Dom::press then fires trusted mousedown and mouseup, moves focus, and fires an activating click, each through Dom::dispatch; Dom::end_task ends the turn.
  5. Script runs. Svelte 5 delegates click to one listener on the root. The core calls it through the JS binding’s invoke hook (qj_invoke in bindings/js/niso_js.c), QuickJS runs the handler, and count and items change. Nothing in the DOM has changed yet: Svelte batches.
  6. Microtasks. When the callback returns, qj_invoke runs a microtask checkpoint. Svelte’s effects run in it: the label’s text node gets a new nodeValue (“Tapped 1 time”) and the each block creates and appends an <li>. Each of those DOM calls is a generated host function, from the same WebIDL as niso_node_set_node_value and niso_node_append_child, and goes through the bracket too.
  7. The bracket closes. ffi::call_reentrant delivers what the call queued (tree moves, custom element reactions, the microtask signal, animation promises), reaches the safe point and returns 0 to Swift.
  8. The host renders right away. Engine.tap calls tick(), which runs the loop (niso_js_run) and asks for a frame (niso_render_frame). The core restyles with stylo, lays out with Taffy and Parley, and crates/niso-core/src/render.rs writes only what changed: a TEXT op with the label’s new glyph runs, CREATE, CHILDREN, FRAME and TEXT for the new list item, and FRAME for whatever moved.
  9. SwiftUI redraws. Engine.apply reads the ops: TEXT (op 7) replaces the node’s glyph runs, FRAME moves a view. SwiftUI redraws the views whose state changed, and the label shows the new count.

On Android the same path runs through hosts/android and JNI (crates/niso-android); on iced through hosts/iced in Rust. Steps 3 to 8 are the same code everywhere.

tests/check.sh runs everything a change has to pass on any machine: the layer lint, cargo test, the timer app in each language, the Svelte fixture, WPT against the recorded expectations, test262’s Intl tests, the Acid tests, and the docs check. --ios, --android and --iced add the device comparisons with Chrome, and --mac replays input into the Mac app and checks what the page saw. The docs check needs the site’s packages, installed once with cd docs/site && pnpm install --frozen-lockfile; on a machine without them, NISO_SKIP_DOCS=1 tests/check.sh runs everything else. The recorded results and what to do when they change are in Testing.

To change Start in Then
A web API’s behavior Its crate, or crates/niso-core/src/api/ Its WPT tests (cargo run --release -p niso-wpt -- --update when more pass)
A new web API idl/ and the crate that will own it Crates and contracts for where it goes; the glue section if it needs JS
Layout or style crates/niso-core, often a blitz or stylo bug A minimal repro under repros/ if the bug is upstream
How a host draws something hosts/<host>/ and the op in rendering.h tests/screens compares the result with Chrome
A native control The CONTROL or TEXT_FIELD op, then each host’s controls file Native controls
An engine bindings/js and the engine’s crate Its notes under Design notes
The docs Keeping the docs current docs/site/check.sh

Every change follows Decisions; a crate’s contract (its CONTRACT.md, listed here) changes only on purpose, with its version bumped. Most boundaries have no contract or no check yet: the boundary table says which, so you know when a change can break a caller without a test failing. Every merged feature updates the docs pages Keeping the docs current names for it, and Freshness lists what may have drifted.