Hello World [V]
Experimental / CI-validated. V is an archetype-A (C-ABI-direct) backend, off the front-page tab set. The binding is generated and exercised by CI rather than hand-maintained.
Introduction
V talks to Azul through a plain C-ABI binding. The generated azul.v
translates the whole FFI surface explicitly: every AzString / AzDom
becomes a V struct, every enum a V enum with an explicit backing
integer, every tagged union a V union, every callback typedef a V
fn-type alias, and every exported symbol a top-level
fn C.AzApp_create(...) extern declaration.
Because a top-level V function compiles to a plain C function, callbacks are passed to Azul directly — like C, Zig and Odin, V needs neither a host-invoker trampoline nor a wrapper-struct dance. You pass the function itself.
You need a recent V toolchain (v version ≥ 0.4). The binding is
shipped as an azul/ subpackage that the module main driver imports
with import azul; you call the raw C symbols through V's C. namespace
(C.AzApp_create) and use the azul.Az* types.
Installation
There is no package-manager story for V yet — you download the native
library, the generated binding into an azul/ subdirectory, and the
hello-world driver, then build the directory with v run .:
# linux
curl -O https://azul.rs/ui/release/0.2.0/libazul.so
curl --create-dirs -o azul/azul.v https://azul.rs/ui/release/0.2.0/azul/azul.v
curl -O https://azul.rs/ui/release/0.2.0/hello-world.v
LD_LIBRARY_PATH=. v run .
# macos
curl -O https://azul.rs/ui/release/0.2.0/libazul.dylib
curl --create-dirs -o azul/azul.v https://azul.rs/ui/release/0.2.0/azul/azul.v
curl -O https://azul.rs/ui/release/0.2.0/hello-world.v
DYLD_LIBRARY_PATH=. v run .
# windows
curl -O https://azul.rs/ui/release/0.2.0/azul.dll
curl --create-dirs -o azul/azul.v https://azul.rs/ui/release/0.2.0/azul/azul.v
curl -O https://azul.rs/ui/release/0.2.0/hello-world.v
v run .
The #flag -L. + #flag -lazul lines inside azul.v make V's C backend
link against the libazul you just downloaded (-L. points at the
current directory). The LD_LIBRARY_PATH=. / DYLD_LIBRARY_PATH=. prefix
is needed at run time because the binary embeds no rpath — the dynamic
loader has to be told where the library lives.
Simple „Counter“ Example
This is the exact hello-world.v shipped in the release (the same file
the end-to-end test builds and clicks through). Functions are called
through V's C. namespace; types come from the imported azul module.
module main
import azul
struct MyDataModel {
mut:
counter u32
}
const my_data_type_id = u64(0x617a756c5f6d646d) // "azul_mdm"
fn my_data_destructor(ptr voidptr) {
}
fn az_str(s string) azul.AzString {
return C.AzString_fromUtf8(s.str, usize(s.len))
}
fn my_data_upcast(model MyDataModel) azul.AzRefAny {
mut local := model
blob := azul.AzGlVoidPtrConst{
ptr: voidptr(&local)
run_destructor: false
}
return C.AzRefAny_newC(blob, usize(sizeof(MyDataModel)), usize(4),
my_data_type_id, az_str('MyDataModel'), my_data_destructor, usize(0), usize(0))
}
fn my_data_downcast(refany &azul.AzRefAny) &MyDataModel {
if !C.AzRefAny_isType(refany, my_data_type_id) {
return unsafe { nil }
}
ptr := C.AzRefAny_getDataPtr(refany)
if isnil(ptr) {
return unsafe { nil }
}
return unsafe { &MyDataModel(ptr) }
}
fn on_click(data azul.AzRefAny, info azul.AzCallbackInfo) azul.AzUpdate {
mut d := data
m := my_data_downcast(&d)
if isnil(m) {
return azul.AzUpdate.DoNothing
}
unsafe {
m.counter++
}
return azul.AzUpdate.RefreshDom
}
fn layout(data azul.AzRefAny, info azul.AzLayoutCallbackInfo) azul.AzDom {
mut d := data
m := my_data_downcast(&d)
if isnil(m) {
return C.AzDom_createBody()
}
counter_val := unsafe { m.counter }
label := C.AzDom_createText(az_str(counter_val.str()))
mut label_wrapper := C.AzDom_createDiv()
css_prop := C.AzCssProperty_fontSize(C.AzStyleFontSize_px(32.0))
C.AzDom_addCssProperty(&label_wrapper, C.AzCssPropertyWithConditions_simple(css_prop))
C.AzDom_addChild(&label_wrapper, label)
mut button := C.AzButton_create(az_str('Increase counter'))
C.AzButton_setButtonType(&button, azul.AzButtonType.Primary)
data_clone := C.AzRefAny_clone(&d)
C.AzButton_setOnClick(&button, data_clone, on_click)
button_dom := C.AzButton_dom(button)
mut body := C.AzDom_createBody()
C.AzDom_addChild(&body, label_wrapper)
C.AzDom_addChild(&body, button_dom)
return body
}
fn main() {
data := my_data_upcast(MyDataModel{ counter: 5 })
mut window := C.AzWindowCreateOptions_create(layout)
window.window_state.title = az_str('Hello World')
window.window_state.size.dimensions.width = 400.0
window.window_state.size.dimensions.height = 300.0
window.window_state.flags.decorations = azul.AzWindowDecorations.NoTitleAutoInject
window.window_state.flags.background_material = azul.AzWindowBackgroundMaterial.Sidebar
mut app := C.AzApp_create(data, C.AzAppConfig_create())
C.AzApp_run(&app, window)
}
Callbacks are bare C function pointers
on_click and layout are ordinary top-level V functions, which the V
compiler lowers to plain C functions — ABI-identical to the C typedefs
AzButtonOnClickCallbackType and AzLayoutCallbackType. You pass the
function itself:
C.AzButton_setOnClick(&button, data_clone, on_click)
window := C.AzWindowCreateOptions_create(layout)
The raw AzButton_setOnClick takes the bare fn pointer, not an
AzButtonOnClickCallback struct — azul.v declares the raw C variant
whose argument is the fn (AzRefAny, AzCallbackInfo) AzUpdate typedef.
There is no host-invoker, no closure allocation, and no hidden registry:
the framework stores your pointer and calls straight back into your V
code on the UI thread.
How RefAny works in V
RefAny is Azul's type-erased, reference-counted box for your
application state. The example hand-rolls the same three pieces the C
AZ_REFLECT macro generates:
- Type identity —
my_data_type_idis a fixed, process-stableu64. (The C macro uses the address of a global; any unique constant is equally valid, and simpler in V.)AzRefAny_isTypeverifies a downcast against it at run time. - Upcast —
AzRefAny_newCcopiessizeof(MyDataModel)bytes into a refcounted heap allocation, so pointing it at a stack local is fine;run_destructor: falsetells libazul not to free the caller's pointer. - Downcast —
AzRefAny_isType+AzRefAny_getDataPtrrecover a typed&MyDataModel; both callbacks bail out (isnil(...)→AzUpdate.DoNothing/createBody()) when the check fails.
C.AzRefAny_clone(&d) bumps the (atomic) reference count — it does not
deep-copy your struct. On click the framework matches the hit-test, calls
on_click with the stored RefAny, your code downcasts and increments
counter, returns azul.AzUpdate.RefreshDom, and the framework re-runs
layout, which reads the new value.
Two more things worth noticing:
- Strings —
AzString_fromUtf8(ptr, len)copies the bytes into a refcounted heap buffer, so passings.str/s.lenfrom a temporary Vstringis safe: theAzStringoutlives the caller's frame. - Typed CSS — instead of parsing a CSS string, the example builds the
property programmatically:
AzStyleFontSize_px(32.0)→AzCssProperty_fontSize→AzCssPropertyWithConditions_simple→AzDom_addCssProperty.
Build and run
# linux
LD_LIBRARY_PATH=. v run .
# macos
DYLD_LIBRARY_PATH=. v run .
# windows (azul.dll in the current directory)
v run .
v run . compiles the current directory (hello-world.v + the imported
azul/ subpackage) and runs it. You should see the window pictured on the
hello-world landing page. Click the button: the
counter increments, layout re-runs, and the new value renders. To ship a
standalone binary, build with v -prod . -o hello-world instead.
Common errors
module "azul" not found— the binding is not where the import expects it.azul.vmust live in anazul/subdirectory of the directory you runv run .in (the install stepscurlit toazul/azul.v).undefined reference to Az...at link time — the linker cannot findlibazul. Keep the#flag -L.line inazul.vand make sure the native library sits in the current directory.- Runtime:
cannot open shared object file/library not found— the binary embeds no rpath, so keep theLD_LIBRARY_PATH=./DYLD_LIBRARY_PATH=.prefix from the install steps. - Undefined symbols mentioning AppKit/OpenGL on macOS — the
#flag darwin -framework …line inazul.vsupplies the system frameworks; make sure it survived any manual edits. - Counter does not update on click —
on_clickreturnedAzUpdate.DoNothing, or the downcast failed. A failed downcast means the type id did not match:my_data_type_idmust be the same constant in the upcast and the downcast.
Coming Up Next
- Application Architecture — architecting a larger Azul application
- Document Object Model — the Dom tree: node types, hierarchy, and CSS
- Hello World [Odin]