Hello World [Pascal]
Introduction
The Pascal binding is a single generated Azul unit (azul.pas) for
Free Pascal 3.2+ that declares every Az* function as
cdecl; external 'azul', plus a small „host-invoker“ layer that lets
you write callbacks as ordinary Object Pascal classes. There is no
extra runtime and no code generator to run on your side: you download
one .pas file and the native library, and fpc does the rest.
Two compiler directives make the FFI work and must be present in any program that uses the unit (the example below has them):
{$mode objfpc}{$H+}— Object Pascal mode: classes,override, and ($H+)AnsiStringas the default string type.{$PACKRECORDS C}— forces C-ABI struct layout, soAz*records passed by value match the Rustextern "C"ABI exactly. Without it, field offsets silently differ and calls corrupt memory.
Installation
You need the Free Pascal Compiler (fpc, 3.2+ — apt install fp-compiler / brew install fpc), the generated azul.pas unit, and
the native library. All three downloads come from the release page and
are built by the release CI.
Linux:
curl -O https://azul.rs/ui/release/0.2.0/libazul.so
curl -O https://azul.rs/ui/release/0.2.0/azul.pas
curl -O https://azul.rs/ui/release/0.2.0/hello-world.pas
fpc -Mobjfpc -Sh -Fl. hello-world.pas
LD_LIBRARY_PATH=. ./hello-world
macOS:
curl -O https://azul.rs/ui/release/0.2.0/libazul.dylib
curl -O https://azul.rs/ui/release/0.2.0/azul.pas
curl -O https://azul.rs/ui/release/0.2.0/hello-world.pas
fpc -Mobjfpc -Sh -Fl. hello-world.pas
DYLD_LIBRARY_PATH=. ./hello-world
Windows:
curl -O https://azul.rs/ui/release/0.2.0/azul.dll
curl -O https://azul.rs/ui/release/0.2.0/azul.pas
curl -O https://azul.rs/ui/release/0.2.0/hello-world.pas
fpc -Mobjfpc -Sh -Fl. hello-world.pas
hello-world.exe
-Fl. adds the current directory to the linker's library search
path. azul.pas carries a {$linklib azul} directive, so FPC links
against libazul automatically — you do not need -k-lazul on
the command line, only -Fl. (or a system-installed libazul) so the
linker can find the library file.
Simple „Counter“ Example
This is the exact program shipped as examples/pascal/hello-world.pas:
program HelloWorld;
{$mode objfpc}{$H+}
{$PACKRECORDS C}
uses
ctypes, sysutils,
Azul;
type
{ Plain data model. }
TMyModel = class
Counter: Integer;
constructor Create(c: Integer);
end;
{ Click handler: bump counter and request a DOM refresh.
Button.onClick is TYPED since the typed-callback API change: derive
from TAzButtonOnClickCallbackInvoker, not the generic TAzCallbackInvoker. }
TMyClickHandler = class(TAzButtonOnClickCallbackInvoker)
procedure Invoke(id: cuint64; arg0: Pointer; arg1: Pointer; out_ptr: Pointer); override;
end;
{ Layout handler: build the DOM. }
TMyLayoutHandler = class(TAzLayoutCallbackInvoker)
procedure Invoke(id: cuint64; arg0: Pointer; arg1: Pointer; out_ptr: Pointer); override;
end;
constructor TMyModel.Create(c: Integer);
begin
Counter := c;
end;
function MakeAzString(const s: ansistring): TAzString;
begin
if Length(s) = 0 then
Result := AzString_fromUtf8(nil, 0)
else
Result := AzString_fromUtf8(PChar(@s[1]), Length(s));
end;
procedure TMyClickHandler.Invoke(id: cuint64; arg0: Pointer; arg1: Pointer; out_ptr: Pointer);
var
m: TObject;
begin
m := azul_refany_get(PAzRefAny(arg0));
if (m <> nil) and (m is TMyModel) then
TMyModel(m).Counter := TMyModel(m).Counter + 1;
if out_ptr <> nil then
PAzUpdate(out_ptr)^ := TAzUpdate_RefreshDom;
end;
procedure TMyLayoutHandler.Invoke(id: cuint64; arg0: Pointer; arg1: Pointer; out_ptr: Pointer);
var
m: TObject;
counter_text, label_wrap, body: TDom;
btn: TButton;
click_handler: TMyClickHandler;
click_cb: TAzButtonOnClickCallback;
click_data: TAzRefAny;
begin
m := azul_refany_get(PAzRefAny(arg0));
if (m = nil) or not (m is TMyModel) then
begin
body := TDom.CreateBody;
if out_ptr <> nil then
PAzDom(out_ptr)^ := body.Release;
body.Free;
Exit;
end;
{ Idiomatic wrapper classes: TDom.CreateText / CreateDiv / CreateBody are
named constructors; builder methods return fresh TDom wrappers and
consume their by-value inputs, so .Free on a consumed wrapper only
releases the object shell, never the DOM it was moved into. }
counter_text := TDom.CreateText(MakeAzString(IntToStr(TMyModel(m).Counter)));
label_wrap := TDom.CreateDiv.WithCss(MakeAzString('font-size: 32px;'))
.WithChild(counter_text);
click_handler := TMyClickHandler.Create;
click_cb := azul_register_buttononclickcallback(click_handler);
click_data := azul_refany_create(TMyModel(m));
btn := TButton.Create(MakeAzString('Increase counter'))
.WithButtonType(TAzButtonType_Primary)
.WithOnClick(click_data, click_cb);
body := TDom.CreateBody.WithChild(label_wrap).WithChild(btn.Dom);
{ Release detaches the raw record: ownership passes to libazul via out_ptr. }
if out_ptr <> nil then
PAzDom(out_ptr)^ := body.Release;
{ Free the wrapper shells (their records were consumed / released above). }
counter_text.Free;
label_wrap.Free;
btn.Free;
body.Free;
end;
var
model: TMyModel;
layout_handler: TMyLayoutHandler;
data: TAzRefAny;
layout_cb: TAzLayoutCallback;
wco: TAzWindowCreateOptions;
cfg: TAzAppConfig;
app: TAzApp;
begin
WriteLn('[azul] Pascal full-GUI hello-world starting.');
model := TMyModel.Create(5);
data := azul_refany_create(model);
layout_handler := TMyLayoutHandler.Create;
layout_cb := azul_register_layoutcallback(layout_handler);
wco := AzWindowCreateOptions_default();
wco.window_state.layout_callback := layout_cb;
wco.window_state.size.dimensions.width := 400.0;
wco.window_state.size.dimensions.height := 300.0;
wco.window_state.flags.decorations := TAzWindowDecorations_NoTitleAutoInject;
wco.window_state.flags.background_material := TAzWindowBackgroundMaterial_Sidebar;
cfg := AzAppConfig_create();
app := AzApp_create(data, cfg);
AzApp_run(@app, wco);
end.
Five things to notice.
- The host-invoker pattern — FPC cannot hand libazul a per-callback
function pointer that captures state (there are no closures with a C
ABI, and struct-by-value callback signatures are off-limits for most
managed FFIs). Instead, the
Azulunit registers one C stub per callback kind with libazul when the unit loads (itsinitializationblock callsAzApp_setButtonOnClickCallbackInvoker,AzApp_setLayoutCallbackInvoker, and so on — about 20 kinds). Your handler is a plain object: subclass the matching invoker class, overrideInvoke, then call the matchingazul_register_*function. That returns a smallTAz*Callbackrecord carrying a numeric handle; when the event fires, libazul calls the unit's stub with that handle, the stub looks your object up in a handle table and dispatches to yourInvoke. - Typed invoker classes — since the typed-callback change every
widget event has its own pair: derive from
TAzButtonOnClickCallbackInvokerand register withazul_register_buttononclickcallback(analogouslyTAzLayoutCallbackInvoker/azul_register_layoutcallback). Deriving from a generic invoker class will not compile against the current unit — theoverridehas no matching virtual method. azul_refany_create/azul_refany_get— the Pascal analogue ofRefAny.azul_refany_create(TObject)stores your object in the unit's handle table and wraps the handle in aTAzRefAnythat libazul carries around;azul_refany_get(PAzRefAny(arg0))in a callback hands the same instance back. Always guard with<> nilandisbefore the typecast, and fall back to an empty body / no-op on mismatch.- Ownership — the handle table owns every object you pass to
azul_refany_createorazul_register_*, andFrees it exactly once when libazul drops the last reference (the table dedups by object identity and refcounts, so re-wrapping the same model on each relayout is safe). So do notFreethose objects yourself — only the short-livedTDom/TButtonwrapper shells you build inside a layout callback are yours toFree. - Raw
Invokesignatures —arg0is the dataPAzRefAny,arg1the (unused here) info pointer, andout_ptris where the result is written: a click handler storesPAzUpdate(out_ptr)^ := TAzUpdate_RefreshDom, a layout handler storesPAzDom(out_ptr)^ := body.Release(the wrapper form) orPAzDom(out_ptr)^ := body(the raw record form). Forgetting theout_ptrwrite is the classic „window opens but nothing happens“ bug. - Fluent wrapper style —
TDom.CreateDiv/TButton.Createare named constructors returning wrapper objects;WithCss,WithChild,WithButtonType,WithOnClickreturn a fresh wrapper so you can chain them (TDom.CreateDiv.WithCss(...).WithChild(...)).body.Releasedetaches the raw record and hands ownership to libazul throughout_ptr. (The rawAzDom_*/AzButton_*record functions are still exported if you prefer the C-style by-value builder.) Strings cross the FFI as UTF-8 buffers:MakeAzStringcopies anAnsiStringviaAzString_fromUtf8.
Build and run
From the directory containing azul.pas, hello-world.pas and the
native library:
fpc -Mobjfpc -Sh -Fl. hello-world.pas
# Linux
LD_LIBRARY_PATH=. ./hello-world
# macOS
DYLD_LIBRARY_PATH=. ./hello-world
# Windows
hello-world.exe
-Mobjfpc -Sh mirror the {$mode objfpc}{$H+} directives on the
command line. azul.pas's {$linklib azul} handles the -lazul for
you. If your linker still cannot find the library file, pass its
directory to the linker explicitly:
fpc -Mobjfpc -Sh -Fl. -k-L. hello-world.pas
Compiling azul.pas takes about two seconds and prints two
Comment level 2 found warnings — these come from directive text
quoted inside the generated header comment and are harmless.
You should see the window pictured on the
hello-world landing page. Click the button: the
counter increments, TMyLayoutHandler.Invoke re-runs, and the new
value renders.
Common errors
ld: symbol(s) not found/cannot find -lazulat the link step — the native library is not where the linker looks. Build from the directory holdinglibazul.{so,dylib}/azul.dlland keep-Fl.(or add the explicit-k-L.linker path shown above).- Runtime: „library not found“ — the loader cannot find the
library. Export
LD_LIBRARY_PATH=.(Linux) /DYLD_LIBRARY_PATH=.(macOS), or placeazul.dllnext to the.exeon Windows. There is no method in an ancestor class to be overridden— you derived from the wrong invoker class or changed theInvokesignature. It must be exactlyInvoke(id: cuint64; arg0: Pointer; arg1: Pointer; out_ptr: Pointer); override;on the typed per-widget class (TAzButtonOnClickCallbackInvoker,TAzLayoutCallbackInvoker, ...).- Counter does not update on click — the handler never wrote
PAzUpdate(out_ptr)^ := TAzUpdate_RefreshDom, orazul_refany_getreturnednil/ a different class and theisguard skipped the increment.WriteLnin the failure branch to verify. - Random crashes when passing your own records to
Az*functions — a unit is missing{$PACKRECORDS C}. Every unit that declares or passesAz*records by value needs it. - Growing memory in long-running apps — handler objects passed to
azul_register_*are currently not freed when the native side releases the handle. The example creates a freshTMyClickHandlerper layout pass, which is fine for a demo but leaks a small object per relayout; in a long-running app, create your invoker instances once and reuse them. - Threads — the handle table behind
azul_refany_create/azul_register_*is not synchronized. Register callbacks and create RefAnys from the main thread only; do not register/release fromAzThreadcallbacks concurrently.
Coming Up Next
- Application Architecture — architecting a larger Azul application
- Document Object Model — the Dom tree: node types, hierarchy, and CSS
- Hello World [Scala]