Skip to content

Commit 33e06bd

Browse files
authored
Add support for lua.dump() and lua.load() based on the std.io.AnyReader and std.io.AnyWriter contracts. (#5)
1 parent cd62350 commit 33e06bd

File tree

2 files changed

+134
-3
lines changed

2 files changed

+134
-3
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ This section describes the current status of Zig language bindings ("the Zig API
125125
| `lua_concat` | ☑️ `lua.concat()` |
126126
| `lua_cpcall` | ☑️📢 `lua.protectedCallCFunction()` |
127127
| `lua_createtable` | ☑️ `lua.createTable()` |
128-
| `lua_dump` ||
128+
| `lua_dump` | ➖ subject to changes. |
129129
| `lua_equal` | ☑️ `lua.equal()` |
130130
| `lua_error` | ☑️📢 `lua.raiseError()` |
131131
| `lua_gc` | ☑️ `lua.gc()` + `lua.gcIsRunning()` |
@@ -149,7 +149,7 @@ This section describes the current status of Zig language bindings ("the Zig API
149149
| `lua_isthread` | ☑️ `lua.isThread()` |
150150
| `lua_isuserdata` | ☑️ `lua.isUserdata()` |
151151
| `lua_lessthan` | ☑️ `lua.lessThan()` |
152-
| `lua_load` ||
152+
| `lua_load` | ➖ subject to changes. |
153153
| `lua_newstate` | ☑️📢 `Lua.init()` |
154154
| `lua_newtable` | ☑️ `lua.newTable()` |
155155
| `lua_newthread` | ☑️ `lua.newThread()` |

src/root.zig

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1387,7 +1387,7 @@ pub const Lua = opaque {
13871387
ok = c.LUA_OK,
13881388

13891389
/// Coroutine has suspended execution via yield. Indicates normal coroutine suspension, not an
1390-
/// error condition. The coroutine can be resumed later with lua_resume().
1390+
/// error condition. The coroutine can be resumed later with `resumeCoroutine()`.
13911391
yield = c.LUA_YIELD,
13921392

13931393
/// Indicates that the last execution results in a Lua runtime error. This may indicate usage of
@@ -1905,6 +1905,91 @@ pub const Lua = opaque {
19051905
return c.lua_yield(asState(lua), nresults);
19061906
}
19071907

1908+
/// Dumps the function on the top of the stack to a binary chunk. This function can be restored to the stack by
1909+
/// calling `load()` with the binary chunk written by this function. The restored function is equivalent to the one
1910+
/// dumped.
1911+
///
1912+
/// As it produces parts of the chunk, `dump()` calls functions on the given writer instance.
1913+
///
1914+
/// This function does not pop the Lua function from the stack.
1915+
///
1916+
/// From: `int lua_dump(lua_State *L, lua_Writer writer, void *data);`
1917+
/// Refer to: https://www.lua.org/manual/5.1/manual.html#lua_dump
1918+
/// Stack Behavior: `[-0, +0, m]`
1919+
pub fn dump(lua: *Lua, writer: std.io.AnyWriter) anyerror!void {
1920+
const DumpContext = struct {
1921+
writer: std.io.AnyWriter,
1922+
1923+
fn dumpAdapter(l: *Lua, bytes: ?*const anyopaque, size: usize, ud: ?*anyopaque) callconv(.c) i32 {
1924+
assert(bytes != null);
1925+
assert(ud != null);
1926+
1927+
_ = l;
1928+
1929+
const context: *@This() = @alignCast(@ptrCast(ud));
1930+
const slice: []const u8 = @as([*]const u8, @ptrCast(bytes))[0..size];
1931+
context.writer.writeAll(slice) catch |err| {
1932+
return @intCast(@intFromError(err));
1933+
};
1934+
return 0;
1935+
}
1936+
};
1937+
1938+
var context: DumpContext = .{
1939+
.writer = writer,
1940+
};
1941+
1942+
const res = c.lua_dump(asState(lua), @ptrCast(&DumpContext.dumpAdapter), &context);
1943+
1944+
return switch (res) {
1945+
0 => return,
1946+
else => |err| {
1947+
const error_value: std.meta.Int(.unsigned, @bitSizeOf(anyerror)) = @intCast(err);
1948+
return @errorFromInt(error_value);
1949+
},
1950+
};
1951+
}
1952+
1953+
/// Loads the function in the given chunk and pushes the valid function to the top of the stack. If there is an
1954+
/// error with the syntax of the function, or the data cannot be loaded, then an error is returned instead.
1955+
///
1956+
/// This function only loads a chunk; it does not run it. Automatically detects whether the chunk is text or binary.
1957+
///
1958+
/// From: `int lua_load(lua_State *L, lua_Reader reader, void *data, const char *chunkname);`
1959+
/// Refer to: https://www.lua.org/manual/5.1/manual.html#lua_load
1960+
/// Stack Behavior: `[-0, +1, -]`
1961+
pub fn load(lua: *Lua, reader: std.io.AnyReader, chunkname: ?[:0]const u8) Lua.Status {
1962+
const LoadContext = struct {
1963+
reader: std.io.AnyReader,
1964+
read_buffer: []u8,
1965+
1966+
fn loadAdapter(l: *Lua, ud: ?*anyopaque, size: *usize) callconv(.c) [*]const u8 {
1967+
assert(ud != null);
1968+
1969+
const context: *@This() = @alignCast(@ptrCast(ud.?));
1970+
const actual = context.reader.read(context.read_buffer) catch |err| {
1971+
_ = l.pushFString("Unable to load function, found error '%s' while reading.", .{@errorName(err).ptr});
1972+
l.raiseError();
1973+
};
1974+
size.* = actual;
1975+
return @ptrCast(context.read_buffer.ptr);
1976+
}
1977+
};
1978+
1979+
// TODO: The read buffer size should comptime constant and user controlled? Do users ever create functions that
1980+
// are more than a few kilobytes?
1981+
// TODO: Should the default be larger?
1982+
var read_buffer: [1024]u8 = undefined;
1983+
var context: LoadContext = .{
1984+
.reader = reader,
1985+
.read_buffer = read_buffer[0..],
1986+
};
1987+
1988+
const s = c.lua_load(asState(lua), @ptrCast(&LoadContext.loadAdapter), &context, if (chunkname) |p| p.ptr else null);
1989+
assert(Lua.Status.is_status(s));
1990+
return @enumFromInt(s);
1991+
}
1992+
19081993
/// Used by C functions to validate received arguments.
19091994
/// Checks whether the function argument `narg` is a number and returns this number cast to a `lua_Integer`.
19101995
///
@@ -3745,3 +3830,49 @@ test "ref and unref in registry" {
37453830
lua.pushValue(Lua.PseudoIndex.Registry);
37463831
try std.testing.expectEqual(length_before, lua.lengthOf(1));
37473832
}
3833+
3834+
test "Lua functions can be serialized and restored using dump() and load()" {
3835+
const lua = try Lua.init(std.testing.allocator);
3836+
defer lua.deinit();
3837+
3838+
var buf: [256]u8 = undefined;
3839+
var fbs_write = std.io.fixedBufferStream(&buf);
3840+
3841+
try lua.doString("return function(x) return x * 2 end");
3842+
try std.testing.expectEqual(1, lua.getTop()); // The stack should contain one value, a function.
3843+
try lua.dump(fbs_write.writer().any());
3844+
3845+
lua.pop(1);
3846+
try std.testing.expectEqual(0, lua.getTop()); // The stack should be empty, ensuring that the function is fully restored from the binary chunk.
3847+
3848+
var fbs_read = std.io.fixedBufferStream(fbs_write.getWritten());
3849+
const status = lua.load(fbs_read.reader().any(), null);
3850+
try std.testing.expectEqual(Lua.Status.ok, status); // The function should be loaded to the stack successfully.
3851+
3852+
lua.pushInteger(21);
3853+
try lua.protectedCall(1, 1, 0);
3854+
try std.testing.expectEqual(42, try lua.toIntegerStrict(-1)); // The function should be the "multiply by two" function from above.
3855+
}
3856+
3857+
test "dump() should report the same errors returned by the AnyWriter" {
3858+
const lua = try Lua.init(std.testing.allocator);
3859+
defer lua.deinit();
3860+
3861+
var buf: [16]u8 = undefined;
3862+
var fbs = std.io.fixedBufferStream(&buf);
3863+
3864+
try lua.doString("return function(x) return x * 2 end");
3865+
const actual = lua.dump(fbs.writer().any());
3866+
try std.testing.expectError(error.NoSpaceLeft, actual);
3867+
}
3868+
3869+
test "load() should report syntax errors when loading invalid binary chunk" {
3870+
const lua = try Lua.init(std.testing.allocator);
3871+
defer lua.deinit();
3872+
3873+
var buf: [3]u8 = .{ 0, 0, 0 };
3874+
var fbs = std.io.fixedBufferStream(&buf);
3875+
3876+
const actual = lua.load(fbs.reader().any(), null);
3877+
try std.testing.expectEqual(Lua.Status.syntax_error, actual);
3878+
}

0 commit comments

Comments
 (0)