Ziglua: Lua for Zig programmers, with the C API kept recognisable
Zig bindings for the Lua C API
At a glance
- What is it?
- Ziglua is a set of Zig bindings for the Lua C API that deliberately mirror the original rather than hide it, so that a Lua reference you already know transfers, while adding the things Zig gives you for free, error unions instead of return codes, null-terminated slices instead of C strings, and type-checked enums where the C API uses integers. It covers Lua 5.1 through 5.5 plus LuaJIT and Luau, and it compiles the Lua C sources itself.
- Who is it for?
- Adopt Ziglua if you are writing Zig and need to embed a Lua interpreter, or need to expose Zig code to an existing Lua installation, since it compiles Lua from source and mirrors the C API closely enough that the Lua manual remains your reference.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 18 days ago.
- What is it written in?
- Mainly Zig, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two ways to use it, and the one that shapes everything
Ziglua is a set of Zig bindings for the Lua C API, and it currently supports the latest releases of Lua 5.1, 5.2, 5.3, 5.4 and 5.5, plus LuaJIT and Luau.
It can be used in two ways. It can be embedded, to statically embed the Lua VM in a Zig program. Or it can be used as a shared module, to create Lua libraries that can be loaded at runtime in other Lua-based software.
In both cases Ziglua will compile Lua from source and link against your Zig code, which makes it easy to create software that integrates with Lua without requiring any system Lua libraries.
That last sentence is the one with the most practical consequence. The usual way to use Lua from another language is to link against the system Lua, which means a version dependency, a build system dependency and a set of headers you have to be sure match the runtime on the target machine. Compiling Lua from source as part of your build removes all of that, and it is also what makes the shared module route practical, since the module and the Lua it links against are built together.
The primary language is Zig, the licence is MIT, the default branch is main, and the recent releases are 0.7.0 for Zig 0.16.0 from 2026-09-03, 0.6.0 for Zig 0.15.2 from 2025-11-25 and 0.5.0 for Zig 0.14.0 from 2025-09-08. The last push was on 2026-10-04 with the project not archived, and the repository publishes no GitHub releases.
The release numbering is worth understanding because it explains the branch structure. Each version is tied to a specific Zig release, and Ziglua main is kept up to date with Zig master, with a zig-0.15.2 branch existing for Zig 0.15.2 support. So the project tracks Zig's own release cadence, and the versions line up one to one with Zig's majors.
The topics list confirms the shape: binding, library, lua, lua-bindings, package, zig and zig-package, so the project is packaged for the Zig package manager rather than being a source dependency you vendor.
Why the C API shape is kept, and what Zig adds on top
The design philosophy is stated as a contrast. Ziglua is a simple wrapper around the C API, and it aims to mirror the Lua C API as closely as possible while improving ergonomics using Zig's features.
The list of improvements is where it gets interesting, because each one is a specific C API weakness that Zig's type system fixes.
Zig error unions to require failure state handling. The C API returns an int status code and pushes an error object onto the stack, and the compiler cannot help you forget to check. An error union makes the failure case part of the type, so the function cannot be called without the caller handling it.
Null-terminated slices instead of C strings. Lua's C API takes a const char* and reads until the terminator, and getting the length wrong is a classic source of memory bugs. A Zig slice carries its length with it.
Type-checked enums for parameters and return values, where the C API uses plain integers with magic values. Compiler-enforced checking of optional pointers, so a null that the C API would treat as valid input is caught at compile time. And better types in many cases, with bool instead of int as the example given.
Then there are comptime convenience functions to make binding creation easier. The README says nearly every function in the C API is exposed, and that additional convenience functions like toAny and pushAny use comptime reflection to make the API easier to use.
That is the key design decision. Most binding generators make a choice about how to represent the dynamically typed parts of an API, and the choice usually either loses type information or invents a parallel type system. Ziglua keeps the original API shape, one function per C function, and adds a thin set of helpers built on Zig's compile-time reflection for the cases where the C API is genuinely untyped.
So the learning curve is a Lua C API learning curve, which is a well-trodden path, and the ergonomics improvements are additive rather than a redesign. If you have read the Lua manual you can read Ziglua's API and know what most functions do before you look at any documentation for it.
Adding it to a project, and the four build options
The integration instructions are short because the Zig package manager does the work.
Run zig fetch --save git+https://github.com/natecraddock/ziglua to add the most recent commit of ziglua to your build.zig.zon file. And you can add a tag to the URL to use a specific tagged release or commit, for example zig fetch --save git+https://github.com/natecraddock/ziglua#0.3.0.
Then in your build.zig file you use the dependency, and the shape is the standard Zig build pattern:
pub fn build(b: *std.Build) void {
// ... snip ...
const lua_dep = b.dependency("zlua", .{
.target = target,
.optimize = optimize,
});
// ... snip ...
// add the zlua module and lua artifact
exe.root_module.addImport("zlua", lua_dep.module("zlua"));
}The key line is the addImport, which makes the zlua module available under that name in your code, and the artifact line, which you can also use to access the compiled lua library directly. The README notes that this will compile the Lua C sources and link with your project.
There are then four additional options that can be passed to b.dependency().
The lang option sets the Lua language to build and embed, and it defaults to .lua55, with possible values .lua51, .lua52, .lua53, .lua54, .lua55 and luau. Note that LuaJIT is supported by the bindings but is not in this list, so LuaJIT support appears to be handled by a different route, and the default of 5.5 means you get the newest release unless you ask otherwise.
The shared option defaults to false, for embedding in a Zig program, and setting it to true dynamically links the Lua source code, which the README says is useful for creating shared modules. That is the flag that switches between the two usage modes described earlier.
The luau_use_4_vector option defaults to false, and setting it to true uses 4-vectors instead of the default 3-vector in Luau. That is a Luau-specific detail, and it exists because the vector size is baked into the Luau runtime configuration, so it has to be chosen at compile time.
And lua_user_h defaults to null, and you provide a path to an additional header file for the Lua compilation, which must be set to examples/user.h in order to run the multithreaded example. The repository does contain an examples/user.h, so the multithreaded example depends on a custom header being threaded into the Lua build.
Here is the shared library variant, linking against Lua 5.2 dynamically:
const zlua = b.dependency("zlua", .{
.target = target,
.optimize = optimize,
.lang = .lua52,
.shared = true,
});The README has a stray run of backticks after that block, which is a small formatting artefact rather than anything meaningful.
The hello world, and what it shows about the stack model
The example given is the smallest useful one, and it teaches the API in about fifteen lines:
const std = @import("std");
const zlua = @import("zlua");
const Lua = zlua.Lua;
pub fn main(init: std.process.Init) anyerror!void {
// Get an allocator from juicy main
const gpa = init.gpa;
// Initialize the Lua vm
var lua = try Lua.init(gpa);
defer lua.deinit();
// Add an integer to the Lua stack and retrieve it
lua.pushInteger(42);
std.debug.print("{}\n", .{try lua.toInteger(1)});
}The structure follows directly from the C API. Lua.init takes the allocator and returns a Lua instance, with a try because it can fail. The defer lua.deinit() is the cleanup, which in Zig is compile-time checked to run on every path out of the function.
Then lua.pushInteger(42) puts a value on the Lua stack, and try lua.toInteger(1) reads it back from stack position 1, with the try again because the conversion can fail. The stack index of 1 is the Lua convention and it is preserved exactly, which is the point of mirroring the C API.
The comment on the allocator line says get an allocator from juicy main, and the signature pub fn main(init: std.process.Init) anyerror!void is the modern Zig main convention, where the process init carries the general purpose allocator and a deinit that releases it. So the example uses the idiomatic allocator of the moment rather than a raw page allocator.
What the example does not show is any of the conveniences. There is no toAny, no pushAny, and no comptime reflection in sight. That is consistent with the philosophy: the base API is the C API, and the reflection-based helpers are for the cases where you need them.
The examples directory gives the wider picture. There is define-exe.zig, interpreter.zig, luau-bytecode.zig, multithreaded.zig and zig-fn.zig, which suggests the project demonstrates the four directions you might want to go: defining a native function callable from Lua, running a Lua interpreter, working with Luau bytecode, running Lua on multiple threads, and calling a Zig function from Lua.
There is also a test.luau file, which is a Lua test script, and an lpeg/ directory, which is the additional example showing how to compile and link a Lua module written in C like LPeg. That last one is the most instructive, because LPeg is a C library that hooks deep into the Lua internals, and making it work demonstrates that the binding layer is thin enough to sit underneath a real C extension.
How to evaluate it, and the questions to ask first
There are four things to consider.
First, which Lua you actually need. The bindings cover five Lua releases plus Luau, and the lang option defaults to .lua55, so the default is the newest stable. LuaJIT is supported according to the README but is not in the lang option list, so if you need LuaJIT specifically, that is the detail to check. The Luau 4-vector option is similarly a compile-time choice you cannot change later, which is a decision you have to make at the point you set up the dependency.
Second, the Zig version coupling. Each Ziglua release corresponds to a Zig release, 0.7.0 for Zig 0.16.0, 0.6.0 for Zig 0.15.2 and 0.5.0 for Zig 0.14.0, and main tracks Zig master. So the practical question is which Zig version you are on, and you pick the matching Ziglua version or the branch that supports it. If you are on a Zig version between releases, main is the option and it is a moving target.
Third, whether mirroring the C API is what you want. For a project that is essentially a Lua host with a bit of native code, the C API shape is exactly right, and the error unions and typed enums are a real improvement on the original. For a project that wants to expose a Rust-like typed API to Lua, Ziglua will feel like the C API with better error handling, which is not the same thing as a designed API. The two maintenance implications differ too, because a mirrored API is stable across versions while a designed one is yours to evolve.
Fourth, the build model. Lua is compiled from source and linked into your artefact, and the shared mode dynamically links it instead. The embedded route gives you a single self-contained binary with no system Lua dependency, and the shared route gives you a loadable module for an existing Lua installation. Pick the one that matches how your artefact is deployed, because the two produce different files with different requirements.
What you get is a binding that keeps the documentation you already read and adds the type safety the C API could not express, maintained alongside Zig's own release cycle. The cost is version coupling in both Zig and Lua, and a build that compiles a C library you did not write.
Editorial conclusion
Adopt Ziglua if you are writing Zig and need to embed a Lua interpreter, or need to expose Zig code to an existing Lua installation, since it compiles Lua from source and mirrors the C API closely enough that the Lua manual remains your reference. Do not expect a redesigned, strongly typed Lua interface, because the deliberate choice is to keep the original API shape and add Zig's error handling on top, so a new project wanting a typed API would be better served by a different binding. Before you commit, match the Ziglua release to your Zig version, since each release targets one, and decide the lang and shared options at dependency setup time because both are compile-time choices, particularly the Luau 4-vector setting and the extra user header that the multithreaded example requires.
Frequently asked questions
What is Ziglua?
It is a set of Zig bindings for the Lua C API, supporting the latest releases of Lua 5.1, 5.2, 5.3, 5.4 and 5.5 plus LuaJIT and Luau. It can be used embedded, to statically embed the Lua VM in a Zig program, or as a shared module for creating Lua libraries loadable at runtime.
What does Ziglua change compared with the plain Lua C API?
It aims to mirror the C API as closely as possible while improving ergonomics with Zig features: error unions to require failure state handling, null-terminated slices instead of C strings, type-checked enums for parameters and return values, compiler-enforced checking of optional pointers, better types such as bool instead of int, and comptime convenience functions like toAny and pushAny built on reflection.
How do I add Ziglua to a Zig project?
Run zig fetch --save git+https://github.com/natecraddock/ziglua to add the most recent commit to your build.zig.zon, optionally with a tag such as #0.3.0 for a specific release. Then in build.zig call b.dependency("zlua", ...) and add the module with exe.root_module.addImport("zlua", lua_dep.module("zlua")), which compiles the Lua C sources and links them with your project.
Which Lua versions can Ziglua build?
The lang option accepts .lua51, .lua52, .lua53, .lua54, .lua55 and luau, and defaults to .lua55. The shared option defaults to false and switches to dynamic linking for shared modules, and there is also a luau_use_4_vector option to use 4-vectors instead of the default 3-vector in Luau, plus lua_user_h for an extra header in the Lua compilation.
How does Ziglua relate to Zig releases?
Each Ziglua release targets a specific Zig release, with 0.7.0 for Zig 0.16.0, 0.6.0 for Zig 0.15.2 and 0.5.0 for Zig 0.14.0. Main is kept up to date with Zig master, and a separate zig-0.15.2 branch exists for Zig 0.15.2 support. The last push to the repository was on 2026-10-04.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/natecraddock-ziglua)Was this useful?