Hello World [OCaml]
Introduction
The OCaml binding uses ctypes-foreign to
call the prebuilt libazul native library. Callbacks go through libazul's
host-invoker plumbing, so Ctypes never has to synthesize a struct-by-value
trampoline. OCaml is the most explicit of the bindings — user functions return the
raw az_dom Ctypes.structure and the codegen's invoker writes the bytes through an
out-pointer.
Installation
You need OCaml 4.14+ with dune, the ctypes + ctypes-foreign
packages, and the native libazul library.
Recommended: opam
opam repo add azul https://azul.rs/opam
opam install azul
Manual
opam install ctypes ctypes-foreign
# download the native library from /releases into the project dir:
wget -O libazul.dylib https://azul.rs/release/0.2.0/libazul.dylib # macOS
Add the generated azul.ml / azul.mli (from the
examples archive under ocaml/) to your dune project.
Simple „Counter“ Example
(* Avoid `open Azul`: the Azul module shadows Stdlib.String with its own
String wrapper. Reference Azul members explicitly. *)
(* Data model. *)
type my_data_model = { mutable counter : int }
let model = { counter = 5 }
(* Click callback: returns an int Update code. *)
let on_click (data_ptr : unit Ctypes.ptr) (_info : unit Ctypes.ptr) : int =
let ref_ptr = Ctypes.from_voidp Azul.az_ref_any data_ptr in
match Azul.azul_refany_get ref_ptr with
| None -> 0 (* Update.DoNothing *)
| Some (m : my_data_model) ->
m.counter <- m.counter + 1;
1 (* Update.RefreshDom *)
(* Layout callback: returns the raw az_dom structure; the invoker writes the
bytes through the out-pointer. *)
let layout (data_ptr : unit Ctypes.ptr) (_info : unit Ctypes.ptr)
: Azul.az_dom Ctypes.structure =
let ref_ptr = Ctypes.from_voidp Azul.az_ref_any data_ptr in
match Azul.azul_refany_get ref_ptr with
| None -> Azul.raw_dom (Azul.Dom.create_body ())
| Some (m : my_data_model) ->
let click_cb = Azul.azul_register_callback on_click in
let click_data = Azul.azul_refany_create m in
(* with_* consume the receiver as their first arg; flip the arg order
with local helpers so values flow left-to-right under |>. *)
let with_css css d = Azul.Dom.with_css d css in
let with_child child d = Azul.Dom.with_child d child in
let as_btn_type t b = Azul.Button.with_button_type b t in
let on_click_ data cb b = Azul.Button.with_on_click b data cb in
let label_div =
Azul.Dom.create_div ()
|> with_css "font-size: 32px;"
|> with_child (Azul.raw_dom (Azul.Dom.create_text (string_of_int m.counter)))
in
let button_dom =
Azul.Button.create "Increase counter"
|> as_btn_type 1 (* ButtonType.Primary *)
|> on_click_ click_data click_cb
|> Azul.Button.dom
in
Azul.Dom.create_body ()
|> with_child (Azul.raw_dom label_div)
|> with_child button_dom
|> Azul.raw_dom
let () =
let data = Azul.azul_refany_create model in
let wco = Azul.azul_window_create_options_with_layout layout in
let app_config = Azul.AppConfig.create () in
let app = Azul.App.create data (Azul.raw_app_config app_config) in
(* Mark wrappers consumed: their bytes are about to be moved into libazul by
App.run. Without azul_consume, Gc.finalise would later call <X>_delete on
moved memory. Same pattern as Node's _consume and Ruby's Azul._consume. *)
Azul.azul_consume app_config;
Azul.App.run app wco
Four things to notice.
Azul.azul_refany_create/azul_refany_get— wrap/recover your value through a handle. The getter returnsoption, somatch ... | None -> ... | Some m -> ....- Callbacks return raw structures.
layoutreturnsaz_dom Ctypes.structure(Azul.raw_domextracts it from a wrapper); the invoker writes the bytes out for you. Update codes are plain ints (0= DoNothing,1= RefreshDom). with_*consume the receiver. They take the receiver as the first argument, so to use|>you flip the arg order with small local helpers — the value then flows top-down.- Call
azul_consumeon wrappers whose bytes are moved into libazul (e.g. theAppConfigpassed toApp.create), or OCaml's GC finalizer will later double-free the moved memory.
Build and run
dune exec ./hello_world.exe
# or, after `dune build`:
# macOS: DYLD_LIBRARY_PATH=. ./_build/default/hello_world.exe
# linux: LD_LIBRARY_PATH=. ./_build/default/hello_world.exe
You should see the window pictured on the hello-world landing page. Click the button: the counter increments and the layout callback re-runs.
Common errors
Dl.dlopen/ library not found — the native library isn't onDYLD_LIBRARY_PATH/LD_LIBRARY_PATH, or not in the project directory.Unbound module Azul—azul.ml/azul.mliaren't listed in your dunemodules/ not in the project.- SIGABRT in
<U8Vec as Drop>::dropon exit — you forgotazul_consumeon a wrapper whose bytes were moved into libazul; the GC finalizer double-freed it. - Counter does not advance —
on_clickreturned0(DoNothing) instead of1.
Coming Up Next
- Application Architecture — architecting a larger Azul application
- Document Object Model — the Dom tree: node types, hierarchy, and CSS
- Hello World [Rust]