Guide

Hello World [Red]

Status: ALPHA (unverified). These bindings are constructed from the published Red/System specification and have not been compiled with a Red toolchain (none was available at generation time). The FFI mechanism is sound and cited in scripts/RED_FFI_FINDINGS.md, but expect to fix small syntax issues on first build, and see the Honest limitations section below.

Introduction

Red is a full-stack language: a high-level, Rebol-inspired dialect (Red) sits on top of a low-level, statically typed, C-like dialect (Red/System). Both compile with the same ~1 MB toolchain into a single, dependency-free native executable.

Azul's binding targets Red/System, because that is the dialect with a general external-library FFI: the #import directive dlopen-loads libazul at startup and maps every Az* C symbol into scope. High-level interpreted Red (the console) cannot load an arbitrary shared library on its own — it reaches C only through Red/System, via the routine! type and #system-global embedded blocks. So the honest framing is „Azul from Red/System,“ which is still ordinary Red code compiled by the ordinary Red toolchain.

Everything lives in a single generated file, azul.reds, with two layers:

  • Az* — raw #imported C-ABI functions mirroring azul.h one-to-one (AzDom_createBody, AzButton_create, …). Structs are passed and returned by value using Red/System's value keyword, which the spec documents as ABI-compatible with mainstream C compilers.
  • azul-* — a small host-invoker convenience layer: azul-register-<kind> turns a Red/System [callback] function into an Az<Kind>Callback value, and azul-refany-create / azul-refany-get wrap and recover your data-model pointer.

Callbacks dispatch through libazul's host-invoker plumbing: each registered function gets a handle id, and when the framework fires the callback it calls a per-kind invoker inside azul.reds that looks the function up and runs it. Wire those up once at startup with azul-host-invoker-init.

Installation

You need the Red toolchain (a single ~1 MB binary from red-lang.org — the redc compiler front-end can build Red/System programs). No package manager or system libraries are required; Red produces a static, dependency-free executable.

The download set is: the native library, the generated azul.reds bindings, and the counter example source.

Linux:

curl -O https://azul.rs/ui/release/0.2.0/libazul.so
curl -O https://azul.rs/ui/release/0.2.0/azul.reds
curl -O https://azul.rs/ui/release/0.2.0/hello-world.red

macOS:

curl -O https://azul.rs/ui/release/0.2.0/libazul.dylib
curl -O https://azul.rs/ui/release/0.2.0/azul.reds
curl -O https://azul.rs/ui/release/0.2.0/hello-world.red

Windows:

curl -O https://azul.rs/ui/release/0.2.0/azul.dll
curl -O https://azul.rs/ui/release/0.2.0/azul.reds
curl -O https://azul.rs/ui/release/0.2.0/hello-world.red

Build & run

hello-world.red #includes azul.reds, so you compile a single file:

redc -r hello-world.red -o hello-world   # -r = release build
LD_LIBRARY_PATH=. ./hello-world          # libazul must be on the loader path

#import loads libazul at executable startup, so the library must be findable by the OS loader at run time (same directory, or LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / PATH).

The counter, explained

The full source is in examples/red/hello-world.red. The shape mirrors every other Azul binding:

  1. A model! struct holds the counter (starting at 5).
  2. on-click is a [cdecl] callback: it reads the model back out of the AzRefAny* via azul-refany-get, bumps the counter, and writes AzUpdate_RefreshDom through the out pointer.
  3. on-layout builds body > [ div{font-size:32px} > text(counter), button ], registering on-click with azul-register-button-on-click-callback.
  4. main calls azul-host-invoker-init, registers the layout callback, builds the window options, and calls AzApp_run.

Every callback dispatcher takes pointer arguments only (plus one out-pointer for the return) — no aggregate crosses the callback boundary by value. libazul's static thunk does the by-value plumbing on the C side, so the Red/System code only ever sees byte-ptr!s.

High-level Red

To drive Azul from a high-level Red (Red [...] header) program instead, wrap the pieces you need in routine!s and inject the #import with #system-global:

Red [Title: "azul from high-level Red"]

#system-global [ #include %azul.reds ]

make-app: routine [ /local ... ][ ... AzApp_create ... ]

routine! bodies are Red/System, compiled to native code; they are the only place high-level Red values cross into C. This is more ceremony than using Red/System directly, which is why the shipped example is Red/System.

Honest limitations

  • Unverified. No Red toolchain was available to compile-check the output.
  • 64-bit integers. Red/System's integer! is 32-bit and it lacks a portable int64. Host-handle ids stay small (they start at 1) and 64-bit-valued API fields need an int64 shim. See scripts/RED_FFI_FINDINGS.md.
  • Tagged unions. AzOption* / AzResult* / union types are emitted as opaque blobs pending exact-size wiring of the shared layout pass; construct and inspect them through C-API helpers, never by field access. The counter demo only round-trips AzUpdate (a unit enum), so it is unaffected.
  • arm64 by-value aggregates. Red/System claims mainstream-C-ABI compatibility, but the AArch64 rules for large (>16 B) by-value structs are subtle; first verification should be on x86-64.

Common errors

  • redc: cannot open %azul.reds — run the compile from the directory that holds both hello-world.red and azul.reds.
  • Loader can't find libazul — set LD_LIBRARY_PATH=. (Linux), DYLD_LIBRARY_PATH=. (macOS), or put the DLL beside the exe (Windows).
  • Window opens but the button does nothing — you forgot azul-host-invoker-init before AzApp_run, so libazul cannot dispatch into Red.
  • Counter renders but never updates — the click callback did not write AzUpdate_RefreshDom through the out pointer on every code path.

Coming Up Next