Skip to content

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(…).

  • 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, as examples/lang/jvm.sh does.
  • The runtime, written by hand in bindings/jvm/java/dev/niso/dom: Dom (a DOM, its document, window and event loop), handles, callbacks, DOMException, and Module for 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.

bindings/jvm/java/dev/niso/dom/Dom.java
/** 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

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:

examples/lang/java/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:

examples/lang/java/Timer.java
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

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.

Terminal window
JAVA_HOME=$(/usr/libexec/java_home -v 17) examples/lang/java/build.sh
java timer: ok
java binding: ok
java module: ok

On Linux, export JAVA_HOME as the path of your JDK 17 instead.

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.

Terminal window
hosts/android/build.sh --jvm examples/lang/java Timer

Not run for this page: the Android build needs an NDK, which the machines used here didn’t have (Build for Android).

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.

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:

examples/lang/java/build.sh
"$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.