Hello World [Lua]
Introduction
The Lua binding uses LuaJIT's ffi module to call the prebuilt libazul native
library. You work entirely through the idiomatic azul.* wrapper layer — no manual
ffi.cast(...) or raw C.AzXxx_yyy(...) calls. Callbacks route through libazul's
host-invoker plumbing, so LuaJIT never has to synthesize a struct-by-value
trampoline.
Installation
You need LuaJIT 2.1+ (vanilla Lua has no ffi) and the native libazul library.
Recommended: LuaRocks
luarocks install azul --server=https://azul.rs/luarocks
Manual
- Download the native library from the /releases page (
libazul.dylib/libazul.so/azul.dll). - Put the generated
azul.luanext tohello-world.lua(it ships in the examples archive underlua/, and is produced bycargo run --bin azul-doc -- codegen allintotarget/codegen/azul.lua), or pointLUA_PATHat it.
Simple „Counter“ Example
local azul = require('azul')
-- Data model. azul.refany_create(value) wraps any Lua value into an AzRefAny;
-- azul.refany_get(refany) recovers it on the other side.
local model = { counter = 5 }
-- Click callback: a plain Lua function. The wrapper auto-routes it through the
-- host-invoker when you hand it to :set_on_click(...).
local function on_click(data, _info)
local m = azul.refany_get(data)
if m == nil then return azul.Update.DoNothing end
m.counter = m.counter + 1
return azul.Update.RefreshDom
end
-- Layout callback: f(data) -> Dom. Runs on startup and after RefreshDom.
local function layout(data, _info)
local m = azul.refany_get(data)
if m == nil then return azul.Dom.create_body() end
-- add_* mutators return self (chain top-down); with_* consume self.
local label = azul.Dom.create_div()
:add_css_property(azul.CssPropertyWithConditions.simple(
azul.CssProperty.font_size(azul.StyleFontSize.px(32.0))))
:add_child(azul.Dom.create_text(tostring(m.counter)))
local button_dom = azul.Button.create('Increase counter')
:set_button_type(azul.ButtonType.Primary)
:set_on_click(data:clone(), on_click) -- :clone() bumps the refcount
:dom()
return azul.Dom.create_body()
:add_child(label)
:add_child(button_dom)
end
local data = azul.refany_create(model)
-- Fluent :with(opts) recursively assigns nested window-state fields and
-- auto-converts Lua strings to AzString.
local window = azul.WindowCreateOptions.create(layout):with({
window_state = {
title = 'Hello World',
size = { dimensions = { width = 400.0, height = 300.0 } },
flags = {
decorations = azul.WindowDecorations.NoTitleAutoInject,
background_material = azul.WindowBackgroundMaterial.Sidebar,
},
},
})
local app = azul.App.create(data, azul.AppConfig.create())
app:run(window)
-- AzApp's __gc metamethod calls AzApp_delete automatically on collection.
Four things to notice.
azul.refany_create/azul.refany_get— wrap any Lua value into a handle; an internal id-keyed table keeps it alive for the handle's lifetime.data:clone()bumps the refcount (thread-safe) so the click handler can recover it later.- Two builder flavours.
add_*/set_*mutate in place and returnself(chain top-down);with_*consumeselfand return the new value. Both compose. - Plain Lua strings flow through auto-string conversion — pass
'Increase counter'andtostring(m.counter)directly; the wrapper converts toAzString. - Garbage collection is wired.
AzApp's__gcmetamethod callsAzApp_deletefor you whenappis collected.
Build and run
# macOS
DYLD_LIBRARY_PATH=. luajit hello-world.lua
# linux
LD_LIBRARY_PATH=. luajit hello-world.lua
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
module 'azul' not found—azul.luais not onLUA_PATH. Run LuaJIT from the directory that contains it, or setLUA_PATH="./?.lua;$LUA_PATH".cannot open libazul— the native library isn't onDYLD_LIBRARY_PATH/LD_LIBRARY_PATH.attempt to index a nil valuefromffi— you are on vanilla Lua, not LuaJIT. Theffimodule ships only with LuaJIT.NYI: cannot call this C function (yet)atApp.create— LuaJIT'sfficannot call a C function that takes an aggregate by value on some ABIs. On x86-64 (SysV)App.create(.., AppConfig)hits this; it works on arm64/macOS (the struct is passed differently). It is a LuaJIT limitation, not a version issue (a current LuaJIT 2.1 still NYIs) — there is no Lua-side workaround short of a by-pointer C-ABI, so the E2E board marks Lua⊘ SKIPon x86-64.- Counter does not advance —
on_clickreturnedazul.Update.DoNothing.
Coming Up Next
- Application Architecture — architecting a larger Azul application
- Document Object Model — the Dom tree: node types, hierarchy, and CSS
- Hello World [Ruby]