Hello World [Fortran]
Introduction
The Fortran binding targets Fortran 2003+ and talks to the prebuilt
libazul native library through iso_c_binding. It is generated as one
module per api.json module (azul_types_*.f90, azul_ffi_*.f90,
azul_api.f90, and a facade azul_dom.f90, azul_css.f90, ... per
module) behind the azul facade in azul.f90, so both use azul and
use azul_dom, only: dom_t work. Write use azul, only: ... with the
names you need: a bare use azul makes gfortran resolve every one of the
binding's procedures for your unit, which is minutes instead of seconds.
The binding has two layers:
- the wrapper layer — what you write against. One derived type per
class (
dom_t,button_t,app_t,ref_any_t, ...) with type-bound procedures (call app%run(window)),character(len=*)where the API wants a string, plainintegerfor unit enums (ButtonType_Primary,Update_RefreshDom), and ordinary Fortran procedures for callbacks. Factories are module procedures:dom_create_body(),button_create('Increase counter'). az_*— the rawbind(C)interfaces mirroringazul.hone-to-one, underneath. You need them only for something the wrapper layer does not cover: tagged unions (everyAzOption*/AzResult*type) are ABI-opaque blobs because Fortran has no nativeunion, so you construct and inspect those through the C-API helper functions.
use azul is the only use a program needs — the module re-exports the
iso_c_binding entities as well, so reaching down to the raw layer
costs no extra import.
The _t suffix on wrapper types is not decoration. Fortran folds case,
so a type named Dom makes type(Dom) :: dom — the natural variable
name — a redeclaration of the type itself. type(dom_t) :: dom is the
ordinary spelling.
Callbacks are ordinary module functions matching a generated abstract
interface. Registration is implicit: hand the procedure to the method
that takes it, and the binding stores it in a handle table, hands
libazul the id, and dispatches back through a generated bind(C)
invoker when the event fires. There is no init call to forget, no
c_funloc, and no out-pointer to write.
Installation
You need GFortran (any recent version; on macOS brew install gcc
provides it, on Windows use the MinGW-w64 gfortran) and make. The
download set is: the native library, the generated azul.f90 module,
the generated Makefile, and the counter example source.
curl -LO https://azul.rs/ui/release/0.2.0/azul-fortran-0.2.0.tar.gz
tar xzf azul-fortran-0.2.0.tar.gz # azul.f90, Makefile, hello_world.f90
# linux
curl -O https://azul.rs/ui/release/0.2.0/libazul.so
make && ./hello_world
# macOS
curl -O https://azul.rs/ui/release/0.2.0/libazul.dylib
make && DYLD_LIBRARY_PATH=. ./hello_world
# windows (MSYS2 / MinGW-w64 shell, azul.dll next to the .exe)
curl -O https://azul.rs/ui/release/0.2.0/azul.dll
make && hello_world.exe
Use the shipped Makefile instead of invoking gfortran by hand: it
carries the required -ffree-line-length-none flag. The generated
azul.f90 contains declaration lines beyond the F2008 132-column limit
(long widget/callback type names), and without the flag -std=f2008
turns each of them into a hard „Line truncated“ error.
On macOS the Makefile's embedded rpath ($ORIGIN) is an ELF
convention that the Mach-O loader ignores, so run the binary with
DYLD_LIBRARY_PATH=. as shown above (or fix the install name once with
install_name_tool).
Compiling the generated modules (make -j8 compiles independent ones in
parallel; sources.txt lists the order if you drive the compiler by
hand) produces one .o plus a compiler-managed .mod each, which your
program uses — all cached by make, so incremental rebuilds only
recompile your own source.
Simple „Counter“ Example
This is the complete, verified hello_world.f90 (the same file the
install step downloads):
module hello_impl
use azul
implicit none
type :: t_model
integer :: counter = 5
end type t_model
contains
function layout(data, info) result(body)
type(ref_any_t), intent(inout) :: data
type(layout_callback_info_t), intent(inout) :: info
type(dom_t) :: body
class(*), pointer :: model
type(dom_t) :: label
type(button_t) :: button
character(len=16) :: text
model => data%get()
select type (model)
type is (t_model)
write (text, '(I0)') model%counter
class default
text = '?'
end select
label = dom_create_p_with_text(trim(text))
call label%with_css('font-size: 32px;')
button = button_create('Increase counter')
call button%with_button_type(ButtonType_Primary)
call button%with_on_click(data, on_click)
body = dom_create_body()
call body%with_child(label)
call body%with_child(button%dom())
end function layout
function on_click(data, info) result(update)
type(ref_any_t), intent(inout) :: data
type(callback_info_t), intent(inout) :: info
integer :: update
class(*), pointer :: model
model => data%get()
select type (model)
type is (t_model)
model%counter = model%counter + 1
end select
update = Update_RefreshDom
end function on_click
end module hello_impl
program hello_world
use azul
use hello_impl
implicit none
type(app_t) :: app
type(window_create_options_t) :: window
app = app_create(ref_any_create(t_model(5)), app_config_create())
window = window_create_options_create(layout)
call app%run(window)
end program hello_world
Six things to notice.
- Callbacks are ordinary module functions.
layoutandon_clicktake wrapper types and RETURN their result — nobind(C), notype(c_ptr)dummies, no out-pointer to write. Each one matches a generatedabstract interface(layout_callback_iface,button_on_click_callback_iface), so the compiler checks the shape for you. They must live in a MODULE rather than as internal procedures of the main program; an internal procedure would need a compiler-generated executable-stack trampoline that crashes on hardened systems. - The intents are part of the interface.
dataandinfoareintent(inout)because a method that mutates the receiver needs it, and Fortran requires a procedure's dummy characteristics to match the abstract interface exactly. Copy the three declaration lines from the example; a mismatch is a compile error, not a runtime surprise. - Registration is implicit.
call button%with_on_click(data, on_click)andwindow_create_options_create(layout)take the procedure itself; the binding stores it in a handle table, hands libazul the id, and dispatches back through a generated invoker. There is nohost_invoker_initto call and therefore none to forget — the first handle installs the releaser and every per-kind invoker. ref_any_create(t_model(5))/data%get()— the model round-trip.ref_any_createtakes anything (class(*)) and COPIES it into the binding-owned handle table, so the model needs notarget, saveand cannot dangle; the table frees it when libazul drops the last clone of theRefAny. Inside a callbackdata%get()returns aclass(*), pointerto that copy —select typerecovers the concrete type, and writes through it persist.- Builders mutate in place. A method that consumes
selfand returnsSelf(with_css,with_child,with_button_type) is emitted as a SUBROUTINE, so it readscall label%with_css('...')rather thanlabel = label%with_css('...'). Methods that return something else stay functions:button%dom()hands the button's DOM back and marks the button consumed. - Strings are
character. Arguments arecharacter(len=*)and results arecharacter(len=:), allocatable; the binding does theAzStringmarshalling. Unit enums are plainintegerconstants with noAzprefix (ButtonType_Primary,Update_RefreshDom), and booleans arelogical.
Nothing here needs target, save, c_loc, c_f_pointer, or a
use, intrinsic :: iso_c_binding line. If you do reach for the raw
az_* layer, use azul already re-exports those iso_c_binding
entities.
Build and run
make
./hello_world # linux
DYLD_LIBRARY_PATH=. ./hello_world # macOS
You should see the window pictured on the
hello-world landing page: the label renders „5“,
and every click on the button increments it — the click callback bumps
model%counter, returns Update_RefreshDom, and the framework
re-runs layout with the new value.
To run the same headless counter scenario the CI uses:
AZ_E2E=path/to/hello_world_counter.json AZ_BACKEND=headless make run
Common errors
- Thousands of „Line truncated ... -Werror=line-truncation“ errors
compiling
azul.f90— you compiled by hand without-ffree-line-length-none. Use the shippedMakefile, or add the flag to your own build. maketries to runf77— an ancient GNU make builtin default. The shipped Makefile works around it; in your own Makefile setFC = gfortranexplicitly (a plainFC ?=does not override the builtin).- “Interface mismatch in dummy procedure“ — your callback's
declarations do not match the abstract interface. The dummy TYPES,
the INTENTS and the result type all have to agree; copy them from the
example or read the
abstract interfaceblock inazul.f90. - Counter renders but never updates — the click callback returned
something other than
Update_RefreshDom.Update_DoNothingskips the re-layout. - Segfault inside a callback — the callback is an internal procedure of the main program instead of a module procedure.
type(dom_t) :: domerrors on the type name — you dropped the_t. Fortran folds case, so the wrapper types carry the suffix precisely so the obvious variable name stays free.- macOS:
dyld: Library not loaded: libazul.dylib— the Makefile's$ORIGINrpath is Linux-only. Run withDYLD_LIBRARY_PATH=.or rewrite the install name withinstall_name_tool. - “Procedure ... is already defined“ or garbled option/union values
— symptoms of a stale
azul.f90from an older release. Re-downloadazul.f90and theMakefilefrom the same0.2.0as the library; since 0.2.0 tagged unions are ABI-exact opaque blobs and all factory names are unique. - Trying to read
AzOption*/ union fields directly — not supported by design: Fortran has no unions, so these types are opaque byte blobs. Construct and inspect them through the C-API helper functions only.