epoxy

A MoonBit reimplementation of libepoxy: OpenGL function pointer dispatch.

opengl
gl
epoxy
ffi
bindings
moon add tonyfettes/epoxy@0.2.0
Download zip
Version
0.2.0
License
MIT
Last updated
16 days ago
Downloads
23
README

#epoxy (MoonBit)

A MoonBit reimplementation of libepoxy: OpenGL function-pointer dispatch that resolves entry points lazily via dlopen/dlsym and caches them — so callers just use undecorated names like glGetString without worrying about loaders, versions, or extensions.

The library package is the module root: import it as tonyfettes/epoxy. A separate module — tonyfettes/epoxy-generator under generator/ — parses the Khronos registry (registry/gl.xml, 3299 commands) with the xml-mbt pull-parser and emits MoonBit dispatch wrappers for 3213 commands, plus all 6061 GL enum constants. The rest (callbacks, a few exotic pointer shapes, the platform-variant GLhandleARB) are skipped, never miscompiled; four buffer transfer commands with safer public shapes are implemented entirely by hand.

Each wrapper calls its resolved entry point directly through a FuncRef typed to the GL function's exact ABI — there is no per-command C shim. The hand-written C file (epoxy.c) provides the dlopen/dlsym resolver and C-string conversion. Native-pointer representation, methods, borrows, and their three tiny ABI helpers live together in internal/pointer.

Keeping the generator in its own module means the library's dependency closure stays minimal: consumers of tonyfettes/epoxy never inherit the build-time toolchain (xml parser, async IO). The library depends only on moonbitlang/core; its native-pointer support is isolated in an internal package and never appears in the public OpenGL API.

#Status

Proven working on macOS (arm64, Apple OpenGL 2.1 / Metal). The hello_gl example drives a cross-section of generated bindings against a live driver:

$ moon -C examples run hello_gl/main --release GL_VERSION = 2.1 Metal - 90.5 gl_version = 21 (2.1) is_desktop = true has GL_APPLE_vertex_array_object = true GL_MAX_TEXTURE_SIZE = 16384 glGenBuffers -> [1, 2, 3] glShaderSource+compile ok = true glColor3sv round-trip = [1, 0, 0]

See examples/hello_gl for what each line proves. For an actual render, examples/triangle draws the classic RGB gradient triangle in a window (Cocoa, macOS only), spinning until you close it:

$ moon run ./examples/triangle/main --release

#Layout

Three modules share one moon.work workspace: the library (root), the generator, and the examples.

PathRole
resolver.mbt, version.mbt, handles.mbtThe epoxy library (module root, tonyfettes/epoxy): the lazy dlopen/dlsym resolver + self-patching Dispatch slots, the version/extension introspection API, and the opaque-handle types.
epoxy.cHand-written C: the dlopen/dlsym resolver and C-string conversion.
gl.mbtHand-written safe GL wrappers for packed buffer uploads.
gl_generated.mbtGenerated MoonBit FuncRef dispatch wrappers (3213) — no C shim.
gl_generated_enums.mbtGenerated GL enum constants (pub const GL_* : UInt/UInt64/Int, 6061).
internal/glinfo/Pure parsers for the GL_VERSION/GL_EXTENSIONS strings (the introspection logic, unit-tested in isolation). Module-internal — not part of the public API.
internal/pointer/Minimal native Pointer[T] package: pointer methods, array/bytes borrow scopes, and three ABI helpers. Importable only inside this module.
generator/The binding generator — its own module, tonyfettes/epoxy-generator: parse.mbt (streaming registry parse), emit.mbt (classification + codegen), main.mbt (CLI driver), plus its private support packages gen/ (the GLxxx→type table), cdecl/ (C-declarator parser), aliasgroup/ (union-find over <alias> edges).
examples/A separate module (tonyfettes/epoxy-examples); see hello_gl and triangle.
upstream/libepoxyThe reference C implementation (submodule), incl. registry/*.xml.

#How dispatch works

  1. Dispatch::with_aliases("glGenVertexArrays", [...]) creates a slot with an empty cached pointer and its alias group's fallback names.
  2. First call → Dispatch::getdlopen the GL library once (cached), then dlsym the primary name, falling back through the alias names.
  3. The resolved @pointer.Pointer[Unit] from the internal pointer package is cached in the slot and reinterpreted into a FuncRef whose type lowers to the GL function's exact ABI. The wrapper borrows any call-scoped FixedArray/Bytes param into a raw pointer, converts allow-listed vertex-array offsets to pointer-shaped values, calls the FuncRef directly, and narrows any sub-word return (e.g. GLboolean's unsigned char) back to its width.
  4. Subsequent calls skip resolution — just cache read + direct FuncRef call.

Why the sub-word narrow? MoonBit lowers every FuncRef return (Bool, Byte, …) to int32_t and reads the whole return register, so a GL function that returns unsigned char/short would leave the high bits unspecified per the psABI. The generated wrapper always masks/sign-extends the low byte/halfword — the same fixup the old C shim's (T) cast did, now done MoonBit-side.

Like libepoxy, generated public wrappers preserve the GL function's argument shape and have no error channel. A small hand-written layer exposes safer shapes for buffer-transfer commands where raw void* is ambiguous. gl_buffer_data and gl_buffer_sub_data accept packed numeric FixedArray[T] values and derive the byte count, while gl_buffer_allocate represents the NULL allocation form. The generator gives vertex-array setters covered by GL_ARRAY_BUFFER an Int64 byte-offset parameter instead of a MoonBit array, so OpenGL never retains a pointer into MoonBit-managed memory through those APIs. The four buffer-transfer commands are omitted from generated output and implemented in gl.mbt. If an entry point can't be resolved (you called something the current context doesn't provide) the dispatch abort()s with epoxy:glXxx() not found, exactly as upstream does. That's a programming error: gate version/extension-specific calls on gl_version / has_gl_extension first, rather than wrapping every call. So a render loop reads as plain GL, with no raise/try plumbing.

Context creation (CGL in the example) is deliberately not part of dispatch — it stands in for the window-system layer (CGL/EGL/GLX/WGL) an app/toolkit provides. epoxy itself only dlopens.

#FFI marshalling categories

The ~40 GLxxx typedefs collapse onto a handful of representations, all decided in generator/gen/types.mbt:

  • Scalar (most params): GLenum/GLuintUInt, GLint/GLsizeiInt, GLfloatFloat, GLdoubleDouble, GL(u)int64(U)Int64. Direct.
  • Pointer-width: GLintptr/GLsizeiptrInt64 (== intptr_t/ssize_t on the 64-bit native targets).
  • Scalar arrays: const T*/T*FixedArray[T], borrowed to an internal @pointer.Pointer[T] for the call.
  • Byte data / strings: void*/GLchar*/byte buffers → Bytes (borrowed to an internal @pointer.Pointer[Byte]); string returns decode to String via epoxy_cstr.
  • String arrays: const GLchar *const *FixedArray[Bytes] (the layout already is the char** GL wants — glShaderSource &c.).
  • 16-bit arrays: const GLshort*/GLhalf*FixedArray[Int16]/[UInt16].
  • Buffer uploads: gl_buffer_data/gl_buffer_sub_data accept FixedArray[T] where T is one of the sealed packed scalar GlData types; the wrappers derive the exact byte count and borrow the array only for the copying call. gl_buffer_allocate allocates uninitialized storage without a host array.
  • Buffer-backed vertex arrays: allow-listed setters such as gl_vertex_attrib_pointer, its integer/double variants, and the legacy core vertex-array setters accept an Int64 byte offset into the bound GL_ARRAY_BUFFER. Client-side arrays and public pointers are deliberately not part of these APIs.
  • Opaque handles: GLsync/GLeglImageOES/GLeglClientBufferEXT/ GLVULKANPROCNV → distinct #external types (pub type GLsync &c.). All share the void * ABI, so they cross a FuncRef as-is; the names keep the handles from being interchangeable.
  • Deferred: callbacks (GLDEBUGPROC), GLhandleARB (a split ABI — unsignedint elsewhere, void * on Apple — we won't miscompile either), non-string pointer returns, void**/pointer-width arrays, OpenCL interop.

#Build & run

This is a multi-module workspace (moon.work). The module sets preferred_target = "native", so no --target flag is needed; link flags are computed per-platform by a build.js prebuild script.

moon -C generator run . # regenerate bindings from gl.xml moon test # all unit tests moon -C examples run hello_gl/main --release # the demo (release: see examples/hello_gl)

The generator takes its paths on the command line (run moon -C generator run .-- --help for the full list); the defaults point at the in-tree registry and the library package, so the bare command above regenerates in place:

moon -C generator run . -- \ --registry ../upstream/libepoxy/registry/gl.xml \ --out-mbt ../gl_generated.mbt \ --out-enums ../gl_generated_enums.mbt

#Generator coverage

CategoryCountStatus
dispatch wrappers, including allow-listed VBO-offset vertex-array setters3213✅ generated
safe buffer upload/subdata4✅ hand-written
void** out-pointers (glGetPointerv, glMultiDrawElements, …)36deferred
GLhandleARB (platform-variant ABI; modern GLuint form is bound)23deferred
non-string pointer return (void* glMapBuffer)11deferred
pointer-width arrays (GLintptr* in glBindBuffersRange, …)5deferred
callback argument (GLDEBUGPROC)4deferred
non-char double-pointer arrays (GLvdpauSurfaceNV*)2deferred
OpenCL interop type (glCreateSyncFromCLeventARB)1deferred

#Roadmap

  1. ✅ Vertical slice: resolver + self-patching dispatch + scalar/pointer calls.
  2. ✅ Generator: parse registry/gl.xml, emit FuncRef bindings (no C shims).
  3. ✅ Pointer/array params — generated call-scoped Bytes/FixedArray bindings, packed generic buffer uploads, and VBO-only vertex attributes.
  4. ✅ Alias groups (<alias> fallback) + epoxy_gl_version / is_desktop_gl / has_gl_extension.
  5. ✅ String arrays (const GLchar *const *) + 16-bit arrays.
  6. ✅ Opaque object handles (GLsync, GLeglImageOES, GLeglClientBufferEXT, GLVULKANPROCNV) → distinct #external types.
  7. ✅ GL enum constants — all 6061 <enum>s → pub const (UInt/UInt64/Int).
  8. Remaining shapes: non-string pointer returns (→ private opaque pointer), void** out-pointers, callbacks (GLDEBUGPROC trampoline). GLhandleARB needs a platform #ifdef typedef (split ABI) to bind without miscompiling.
  9. GLES / EGL / GLX / WGL window-system layers; Linux + Windows loaders.

⚠️ Package is too large to be displayed

This package contains over 9288 items.