gloam

A loader generator for Vulkan, OpenGL, OpenGL ES, EGL, GLX, and WGL. Reads Khronos XML spec files and generates C or Rust dispatch code.
gloam is a clean-room rewrite of GLAD in Rust, with first-class support for ANGLE extension supplementals, Vulkan command-scope inference, and a hash-based extension detection strategy that avoids string comparisons at load time.
Quick start
# 1. Populate bundled XML specs and headers (one-time setup, or to refresh).
# 2. Build.
# 3. Generate a C loader for OpenGL 3.3 core + all extensions.
# 4. Generate a Rust loader for Vulkan 1.3.
# 5. Generate a combined GL 3.3 + GLES 2.0 + Vulkan + EGL loader.
Command reference
gloam [OPTIONS] <COMMAND>
Options:
--api <SPEC> API specifiers (required). Comma-separated list of
name[:profile][=major.minor] tokens. Profile is
required for GL (core|compat). Version is optional
(latest if omitted). Examples:
gl:core=3.3
gles2=3.0
gl:core=3.3,gles2=3.0
vk=1.3
egl=1.5
--extensions <FILTER> Restrict extensions. Either a path to a file (one
name per line) or a comma-separated inline list.
Omit to include all extensions supported by the
requested API.
--promoted Automatically include any extension whose commands
were promoted into the requested core version, even
if not listed in --extensions. Handles both
same-name promotion (e.g. ARB_copy_buffer) and
renamed promotion (e.g. ARB_multitexture →
glActiveTexture). Scoped per-API to prevent
cross-contamination in merged builds.
--predecessors Automatically include any extension that is a
predecessor of an already-selected extension — i.e.
its commands or enums are aliases of those in the
selected set. Follows chains to a fixed point, so
indirect predecessors are included too. Runs after
--promoted, so promoted extensions also seed the
predecessor search.
--merge Merge multiple APIs of the same spec into a single
output file. Required when combining gl and gles2.
--out-path <DIR> Output directory [default: .]
--quiet Suppress progress messages.
--fetch Fetch XML specs from upstream Khronos URLs instead
of the bundled copies. Any fetch failure is fatal.
Commands:
c Generate a C loader.
--alias Enable bijective function-pointer alias resolution at
load time. If the canonical slot is null but an alias
was loaded by the driver (or vice versa), the pointer
is propagated to both slots.
--loader Include a built-in dlopen/LoadLibrary convenience layer.
rust Generate a Rust loader.
--alias Enable bijective function-pointer alias resolution.
Extension selection flags
The three extension-related flags are orthogonal and compose freely:
| Flag | What it does |
|---|---|
--alias |
Runtime: propagates loaded function pointers to alias slots at load time |
--promoted |
Selection: adds extensions whose commands were promoted into requested core |
--predecessors |
Selection: adds predecessor extensions of the already-selected set |
Typical use with an explicit --extensions list:
# Explicit list only — exactly what you asked for.
# Explicit list + promoted ARB predecessors of core functions.
# Explicit list + promoted + extension-to-extension predecessor chains.
Generated C output
For --api gl:core=3.3 c --loader, gloam emits into the output directory:
include/
gloam/
gl.h # public header — include this in your project
KHR/
khrplatform.h # auxiliary headers copied from bundled/
xxhash.h # single-file xxHash amalgamation used by the loader
src/
gl.c # loader implementation
When generating without --merge, each API gets its own stem:
gles2 → gles2.h / gles2.c, gl → gl.h / gl.c, etc.
include/gloam/gl.h
GloamAPIProcopaque function pointer type andGloamLoadFunccallback typedefAPIENTRY/APIENTRYP/GLOAM_API_PTR/GLAPIENTRYcalling convention macrostypedefdeclarations in topological dependency order#defineconstants for all required enums- PFN typedef for every selected command (e.g.
PFNGLACTIVETEXTUREPROC) GloamGLContextstruct with:featArray[]— oneunsigned charflag per version featureextArray[]— oneunsigned charflag per extensionpfnArray[]/ named union members — one function pointer slot per command
- Feature presence macros (
GLOAM_GL_VERSION_3_3, …) - Extension presence macros (
GLOAM_GL_ARB_SYNC, …) #define glFoo (gloam_gl_context.Foo)dispatch macros (or prototypes under__INTELLISENSE__)gloamLoadGLContext(ctx, getProcAddr)/gloamLoadGL(getProcAddr)declarations- With
--loader:gloamLoaderLoadGLContext/gloamLoaderUnloadGLContext/gloamLoaderResetGLContext(and non-Context variants) declarations
src/gl.c
- Static function-name string table (
kFnNames[]) - Feature PFN range table (contiguous-run compressed)
- Per-API extension hash table (pre-baked XXH3-64, sorted for binary search)
- Extension PFN range table and index subset per API
gloamLoadGLContext():- Bootstraps
glGetStringthen callsfind_coreto parseGL_VERSIONand setfeatArraybits - Bulk-loads function pointers for each enabled feature via the range table
- Hashes driver-reported extension names, Shellsorts them, binary-searches
against the pre-baked table, sets
extArraybits for matches - Loads function pointers for each detected extension via the range table
- With
--alias: resolves bijective function-pointer alias pairs
- Bootstraps
- With
--loader:gloamLoaderLoad/Unload/ResetGLContext()— opens the platform GL library, wires upwglGetProcAddress/glXGetProcAddressARBwith correct calling conventions, delegates togloamLoadGLContext, stores the library handle incontext->glad_loader_handle
Extension detection strategy
At load time, the generated code:
- Calls
glGetIntegerv(GL_NUM_EXTENSIONS, &n)to get the count. - Calls
glGetStringi(GL_EXTENSIONS, i)for eachi, hashes each name with XXH3-64 (the same algorithm used at generator time), and stores the hashes in a heap-allocateduint64_t[]. - Shellsorts the array in-place (Ciura gap sequence — no extra memory, ~160 bytes of code).
- Binary-searches the sorted driver hashes against the pre-baked known extension hash table embedded in the generated source.
This gives O(n log n) total work to detect all extensions, with O(log n) per lookup, and zero string comparisons at runtime.
Vulkan loading
Vulkan function loading requires knowing which vkGet*ProcAddr entry point
to use for each command. gloam infers this from the first parameter type:
| First parameter | Scope | Loaded via |
|---|---|---|
| (none / non-handle) | Global | vkGetInstanceProcAddr(NULL, name) |
VkInstance / VkPhysicalDevice |
Instance | vkGetInstanceProcAddr(instance, name) |
VkDevice / VkQueue / VkCommandBuffer |
Device | vkGetDeviceProcAddr(device, name) |
vkGetInstanceProcAddr itself |
Unknown | dlsym / GetProcAddress |
Multi-call loading model
gloamLoadVulkanContext (and gloamLoaderLoadVulkanContext) may be called
multiple times on the same context as the application progresses through
Vulkan initialisation. Each call is additive — it fills in whatever
the current set of live handles allows, without wiping previous work:
// Pass 1: all handles NULL — loads Global-scope functions,
// detects available instance extensions.
;
// ... create VkInstance based on detected extensions ...
// Pass 2: instance live — loads Instance-scope functions,
// detects available device extensions.
;
// ... create VkPhysicalDevice / VkDevice ...
// Pass 3: device live — loads Device-scope functions.
;
Caller-managed library handle
The library handle is stored in context->glad_loader_handle so you can
supply your own:
// Pre-populate to skip dlopen:
ctx->glad_loader_handle = my_vk_handle;
;
// During cleanup, zero the field first to prevent gloam from closing
// a handle you still own:
ctx->glad_loader_handle = NULL;
;
glad_loader_handle is present on all context types (GL, EGL, VK, …)
so this pattern is consistent across APIs.
Bundled files
bundled/ contains compile-time snapshots of upstream XML specs and
auxiliary headers. They are embedded into the binary via include_str!
so the binary is fully self-contained.
To refresh them:
The --fetch CLI flag bypasses the bundled copies and fetches directly
from upstream at generation time. Any failure is fatal.
Alias resolution (--alias)
When --alias is passed, the generated loader emits a runtime resolver:
after loading all function pointers, if the canonical slot for an alias
pair is null but the alias slot was loaded by the driver (or vice versa),
the loaded pointer is propagated to both slots. This is useful for
extension functions that were later promoted to core under a new name,
where a driver may only expose one spelling.
Note: --alias is a runtime concern — it does not affect which extensions
are selected. For selection-time alias expansion see --promoted and
--predecessors.
Building from source
Requires Rust 1.75 or later.