mxpv/luaz
Zero-cost Luau wrapper for Zig
luaz
is a zero-cost wrapper library for Luau
.
Unlike other libraries, it focuses specifically on Luau
, providing idiomatic Zig
bindings that leverage Luau's unique features
and performance characteristics.
The build system provides prebuilt Luau tools out of the box:
luau-compiler
: Compile Luau source to bytecodeluau-analyzer
: Typecheck and lint Luau codeThese tools make it easy to compile, analyze, and embed Luau
scripts directly into your Zig applications.
Zig
and Lua
Luau
Sandboxing APIs for secure execution environmentsLuau
code generation for improved performance on supported platformsluau-compile
and luau-analyze
) provided by the build systemFull API documentation is available at: https://mxpv.github.io/luaz/#luaz.lua
The documentation is automatically updated on every change to the main
branch.
[!TIP] For a comprehensive overview of all features, see the guided tour example which can be run with
zig build guided-tour
.
The following example demonstrates some basic use cases.
Lua
takes optional Zig allocator._G
is available via globals()
table.eval
is a helper function that compiles Lua code to Luau bytecode and executes it.set
and get
are used to pass and retrieve data.const std = @import("std");
const luaz = @import("luaz");
const assert = std.debug.assert;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var lua = try Lua.init(&gpa.allocator); // Create Lua state with custom allocator
defer lua.deinit(); // Clean up Lua state
// Set a global variable
try lua.globals().set("greeting", "Hello from Zig!");
// Get and verify the global variable
const value = try lua.globals().get("greeting", []const u8);
assert(std.mem.eql(u8, value.?, "Hello from Zig!"));
// Evaluate Lua code and get result
const result = try lua.eval("return 2 + 3 * 4", .{}, i32);
assert(result == 14);
}
NOTE Ideally, the bundled
luau-compile
tool should be used to precompile Lua scripts offline.
Zig structs and arrays are automatically converted to Lua tables:
const std = @import("std");
const luaz = @import("luaz");
const Point = struct { x: f64, y: f64 };
pub fn main() !void {
var lua = try Lua.init(null);
defer lua.deinit();
// Struct becomes a Lua table with field names as keys
const point = Point{ .x = 10.5, .y = 20.3 };
try lua.globals().set("point", point);
// Array becomes a Lua table with 1-based integer indices
const numbers = [_]i32{ 1, 2, 3, 4, 5 };
try lua.globals().set("numbers", numbers);
// Access from Lua
const x = try lua.eval("return point.x", .{}, f64); // 10.5
const first = try lua.eval("return numbers[1]", .{}, i32); // 1
const length = try lua.eval("return #numbers", .{}, i32); // 5
}
Both Lua functions can be called from Zig and Zig functions from Lua with automatic type conversion and argument handling.
const std = @import("std");
const luaz = @import("luaz");
const assert = std.debug.assert;
fn sum(a: i32, b: i32) i32 {
return a + b;
}
pub fn main() !void {
var lua = try Lua.init(null); // Use default allocator
defer lua.deinit();
// Register Zig function in Lua
try lua.globals().set("sum", sum);
// Call Zig function from Lua
const result1 = try lua.eval("return sum(10, 20)", .{}, i32);
assert(result1 == 30);
// Define Lua function
_ = try lua.eval("function multiply(x, y) return x * y end", .{}, void);
// Call Lua function from Zig
const result2 = try lua.globals().call("multiply", .{6, 7}, i32);
assert(result2 == 42);
// Closures with captured values
const table = lua.createTable(.{});
defer table.deinit();
fn getGlobal(upv: Lua.Upvalues(*Lua), key: []const u8) !i32 {
return try upv.value.globals().get(key, i32) orelse 0;
}
const lua_ptr = @constCast(&lua);
try table.setClosure("getGlobal", lua_ptr, getGlobal);
try lua.globals().set("funcs", table);
try lua.globals().set("myValue", @as(i32, 123));
const result3 = try lua.eval("return funcs.getGlobal('myValue')", .{}, i32);
assert(result3 == 123);
}
luaz has automatic compile-time binding generation for user data. It supports constructors, static and instance
methods. If a struct has deinit
, it'll be automatically invoked on garbage collection.
const std = @import("std");
const luaz = @import("luaz");
const assert = std.debug.assert;
const Counter = struct {
value: i32,
pub fn init(initial: i32) Counter {
return Counter{ .value = initial };
}
pub fn deinit(self: *Counter) void {
std.log.info("Counter with value {} being destroyed", .{self.value});
}
pub fn getMaxValue() i32 {
return std.math.maxInt(i32);
}
pub fn increment(self: *Counter, amount: i32) i32 {
self.value += amount;
return self.value;
}
pub fn getValue(self: *const Counter) i32 {
return self.value;
}
// Metamethods for arithmetic operations
pub fn __add(self: Counter, other: i32) Counter {
return Counter{ .value = self.value + other };
}
pub fn __tostring(self: Counter) []const u8 {
return std.fmt.allocPrint(std.heap.page_allocator, "Counter({})", .{self.value}) catch "Counter";
}
pub fn __len(self: Counter) i32 {
return self.value;
}
};
pub fn main() !void {
var lua = try luaz.Lua.init(null);
defer lua.deinit();
// Register Counter type with Lua
try lua.registerUserData(Counter);
_ = try lua.eval(
\\local counter = Counter.new(10) -- Use constructor
\\assert(counter:increment(5) == 15) -- Call instance method
\\assert(counter:getValue() == 15) -- Get current value
\\
\\local max = Counter.getMaxValue() -- Call static method
\\assert(max == 2147483647) -- Max i32 value
\\
\\-- Metamethods in action
\\local new_counter = counter + 5 -- Uses __add metamethod
\\assert(new_counter:getValue() == 20)
\\assert(#counter == 15) -- Uses __len metamethod
\\print(tostring(counter)) -- Uses __tostring metamethod
, .{}, void);
}
Efficient string building using Luau's StrBuf API with automatic memory management.
fn buildGreeting(upv: Lua.Upvalues(*Lua), name: []const u8, age: i32) !Lua.StrBuf {
var buf: Lua.StrBuf = undefined;
buf.init(upv.value);
buf.addString("Hello, ");
buf.addLString(name);
buf.addString("! You are ");
try buf.add(age);
buf.addString(" years old.");
return buf;
}
// Register and call from Lua
try lua.globals().setClosure("buildGreeting", &lua, buildGreeting);
const result = try lua.eval("return buildGreeting('Alice', 25)", .{}, []const u8);
WARNING This library is still evolving and the API is not stable. Backward incompatible changes may be introduced up until the 1.0 release. Consider pinning to a specific commit or tag if you need stability.
The build system provides prebuilt Luau tools that can be invoked directly:
$ zig build luau-analyze -- --help
Usage: /Users/.../luaz/.zig-cache/o/.../luau-analyze [--mode] [options] [file list]
Available modes:
omitted: typecheck and lint input files
--annotate: typecheck input files and output source with type annotations
Available options:
--formatter=plain: report analysis errors in Luacheck-compatible format
--formatter=gnu: report analysis errors in GNU-compatible format
--mode=strict: default to strict mode when typechecking
--timetrace: record compiler time tracing information into trace.json
$ zig build luau-compile -- --help
Usage: /Users/.../luaz/.zig-cache/o/.../luau-compile [--mode] [options] [file list]
Available modes:
binary, text, remarks, codegen
Available options:
-h, --help: Display this usage message.
-O<n>: compile with optimization level n (default 1, n should be between 0 and 2).
-g<n>: compile with debug level n (default 1, n should be between 0 and 2).
--target=<target>: compile code for specific architecture (a64, x64, a64_nf, x64_ms).
...
These projects served as inspiration and are worth exploring:
This project is licensed under the MIT License - see the LICENSE file for details.