From 57c0a8c95f0a64c3289a67849dd430d01b0b0d96 Mon Sep 17 00:00:00 2001 From: Janne Hellsten Date: Thu, 11 Jan 2024 00:03:12 +0200 Subject: [PATCH 1/2] Add support for tagged userdata and userdata destructors Luau doesn't support the usual metatable __gc method, instead userdatadtors should be used. There's more information available about these differences here: https://github.com/luau-lang/luau/issues/251#issuecomment-981817554 --- src/libluau.zig | 55 +++++++++++++++++++++++++++++++++++++++++ src/tests.zig | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/src/libluau.zig b/src/libluau.zig index 319ae3e..169129a 100644 --- a/src/libluau.zig +++ b/src/libluau.zig @@ -36,6 +36,9 @@ pub const AllocFn = *const fn (data: ?*anyopaque, ptr: ?*anyopaque, osize: usize /// See https://www.lua.org/manual/5.1/manual.html#lua_CFunction for the protocol pub const CFn = *const fn (state: ?*LuaState) callconv(.C) c_int; +/// Type for C userdata destructors +pub const CUserdataDtorFn = *const fn (userdata: *anyopaque) callconv(.C) void; + /// The internal Lua debug structure /// See https://www.lua.org/manual/5.1/manual.html#lua_Debug const Debug = c.lua_Debug; @@ -566,6 +569,41 @@ pub const Lua = struct { return @as([*]T, @ptrCast(@alignCast(ptr)))[0..size]; } + pub fn newUserdataTagged(lua: *Lua, comptime T: type, tag: i32) *T { + const UTAG_PROXY = c.LUA_UTAG_LIMIT + 1; // not exposed in headers + std.debug.assert((tag >= 0 and tag < c.LUA_UTAG_LIMIT) or tag == UTAG_PROXY); // Luau will do the same assert, this is easier to debug + // safe to .? because this function throws a Lua error on out of memory + // so the returned pointer should never be null + const ptr = c.lua_newuserdatatagged(lua.state, @sizeOf(T), tag).?; + return opaqueCast(T, ptr); + } + + /// This function allocates a new userdata of the given type with an associated + /// destructor callback. + /// + /// Returns a pointer to the Lua-owned data + /// + /// Note: Luau doesn't support the usual Lua __gc metatable destructor. Use this instead. + pub fn newUserdataDtor(lua: *Lua, comptime T: type, dtor_fn: CUserdataDtorFn) *T { + // safe to .? because this function throws a Lua error on out of memory + // so the returned pointer should never be null + const ptr = c.lua_newuserdatadtor(lua.state, @sizeOf(T), @ptrCast(dtor_fn)).?; + return opaqueCast(T, ptr); + } + + /// Set userdata tag at the given index + pub fn setUserdataTag(lua: *Lua, index: i32, tag: i32) void { + std.debug.assert((tag >= 0 and tag < c.LUA_UTAG_LIMIT)); // Luau will do the same assert, this is easier to debug + c.lua_setuserdatatag(lua.state, index, tag); + } + + /// Returns the tag of a userdata at the given index + pub fn userdataTag(lua: *Lua, index: i32) !i32 { + const tag = c.lua_userdatatag(lua.state, index); + if (tag == -1) return error.Fail; + return tag; + } + /// Pops a key from the stack, and pushes a key-value pair from the table at the given index. /// See https://www.lua.org/manual/5.1/manual.html#lua_next pub fn next(lua: *Lua, index: i32) bool { @@ -884,6 +922,11 @@ pub const Lua = struct { return error.Fail; } + pub fn toUserdataTagged(lua: *Lua, comptime T: type, index: i32, tag: i32) !*T { + if (c.lua_touserdatatagged(lua.state, index, tag)) |ptr| return opaqueCast(T, ptr); + return error.Fail; + } + /// Returns the `LuaType` of the value at the given index /// Note that this is equivalent to lua_type but because type is a Zig primitive it is renamed to `typeOf` /// See https://www.lua.org/manual/5.1/manual.html#lua_type @@ -1420,6 +1463,7 @@ pub const ZigFn = fn (lua: *Lua) i32; pub const ZigContFn = fn (lua: *Lua, status: Status, ctx: i32) i32; pub const ZigReaderFn = fn (lua: *Lua, data: *anyopaque) ?[]const u8; pub const ZigWriterFn = fn (lua: *Lua, buf: []const u8, data: *anyopaque) bool; +pub const ZigUserdataDtorFn = fn (data: *anyopaque) void; fn TypeOfWrap(comptime T: type) type { return switch (T) { @@ -1427,6 +1471,7 @@ fn TypeOfWrap(comptime T: type) type { ZigFn => CFn, ZigReaderFn => CReaderFn, ZigWriterFn => CWriterFn, + ZigUserdataDtorFn => CUserdataDtorFn, else => @compileError("unsupported type given to wrap: '" ++ @typeName(T) ++ "'"), }; } @@ -1441,6 +1486,7 @@ pub fn wrap(comptime value: anytype) TypeOfWrap(@TypeOf(value)) { ZigFn => wrapZigFn(value), ZigReaderFn => wrapZigReaderFn(value), ZigWriterFn => wrapZigWriterFn(value), + ZigUserdataDtorFn => wrapZigUserdataDtorFn(value), else => @compileError("unsupported type given to wrap: '" ++ @typeName(T) ++ "'"), }; } @@ -1456,6 +1502,15 @@ fn wrapZigFn(comptime f: ZigFn) CFn { }.inner; } +/// Wrap a ZigFn in a CFn for passing to the API +fn wrapZigUserdataDtorFn(comptime f: ZigUserdataDtorFn) CUserdataDtorFn { + return struct { + fn inner(userdata: *anyopaque) callconv(.C) void { + return @call(.always_inline, f, .{userdata}); + } + }.inner; +} + /// Wrap a ZigReaderFn in a CReaderFn for passing to the API fn wrapZigReaderFn(comptime f: ZigReaderFn) CReaderFn { return struct { diff --git a/src/tests.zig b/src/tests.zig index 8d2769d..8a3e776 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -2184,3 +2184,68 @@ test "compile and run bytecode" { // produced bytecode in text format, but the API doesn't support it. try testing.expect(bc1.len < bc2.len); } + +test "userdata dtor" { + if (ziglua.lang != .luau) return; + var gc_hits: i32 = 0; + + const Data = struct { + gc_hits_ptr: *i32, + + pub fn dtor(udata: *anyopaque) void { + const self: *@This() = @alignCast(@ptrCast(udata)); + self.gc_hits_ptr.* = self.gc_hits_ptr.* + 1; + } + }; + + // create a Lua-owned pointer to a Data, configure Data with a destructor. + { + var lua = try Lua.init(testing.allocator); + defer lua.deinit(); // forces dtors to be called at the latest + + var data = lua.newUserdataDtor(Data, ziglua.wrap(Data.dtor)); + data.gc_hits_ptr = &gc_hits; + try expectEqual(@as(*const anyopaque, @ptrCast(data)), try lua.toPointer(1)); + try testing.expectEqual(@as(i32, 0), gc_hits); + lua.pop(1); // don't let the stack hold a ref to the user data + lua.gcCollect(); + try testing.expectEqual(@as(i32, 1), gc_hits); + lua.gcCollect(); + try testing.expectEqual(@as(i32, 1), gc_hits); + } +} + +test "tagged userdata" { + if (ziglua.lang != .luau) return; + + var lua = try Lua.init(testing.allocator); + defer lua.deinit(); // forces dtors to be called at the latest + + const Data = struct { + val: i32, + }; + + // create a Lua-owned tagged pointer + var data = lua.newUserdataTagged(Data, 13); + data.val = 1; + + const data2 = try lua.toUserdataTagged(Data, -1, 13); + try testing.expectEqual(data.val, data2.val); + + var tag = try lua.userdataTag(-1); + try testing.expectEqual(@as(i32, 13), tag); + + lua.setUserdataTag(-1, 100); + tag = try lua.userdataTag(-1); + try testing.expectEqual(@as(i32, 100), tag); + + // Test that tag mismatch error handling works. Userdata is not tagged with 123. + try expectError(error.Fail, lua.toUserdataTagged(Data, -1, 123)); + + // should not fail + _ = try lua.toUserdataTagged(Data, -1, 100); + + // Integer is not userdata, so userdataTag should fail. + lua.pushInteger(13); + try expectError(error.Fail, lua.userdataTag(-1)); +} From 14a00f24bff6caf98105d8ff8d63a13e899cb6f1 Mon Sep 17 00:00:00 2001 From: Janne Hellsten Date: Fri, 12 Jan 2024 19:55:16 +0200 Subject: [PATCH 2/2] Cleanup unnecessary use of @as(), drop testing. prefix from expectEqual --- src/tests.zig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/tests.zig b/src/tests.zig index 8a3e776..ad02123 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -2168,7 +2168,7 @@ test "compile and run bytecode" { try lua.loadBytecode("...", bc); try lua.protectedCall(0, 1, 0); const v = try lua.toInteger(-1); - try testing.expectEqual(@as(i32, 133), v); + try expectEqual(133, v); // Try mutable globals. Calls to mutable globals should produce longer bytecode. const src2 = "Foo.print()\nBar.print()"; @@ -2182,7 +2182,7 @@ test "compile and run bytecode" { defer testing.allocator.free(bc2); // A really crude check for changed bytecode. Better would be to match // produced bytecode in text format, but the API doesn't support it. - try testing.expect(bc1.len < bc2.len); + try expect(bc1.len < bc2.len); } test "userdata dtor" { @@ -2205,13 +2205,13 @@ test "userdata dtor" { var data = lua.newUserdataDtor(Data, ziglua.wrap(Data.dtor)); data.gc_hits_ptr = &gc_hits; - try expectEqual(@as(*const anyopaque, @ptrCast(data)), try lua.toPointer(1)); - try testing.expectEqual(@as(i32, 0), gc_hits); + try expectEqual(@as(*anyopaque, @ptrCast(data)), try lua.toPointer(1)); + try expectEqual(0, gc_hits); lua.pop(1); // don't let the stack hold a ref to the user data lua.gcCollect(); - try testing.expectEqual(@as(i32, 1), gc_hits); + try expectEqual(1, gc_hits); lua.gcCollect(); - try testing.expectEqual(@as(i32, 1), gc_hits); + try expectEqual(1, gc_hits); } } @@ -2233,11 +2233,11 @@ test "tagged userdata" { try testing.expectEqual(data.val, data2.val); var tag = try lua.userdataTag(-1); - try testing.expectEqual(@as(i32, 13), tag); + try testing.expectEqual(13, tag); lua.setUserdataTag(-1, 100); tag = try lua.userdataTag(-1); - try testing.expectEqual(@as(i32, 100), tag); + try testing.expectEqual(100, tag); // Test that tag mismatch error handling works. Userdata is not tagged with 123. try expectError(error.Fail, lua.toUserdataTagged(Data, -1, 123));