Guide

Hello World [Nim]

Introduction

Nim compiles to C, so it talks to Azul through the plain C ABI with no runtime shim in between. The generated azul.nim declares every C struct as a {.bycopy.} object, every C enum as a size-pinned Nim enum, and every AzFoo_bar function as a proc … {.importc, cdecl, dynlib: azulLib.}. The dynlib pragma dlopens the shared library at run time, so you do not even pass link flags — the .so / .dylib / .dll only has to be discoverable.

The important part for a GUI framework is callbacks. A top-level Nim proc (data: AzRefAny; info: AzCallbackInfo): AzUpdate {.cdecl.} is a real C function pointer — it is handed straight to AzButton_setOnClick. Nim is therefore one of the C-ABI-direct bindings (like Zig, Odin, and C itself): no host-invoker trampoline, no handle table, no wrapper-struct dance. The counter below passes its onClick and layout procs directly to Azul, exactly like the C and Zig examples.

azul.nim also ships an idiomatic wrapper layer that drops the Az prefix (domCreateBody(), button.setButtonType(...)); the counter uses the raw Az* layer because that is the path exercised end-to-end by the test suite.

You need Nim 1.6 or newer (tested against 2.0).

Installation

There is no package-manager story yet — download the native library, the generated binding, and the hello-world into one directory and build it there. Because azul.nim uses the dynlib pragma, nim c does not link libazul at compile time; the library is loaded at run time, which is why the run step sets *_LIBRARY_PATH=..

# linux
curl -O https://azul.rs/ui/release/0.2.0/libazul.so
curl -O https://azul.rs/ui/release/0.2.0/azul.nim
curl -O https://azul.rs/ui/release/0.2.0/hello-world.nim
nim c -d:release hello-world.nim
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.nim
curl -O https://azul.rs/ui/release/0.2.0/hello-world.nim
nim c -d:release hello-world.nim
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.nim
curl -O https://azul.rs/ui/release/0.2.0/hello-world.nim
nim c -d:release hello-world.nim
.\hello-world.exe

The macOS framework dependencies (Foundation, AppKit, OpenGL, …) are carried inside libazul.dylib's own load commands, so they are pulled in automatically when the library is dlopen'd — you do not link them yourself.

Simple „Counter“ Example

This is the exact hello-world.nim shipped in the release (the same file the end-to-end test builds and clicks through):

import azul

# ── Data model ─────────────────────────────────────────────────────────
type
  MyDataModel = object
    counter: uint32

var myDataTypeToken: uint8 = 0
proc myDataTypeId(): uint64 = cast[uint64](addr myDataTypeToken)

proc myDataDestructor(p: pointer) {.cdecl.} = discard

proc azStr(s: string): AzString =
  if s.len == 0:
    AzString_fromUtf8(nil, csize_t(0))
  else:
    AzString_fromUtf8(cast[ptr uint8](s.cstring), csize_t(s.len))

proc myDataUpcast(model: MyDataModel): AzRefAny =
  var local = model
  let blob = AzGlVoidPtrConst(`ptr`: cast[pointer](addr local), run_destructor: false)
  AzRefAny_newC(
    blob,
    csize_t(sizeof(MyDataModel)),
    csize_t(alignof(MyDataModel)),
    myDataTypeId(),
    azStr("MyDataModel"),
    myDataDestructor,
    csize_t(0), csize_t(0))

proc myDataDowncast(refany: ptr AzRefAny): ptr MyDataModel =
  if not AzRefAny_isType(refany, myDataTypeId()): return nil
  let p = AzRefAny_getDataPtr(refany)
  if p == nil: return nil
  cast[ptr MyDataModel](p)

# ── Callback: button click ─────────────────────────────────────────────
proc onClick(data: AzRefAny, info: AzCallbackInfo): AzUpdate {.cdecl.} =
  var d = data
  let m = myDataDowncast(addr d)
  if m == nil: return AzUpdate_DoNothing
  m.counter += 1
  return AzUpdate_RefreshDom

# ── Layout callback ────────────────────────────────────────────────────
proc layout(data: AzRefAny, info: AzLayoutCallbackInfo): AzDom {.cdecl.} =
  var d = data
  let m = myDataDowncast(addr d)
  if m == nil: return AzDom_createBody()

  let label = AzDom_createText(azStr($m.counter))
  var labelWrapper = AzDom_createDiv()
  let cond = AzCssPropertyWithConditions_simple(
    AzCssProperty_fontSize(AzStyleFontSize_px(32.0'f32)))
  AzDom_addCssProperty(addr labelWrapper, cond)
  AzDom_addChild(addr labelWrapper, label)

  var button = AzButton_create(azStr("Increase counter"))
  AzButton_setButtonType(addr button, AzButtonType_Primary)
  let dataClone = AzRefAny_clone(addr d)
  AzButton_setOnClick(addr button, dataClone, onClick)
  let buttonDom = AzButton_dom(button)

  var body = AzDom_createBody()
  AzDom_addChild(addr body, labelWrapper)
  AzDom_addChild(addr body, buttonDom)
  return body

# ── Main ───────────────────────────────────────────────────────────────
proc main() =
  let model = MyDataModel(counter: 5)
  let data = myDataUpcast(model)

  var window = AzWindowCreateOptions_create(layout)
  window.window_state.title = azStr("Hello World")
  window.window_state.size.dimensions.width = 400.0'f32
  window.window_state.size.dimensions.height = 300.0'f32
  window.window_state.flags.decorations = AzWindowDecorations_NoTitleAutoInject
  window.window_state.flags.background_material = AzWindowBackgroundMaterial_Sidebar

  var app = AzApp_create(data, AzAppConfig_create())
  AzApp_run(addr app, window)
  AzApp_delete(addr app)

main()

How it works

The data model and RefAny

Your application state lives in a plain Nim object. To hand it to Azul you wrap it in an AzRefAny — a reference-counted, type-tagged box. myDataUpcast copies the struct into a fresh heap allocation via AzRefAny_newC; myDataDowncast checks the type tag and returns a typed ptr MyDataModel back out. The type id is just the address of a global byte that is never read — a cheap, process-unique token.

AzGlVoidPtrConst has a field literally named ptr, which is a Nim keyword, so azul.nim emits it backtick-stropped and you construct it as AzGlVoidPtrConst(ptr: …, run_destructor: false).

Callbacks pass straight through

onClick and layout are ordinary top-level procs marked {.cdecl.}. That calling-convention annotation makes each one a genuine C function pointer whose signature matches the generated AzButtonOnClickCallbackType / AzLayoutCallbackType proc types, so they are passed directly to AzButton_setOnClick and AzWindowCreateOptions_create. There is no marshalling layer — this is the whole reason Nim is a C-ABI-direct binding.

Inside the callback you re-downcast the AzRefAny to reach your state. onClick bumps the counter and returns AzUpdate_RefreshDom, which tells Azul to re-run layout; layout rebuilds the DOM from the current counter value.

Building the DOM

layout builds a <body> containing a <div> (font-size 32px) wrapping the counter text, plus an „Increase counter“ button. Builder functions such as AzDom_addChild / AzDom_addCssProperty take a ptr AzDom to the node being mutated (hence addr labelWrapper) and consume their by-value inputs. AzButton_dom converts the finished button into a DOM node.

Memory ownership

Azul consumes the by-value records you pass into it (the AzDom, the AzRefAny clone, the AzWindowCreateOptions), so you do not free them yourself — ownership transfers across the FFI boundary. AzApp_run drives the event loop until the window closes; AzApp_delete then releases the app. The one clone you make explicitly (AzRefAny_clone) is the copy the button holds onto for its click handler; Azul drops it when the button is destroyed.