Guide

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 identitymy_data_type_id is a fixed, process-stable u64. (The C macro uses the address of a global; any unique constant is equally valid, and simpler in V.) AzRefAny_isType verifies a downcast against it at run time.
  • UpcastAzRefAny_newC copies sizeof(MyDataModel) bytes into a refcounted heap allocation, so pointing it at a stack local is fine; run_destructor: false tells libazul not to free the caller's pointer.
  • DowncastAzRefAny_isType + AzRefAny_getDataPtr recover 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:

  • StringsAzString_fromUtf8(ptr, len) copies the bytes into a refcounted heap buffer, so passing s.str / s.len from a temporary V string is safe: the AzString outlives the caller's frame.
  • Typed CSS — instead of parsing a CSS string, the example builds the property programmatically: AzStyleFontSize_px(32.0)AzCssProperty_fontSizeAzCssPropertyWithConditions_simpleAzDom_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.v must live in an azul/ subdirectory of the directory you run v run . in (the install steps curl it to azul/azul.v).
  • undefined reference to Az... at link time — the linker cannot find libazul. Keep the #flag -L. line in azul.v and 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 the LD_LIBRARY_PATH=. / DYLD_LIBRARY_PATH=. prefix from the install steps.
  • Undefined symbols mentioning AppKit/OpenGL on macOS — the #flag darwin -framework … line in azul.v supplies the system frameworks; make sure it survived any manual edits.
  • Counter does not update on clickon_click returned AzUpdate.DoNothing, or the downcast failed. A failed downcast means the type id did not match: my_data_type_id must be the same constant in the upcast and the downcast.

Coming Up Next