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 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
azul.js looks for the native library (libazul.dylib / libazul.so /
azul.dll) in this order:
$AZ_LIB— explicit path to the library file (overrides everything),- the directory containing
azul.jsitself, $AZ_LIB_DIR— directory containing the library,- the current working directory,
- 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 containingazul.js, or fix therequirepath.- 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 putlibazul.dylib/libazul.so/azul.dllnext toazul.js, or pointAZ_LIB_DIRat the directory that contains it (orAZ_LIBat the file itself). - 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]