Guide

Hello World [Java]

Introduction

The Java binding loads the prebuilt libazul native library through JNA. You write ordinary Java — a plain data class, a typed LayoutCallback that returns a Dom, and the wrapper-class App API — and the generated wrappers handle the FFI. No manual byte splicing, no Pointer arithmetic in your code.

Installation

You need JDK 17+, Maven, 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 Maven Central and depend on rs.azul:azul (the jar bundles the com.azul bindings plus a Linux x86-64 libazul.so as a JNA resource; on other platforms also do step 1 below so JNA finds the native library):

<repositories>
  <repository>
    <id>azul-rs</id>
    <url>https://azul.rs/ui/maven</url>
  </repository>
</repositories>
<dependencies>
  <dependency>
    <groupId>rs.azul</groupId>
    <artifactId>azul</artifactId>
    <version>0.2.0</version>
  </dependency>
</dependencies>

Alternatively, install manually. Only JNA itself (net.java.dev.jna:jna:5.14.0) comes from Maven Central as usual.

  1. Download the native library from the release page (libazul.dylib / libazul.so / azul.dll) and keep it in your working directory or pass -Djna.library.path=..

  2. Download the generated com.azul wrapper sources and unpack them into your source tree:

    wget https://azul.rs/ui/release/0.2.0/azul-java.zip
    unzip azul-java.zip -d src/main/java/com/azul/
    

Simple „Counter“ Example

package com.azul;

import com.sun.jna.Pointer;

public final class HelloWorld {

    // Plain data class - the "single source of truth" for app state.
    public static final class MyDataModel {
        public int counter;
        public MyDataModel(int counter) { this.counter = counter; }
    }

    private static final MyDataModel MODEL = new MyDataModel(5);

    // Click callback. refanyGet recovers your object from the handle; write the
    // Update value back through the out-pointer.
    private static final AzulNativeManaged.ButtonOnClickCallbackInvokerCallback ON_CLICK =
        (long id, Pointer dataPtr, Pointer infoPtr, Pointer outPtr) -> {
            Object m = AzulHostInvoker.refanyGet(dataPtr);
            int result = Update.DoNothing.value;
            if (m instanceof MyDataModel) {
                ((MyDataModel) m).counter += 1;
                result = Update.RefreshDom.value;
            }
            outPtr.setInt(0, result);
        };

    // Typed layout callback: returns a Dom directly. The host-invoker bridge
    // splices the Dom bytes into libazul's out-pointer for you.
    private static final AzulHostInvoker.LayoutCallback LAYOUT =
        (long id, Pointer dataPtr, Pointer infoPtr) -> {
            Object recovered = AzulHostInvoker.refanyGet(dataPtr);
            if (!(recovered instanceof MyDataModel)) {
                return Dom.createBody();
            }
            MyDataModel m = (MyDataModel) recovered;
            Dom label = Dom.createDiv()
                .withCss("font-size: 32px;")
                .withChild(Dom.createText(String.valueOf(m.counter)));
            Dom buttonDom = Button.create("Increase counter")
                .withButtonType(ButtonType.Primary.value)
                .onClick(m, ON_CLICK)
                .dom();
            return Dom.createBody()
                .withChild(label)
                .withChild(buttonDom);
        };

    public static void main(String[] args) {
        // try-with-resources disposes the App (C-side delete) on exit.
        try (App app = App.create(AzulHostInvoker.refanyWrap(MODEL), AppConfig.create())) {
            app.run(WindowCreateOptions.create(LAYOUT));
        }
    }
}

Four things to notice.

  • AzulHostInvoker.refanyWrap / refanyGet — your MyDataModel is wrapped once and the same instance is handed back to every callback. Use instanceof to guard the cast and fall back to Dom.createBody() / Update.DoNothing on mismatch.
  • Typed LayoutCallback SAM — returns a Dom directly; the bridge handles the byte-splice into the native out-pointer. Click handlers use the event's typed SAM (ButtonOnClickCallbackInvokerCallback for Button.onClick) and write the Update int via outPtr.setInt(0, ...).
  • Wrapper-class fluent APIDom.createBody().withChild(...) and Button.create(label).withButtonType(...).onClick(data, fn).dom(). AzulString decodes to java.lang.String via .toString().
  • try (App app = ...) releases native memory deterministically — close() calls the C-side delete.

Build and run

Use the ready-made Maven project from the repository — examples/java/pom.xml — as your project pom. It bundles JNA into the output jar via maven-shade-plugin and sets Main-Class: com.azul.HelloWorld in the manifest, so a plain java -jar works with no manual classpath:

mvn package
# macOS — -XstartOnFirstThread is REQUIRED so libazul's NSApplication loop
# pumps on the JVM main thread.
java -XstartOnFirstThread -Djna.library.path=. -jar target/hello-world-1.0.0.jar

On Linux/Windows drop -XstartOnFirstThread. -Djna.library.path=. points JNA at the directory holding libazul.dylib / libazul.so / azul.dll.

You should see the window pictured on the hello-world landing page. Click the button: the counter increments and the layout callback re-runs.

Common errors

  • UnsatisfiedLinkError / library not found — the native library isn't on -Djna.library.path / DYLD_LIBRARY_PATH / LD_LIBRARY_PATH.
  • Window never appears / instant crash on macOS — you omitted -XstartOnFirstThread. Cocoa requires the event loop on the main thread.
  • Counter does not advance — the click handler wrote Update.DoNothing.value. Write Update.RefreshDom.value to outPtr after mutating.

Coming Up Next