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.

There is no opam package yet - install manually: the bundle holds the generated units (azul.ml, the facade you open, plus one azul_*.ml / .mli per api.json module it includes), the dune scaffolding (dune with both the azul library and the hello_world executable stanza, dune-project) and the counter example, so unpacking it gives a complete dune project:

opam install ctypes ctypes-foreign dune
curl -LO https://azul.rs/ui/release/0.2.0/azul-ocaml-0.2.0.tar.gz
tar xzf azul-ocaml-0.2.0.tar.gz

# the native library, into the same directory:
curl -O https://azul.rs/ui/release/0.2.0/libazul.dylib   # macOS
curl -O https://azul.rs/ui/release/0.2.0/libazul.so      # linux
curl -O https://azul.rs/ui/release/0.2.0/azul.dll        # windows

LD_LIBRARY_PATH=. dune exec ./hello_world.exe      # linux (macOS: DYLD_LIBRARY_PATH=.)

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) ->
      (* Button.onClick is TYPED: Button.with_on_click takes an
         az_button_on_click_callback, so register through the
         button-specific helper (the generic azul_register_callback
         returns an az_callback and will not type-check here). *)
      let click_cb   = Azul.azul_register_button_on_click_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_span_with_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 returns option, so match ... | None -> ... | Some m -> ....
  • Callbacks return raw structures. layout returns az_dom Ctypes.structure (Azul.raw_dom extracts 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_consume on wrappers whose bytes are moved into libazul (e.g. the AppConfig passed to App.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 on DYLD_LIBRARY_PATH / LD_LIBRARY_PATH, or not in the project directory.
  • Unbound module Azul — the generated azul*.ml / .mli units aren't covered by your dune modules field / not in the project.
  • SIGABRT in <U8Vec as Drop>::drop on exit — you forgot azul_consume on a wrapper whose bytes were moved into libazul; the GC finalizer double-freed it.
  • Counter does not advance — on_click returned 0 (DoNothing) instead of 1.