Hello World [Common Lisp]
Introduction
The Common Lisp binding calls the prebuilt libazul native library through
CFFI with the cffi-libffi extension — no C
compile step. The generated bindings live in azul.lisp (the :azul ASDF
system); callbacks route through libazul's host-invoker plumbing, so CFFI
never has to synthesize a struct-by-value trampoline. cffi-libffi is what
lets the by-value struct calls the host-invoker thunks expect go through.
Installation
You need a Common Lisp implementation (SBCL recommended),
Quicklisp, and the cffi + cffi-libffi
systems (Quicklisp pulls these in). cffi-libffi needs the system libffi
headers:
# libffi dev headers (macOS: brew install libffi ; Debian/Ubuntu:)
sudo apt-get install libffi-dev
# copy the native library next to the example so the loader finds it
cp /path/to/target/release/libazul.dylib examples/lisp/
The two ASDF systems are already wired: azul.asd defines :azul (the
generated azul.lisp), and azul-example.asd defines :azul-example, which
depends on :azul, :cffi, and :cffi-libffi.
Running
Point ASDF at the example directory and quickload the system:
cd examples/lisp
AZ_LIB_DIR=. sbcl --non-interactive \
--eval '(push #p"./" asdf:*central-registry*)' \
--eval '(ql:quickload :azul-example)' \
--eval '(azul-hello:run-app)'
The program
examples/lisp/hello-world.lisp builds a counter: a 32px label showing the
count and an „Increase counter“ button that bumps it. The model is a mutable
cons cell so the same Lisp object is recovered inside every callback through
the RefAny host-handle table.
(defpackage #:azul-hello (:use #:cl) (:export #:run-app))
(in-package #:azul-hello)
;; A cons cell so the counter is mutable in place through the host-handle table.
(defun make-model () (cons :counter 5))
(defun on-click (data info)
(declare (ignore info))
(let ((m (refany-get data))) ; recover the same cons cell
(if m
(progn (incf (cdr m)) (az-update-refresh-dom))
(az-update-do-nothing))))
(defun layout (data info)
(declare (ignore info))
(let ((m (refany-get data)))
;; build div{font-size:32px} > text(counter) + Button, return a raw AzDom
...))
How callbacks work
refany-createwraps a Lisp value in anAzRefAny(an opaque host handle);refany-getrecovers it inside a callback. Pass the same model to the app and to the button so both see the same counter.register-callbackreturns the matchingAz<Kind>record. The generated invoker fires your function and writes its return value back through the callback out-pointer — a layout callback returns aDom, an on-click callback returns anAzUpdate.- The
LayoutCallbackis installed intowindow_state.layout_callbackbeforeApp.run.
Build the DOM with the raw AzDom_* / AzButton_* FFI calls (as the example
does): these are the exact by-value struct calls the host-invoker thunks
expect, and the raw records carry no destructor, so the moved-out structs are
never double-freed.
Status
The counter demo passes the headless E2E
(scripts/e2e_language_matrix.sh lisp → counter 5 → 6 → 8, „test result: ok“).
On macOS a real windowed run is limited: App.run can't co-host with SBCL's
runtime ownership of NSApplication, so the windowed path is a smoke; the
headless E2E is the supported verification. See examples/lisp/README.md for
the ASDF/fasl-cache details.