Mach bindings for GLFW 3.4, with upstream's pending
IME support patched in (see IME): a thin raw C-ABI layer
plus an idiomatic Mach API on top. Project id is glfw, so consumers reach
everything as glfw.*.
use glfw;
use std.types.error.err;
use std.types.option.opt;
use std.types.result.res;
fun example() err[glfw.Error] {
val started: err[glfw.Error] = glfw.init();
if (sel started.err) { ret started; }
val opened: res[glfw.Window, glfw.Error] = glfw.open_window(1280, 720, "hello");
if (sel opened.err) {
glfw.terminate();
ret err[glfw.Error].err{opened.err};
}
val w: glfw.Window = opened.ok;
glfw.make_context_current(opt[glfw.Window].some{w});
for (!glfw.window_should_close(w)) {
glfw.swap_buffers(w);
glfw.poll_events();
}
glfw.destroy_window(w);
glfw.terminate();
ret err[glfw.Error].ok{};
}
Consuming projects vendor the bindings as a normal Mach dependency. GLFW is
vendored and linked statically, and the link requirements cascade from
mach-glfw's own manifest, so consumers declare nothing beyond the dependency
itself. Add it with mach dep add, which declares the dependency at a caret
range over the newest compatible release and realizes it:
mach dep add . glfw --git https://github.com/briar-systems/mach-glfwThat writes this stanza to mach.toml:
[dep.glfw]
git = "https://github.com/briar-systems/mach-glfw"
version = "^0.10.0"Requires Mach 6 and std 9.0.
- Complete coverage of the GLFW 3.4 window/input/monitor API, plus the IME API of the vendored patch.
- Zero-cost: the idiomatic layer is thin wrappers over
ext funimports; no allocation, no registries, no hidden state beyond what GLFW itself keeps. - Mach-idiomatic naming and types (
snake_case,bool,str, records), while staying recognizable to anyone who knows the GLFW C API.
- Native-handle access (
glfw3native.h) — platform-specific, deferred. - An OpenGL loader.
get_proc_addressexposesglfwGetProcAddress; GL bindings belong in a separate project. glfwInitAllocator— Mach-side custom allocators for GLFW are deferred.
Two layers:
src/
c.mach raw layer: every ext fun import, C types verbatim
lib/
glfw.mach library surface and artifact entry: generated, forwards
every public symbol
core.mach init/terminate, version, events, time, context
error.mach the Error tag and the error query
hint.mach init & window hint ids and values
window.mach Window + lifecycle, attributes, context, window callbacks
monitor.mach Monitor, video modes, gamma
input.mach keys, mouse, Cursor objects, clipboard, joystick, gamepad
key.mach key code, action, and modifier constants
mouse.mach mouse button and cursor shape constants
joystick.mach joystick, hat, and gamepad constants
vulkan.mach Vulkan support query, instance extensions, surface creation
demo/
window/ the demo, its own project that consumes this library
One file mirroring glfw3.h declaration order. Every GLFW function is a
pub ext fun attributed to the stable logical library name glfw, with its
C name and C-faithful types:
#[library("glfw")]
pub ext fun glfwCreateWindow(width: i32, height: i32, title: *u8, monitor: ptr, share: ptr) ptr;
Type mapping:
| C | Mach |
|---|---|
int, enum |
i32 |
unsigned int |
u32 |
float / double |
f32 / f64 |
const char* |
*u8 |
GLFWwindow*, GLFWmonitor*, GLFWcursor* (opaque) |
ptr |
GLFWvidmode*, GLFWimage*, … (transparent structs) |
pointer to a Mach rec with identical layout |
| callback function pointers | fun(...) types, C-faithful signatures |
uint64_t |
u64 |
Transparent structs (GLFWvidmode, GLFWgammaramp, GLFWimage,
GLFWgamepadstate) are declared as recs in c.mach with C layout and
re-exported by the idiomatic layer.
No constants live in c.mach — they belong to the domain modules, which own
the names (key.SPACE, not GLFW_KEY_SPACE; the glfw. namespace already
says "GLFW").
Naming is mechanically derived from the C API, so any GLFW reference maps
directly and a generator could reproduce the surface: functions are the C
name minus the glfw prefix, snake_cased (glfwCreateWindow →
create_window, glfwWindowShouldClose → window_should_close); constants
are the C macro minus only GLFW_ (GLFW_KEY_ESCAPE → KEY_ESCAPE). Every
name is globally unique, which lets lib/glfw.mach flatten all of them onto one
namespace.
A small set of convenience helpers has no C counterpart:
window_from_handle() / monitor_from_handle() (rewrap raw callback
pointers), open_window() (windowed create_window sugar), and the
glfw.error query functions.
Types:
- Opaque handles wrap in single-field records:
pub rec Window { handle: ptr; },Monitor,Cursor. Passed by value (one pointer wide). A handle record always holds a live handle. An optional one isopt[Window],opt[Monitor]oropt[Cursor], as an argument (create_window's monitor and share,set_window_monitor,make_context_current,set_cursor) and as a result (get_primary_monitor,get_window_monitor,get_current_context). bool(std.types.bool) replacesGLFW_TRUE/GLFW_FALSEreturns and parameters;str(std.types.string) replacesconst char*. Strings returned by GLFW are GLFW-owned; the docs state their lifetime.- Scalar out-params stay out-params (
get_window_size(w, ?width, ?height)), the Mach idiom for multiple returns.
Error model:
glfw.error.Erroris a closed tag with one case per GLFW error code, plusunrecognized(a code this binding does not name) andunreported(GLFW refused without recording one).- Calls GLFW can refuse clear the thread's last error, make the call, and
report the recorded error on refusal:
initandupdate_gamepad_mappingsreturnerr[Error],create_window,open_window,create_cursorandcreate_standard_cursorreturnres[T, Error].create_window_surfacereturnsres[u64, SurfaceError], which carries either a GLFW refusal or the VkResult of the platform surface call. - Absence is
opt: strings GLFW may not have (get_key_name,get_clipboard_string,get_joystick_name, ...), single GLFW-owned records (get_video_mode,get_gamma_ramp,get_gamepad_state), and function addresses (get_proc_address,get_instance_proc_address). Arrays returned with a count report emptiness through the count. - Everything else follows GLFW semantics: misuse fires the error callback and
sets the last error, which
take_error()reads and clears. The callback receives the raw code, anderror_from_codeclassifies it. - The raw
glfw.clayer keeps GLFW's C results unchanged.
Callback model:
-
Callbacks are plain Mach functions; Mach compiles to the SysV C ABI on the supported target, so a
funpasses directly to GLFW. A display-free test incore.machpins this ABI guarantee (GLFW invokes a Mach error callback). -
Callback
deftypes live in the module that owns the setter and use raw C-faithful signatures — first parameterptr(theGLFWwindow*), notWindow, because GLFW is the caller and the C ABI is the contract:pub def KeyFun: fun(ptr, i32, i32, i32, i32); # window, key, scancode, action, mods pub fun set_key_callback(w: Window, cb: KeyFun) { c.glfwSetKeyCallback(w.handle, cb); }Inside a callback, rewrap with
window_from_handle(h). Setters return nothing (the previous-callback return is dropped; v1 keeps the surface small); clear one by passing nil cast to the callback type (nil::KeyFun). -
There is no closure capture in Mach; callback state goes in module-level
vars or throughset_window_user_pointer/get_window_user_pointer(glfwSetWindowUserPointer).
lib/glfw.mach re-exports every public symbol of every split module (Mach has no
import splat, so the surface is explicit fwd lines). It is generated by
tools/surface.sh gen, and tools/surface.sh check fails if it drifts from the
split modules. It is the entry of the default [artifact.glfw]
static library, so a bare use glfw; resolves to it, binding the leaf as glfw and giving the whole
API as glfw.init(), glfw.create_window(...), glfw.KEY_ESCAPE. The split
modules (glfw.core, glfw.window, …) remain importable individually for
smaller dependency surfaces.
GLFW 3.4 is vendored under vendor/glfw/, with the upstream IME patch applied,
and compiled into a static archive
per target, so a shipped binary carries GLFW itself and end users need nothing
installed. [step.build-glfw] runs tools/build-glfw.sh, which selects the
compilation units and defines for the active build cell from MACH_TARGET_OS;
[link.glfw-static] consumes the resulting libglfw.a.
vendor/glfw/UPSTREAM records the pinned tag, the IME patch's upstream
commit and how it was adapted to 3.4, and how to bump both. The vendored
tree carries GLFW's zlib licence as vendor/glfw/LICENSE.md.
Build-time toolchain. A target matching the host builds with the system
cc; any other target goes through zig (zig cc,
zig ar), which supplies the cross sysroots. CC, AR and SYSROOT
override the C build defaults; ZIG selects the Zig executable used to
materialize the Windows runtime archives.
Per target:
| Target | Also needs |
|---|---|
| linux | X11, Wayland and xkbcommon headers, plus wayland-scanner (xorg-dev libwayland-dev libwayland-bin libxkbcommon-dev on Debian/Ubuntu; libx11 wayland libxkbcommon on Arch) |
| windows | zig; its mingw-w64 headers and static CRT supply the Win32 declarations and ordinary C runtime routines. tools/materialize-mingw-runtime.sh asks that Zig invocation for its target-matched MinGW/compiler runtime archives; the manifest maps GLFW's remaining kernel32, user32, gdi32, shell32, and UCRT imports to their DLLs |
| darwin | the Apple SDK, so a macOS host or MACOS_SDK pointing at an SDK root. The manifest attributes the archive's measured foreign imports to libSystem, libobjc, AppKit, Foundation, CoreFoundation, CoreGraphics, CoreServices, and IOKit; Cocoa, CoreVideo, and OpenGL remain declared framework dependencies. zig carries no framework headers and Apple's SDK is not redistributable, so darwin cannot be cross-built from linux |
Both the X11 and Wayland backends are compiled in on linux; glfwInit picks
one at runtime, as a distro build of GLFW does. The X11, Wayland and OpenGL
client libraries stay dynamic system dependencies — GLFW dlopens them by
soname and never links them — so they are not statically bound and are not
listed in the manifest. Only libc-level libraries (pthread, m, dl, rt)
are linked on linux. Windows links GLFW's ordinary MinGW/compiler runtime code
statically, then imports the measured kernel32, user32, gdi32, shell32, and UCRT
surface from the operating system.
Every raw GLFW import uses #[library("glfw")], the stable logical dependency
name rather than a platform filename. Static builds resolve those declarations
from the vendored archive. The system fallback maps the same identity to the
selected target's concrete dependency: the resolved ELF SONAME (for example
libglfw.so.3) on Linux, glfw3.dll on Windows, or the resolved dylib's
LC_ID_DYLIB install name on Darwin.
The linux archive is built with -fPIC, so libc data reaches it through the GOT
that Mach's ELF linker binds.
Darwin retains the toolchain's normal PIC code generation.
System-GLFW fallback. The system entries remain declared in mach.toml
as an opt-in. tools/system-glfw.sh rewrites the manifest to build against an
installed GLFW ≥ 3.4 (pacman -S glfw, apt install libglfw3-dev, …) instead
of the vendored source. No released GLFW carries the IME API, so the IME
functions resolve only against the vendored archive. A consumer receives every link entry the library
exports, whatever the library's artifact names, so the script swaps
"glfw-static" for "glfw" and "glfw-win" in [artifact.glfw] and moves
export = true from [link.glfw-static] onto the two system entries.
CI builds the vendored archive and runs the tests on each runner's own target: linux on every pull request, and linux x86_64 and aarch64, windows and darwin before a release. Darwin builds run natively because the Apple SDK needed by the Objective-C backend cannot be redistributed to a Linux cross-runner, so release archives cover linux and windows only.
| Domain | In v1 |
|---|---|
| Init/terminate, init hints, version, error | yes |
| Window: create/destroy, hints, attributes, pos/size/limits/aspect, title, icon, show/hide/focus/minimize/maximize/attention, opacity, monitor mode, user pointer, all callbacks | yes |
| Context: make current, swap buffers/interval, proc address, extension query | yes |
| Monitor: enumerate, primary, pos/workarea/physical/scale/name, video modes, gamma, monitor callback | yes |
| Input: input modes, raw mouse motion, key/scancode/name, mouse buttons, cursor pos/enter, custom + standard cursors, clipboard, time/timer, key/char/mouse/scroll/drop callbacks | yes |
| IME: preedit callback, candidate window rectangle, preedit reset, text input focus, IME status, candidate list | yes (vendored patch) |
| Joystick/gamepad: presence, axes/buttons/hats, GUID, gamepad mappings/state, joystick callback | yes |
| Vulkan: support query, required instance extensions, surface creation, instance proc address | yes |
| Native handles | no (deferred) |
GLFW 3.4 has no input method API. The vendored GLFW carries upstream's pending
IME support (glfw/glfw#2130), and the
bindings wrap it under its upstream names, so a GLFW release that merges it is
a drop-in. One local fix to its wayland backend is carried until upstream has
it (see vendor/glfw/UPSTREAM):
set_preedit_callback(w, cb)reports the composition in progress as codepoints, split into attributed blocks, with the focused block and the caret as a codepoint index. A count of zero means the composition ended, committed or cancelled, and can also arrive when none was in progress. Committed text arrives through the char callback.set_preedit_cursor_rectangle(w, x, y, w, h)sets the rectangle, in window coordinates, that the IME places its candidate window against, normally the caret.get_preedit_cursor_rectanglereads it back.set_text_input_focus(w, focused)tells the platform whether a text field has focus. A window never told keeps the platform's default behaviour.reset_preedit_text, theIMEinput mode withset_ime_status_callback, and the candidate list (hint.MANAGE_PREEDIT_CANDIDATE,set_preedit_candidate_callback,get_preedit_candidate) complete the API.
Per platform, from the patch:
| Platform | Preedit callback | Candidate rectangle | Not available (no-op) |
|---|---|---|---|
| win32 | yes (IMM32, loaded at runtime) | yes | none |
| cocoa | yes, the caret is always at the end | yes | candidate list |
| wayland | yes, with a compositor offering text-input v3 or v1 | yes | reset_preedit_text, IME status, candidate list |
| x11 | only with hint.X11_ONTHESPOT |
only without it | candidate list, and reset_preedit_text and IME status unless on-the-spot |
X11 defaults to the over-the-spot XIM style: the input method draws the
composition itself, next to the candidate rectangle, and no preedit callback
fires. The X11_ONTHESPOT init hint switches to on-the-spot, where the
application receives and draws the composition but the input method places its
candidate window on its own.
demo/window/ is its own project, so the library declares no binary. It
consumes the library the way any project would: [dep.glfw] is a path
dependency on this checkout, ../.., beside its own pin of std, it imports the
bare use glfw;, and the vendored archive's links cascade to it from the
library's manifest. Error callback installed, window + OpenGL
context, glClearColor/glClear loaded through get_proc_address, animated
clear color, ESC closes via key callback. Serves as living documentation of
the callback, context, and event-loop idioms.
Pass --smoke to initialize GLFW, report its version, and terminate without
opening a window, for runtime and linker validation. Build
and run it from the repository root:
mach dep pull demo/window
mach build demo/window
mach run demo/window -- --smokeA path dependency is a copy, so run mach dep pull demo/window again after
changing the bindings or the manifest.
test blocks live beside the code they cover and are display-free: the
error code classification and the pre-init error path (which doubles as the C→Mach
callback ABI regression test). Paths that need a live window — context
creation, swap, input events — are exercised by running the demo, not by
mach test.