Skip to content

Rust

Checked against f15b998 on 2026-11-10. The files it covers are unchanged since.

niso is written in Rust, but there’s no Rust client crate for app code yet: a Rust app reaches the DOM the way a C app does, through the C API’s niso_* functions, as unsafe extern "C" calls. DECISIONS plans client bindings for Rust generated from the same WebIDL as the JS glue; they’re not on main.

examples/lang/rust is the timer app as a crate, niso-timer, built as a cdylib that exports niso_app_main (Native apps). Its src/niso.rs declares the part of niso.h the app uses by hand, and a test (examples/lang/rust/tests/layout.rs) checks that the structs it mirrors have the core’s layout.

The app wraps the calls it repeats in a small Dom type that turns statuses into Results:

examples/lang/rust/src/lib.rs
impl Dom {
fn append(self, parent: niso_handle, tag: &str, text: &str) -> Result<niso_handle> {
let (mut el, mut added) = (0, 0);
unsafe {
ok(niso_document_create_element(self.0, niso_dom_document(self.0), tag.as_ptr(), tag.len(), &mut el))?;
if !text.is_empty() {
ok(niso_node_set_text_content(self.0, el, text.as_ptr(), text.len()))?;
}
ok(niso_node_append_child(self.0, parent, el, &mut added))?;
}
Ok(el)
}
fn set_attribute(self, el: niso_handle, name: &str, value: &str) -> Result {
ok(unsafe { niso_element_set_attribute(self.0, el, name.as_ptr(), name.len(), value.as_ptr(), value.len()) })
}
fn time_stamp(self, event: niso_handle) -> f64 {
let mut t = 0.0;
unsafe { niso_event_get_time_stamp(self.0, event, &mut t) };
t
}
}

examples/lang/rust/src/lib.rs, line 68

Listeners get a pointer to the app’s state as their userdata. Each holds a reference count, which the DOM gives back through the drop function when it releases the listener:

examples/lang/rust/src/lib.rs
fn share(state: &Rc<State>) -> *mut c_void {
Rc::into_raw(Rc::clone(state)) as *mut c_void
}

examples/lang/rust/src/lib.rs, line 133

examples/lang/rust/src/lib.rs
#[unsafe(no_mangle)]
pub extern "C" fn niso_app_main(dom: *mut niso_dom) -> i32 {
status(build(Dom(dom)))
}

examples/lang/rust/src/lib.rs, line 299

The library leaves the niso_* symbols to the host it’s loaded into. ELF linkers allow that by default; on Apple platforms the crate’s build.rs passes -undefined dynamic_lookup.

The headless test, examples/lang/rust/tests/timer.rs, is the host: it depends on niso-capi for the niso_* functions, makes a DOM, calls niso_app_main, taps the buttons, runs timers in virtual time and reads the render stream.

Terminal window
cargo test -q -p niso-timer

examples/lang/rust/build.sh runs the same build and test. The crate is a workspace member, so cargo test --workspace runs it too.

A Rust program can also be a host. niso-quickjs’s Js type makes a DOM with the QuickJS engine attached and runs scripts and HTML pages against it; niso-run is a small program on it (niso-run.rs):

crates/niso-quickjs/src/lib.rs
pub fn with_scale(width: u32, height: u32, scale: f32) -> Js {
let dom = niso_capi::niso_dom_new(width, height, scale);
let js = unsafe { niso_js_new(dom.cast()) };
assert!(!js.is_null(), "the JS glue failed to load");
Js { js, dom, loader: None }
}

crates/niso-quickjs/src/lib.rs, line 61

Js::eval, load_html, set_loader, run and run_until_idle cover what niso-run does; js.dom() gives the raw DOM for the C API’s runtime calls (rendering a frame, taps). These are workspace crates, not published ones: depend on them by path from a checkout. The desktop host, hosts/iced, is a complete Rust host built this way. For the crates and what each promises, see the crate contracts.