niso-host
Generated from crates/niso-host/CONTRACT.md when the site was built at 7aa23e2.
Purpose
Section titled “Purpose”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.
Public surface
Section titled “Public surface”- Rust:
Cxand 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 theirnodeTypenumbers) andAttribute(an attribute’s namespace, prefix, local name and value, borrowed).HostServices, what a host implements (crates callCx, which adds the borrow checks; its providedas_anylets a host’s own sources and tasks reach it from aCx;with_treeandattribute_nodeare provided as NotSupported for a host with no document), andHostState, 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), theWaketrait a host implements (below, “Wake”),WakeFnandCWake, 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, andcapi::panic_error, a caught panic as the Internal error it reports.testing(below) and, behind thecontract-testsfeature,contract(host_suite,Harness,Nested, and the suite’s objects;tree_suite,TreeHarness, itsSampletrees andread).
- 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.webidlcomes 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.
Host requirements
Section titled “Host requirements”This crate is the host contract, so the requirements are on hosts:
- A host keeps one
HostStateper world and installs crates into it withHostState::install, passing a check that refuses an interface it does not know. A refused crate installs nothing: no slot, no hook. - At the end of every C API call and every task it calls
safe_point(capi::calldoes for a call), and returns or reports its error. - 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 bycheckpoint_end. releasethat 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 toHostState::defer_free.- 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’sniso_dom_runalways takes it when its loop ends, and returns the loop’s own error first if it has one; FakeHost’srun_tasksandadvancereturn the first error of any kind. - Crate timers follow the timer rules below, in an id space of their own.
- When it goes, it gives every crate object still in its table its
freedat one last safe point, then drops the crates’ slots, with panics caught. as_anyis for the host’s own sources and tasks, which find their host from aCxwithCx::host_any(the document’sdom_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_anyis the only way from aCxpast 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.- A host with a document implements
with_tree, lending aTreefor the closure only, andattribute_node. Nothing may change its trees while one is lent:Cxkeeps crates from it (below), and the host’s own code must not either. A host without a document keeps the defaults.
May assume from dependencies
Section titled “May assume from dependencies”- niso-base:
Obj,OBJECT_BIT,Error,ErrorCode,Result,err.
Script, borrows and panics
Section titled “Script, borrows and panics”- 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::treerecords 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_anyand anything that can run script panic, naming the lent tree, and lending it from insideobjectpanics too. ANode<'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 withTree::nodein its next lending. freedand checkpoint-end hooks may not run script. They run where the host is between tasks, so afirefrom 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(eachfreed; 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 callsCxdirectly, unwinds into that host code, and if anextern "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.
The tree
Section titled “The tree”What Cx::tree lends is DOM’s view of the host’s trees, every document’s
and every node outside one:
- Nodes.
Tree::nodechecks a handle once (TypeError for an object’s, InvalidHandle for one that names no node); aNodegives the same handle back. Every node has oneNodeKind, and the attached Attr of an element’s attribute exists only onceattribute_nodemakes it. - Structure.
parent,first_child,last_child,next_siblingandprevious_siblingare 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_elementis its element, until its attribute is removed and it keeps the value alone. - Names.
local_nameis an element’s or attribute’s local name or a processing instruction’s target, empty for other nodes;namespaceis None for no namespace (never empty) and for nodes with no name;prefixlikewise. - Attributes. An element’s are
attribute(e, 0)toattribute(e, attribute_count(e) - 1), in their order (a new one goes last, one set again keeps its place);attribute_by_namefinds one by namespace and local name, case-sensitively, with its index. Namespace declarations are attributes too. Other nodes have none. - Data.
datais 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_textappends the data of the descendant text nodes, CDATA sections included, in tree order. - Order.
compareis 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_documentis 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_documenttells an HTML document from an XML one.is_connectedis DOM’s: the node’s root is a document, so an Attr never is. - Version.
versionchanges 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.
Events on object targets
Section titled “Events on object targets”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.
Timers
Section titled “Timers”- A crate timer runs its task once,
msafterset_timeron the host’s clock.msbelow 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
clearTimeoutandclearIntervalcannot clear one, andclear_timercannot 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.
Teardown
Section titled “Teardown”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.
The clock and the waker
Section titled “The clock and the waker”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.
When checkpoint-end hooks run
Section titled “When checkpoint-end hooks run”- 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.
Guarantees
Section titled “Guarantees”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.
- Objects get distinct handles with the top bit set;
Cx::objectreaches an object only as its own type (TypeError otherwise, and for a node); the host reports the interface the object names. identity. - A freed object’s handle is stale for good (InvalidHandle), even once its place in the table is reused. generations.
- 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.
freedruns at the end of the call or task that dropped the last reference, never during it. freed_at_a_safe_point.- The handles an object’s
heldvisits are released after itsfreed, in visit order. held_handles_go_after_freed. firereportscanceledonly for a cancelable event a listener canceled,listener_threwwhen a listener threw, and dispatches to an object target in the phases above. fire.- A listener reads the payload an event was fired with, as its own type only. payloads.
- 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. - A crate reaches the slots it installed, and only those; slot state lasts across calls. slots.
- 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.
- Timers run once due, in due order; a cleared timer never runs. timers.
- A timer’s task that fails or panics is reported as a task’s is, and later timers run. timer_failures.
- 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.
- A
freedthat 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; afreedthat runs script panics. freed_failures. - A safe point inside a borrow runs no
freedand loses none. borrows_across_freed. - 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.
- Reading an event’s payload borrows it: releasing the event from the reader panics and leaves the event as it was. payload_borrows.
- Crate timers follow the timer rules above (own ids, not rounded, no nesting clamp). crate_timers.
- A C API call nested in a listener during
fireends at its own safe point, a failure in it is its own, and the dispatch goes on. nested_calls_during_fire. - A run moves
nowat least to each timer’s due time as it runs it; the waker is one per host andwakeruns the host’s hook. clock_and_waker. - 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;
WakeandWakerare 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.
Threads
Section titled “Threads”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.
Performance and memory
Section titled “Performance and memory”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.
Standalone use
Section titled “Standalone use”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.