Vendoring raylib
raylib is a C library for windows, drawing and sound. This chapter wires it into a Zig project.
The one line version, and why it fails
raylib ships a build.zig. Using it should be one line:
const raylib = b.dependency("raylib", .{ .target = target, .optimize = optimize });
On Zig master today that line does not compile. Not the library. The line.
raylib/build.zig:95:10: error: member function expected 1 argument(s), found 2
raylib/build.zig:762:23: error: no field named 'build_root' in struct 'Build'
Naming a package in build.zig.zon makes the build runner import that package’s
build.zig. Every dependency’s build script is compiled before any of your code
is. So a dependency whose build script has not caught up with Zig master fails
your build before it reaches you.
Both errors are in raylib’s Wayland detection, which this project does not use. That makes no difference. The whole build script has to type-check before your code is reached.
raylib-zig, the usual binding, is further behind. It pins raylib 5.5 and calls
three standard library functions that no longer exist on master.
Neither project is doing something wrong. They track releases. This guide tracks master. That gap is where the breakage lives, and it is better to know that before you take the dependency.
Depend on the C instead
raylib’s C changes far more slowly than Zig’s build API. So this project depends on the C.
1. Fetch the source
Clone raylib at a pinned commit into a directory the build can read. Pin it: an unpinned dependency changes under you.
git init vendor/raylib
git -C vendor/raylib remote add origin https://github.com/raysan5/raylib.git
git -C vendor/raylib fetch --depth 1 origin <commit>
git -C vendor/raylib checkout FETCH_HEAD
Keep src/ and the licence. Delete the rest. raylib’s examples/ directory is
72 MB by itself, and none of it gets compiled. fetch-raylib.sh in this project
does those steps and records the commit it fetched.
Add vendor/ to .gitignore. The script is the record. The vendored copy is
not.
2. Do not declare it in build.zig.zon
This is the part that matters. vendor/raylib stays a plain directory of C
files. The build runner imports the build script of every declared package and
never looks at a plain directory, so raylib’s build.zig sits there unread.
3. Compile the C
const mod = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
});
const lib = b.addLibrary(.{ .name = "raylib", .linkage = .static, .root_module = mod });
mod.addCMacro("PLATFORM_DESKTOP_GLFW", "");
mod.addCMacro("GRAPHICS_API_OPENGL_33", "");
mod.addCMacro("_GLFW_X11", "");
mod.addIncludePath(b.path("vendor/raylib/src/platforms"));
mod.addIncludePath(b.path("vendor/raylib/src/external/glfw/include"));
mod.addCSourceFiles(.{
.root = b.path("vendor/raylib/src"),
.files = &.{ "rcore.c", "rshapes.c", "rtextures.c", "rtext.c", "raudio.c", "rglfw.c" },
.flags = &.{"-std=gnu99"},
});
for ([_][]const u8{ "GL", "X11", "Xrandr", "Xinerama", "Xi", "Xcursor" }) |name| {
mod.linkSystemLibrary(name, .{});
}
That is seven C files. rglfw.c is raylib’s bundled GLFW, the windowing layer.
The rest are raylib’s modules, switched on and off by the SUPPORT_MODULE_*
macros.
On Debian or Ubuntu the system libraries come from:
sudo apt-get install libx11-dev libxrandr-dev libxinerama-dev \
libxi-dev libxcursor-dev libgl1-mesa-dev
No ALSA package. raylib’s audio backend loads libasound at runtime rather than including its header, so the game compiles without it and stays silent where there is no device.
4. Turn raylib.h into a Zig module
@cImport was removed from the language. C headers now arrive through a build
step.
const translated = b.addTranslateC(.{
.root_source_file = b.path("vendor/raylib/src/raylib.h"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
const rl = translated.createModule();
rl.linkLibrary(lib);
exe_mod.addImport("rl", rl);
5. Use it
const rl = @import("rl");
pub fn main() void {
rl.InitWindow(540, 960, "Lane Dodger");
defer rl.CloseWindow();
while (!rl.WindowShouldClose()) {
rl.BeginDrawing();
rl.ClearBackground(rl.RAYWHITE);
rl.DrawText("hello", 20, 20, 24, rl.BLACK);
rl.EndDrawing();
}
}
Names come through translate-c unchanged, so raylib’s own documentation applies directly.
The whole thing
fn raylibModule(The macros are copied from raylib’s own build script. That is the cost of this approach. When raylib changes its configuration, this function has to follow. It is about forty lines, and a mismatch fails at compile time rather than quietly producing a different library.
The same source, on a canvas
The platform choice reaches two places: the raylib module below, and a separate
addWeb step that runs emcc.
Desktop is PLATFORM_DESKTOP_GLFW with OpenGL 3.3 and raylib’s bundled GLFW.
Web is PLATFORM_WEB with GLES 3, and rglfw.c is not compiled at all, because
Emscripten supplies its own GLFW bound to the canvas.
Zig compiles the game and raylib to a static library for wasm32-emscripten.
emcc does the link. The JavaScript glue, the GL context and the canvas setup
come from Emscripten.
/// Build the game for a browser canvas.
///
/// Zig compiles our code and raylib's to a static library for
/// `wasm32-emscripten`, and `emcc` does the link, because the JavaScript glue,
/// the GL context and the canvas plumbing are all things only Emscripten knows
/// how to emit. That split is also why `main.zig` has a `Game.frame`: emcc
/// hands the loop to the browser and never returns.
fn addWeb(Two flags there prevent silent failures.
-sMIN_WEBGL_VERSION=2. GLES 3 emits #version 300 es shaders, which need a
WebGL 2 context. Emscripten creates a WebGL 1 one unless told otherwise. The
symptom is a game that loads, runs, reports no error, and compiles not one
shader.
.sanitize_c = .off on the raylib module. Zig instruments C with UBSan in the
debug modes and links its own runtime to catch the reports. That runtime comes
with a Zig link, and this link is done by emcc, so the instrumentation leaves
dozens of undefined __ubsan_* symbols behind.
Running it
cd examples/lane-dodger
./fetch-raylib.sh
zig build run -Doptimize=ReleaseFast
The browser build is a 200 KB .wasm plus about the same amount of JavaScript.
It needs the Emscripten SDK:
zig build web -Doptimize=ReleaseFast
python3 -m http.server -d zig-out/web 8080