Guide

Hello World [Kotlin]

Introduction

The Kotlin binding rides on the same JNA layer as Java, so it loads the prebuilt libazul native library directly. You write idiomatic Kotlin — a data class, a LayoutCallback SAM that returns a Dom, and the companion-object App factory — and the generated wrappers handle the FFI.

Installation

You need Kotlin 1.9+, JDK 17+, JNA 5.14+, and the native libazul library.

Azul is not on Maven Central, but there is a self-hosted maven2 repository served from azul.rs (generated by the release CI). Add it next to mavenCentral() and depend on rs.azul:azul (the jar bundles the Java bindings plus a Linux x86-64 libazul.so as a JNA resource; on other platforms also do step 1 below). Only JNA itself (net.java.dev.jna:jna:5.14.0) comes from Maven Central as usual:

repositories {
    mavenCentral()
    maven { url = uri("https://azul.rs/ui/maven") }
}
dependencies {
    implementation("net.java.dev.jna:jna:5.14.0")
    implementation("rs.azul:azul:0.2.0")
}

Alternatively, install manually:

  1. Download the native library from the release page (libazul.dylib / libazul.so / azul.dll).

  2. Add the generated Azul.kt bindings (plus optional Gradle scaffolding) to your sources:

    wget https://azul.rs/ui/release/0.2.0/Azul.kt
    wget https://azul.rs/ui/release/0.2.0/build.gradle.kts
    wget https://azul.rs/ui/release/0.2.0/settings.gradle.kts
    

The native library must be discoverable via -Djna.library.path / DYLD_LIBRARY_PATH / LD_LIBRARY_PATH / PATH.

Simple „Counter“ Example

package com.azul

import com.sun.jna.Pointer

// Plain data class - the "single source of truth" for app state.
class MyDataModel(var counter: Int)
private val MODEL = MyDataModel(5)

// Click callback: write the Update int through the out-pointer.
private val onClick = AzulNativeManaged.ButtonOnClickCallbackInvokerCallback { _, dataPtr, _, outPtr ->
    val m = AzulHostInvoker.refanyGet(dataPtr)
    val result = if (m is MyDataModel) { m.counter += 1; Update.RefreshDom.value }
                 else Update.DoNothing.value
    outPtr!!.setInt(0, result)
}

// Typed layout callback: returns a Dom directly; the bridge splices the bytes
// into the native out-pointer internally.
private val layout = AzulHostInvoker.LayoutCallback { _, dataPtr, _ ->
    val m = AzulHostInvoker.refanyGet(dataPtr)
    if (m !is MyDataModel) {
        Dom.createBody()
    } else {
        val label = Dom.createDiv()
            .withCss("font-size: 32px;")
            .withChild(Dom.createText(m.counter.toString()))
        val buttonDom = Button.create("Increase counter")
            .withButtonType(ButtonType.Primary.value)
            .onClick(m, onClick)
            .dom()
        Dom.createBody()
            .withChild(label)
            .withChild(buttonDom)
    }
}

fun main() {
    // `use { }` disposes the App (C-side delete) when the block exits.
    App.create(AzulHostInvoker.refanyWrap(MODEL), AppConfig.create()).use { app ->
        app.run(WindowCreateOptions.create(layout))
    }
}

Three things to notice.

  • refanyWrap / refanyGet with is smart-casts — the same object instance is handed back to every callback; if (m is MyDataModel) both guards and smart-casts. On mismatch return Dom.createBody() / Update.DoNothing.value.
  • LayoutCallback SAM returns Dom — the companion WindowCreateOptions.create factory hides the host-invoker register + JNA byte-splice. Note the !! on the nullable Pointer? out-pointer before setInt.
  • Fluent wrapper APIDom.createBody().withChild(...) and Button.create(...).withButtonType(...).onClick(data, fn).dom(). The click handler is the event's typed SAM (ButtonOnClickCallbackInvokerCallback for Button.onClick). AzulString.toString() decodes UTF-8 into kotlin.String.

Build and run

kotlinc -J-Xmx4g -cp $JNA_JAR Azul.kt HelloWorld.kt \
    -include-runtime -d hello-world.jar
# macOS requires -XstartOnFirstThread (Cocoa main-thread rule).
DYLD_LIBRARY_PATH=. java -XstartOnFirstThread -Djna.library.path=. \
    -cp hello-world.jar:$JNA_JAR com.azul.HelloWorldKt

$JNA_JAR points at your jna-5.14.0.jar (from Maven Central, net.java.dev.jna:jna:5.14.0). On Linux/Windows drop -XstartOnFirstThread and use LD_LIBRARY_PATH / PATH.

Alternatively — and recommended — use the Gradle project from the repository's examples/kotlin/ directory (build.gradle.kts): gradle run pulls JNA from Maven Central, compiles Azul.kt + HelloWorld.kt with daemon caching (the 4 GB compiler heap is preset in gradle.properties), and wires jna.library.path onto the run task for you.

You should see the window pictured on the hello-world landing page.

Common errors

  • UnsatisfiedLinkError — native library not on the JNA library path.
  • No window on macOS-XstartOnFirstThread missing.
  • Counter does not advance — the click handler wrote Update.DoNothing.value.
  • NullPointerException on outPtr — the !! unwrap on the SAM's nullable Pointer? arg is required; keep it.
  • Process hangs at exit on Windows — the example builds and runs the whole headless layout, but the JVM may not terminate afterwards. This is a known JNA-on-Windows behaviour: the JVM exits only once all non-daemon threads end and the native event queue is drained, so a native (libazul) thread or window left on the JVM thread keeps it alive. The binding itself is fine — it passes the full run on macOS, and the Java binding (same JVM) runs on Windows — so a fix needs a Windows-host thread dump of the hung JVM. The E2E board reports Kotlin ⊘ SKIP on Windows for this reason.

Coming Up Next