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.
Following a frame
Section titled “Following a frame”- The API:
idl/*.webidldeclares what niso implements.crates/niso-idlreads it and generatesniso.h, the JS binding’s host functions and the JVM classes. The hand-written part of the C API is the header sections incrates/*/include/, composed intoniso_runtime.hbycrates/niso-compose(C API reference). - Script: a page’s JavaScript runs in an engine bound in
bindings/js(niso_js.c, the glue inbindings/js/glueand each crate’sglue/), with the engine itself behindniso_engine.h:crates/niso-quickjs,crates/niso-porffor,crates/niso-napi. Native apps skip this and callniso.hdirectly (examples/lang), or the JVM classes (bindings/jvm,crates/niso-jvm). - The document:
crates/niso-corewraps 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 incrates/niso-core/src/apiwhere they haven’t yet. - The event loop:
crates/niso-schedruns tasks, timers and animation frames for every language; the host calls it once per display frame. - The host:
hosts/apple(SwiftUI),hosts/android(Compose, withcrates/niso-androidfor JNI) andhosts/icedapply the render stream to native views and send input back. They link the core differently: the Apple host linkscrates/niso-app(one static library with QuickJS), the Android hostcrates/niso-android(which brings in niso-jvm and niso-quickjs), and the iced host depends on niso-quickjs directly.
One tap, through every layer
Section titled “One tap, through every layer”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.
- SwiftUI sees the tap. A
SpatialTapGestureon the page (hosts/apple/Sources/Platform/Input.swift) callsengine.tap(location)with the point in the page’s coordinates, which are CSS pixels. - The host calls the C API.
Engine.tap(hosts/apple/Sources/Engine.swift) callsniso_dom_tap(dom, x, y, &target). - The call enters the bracket.
niso_dom_tapincrates/niso-capi/src/lib.rsrunsDom::tapinsideffi::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. - 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::pressthen fires trustedmousedownandmouseup, moves focus, and fires an activatingclick, each throughDom::dispatch;Dom::end_taskends the turn. - Script runs. Svelte 5 delegates
clickto one listener on the root. The core calls it through the JS binding’s invoke hook (qj_invokeinbindings/js/niso_js.c), QuickJS runs the handler, andcountanditemschange. Nothing in the DOM has changed yet: Svelte batches. - Microtasks. When the callback returns,
qj_invokeruns a microtask checkpoint. Svelte’s effects run in it: the label’s text node gets a newnodeValue(“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 asniso_node_set_node_valueandniso_node_append_child, and goes through the bracket too. - The bracket closes.
ffi::call_reentrantdelivers what the call queued (tree moves, custom element reactions, the microtask signal, animation promises), reaches the safe point and returns 0 to Swift. - The host renders right away.
Engine.tapcallstick(), 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, andcrates/niso-core/src/render.rswrites only what changed: aTEXTop with the label’s new glyph runs,CREATE,CHILDREN,FRAMEandTEXTfor the new list item, andFRAMEfor whatever moved. - SwiftUI redraws.
Engine.applyreads the ops:TEXT(op 7) replaces the node’s glyph runs,FRAMEmoves 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.
Checking a change
Section titled “Checking a change”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.
Where to start
Section titled “Where to start”| 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.