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 — the same azul.js covers all three runtimes. 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.
Recommended: npm
npm install azul
Note
The 0.2.0 package is hosted on azul.rs. If the npm registry does not yet resolve it, install the tarball directly:
npm install https://azul.rs/npm/azul-0.2.0.tgz
Manual
npm install koffi
# download the native library from /releases into the working dir:
wget -O libazul.dylib https://azul.rs/release/0.2.0/libazul.dylib # macOS
Drop the generated azul.js next to your script (it ships in the
examples archive under node/).
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 withif (m == null).- Callbacks are plain functions
(dataPtr, info) => ...returningUpdate.*(or aDomfor layout). ThecreateWithLayoutfactory registers them for you. - Enums and helpers are top-level on the module:
Update.RefreshDom,ButtonType.Primary. ForOption/Result/Vec, use the module helpers (azul.optionToNullable,azul.resultUnwrap) — koffi unions carry no methods. - Keep the
uncaughtExceptionhandler — it logs exceptions thrown inside koffi callbacks before libffi canSIGABRTthe process.
Build and run
node hello-world.js
# or:
bun run hello-world.js
deno run --allow-ffi --unstable-ffi hello-world.js
The native library must be in the working directory or on DYLD_LIBRARY_PATH /
LD_LIBRARY_PATH / PATH. You should see the window pictured on the
hello-world landing page.
Common errors
Cannot find module './azul.js'— run from the directory containingazul.js, or fix therequirepath.- koffi fails to load
libazul— the native library isn't discoverable; put it in the working dir or set the loader path. - Process aborts on click with no stack — a callback threw; the
uncaughtExceptionlogger surfaces it. Counter not advancing usually means you returnedUpdate.DoNothing.
Coming Up Next
- Application Architecture — architecting a larger Azul application
- Document Object Model — the Dom tree: node types, hierarchy, and CSS
- Hello World [OCaml]