Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Functions

The core idea is simple: a Zig function is a Lua-callable function. You declare typed parameters, zua decodes the Lua arguments into them, and you return a Result.

Your first function

fn add(a: i32, b: i32) Result(i32) {
    return Result(i32).ok(a + b);
}

To register it, decide whether the function needs access to the Zua instance. If not, use ZuaFn.pure:

globals.setFn("add", ZuaFn.pure(add, .{
    .parse_error = "add expects (i32, i32)",
}));

If it does, use ZuaFn.from and add *Zua as the first parameter:

fn greet(z: *Zua, name: []const u8) Result([]const u8) {
    const msg = std.fmt.allocPrint(z.allocator, "hello, {s}", .{name})
        catch return Result([]const u8).errStatic("out of memory");
    return Result([]const u8).owned(msg);
}

globals.setFn("greet", ZuaFn.from(greet, .{
    .parse_error = "greet expects (string)",
}));

The parse_error message is what Lua sees if the arguments do not match. Make it useful.

Return values

Result(T) wraps your return type. For a single value, ok is all you need:

fn answer(_: *Zua) Result(i32) {
    return Result(i32).ok(42);
}

For multiple return values, use an anonymous tuple:

fn minmax(_: *Zua, a: f64, b: f64) Result(.{ f64, f64 }) {
    return Result(.{ f64, f64 }).ok(.{ @min(a, b), @max(a, b) });
}

For no return value:

fn log(_: *Zua, msg: []const u8) Result(.{}) {
    std.log.info("{s}", .{msg});
    return Result(.{}).ok(.{});
}

Allocated strings

When you allocate a string to return, use owned instead of ok. The trampoline takes ownership and frees it after pushing to Lua:

fn format(z: *Zua, value: i32) Result([]const u8) {
    const text = std.fmt.allocPrint(z.allocator, "value={d}", .{value})
        catch return Result([]const u8).errStatic("out of memory");
    return Result([]const u8).owned(text);
}

Always allocate with z.allocator. The trampoline frees with the same allocator, so mixing allocators will go wrong.

Errors

Three constructors cover the common cases:

return Result(i32).errStatic("missing pid");
return Result(i32).errOwned(z.allocator, "pid {d} out of range", .{pid});
return Result(i32).errZig(err);

errStatic takes a string literal. errOwned formats a message on the allocator and frees it before raising the Lua error. errZig surfaces a Zig error value by name.

Errors always raise a Lua error after your function returns. Zig defer blocks run normally, which is the main reason zua uses this pattern instead of calling lua_error directly from inside the callback.

Callbacks can also return !Result(T). Zig errors propagate through the trampoline and become Lua errors automatically:

fn readFile(z: *Zua, path: []const u8) !Result([]const u8) {
    const contents = try std.fs.cwd().readFileAlloc(z.allocator, path, 1024 * 1024);
    return Result([]const u8).owned(contents);
}

Configuring error messages

The second argument to ZuaFn.pure and ZuaFn.from is a ZuaFnErrorConfig. You can pass an anonymous struct literal for convenience, but the full type gives you three fields:

globals.setFn("read_file", ZuaFn.from(readFile, ZuaFnErrorConfig{
    .parse_error = "read_file expects (string)",
    .zig_err_fmt = "read_file failed: {s}",
    .zig_err_hook = null,
}));

parse_error is what Lua sees when argument decoding fails. zig_err_fmt is a format string that receives the Zig error name as {s}. If you need to produce a more descriptive message at runtime, use zig_err_hook instead:

fn describeError(z: *Zua, err: anyerror) []const u8 {
    return std.fmt.allocPrint(z.allocator, "file error ({s}): check path and permissions", .{
        @errorName(err),
    }) catch "file error";
}

globals.setFn("read_file", ZuaFn.from(readFile, ZuaFnErrorConfig{
    .parse_error = "read_file expects (string)",
    .zig_err_hook = describeError,
}));

The hook takes precedence over zig_err_fmt when both are set. The returned string must be allocated with z.allocator. zua frees it after raising the Lua error.

Optional parameters

Declare optional parameters as ?T. A missing trailing argument or an explicit nil both decode as null:

fn add(a: i32, b: ?i32) Result(i32) {
    return Result(i32).ok(a + (b orelse 0));
}

Lua can call this as add(1) or add(1, nil) and both work.

Supported parameter types

i32, i64, f32, f64, bool, []const u8, [:0]const u8, structs, and ?T for any of the above. Tables and userdata are covered in later chapters.