diff --git a/src/libluau.zig b/src/libluau.zig index 9f04375..5c1c400 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 @@ -1414,6 +1457,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) { @@ -1421,6 +1465,7 @@ fn TypeOfWrap(comptime T: type) type { ZigFn => CFn, ZigReaderFn => CReaderFn, ZigWriterFn => CWriterFn, + ZigUserdataDtorFn => CUserdataDtorFn, else => @compileError("unsupported type given to wrap: '" ++ @typeName(T) ++ "'"), }; } @@ -1435,6 +1480,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) ++ "'"), }; } @@ -1450,6 +1496,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 414976c..de283b2 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -2152,3 +2152,68 @@ test "getstack" { \\g() ); } + +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)); +}