Novel Boby Alex

Zig's Compile Time Metaprogramming is Cool and Awesome.

Aug 11, 2026

It’s also useful.

The Problem

I’m building a desktop application from scratch and I want to do hardware rendering with OpenGL.

OpenGL is not a typical library. It can’t be statically linked, nor can it be shipped as a dynamic library. OpenGL is an API specification that different vendors provide an implementation for.

To actually call OpenGL functions, they have to be loaded at runtime. There are libraries like GLEW or GLAD that can do this automatically, but I’m not using them because I’m trying to keep dependencies as low as possible.

To load an OpenGL function manually on Windows I can call wglGetProcAddress1 with the name of the function and it will return a pointer to the function. I just have to write the declaration for each function pointer and then load them at runtime.

// This will store the function pointer.
pub var Clear: *const fn (mask: u32) callconv(.c) void = undefined;
// rest of the functions...

// The '!' before void means that it can return either an error or void.
pub fn load_functions() !void {
    // If the wglGetProcAddress call returns null we return an error.
    Clear = @ptrCast(platform.gl_get_proc_address("glClear") orelse {
        return error.Function_Not_Found;
    });
    // rest of the functions...
}

The platform.gl_get_proc_address function wraps the call to wglGetProcAddress with some extra steps2.

But that’s a lot of typing. Won’t be cool and awesome if I could loop through every declaration in a struct, check if it’s a function pointer, turn the name of the declaration into a string, attach gl in front of it, and pass it into gl_get_proc_address.

Not something you can do without complicated macros or code generation, right?

The Solution

See if you can understand the following code:

pub const gl = struct {
    pub var Clear: *const fn (mask: u32) callconv(.c) void = undefined;
    // rest of the functions...
    
    pub const COLOR_BUFFER_BIT = 0x00004000;
    // rest of the constants...
};

fn load_all_functions() !void {
    inline for (@typeInfo(gl).@"struct".decls) |declaration| {
        const info = @typeInfo(@TypeOf(@field(gl, declaration.name)));
        if (info == .pointer and @typeInfo(info.pointer.child) == .@"fn") {
            const full_name = comptime std.fmt.comptimePrint(
                "gl{s}",
                .{declaration.name},
            );
            @field(gl, declaration.name) = @ptrCast(
                platform.gl_get_proc_address(full_name) orelse {
                    return error.Function_Not_Found;
                },
            );
        }
    }
}

This is Zig code that does exactly what I described above.

Zig lets you inspect and manipulate types at compile time. Because Zig’s reflection is limited to compile time execution, there are no runtime costs for using it.

Let’s go through the code line by line.

inline for (@typeInfo(gl).@"struct".decls) |declaration| {

This loops through every declaration in the struct. The inline for unrolls the loop at compile time so that I can work with types inside it.

@typeInfo is a Zig builtin function that takes a type and returns a union containing information about the type. I am accessing its @"struct" field to get information about the struct, which has a decls field containing a list of declarations.

// declaration.name gives me the name as a string
const info = @typeInfo(@TypeOf(@field(gl, declaration.name)));

For each declaration I need to get its type information. I can use the @field builtin to access the declaration by name. Then I can find its type with @TypeOf and use @typeInfo again to get the the union containing type information.

if (info == .pointer and @typeInfo(info.pointer.child) == .@"fn") {

Then I check declaration’s type info union to see if its a pointer and if the type its pointing to is a function. I’m using the == operator to check which field of a union is active.

const full_name = comptime std.fmt.comptimePrint(
    "gl{s}",
    .{declaration.name},
);

I’m formatting a string at compile time. I take the name of the declaration, lets say "Clear" for example, and attach "gl" to the front to make "glClear".

@field(gl, declaration.name) = @ptrCast(
    platform.gl_get_proc_address(full_name) orelse {
        return error.Function_Not_Found;
    },
);

This code passes the string to gl_get_proc_address which returns a ?*anyopaque (equivalent to void* in C or rawptr in Odin). The ? means that the type is nullable.

To get a useable value, I can unwrapped it with orelse giving me a *anyopaque. If the ?*anyopaque was null during the unwrap we return an error.

The @ptrCast takes the resulting *anyopaque and casts it to the type of the declaration.

I used @field earlier to access a value by name, but it can also be used to set the value. I’m using it to assign the function pointer to the declaration in the gl struct.

That’s it. Now I can define more OpenGL functions and start using them.

pub const gl = struct {
    pub var ClearColor: *const fn // omitted
    pub var Clear: *const fn      // for
    pub var Viewport: *const fn   // brevity

    pub const COLOR_BUFFER_BIT = 0x00004000;
};

pub fn main() !void {
    // set up window and OpenGL context
    try load_all_functions();
    while (app_running) {
        // poll events
        
        gl.Viewport(0, 0, window.width, window.height);
        gl.ClearColor(0.39, 0.58, 0.92, 1.0);
        gl.Clear(gl.COLOR_BUFFER_BIT);
    
        // swap buffers
    } 
}

Cool and Awesome

You can see that Zig doesn’t have an arcane macro syntax, You just write regular Zig code that runs at compile time.

I only started programming in Zig a few days ago and that didn’t stop me from picking up its metaprogramming features. Not because I have some sort of special ability to understand it, but because it’s simple, well designed, and easy to use.

I hope you give Zig a try.

I did something simple here but you can go ham with this. For example, you could generate the gl struct at compile time using the @Struct builtin.

This article was handwritten by me, if you notice any errors please direct them to my email.


  1. I’m oversimplifying to focus on the topic at hand. For the details see: https://wikis.khronos.org/opengl/Creating_an_OpenGL_Context_(WGL)↩︎

  2. The extra steps: https://wikis.khronos.org/opengl/Load_OpenGL_Functions#Windows↩︎