Last updated on

wrapping SDL3 for Zig


SDL3 just got released not so long ago, and while it is an amazing library providing many functionalities across multiple platforms, there is something that bothers me a little bit.

Lately I switched to Zig for most of my development time as I enjoyed many of the features it brings to the table, one being the great C interoperability. However, C library APIs can be a little shocking to work with for me and SDL is an example of that.

Using straight up C, writting a simple event loop would result in something like:

const c = @cImport({
    @cInclude("SDL3/SDL.h");
    @cDefine("SDL_MAIN_HANDLED", {});
    @cInclude("SDL3/SDL_main.h");
});

pub fn main() !void {
    try errify(c.SDL_Init(c.SDL_INIT_VIDEO));
    defer c.SDL_Quit();

    const window = try errify(c.SDL_CreateWindow(
        "redbed",
        1280,
        720,
        c.SDL_WINDOW_RESIZABLE,
    ));
    defer c.SDL_DestroyWindow(window);

    main_loop: while (true) {
        var event: c.SDL_Event = undefined;

        while (c.SDL_PollEvent(&event)) {
            if (event.type == c.SDL_EVENT_QUIT) break :main_loop;
        }
    }
}

It is ok for me, but I want better. So I wrote a partial wrapper to take advantage of Zig’s coding patterns and called it zsdl.

I decided to respect the namespaces and group objects and functions in separate files. This is a glimpse of my approach, a code snippet from video.zig which contains definitions for Display and Window among a few more.

pub const Window = struct {
    ptr: *c.SDL_Window,

    /// Create a window with the specified dimensions and flags.
    pub fn create(
        title: [:0]const u8,
        width: comptime_int,
        height: comptime_int,
        flags: WindowFlags,
    ) !Window {
        return .{
            .ptr = try errify(c.SDL_CreateWindow(
                title.ptr,
                width,
                height,
                flags.toInt(),
            )),
        };
    }
}

Notice the use of errify() for cleaner error handling, this function aimed to resolve types at comptime:

pub inline fn errify(value: anytype) error{SdlError}!switch (@typeInfo(@TypeOf(value))) {
    .bool => void,
    .pointer, .optional => @TypeOf(value.?),
    .int, .float => @TypeOf(value),
    else => @compileError("unerrifiable type: " ++ @typeName(@TypeOf(value))),
} {
    return switch (@typeInfo(@TypeOf(value))) {
        .bool => if (!value) error.SdlError,
        .pointer, .optional => value orelse error.SdlError,
        .int, .float => if (value == 0) return error.SdlError else value,
        else => comptime unreachable,
    };
}

The final look of the first example would end up like this:

const zsdl = @import("zsdl");

pub fn main() !void {
    try zsdl.init(.{ .video = true });
    defer zsdl.quit();

    const window = try zsdl.video.Window.create(
        "redbed",
        1280,
        720,
        .{ .resizable = true },
    );
    defer window.destroy();

    main_loop: while (true) {
        while (zsdl.events.pollEvent()) |event| {
            if (event == .quit) break :main_loop;
        }
    }
}

Despite needing some polish and not covering the full api, this is already enough for me to get started in some SDL coding.