Skip to content

Native modules

Generated from docs/modules.md when the site was built at 7aa23e2.

A native module is an API an app’s own native code gives its pages, the way ihatemyphone’s phone module gives its Svelte pages the volume, the screenshots and the assistant setup. WebIDL is the single source of its types: niso-idl generates the page’s import with WebIDL’s conversions, its TypeScript declarations, and the native base class from one IDL file. Calls cross the core’s C ABI (niso_dom_define_module in niso_runtime.h) as value lists, so a binding for another language (Swift next) needs only its own generator, not a new protocol.

It takes one IDL file, one Kotlin class and one import. A module is a package, a directory named after it, beside the app:

phone/
phone.webidl the API
android/build.gradle.kts an Android library: generates the Java from the IDL
android/src/main/kotlin/... the class behind it
  1. Declare the API as a [NisoModule=<name>] interface. Operations, attributes, enums, dictionaries, sequences and nullable types convert as WebIDL says. A Promise<T> operation settles later, and an event handler attribute (attribute LevelHandler? onlevelchange) is an event the native side sends.

    enum Stream { "media", "ring" };
    callback LevelHandler = undefined (Stream stream, double level);
    [NisoModule=phone]
    interface Phone {
    double setLevel(Stream stream, double level);
    Promise<boolean> screenshot();
    attribute LevelHandler? onlevelchange;
    };
  2. Implement the generated base class. Each operation and attribute is an abstract method. A promise’s method gets a Resolver to settle, now or later, from any thread. Each event has a method that sends it. A DOMException it throws reaches the page as itself, an IllegalArgumentException as a TypeError, and anything else as an Error.

    class PhoneModule(context: Context) : Phone(), PageLifecycle {
    override fun setLevel(stream: Stream, level: Double) = volume.set(stream, level)
    override fun screenshot(result: Resolver<Boolean>) = save { ok -> result.resolve(ok) }
    fun changed(stream: Stream) = onlevelchange(stream, level(stream))
    }
  3. Import it in the page. examples/build.mjs resolves niso:<name> to the IDL file beside the page or above it (directly or in its package), and writes <name>.d.ts beside the IDL file for editors.

    import { phone } from 'niso:phone';
    phone.onlevelchange = (stream, level) => { ... };

The Android app’s settings include every package beside it as a Gradle project (examples/ihatemyphone/android/settings.gradle.kts), so the app adds it with implementation(project(":phone")) and gives the instance to its page: Engine(context, script, name, modules = listOf(phone)). The page gets it before its script runs. Headless tests define a fake subclass on a dev.niso.dom.Dom with dom.define(fake), as examples/ihatemyphone/test does.

Results and events reach the page only as tasks of its event loop, never in the middle of a script. An event the native side sends while no page has the module is dropped, so a page reads state through an attribute (phone.shown) when it starts.

A module that implements dev.niso.host.PageLifecycle hears onResume, onPause and onNewIntent for its page. PageView sends them from the lifecycle of the activity or window it is in, with no code in the app.

Permissions follow Tauri’s pattern in IDL. checkPermissions() returns a dictionary of "granted" | "denied" | "prompt" states, and requestPermissions() asks and resolves with the new states. ihatemyphone’s requestPermissions opens the assistant settings and resolves from onResume, once the user is back.

  • Streams: a module streams through events today. A Tauri-style channel, a callback argument to one call, would be a callback type in an operation’s arguments with its own id on the wire. niso-idl rejects that for now.
  • Access control: every call goes through niso_dom_module_call, so a per-page allowlist of modules and members (Tauri’s capabilities) would sit there.
  • Activity results: PageLifecycle has no onActivityResult, because ihatemyphone needs none. Its settings round trip resolves on resume.
  • Swift: the C ABI is ready, but there is no generator for it yet.