Hi folks! Earlier this year I started working on a 2D platformer in C using raylib. The project has been going really well, but I’m starting to miss some of the features that modern languages provide. I’ve heard that Zig has excellent C interoperability, so I’m curious how well it works in practice. I know I can use @cImport, but I’ve also seen people recommend using hand-tweaked bindings instead. Which approach would you recommend? I’m also considering switching from raylib to SDL3, partly to remove a few abstraction layers and better understand what’s happening under the hood, just for fun. Has anyone here made a similar transition? How was the experience?
Just to briefly touch on the choice between raylib or SDL3, it is simply a matter of taste. SDL3 isn’t really offering you a much lower abstraction, each project is abstracting over the same things in different ways, which is providing a single interface to various operating system/graphics APIs. Both are battle-tested and fantastic frameworks, but if you want to try switching to SDL3 for a learning experience, then by all means go for it.
To address your main question, yes C interop in Zig is really good, much better than it typical in other languages. Interop with C is a first-class feature of the language, not an afterthought or third-party FFI layer: you can drop a few lines of code in your build system to generate the bindings from C headers, link the library, and get busy using the C code within your Zig code.
That said, the bindings generator is universal, and hand-made bindings with the full context of the API and understanding of its design are typically always going to made to be easier to use. Often this just comes down to fixing up a bunch of minor things such as:
- Replacing raw integers with enums
- Adjusting some pointer types to be more idiomatic for Zig, such as wrapping a function that expects a raw pointer and length to use a slice, etc.
- Name mangling to fit Zig conventions
- Wrapping functions within types
This list is far from comprehensive, but regarding the last point an example would be this:
What you would expect to see in raw generated bindings:
const handle = c.glfwCreateWindow(800, 600, "Title" null, null) orelse {
const err = c.glfwGetError();
return switch (err) {
c.GLFW_INVALID_VALUE => error.InvalidValue,
// you get the idea...
};
}:
c.glfwMakeContextCurrent(handle);
…might become something like this in a hand-written library:
const window = try glfw.Window.init(800, 600, "Title", .{});
window.makeCurrent();
Both are completely valid avenues to take. If using the raw C libraries that you are familiar is what you want, stick with the raw bindings. This is often preferred by many for graphics libraries like OpenGL. Otherwise if you want to use these libraries in a way that feels more like using idiomatic Zig code, then opt for a hand-written wrapper that aims to accomplish that. Try both, see which you prefer. There is no wrong answer here.
@ForeverZer0 Thanks for the help and the heads-up!
Just adding a few notes, maybe you saw it already but I’m not sure based on your question.
I’m not an expert on the topic but recently dive in some of that so maybe this will be useful.
- upstream Raylib actually support zig build so you can basically just add it as a dependency of a zig project and access the C like api
- there is a raylib zig library that provides a zuggified api. I didn’t use much directly but seems very complete and well maintained
- I know that stuff because I spent time on DVUI, that uses both raylib & raylib-zig as a backend. It also uses SDL3 (in direct C function calling style). I mention it because I remember seeing side to side the different backends implementing the same interface really helped me click, and this might help to get a good sense of how similar and or different theses 3 approaches are. (However, dvui build script is long and complex, so maybe it’s a bit much if you are not so familiar, and It’s possible that this did enlighten to me only because I was interested in DVUI itself, so YMMV)
And I totally support trying going a bit through the boilerplate of a wrapper yourself, I found this a very nice learning experience.
Have fun