Skip to content

niso-host

Generated from crates/niso-host/CONTRACT.md when the site was built at 7aa23e2.

The contract between a web API crate and the program that hosts it. A crate reaches its host only through Cx: the clock, the waker, its own extension slot, the object table, events, the task queue, timers and a read-only view of the document’s trees. niso’s document (niso-core’s Dom, through niso_core::host::DomHost) is one host; testing::FakeHost is another, and both pass contract::host_suite, so a crate tested on FakeHost sees the same behaviour in the document.

It does not own a scheduler (niso-sched: Source and the run loop), the document’s nodes (a host lends them to read through Tree; changing them is the document’s own API), or any subsystem’s state.

  • Rust:
    • Cx and its methods: with, try_with, new_object, object, retain, release, queue_task, set_timer, clear_timer, fire, event_payload, tree, attribute_node, waker, now, host_any.
    • tree: Tree, the read-only view a host lends (below, “The tree”), Node<'t> (from_handle, handle; it lives for one lending), NodeKind (DOM’s node types, with their nodeType numbers) and Attribute (an attribute’s namespace, prefix, local name and value, borrowed).
    • HostServices, what a host implements (crates call Cx, which adds the borrow checks; its provided as_any lets a host’s own sources and tasks reach it from a Cx; with_tree and attribute_node are provided as NotSupported for a host with no document), and HostState, what every host keeps for its crates: install, defer_free, frees_pending, report_failure, take_failure.
    • PlatformObject (interface, held, freed, heap_size), InterfaceInfo (name), EventInit, Dispatched, TaskSource, Task, TimerId.
    • ExtKey<T>, Registrar (interfaces, ext, checkpoint_end).
    • safe_point, run_task, checkpoint_end: the host side of the lifecycle, which a host calls at the matching points of its loop. Each returns the error a crate’s code failed or panicked with.
    • Waker (set, set_wake, wake, signal_fd, drain; moved from niso-core’s modules.rs), the Wake trait a host implements (below, “Wake”), WakeFn and CWake, its C adapter; Inbox<T> (push, take, is_empty).
    • capi::call, which runs a crate’s function as one C API call, capi::nested, the call a binding makes when script calls back in while a crate’s function is on the stack, and capi::panic_error, a caught panic as the Internal error it reports.
    • testing (below) and, behind the contract-tests feature, contract (host_suite, Harness, Nested, and the suite’s objects; tree_suite, TreeHarness, its Sample trees and read).
  • C: none of its own yet. The status a call returns is niso-base’s ErrorCode. The wake hook keeps its C type, niso_wake_fn, in niso-sched’s include/event-loop.h.
  • IDL, glue: none yet (idl/platform.webidl comes with per-crate IDL, step 6).

Not yet here, each arriving with its first user: HostServices::agent and origin (the first crate that needs them), the other hook kinds of architecture.md 3.2 (rendering steps, pre-layout, call brackets, nodes dropped, element interfaces, render contributors, worker seeds, memory reports). Source is niso-sched’s.

This crate is the host contract, so the requirements are on hosts:

  1. A host keeps one HostState per world and installs crates into it with HostState::install, passing a check that refuses an interface it does not know. A refused crate installs nothing: no slot, no hook.
  2. At the end of every C API call and every task it calls safe_point (capi::call does for a call), and returns or reports its error.
  3. It runs queued tasks and timer callbacks with run_task, in FIFO order, a batch at a time, and after each one performs a microtask checkpoint followed by checkpoint_end.
  4. release that takes a crate’s object to no references removes it from the table at once (its handle is stale from then on) and hands it to HostState::defer_free.
  5. An error a lifecycle point returns where nobody can take it (a hook run from the binding’s checkpoint, a safe point after a task, a crate timer’s task) goes to HostState::report_failure, and the host’s run goes on and returns it (take_failure). The document’s niso_dom_run always takes it when its loop ends, and returns the loop’s own error first if it has one; FakeHost’s run_tasks and advance return the first error of any kind.
  6. Crate timers follow the timer rules below, in an id space of their own.
  7. When it goes, it gives every crate object still in its table its freed at one last safe point, then drops the crates’ slots, with panics caught.
  8. as_any is for the host’s own sources and tasks, which find their host from a Cx with Cx::host_any (the document’s dom_of). Crates must not call it: a crate that downcasts its host works on that host only, which is what this contract exists to prevent. No lint checks it; review does. Cx::host_any is the only way from a Cx past its checks to the host, and it panics while the tree is lent, since the host’s own code could change the trees through it.
  9. A host with a document implements with_tree, lending a Tree for the closure only, and attribute_node. Nothing may change its trees while one is lent: Cx keeps crates from it (below), and the host’s own code must not either. A host without a document keeps the defaults.
  • niso-base: Obj, OBJECT_BIT, Error, ErrorCode, Result, err.
  • No borrow across script. A slot (with), an object (object) or an event payload (event_payload) is borrowed while its closure runs. A call that can run script (fire, a task) panics if anything is borrowed; a table call (new_object, object, retain, release, event_payload) panics if an object or payload is borrowed; a slot reentered panics. Each panic names what is borrowed.
  • The tree is lent, not borrowed piecemeal. Cx::tree records one borrow for the whole closure, and every read inside it is a plain call. While it is lent, only reads are allowed: the tree, slots (with), now, waker, and lending it again. A table call (new_object, object, retain, release, event_payload, attribute_node), queue_task, set_timer, clear_timer, host_any and anything that can run script panic, naming the lent tree, and lending it from inside object panics too. A Node<'t> or a borrowed string does not outlive the closure: the compiler refuses one that would, so a crate keeps a node’s handle and checks it again with Tree::node in its next lending.
  • freed and checkpoint-end hooks may not run script. They run where the host is between tasks, so a fire from one panics (“script may not run in freed”, “… in a checkpoint-end hook”). They may queue tasks, set timers, touch slots and the table, and release handles. No need to run script from either has come up; a crate that has one queues a task.
  • Where a panic is caught. Every entry point that runs a crate’s code for a host catches its panics and makes them Internal errors with the panic’s message: capi::call (the call’s status), run_task (the task’s error), safe_point (each freed; the others still run, and what each held is still released), checkpoint_end (each hook; the others still run). A panic anywhere else, in host code that calls Cx directly, unwinds into that host code, and if an extern "C" frame is on the way the process aborts. A host therefore reaches crate code only through these four, and through its teardown, which catches panics the same way.
  • Safe points inside a borrow. A safe point reached while anything is borrowed (a C API call nested inside a slot borrow) runs no freed; the frees wait for the next safe point, none is lost.

What Cx::tree lends is DOM’s view of the host’s trees, every document’s and every node outside one:

  • Nodes. Tree::node checks a handle once (TypeError for an object’s, InvalidHandle for one that names no node); a Node gives the same handle back. Every node has one NodeKind, and the attached Attr of an element’s attribute exists only once attribute_node makes it.
  • Structure. parent, first_child, last_child, next_sibling and previous_sibling are DOM’s, and agree with each other, after any change as before it. Doctypes are children like any node; adjacent text nodes stay separate. A removed node has no parent or siblings and keeps its children. An Attr has no parent, children or siblings: owner_element is its element, until its attribute is removed and it keeps the value alone.
  • Names. local_name is an element’s or attribute’s local name or a processing instruction’s target, empty for other nodes; namespace is None for no namespace (never empty) and for nodes with no name; prefix likewise.
  • Attributes. An element’s are attribute(e, 0) to attribute(e, attribute_count(e) - 1), in their order (a new one goes last, one set again keeps its place); attribute_by_name finds one by namespace and local name, case-sensitively, with its index. Namespace declarations are attributes too. Other nodes have none.
  • Data. data is character data (text, CDATA section, comment, processing instruction) or an Attr’s value (its element’s attribute’s, while it has one), and empty for other nodes. append_text appends the data of the descendant text nodes, CDATA sections included, in tree order.
  • Order. compare is document order: a node before its descendants, an element’s attributes after it and before its children, in their order. Trees compare in an order that is consistent but unspecified, and each tree’s nodes stay together in it.
  • Documents. node_document is DOM’s node document (a document’s is itself, an Attr’s that of the document that made it or its element’s); a node inserted into another document’s tree, or adopted, moves to that document with its descendants and their elements’ Attr nodes; is_html_document tells an HTML document from an XML one. is_connected is DOM’s: the node’s root is a document, so an Attr never is.
  • Version. version changes with every change to any tree and with nothing else: a node in or out, a node adopted into another document, an attribute set or removed, the data of any character data node or the value of any Attr changed, a document or fragment parsed, a deep clone made.

Cx::attribute_node(element, index) gives the Attr node of an element’s attribute, made the first time and the same node after (NotFound if there is no attribute there). It is a table call, outside any lending: a crate that found an attribute while the tree was lent asks for its node after.

For an object target, the event path is the target then the parents given. Dispatch runs, as DOM does for nodes: the capture phase on the parents from the last to the first (capturing listeners), then the target (capturing listeners, then the others), then, only for a bubbling event, the parents from the first to the last (non-capturing listeners). Both hosts do this; FakeHost’s on_capture adds a capturing listener.

  • A crate timer runs its task once, ms after set_timer on the host’s clock. ms below 0, or NaN, is 0; it is not rounded (script’s timers are whole milliseconds, a crate’s need not be).
  • Crate timers take no part in HTML’s timer nesting: a crate timer’s delay is never clamped to 4 ms however deep script’s timers are nested, and its task runs at nesting level 0, so a script timer it sets starts at level 1.
  • Crate timers have their own ids: script’s clearTimeout and clearInterval cannot clear one, and clear_timer cannot clear a script timer. Clearing an id that is not a pending crate timer does nothing.
  • Timers due at the same time run in the order they were set. The tasks already queued run before a timer.
  • A timer’s task that fails or panics is reported as a task’s is (guarantee 10), and the other timers still run.
  • In the document, a worker that closes clears its crate timers with its script timers.

When a host goes (niso_dom_free, or a FakeHost dropped), every crate object still in its table gets its freed at one last safe point, whatever references it still had: what a binding or another object held no longer exists. The usual rules hold there (no script; the handles it held are released, and are already stale if they were crate objects too). Then the crates’ slots drop. A panic in a freed, an object’s Drop or a slot’s Drop is caught and dropped, since teardown has no caller to report it to and niso_dom_free must not unwind.

now is milliseconds on the run loop’s clock. FakeHost’s clock is virtual: it moves only when advance runs a timer, to that timer’s due time. The document’s is real time plus the time its runs skip ahead to the next due item when nothing is live (niso_dom_advance_clock); a run does not skip past the last item it ran. Each host has one Waker (waker() returns the same Arc each time), and wake runs the host’s wake hook.

  • The document: inside niso_dom_end_microtask_checkpoint, after IndexedDB’s cleanup. Bindings end a microtask checkpoint from the task hook, after every task, timer, animation frame callback, message and module task, and after a callback no script called. A document with no binding (no task hook) runs no checkpoints, so its hooks do not run.
  • FakeHost: after every task and timer it runs, before that task’s safe point, as the document’s task hook does. It runs none after a C API call (call), as the document runs none there.

Each is checked by contract::host_suite, which every host runs: FakeHost in this crate’s tests, the document in niso-core’s tests/host.rs.

  1. Objects get distinct handles with the top bit set; Cx::object reaches an object only as its own type (TypeError otherwise, and for a node); the host reports the interface the object names. identity.
  2. A freed object’s handle is stale for good (InvalidHandle), even once its place in the table is reused. generations.
  3. Every retain needs a release; the object goes with the last, and a release without a retain is InvalidState. A new object has no references. release_order.
  4. freed runs at the end of the call or task that dropped the last reference, never during it. freed_at_a_safe_point.
  5. The handles an object’s held visits are released after its freed, in visit order. held_handles_go_after_freed.
  6. fire reports canceled only for a cancelable event a listener canceled, listener_threw when a listener threw, and dispatches to an object target in the phases above. fire.
  7. A listener reads the payload an event was fired with, as its own type only. payloads.
  8. A slot or object borrowed across a call that can run script, the table touched while an object is borrowed, or a slot reentered, panics naming what is borrowed; the C boundary (capi::call) turns the panic into an Internal error with that message (architecture.md, decision 1). no_borrow_across_script.
  9. A crate reaches the slots it installed, and only those; slot state lasts across calls. slots.
  10. Queued tasks run after the call that queued them, FIFO, tasks they queue after those queued before; a failing task does not stop the rest, and its error is returned once the batch is done; the checkpoint-end hooks run after each task, before the next starts. tasks.
  11. Timers run once due, in due order; a cleared timer never runs. timers.
  12. A timer’s task that fails or panics is reported as a task’s is, and later timers run. timer_failures.
  13. A checkpoint-end hook that panics is reported as Internal while the hooks after it run; one that runs script panics and is reported the same way. checkpoint_end_hooks.
  14. A freed that panics is the Internal error of the call or run whose safe point ran it, after the call’s own error; what it held is still released; a freed that runs script panics. freed_failures.
  15. A safe point inside a borrow runs no freed and loses none. borrows_across_freed.
  16. A task that panics while holding a borrow is an Internal error, leaves no borrow behind, and the rest of its batch runs. task_failures.
  17. Reading an event’s payload borrows it: releasing the event from the reader panics and leaves the event as it was. payload_borrows.
  18. Crate timers follow the timer rules above (own ids, not rounded, no nesting clamp). crate_timers.
  19. A C API call nested in a listener during fire ends at its own safe point, a failure in it is its own, and the dispatch goes on. nested_calls_during_fire.
  20. A run moves now at least to each timer’s due time as it runs it; the waker is one per host and wake runs the host’s hook. clock_and_waker.
  21. When a host goes, every crate object still in its table gets its freed, and a panic in one does not escape. teardown.

The tree’s guarantees are checked by contract::tree_suite, which every host with a document runs (FakeHost in this crate’s tests, the document in niso-core’s tests/host.rs), on the trees of contract::Sample: an HTML document with a doctype, attributes in two namespaces, text, a comment, a processing instruction and an SVG element; a detached element, a fragment and a lone Attr; and an XML document with a CDATA section.

  • T1. A handle finds its node and comes back from it; an object’s is a TypeError, one no node has InvalidHandle. tree_handles.
  • T2. Kinds, and names as “Names” above says. tree_kinds_and_names.
  • T3. Children forwards and backwards, and parents, agree; roots and Attrs have no parent. tree_structure.
  • T4. Attributes by index, in order, and by name, with their index; set again in place. tree_attributes.
  • T5. Data, and descendant text content. tree_data_and_text.
  • T6. Document order within a tree; between trees consistent, each tree together; after removing the first, a middle and the last child and moving nodes, the order and both sibling directions are the new ones, and removed nodes have no links but keep their children. tree_order.
  • T7. Node documents; HTML and XML documents. tree_documents.
  • T8. Connected when the root is a document, through removal, insertion and moves between trees, with siblings and order following. tree_connected.
  • T9. The version moves with every change (insertion, removal, an attribute added, changed or removed, text, comment, processing instruction and CDATA data, a lone and an attached Attr’s value, an adoption) and no read; the reads after each see the change. tree_versions.
  • T10. Attr nodes: made once, their element’s, after it and before its children in order, with live values, never connected. tree_attribute_nodes.
  • T11. While the tree is lent only reads run; everything else (new_object, object, retain, release, event_payload, attribute_node, tasks, timers, fire, host_any) panics, is an Internal error at the C boundary, and leaves no borrow behind. tree_borrows.
  • T12. Appending into another document’s tree, and adopting a parentless node, move the node, its descendants and its Attr nodes to that document; adopting out of a tree removes the node from it. tree_adoption.

How a host is woken when another thread queues it a task (architecture.md 7.5): a Rust host implements Wake (Send + Sync) and sets it with Waker::set_wake; a C host’s niso_wake_fn and userdata are one through CWake, which Waker::set (and so niso_dom_set_wake_hook) builds. The contract suite is contract::wake::wake_suite, over a WakeHarness (the waker, and how the host installs a Wake); it runs on the waker with a Rust Wake (Direct), on the waker with that Wake behind the C hook (ThroughC, over vtable and CHook, which frees the userdata a setter replaced as a C host does), and on the document in niso-core.

  • W1. Each wake calls the hook once; Wake and Waker are Send and Sync. wakes_once.
  • W2. With no hook, or a null one, a wake calls nothing; the signal (signal_fd) is written either way. none_is_none.
  • W3. A hook is dropped once: the one a setter replaced by the time the setter returns, the last one with the waker. A C hook’s userdata stays the host’s. dropped_once.
  • W4. Wakes from other threads, at once, each call the hook. from_other_threads.
  • W5. The hook is called outside the waker’s lock: one that sets the hook on its own thread does not deadlock, and goes once its call returns. outside_the_lock.
  • W6. Setting the hook waits for the calls other threads have in flight. setting_waits_for_other_threads.
  • W7. A panic in the hook is caught: it does not unwind into the waking thread or through C, and leaves no call in flight behind. panics_are_caught.

The fakes are contract::wake::Counting (counts its wakes and its drops, and runs a closure in each wake) and the harnesses above.

Cx, HostState and the object table are the agent’s thread’s only and not Send. Waker and Inbox<T> (for T: Send) are Send + Sync and are what other threads touch: push into an inbox, then Waker::wake. A wake hook must only schedule a run and never call niso.

A slot lookup is an index into a vector plus a RefCell flag; an object lookup is the host’s table lookup plus a TypeId compare. The borrow record is a vector push and pop per with, object and event_payload. A safe point with nothing to free is one inlined emptiness check (HostState::frees_pending), made before any call into this crate. Inbox::is_empty takes no lock. Crate objects cost one allocation each. Lending the tree is one borrow record push and pop, however much is read; each read is one indirect call, with names, values and data borrowed, never copied. The document numbers a node’s sibling index, with its parent’s other children, the first time a sibling step asks for it in a lending (linear in that parent’s children), and a tree’s order the first time a comparison needs it (linear in the tree); both live in one vector over the range of slots numbered so far rather than from slot 0, so a walk is linear in what it visits and a lending that asks about one row of a table numbers, and allocates for, that row only. Its kind is a match on the node, plus a hash lookup for comments and text. The document keeps its HostState boxed, so the &HostState its host hands out does not alias the Dom it reaches by pointer.

testing::FakeHost: a virtual clock (advance), a generational object table, one FIFO (run_tasks), timers, listeners as Rust closures (on and on_capture, with Outcome::Cancel and Outcome::Throw for canceling and throwing listeners), a record of every event fired with its payload readable (fired), the source of every task run (task_sources), a counting waker (wakes), and trees (tree_mut, a FakeTree): documents and nodes of every kind, made, appended, removed, adopted and changed as script would (an append adopts into the parent’s document; a removed attribute’s Attr keeps its value), all by handle, with node handles that retain and release count and that live as long as the host. It passes every guarantee above. It runs no script, so it has no script timers (guarantee 18’s id check holds trivially there) and accepts every interface at install.

FakeHost runs any L1 crate from a plain Rust test: let mut host = FakeHost::new(); host.install(my_crate::register); host.call(|cx| ...). A real program implements HostServices for its own loop, as niso-core’s DomHost does.