Java and Kotlin
Checked against 0015716 on 2026-11-10. The files it covers are unchanged since.
Java and Kotlin drive niso through the JVM binding: the package
dev.niso.dom, with a class per DOM interface (Document, Element,
HTMLButtonElement, …) generated from niso’s WebIDL, over JNI functions in
a native library. Code written against it reads like a page’s script with
Java’s types: doc.createElement("button"), el.addEventListener("click", …),
window.requestAnimationFrame(…).
What’s in the binding
Section titled “What’s in the binding”- Generated classes.
crates/niso-jvm’s build script writes a Java class for each interface in the binding’s subset of the WebIDL, with the JNI functions behind them, into cargo’s build output (OUT_DIR/java). They aren’t committed; build the crate to get them, asexamples/lang/jvm.shdoes. - The runtime, written by hand in
bindings/jvm/java/dev/niso/dom:Dom(a DOM, its document, window and event loop), handles, callbacks,DOMException, andModulefor native modules. libniso_jvm, the native half: the C API with the JNI functions and the JS engine for pages. The Android host links the same functions into its own library,libniso_android.so.
A Dom is single-threaded, like the C API: call it only on the thread that
made it. Java holds each handle while its wrapper object lives, so you don’t
release nodes by hand.
/** A new DOM with a viewport of width x height device pixels at scale device pixels per CSS pixel. */ public Dom(int width, int height, float scale) { this(Natives.rt_new(width, height, scale), true); }bindings/jvm/java/dev/niso/dom/Dom.java, line 47
The timer app in Java
Section titled “The timer app in Java”A JVM app is a Consumer<Dom>: it builds its document in accept, and from
then on its code runs in the listeners and frame callbacks it registered.
Timer.java:
public void accept(Dom dom) { doc = dom.getDocument(); window = dom.getWindow(); append(doc.getHead(), "style", CSS);
main = append(doc.getBody(), "main", null); elapsed = (HTMLElement) append(main, "h1", null); elapsed.setId("elapsed"); elapsedText = doc.createTextNode(""); elapsed.appendChild(elapsedText); minute = (HTMLProgressElement) append(main, "progress", null); minute.setId("minute"); minute.setAttribute("max", "60000");
Element buttons = append(main, "div", null); buttons.setClassName("buttons"); start = button(buttons, "start", "Start", this::onStart); stop = button(buttons, "stop", "Stop", this::onStop); lap = button(buttons, "lap", "Lap", this::onLap); reset = button(buttons, "reset", "Reset", this::onReset);
Element label = append(main, "label", "Keep screen on"); awake = (HTMLInputElement) append(label, "input", null); awake.setId("awake"); awake.setAttribute("type", "checkbox"); awake.setAttribute("switch", ""); awake.addEventListener("change", e -> { if (awake.getChecked()) { main.setAttribute("data-keep-awake", ""); } else { main.removeAttribute("data-keep-awake"); } });
laps = append(main, "ol", null); laps.setId("laps"); show(0); updateButtons(); }examples/lang/java/Timer.java, line 96
Listeners are lambdas or method references:
private HTMLButtonElement button(Node parent, String id, String label, EventListener listener) { HTMLButtonElement b = (HTMLButtonElement) append(parent, "button", label); b.setId(id); b.addEventListener("click", listener); return b; }examples/lang/java/Timer.java, line 88
Build and test
Section titled “Build and test”examples/lang/java/build.sh
builds the binding and the app with javac --release 11, then runs three
tests headless against libniso_jvm: the timer
(TimerTest.java,
which is its own host: it makes a Dom, calls accept, taps, runs timers
and reads the render stream), the binding’s own test, and a page calling a
JVM native module. It needs JAVA_HOME to name a JDK 17.
JAVA_HOME=$(/usr/libexec/java_home -v 17) examples/lang/java/build.shjava timer: okjava binding: okjava module: okOn Linux, export JAVA_HOME as the path of your JDK 17 instead.
On Android
Section titled “On Android”The Android host runs a JVM app in place of a page: --jvm DIR CLASS builds
the Java sources in DIR into the app and runs CLASS.
hosts/android/build.sh --jvm examples/lang/java TimerNot run for this page: the Android build needs an NDK, which the machines used here didn’t have (Build for Android).
Kotlin
Section titled “Kotlin”There’s no Kotlin-specific binding: Kotlin uses the dev.niso.dom classes
as it uses any Java library. The Android host’s own Kotlin does: its
Engine.kt
takes an app as a Consumer<Dom> and starts it with the page’s Dom.
Native modules
Section titled “Native modules”The JVM side can also go the other way and give pages an API: a native
module is a WebIDL interface marked [NisoModule] that Java or Kotlin
implements and a page imports as niso:<name>. niso-idl’s niso-module
tool generates the Java side from the IDL file; the Java build script does
this for its test module,
greeter.webidl:
"$NISO_MODULE" java greeter.webidl greeter "$out/src""$NISO_MODULE" js greeter.webidl | sed 's/^export //' >"$out/greeter.js"examples/lang/java/build.sh, line 11
examples/ihatemyphone’s phone module is one written in Kotlin for Android
(PhoneModule.kt). See Native modules and the
module design notes.