Guide

Hello World [Node.js]

Introduction

The JavaScript binding loads the prebuilt libazul native library via koffi on Node, bun:ffi on Bun, and Deno.UnsafeCallback on Deno. Node.js is the verified runtime; the same azul.js also detects Bun and Deno, but those paths are experimental (callback return values are not yet written back to native memory there). You write ordinary JS: a plain object, a function per callback, and the smart createWithLayout factory.

Installation

You need Node.js 16+ (or Bun / Deno), the koffi package, and the native libazul library.

The name azul on npmjs.org belongs to an unrelated project, so azul ships from its own index at azul.rs. The quickest install is the hosted tarball — it bundles libazul for Linux/macOS/Windows and pulls in koffi automatically:

npm install https://azul.rs/ui/npm/azul-0.2.0.tgz

With that installed you require('azul'). To wire it up by hand instead:

npm install koffi
# download the native library into the working dir:
wget -O libazul.dylib https://azul.rs/ui/release/0.2.0/libazul.dylib   # macOS
wget -O libazul.so    https://azul.rs/ui/release/0.2.0/libazul.so      # linux
# windows: download https://azul.rs/ui/release/0.2.0/azul.dll

Then drop the generated azul.js binding next to your script:

wget https://azul.rs/ui/release/0.2.0/azul.js

Simple „Counter“ Example

'use strict';

const azul = require('./azul.js');
const {
    App, AppConfig, Button, ButtonType, Dom,
    CssProperty, CssPropertyWithConditions, StyleFontSize,
    Update, WindowBackgroundMaterial, WindowCreateOptions, WindowDecorations,
    refanyCreate, refanyGet,
} = azul;

const model = { counter: 5 };

// Click callback. refanyGet recovers your object from the handle.
function onClick(dataPtr, _info) {
    const m = refanyGet(dataPtr);
    if (m == null) return Update.DoNothing;
    m.counter += 1;
    return Update.RefreshDom;
}

// Layout callback: f(data) -> Dom.
function layout(dataPtr, _info) {
    const m = refanyGet(dataPtr);
    if (m == null) return Dom.create_body();

    const label = Dom.create_div()
        .with_css_property(
            CssPropertyWithConditions.simple(
                CssProperty.font_size(StyleFontSize.px(32.0))))
        .with_child(Dom.create_text(String(m.counter)));

    const button = Button.create('Increase counter')
        .with_button_type(ButtonType.Primary)
        .on_click(model, onClick);

    return Dom.create_body()
        .with_child(label)
        .with_child(button.dom());
}

// Safety net: log koffi callback exceptions before they SIGABRT via libffi.
process.on('uncaughtException', (e) => {
    console.error('[azul] uncaught:', e && e.stack ? e.stack : e);
});

// Smart factory hides the host-invoker register + layout_callback splice;
// .with(opts) recursively assigns nested fields and converts JS strings.
const window = WindowCreateOptions.createWithLayout(layout).with({
    window_state: {
        title: 'Hello World',
        size: { dimensions: { width: 400.0, height: 300.0 } },
        flags: {
            decorations: WindowDecorations.NoTitleAutoInject,
            background_material: WindowBackgroundMaterial.Sidebar,
        },
    },
});

App.create(refanyCreate(model), AppConfig.create()).run(window);

Four things to notice.

  • refanyCreate / refanyGet — wrap any JS object into a handle; the same object is handed back to callbacks. Guard with if (m == null).
  • Callbacks are plain functions (dataPtr, info) => ... returning Update.* (or a Dom for layout). The createWithLayout factory registers them for you.
  • Enums and helpers are top-level on the module: Update.RefreshDom, ButtonType.Primary. For Option/Result/Vec, use the module helpers (azul.optionToNullable, azul.resultUnwrap) — koffi unions carry no methods.
  • Keep the uncaughtException handler — it logs exceptions thrown inside koffi callbacks before libffi can SIGABRT the process.

Build and run

node hello-world.js
# or:
bun  run hello-world.js
deno run --allow-ffi --unstable-ffi hello-world.js

azul.js looks for the native library (libazul.dylib / libazul.so / azul.dll) in this order:

  1. $AZ_LIB — explicit path to the library file (overrides everything),
  2. the directory containing azul.js itself,
  3. $AZ_LIB_DIR — directory containing the library,
  4. the current working directory,
  5. the system loader search path (DYLD_LIBRARY_PATH / LD_LIBRARY_PATH / PATH).

With the download steps above (library and azul.js in the same directory) it is found automatically — no environment variables needed. You should see the window pictured on the hello-world landing page.

Common errors

  • Cannot find module './azul.js' — run from the directory containing azul.js, or fix the require path.
  • koffi fails to load libazul — the native library isn't in any of the locations listed under „Build and run“; the simplest fix is to put libazul.dylib / libazul.so / azul.dll next to azul.js, or point AZ_LIB_DIR at the directory that contains it (or AZ_LIB at the file itself).
  • Process aborts on click with no stack — a callback threw; the uncaughtException logger surfaces it. Counter not advancing usually means you returned Update.DoNothing.

Coming Up Next