Guide

Hello World [Racket]

Introduction

The Racket binding calls the prebuilt libazul native library through Racket's built-in ffi/unsafe + ffi/unsafe/define libraries. Because ffi/unsafe is libffi-backed, a Racket closure passed for a function-pointer argument becomes a real C function pointer — Racket is C-ABI-direct for callbacks (archetype A). You pass plain Racket procedures straight to button-set-on-click; the generated azul.rkt routes them through libazul's host-invoker plumbing (a pointer-arg invoker plus an out-pointer for aggregate returns like the layout callback's AzDom), so the ctx/RefAny lifetime story matches every other binding.

azul.rkt gives you an idiomatic, non-prefixed surface: (dom-add-child dom child), (button-create label), (make-window-create-options), plus the raw Az* bindings and cstruct accessors (make-AzDom, AzDom-..., set-AzFullWindowState-title!, ...) for when you need them.

The GC-retention gotcha (read this first)

Racket is garbage-collected. The ffi_closure behind a _fun callback is only kept alive while the Racket procedure it wraps stays reachable. If you build a callback in a let that then goes out of scope, the GC frees the closure and the next C call into it crashes.

azul.rkt defends against this by storing every user callback in a module-level azul-handles hash (keyed by host-handle id) and pinning every per-kind invoker closure in a module-level live-pins list — both are process-lifetime roots. On your side the rule is simple: bind your callbacks with a top-level (define ...) (or otherwise keep a reference), never an anonymous lambda that escapes. The example keeps on-click and layout as top-level defines for exactly this reason.

Installation

You need Racket 8.x (any recent CS or BC build) and the native libazul library. There is no raco pkg catalogue entry yet — install manually:

Linux

curl -O https://azul.rs/ui/release/0.2.0/libazul.so
curl -O https://azul.rs/ui/release/0.2.0/azul.rkt
curl -O https://azul.rs/ui/release/0.2.0/hello-world.rkt
AZ_LIB_DIR=. racket hello-world.rkt

macOS

curl -O https://azul.rs/ui/release/0.2.0/libazul.dylib
curl -O https://azul.rs/ui/release/0.2.0/azul.rkt
curl -O https://azul.rs/ui/release/0.2.0/hello-world.rkt
AZ_LIB_DIR=. racket hello-world.rkt

Windows

curl -O https://azul.rs/ui/release/0.2.0/azul.dll
curl -O https://azul.rs/ui/release/0.2.0/azul.rkt
curl -O https://azul.rs/ui/release/0.2.0/hello-world.rkt
racket hello-world.rkt

azul.rkt reads AZ_LIB_DIR and tries <AZ_LIB_DIR>/libazul first, then falls back to the bare name on the dynamic loader's default search path. It is also produced by cargo run --bin azul-doc -- codegen all into target/codegen/azul.rkt if you build from a checkout.

Simple „Counter“ Example

#lang racket/base
(require "azul.rkt")

;; Mutable model.
(define model (box 5))

(define (az-str s)
  (define b (string->bytes/utf-8 s))
  (AzString_copyFromBytes b 0 (bytes-length b)))

;; Click callback: return an Update enum. Top-level define => GC-retained.
(define (on-click data-ptr info-ptr)
  (set-box! model (add1 (unbox model)))
  AzUpdate_RefreshDom)

;; Layout callback: f(data, info) -> AzDom. Runs on startup and each RefreshDom.
(define (layout data-ptr info-ptr)
  (define label (dom-create-text (az-str (number->string (unbox model)))))
  (define wrap (dom-create-div))
  (dom-add-css-property
   wrap
   (css-property-with-conditions-simple
    (css-property-font-size (style-font-size-px 32.0))))
  (dom-add-child wrap label)
  (define btn (button-create (az-str "Increase counter")))
  (button-set-button-type btn AzButtonType_Primary)
  (button-set-on-click btn (refany-create model) on-click)  ; plain procedure
  (define body (dom-create-body))
  (dom-add-child body wrap)
  (dom-add-child body (button-dom btn))
  body)

(define (run-app)
  (define app (app-create (refany-create model) (app-config-create)))
  (define wco (make-window-create-options))
  (define ws (AzWindowCreateOptions-window-state wco))
  (set-AzFullWindowState-title! ws (az-str "Hello World"))
  ;; The raw AzWindowCreateOptions_create takes a bare LayoutCallbackType and
  ;; discards the ctx, so splice the registered wrapper (which carries it) into
  ;; window_state.layout_callback with the generated cstruct setter.
  (set-AzFullWindowState-layout-callback! ws (register-callback "LayoutCallback" layout))
  (app-run app wco))

(run-app)

What is happening

  • (refany-create value) wraps any Racket value in an AzRefAny; (refany-get ptr) recovers it inside a callback. Both share libazul's host-handle table, so the value stays alive exactly as long as a RefAny clone does.
  • (button-set-on-click btn data on-click) accepts a plain procedure. The wrapper calls (register-callback "ButtonOnClickCallback" on-click) for you, which stores the procedure (GC-retention) and returns the callback wrapper struct the framework stores.
  • The layout callback returns an AzDom (a 240-byte aggregate). The per-kind invoker writes it back through an out-pointer using ctype-sizeof, so the aggregate return crosses the C boundary intact.
  • Non-prefixed wrappers (dom-add-child, button-create, ...) drop the Az<Class>_ prefix; the raw Az* bindings and cstruct setters remain available for the small amount of nested-field plumbing the window options need.

Callbacks in depth

register-callback allocates a monotonic host-handle id, stores your procedure under it, and returns the matching Az<Kind> wrapper struct built by Az<Kind>_createFromHostHandle. When the framework fires the callback, libazul's static thunk calls back into azul.rkt's per-kind invoker with that id; the invoker looks your procedure up in azul-handles and calls it. When the last RefAny clone tied to a handle drops, libazul calls the releaser and the entry is removed — so callbacks are cleaned up without you tracking lifetimes manually.

The one rule you own: don't let your callback procedures become unreachable. Top-level (define ...) is the idiomatic way to guarantee that.