Skip to content

Commit

Permalink
Add support for tagged userdata and userdata destructors
Browse files Browse the repository at this point in the history
Luau doesn't support the usual metatable __gc method, instead
userdatadtors should be used.  There's more information
available about these differences here:

luau-lang/luau#251 (comment)
  • Loading branch information
nurpax committed Jan 10, 2024
1 parent b71a42e commit f7ae737
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 0 deletions.
48 changes: 48 additions & 0 deletions src/zigluau/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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;
Expand Down Expand Up @@ -560,6 +563,34 @@ pub const Lua = struct {
return @as([*]T, @ptrCast(@alignCast(ptr)))[0..size];
}

pub fn newUserdataTagged(lua: *Lua, comptime T: type, tag: i32) *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_newuserdatatagged(lua.state, @sizeOf(T), tag).?;
return opaqueCast(T, ptr);
}

/// This function allocates a new userdata of the given type.
/// 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 {
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 {
return c.lua_userdatatag(lua.state, index);
}

/// 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 {
Expand Down Expand Up @@ -878,6 +909,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
Expand Down Expand Up @@ -1396,13 +1432,15 @@ 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) {
LuaState => Lua,
ZigFn => CFn,
ZigReaderFn => CReaderFn,
ZigWriterFn => CWriterFn,
ZigUserdataDtorFn => CUserdataDtorFn,
else => @compileError("unsupported type given to wrap: '" ++ @typeName(T) ++ "'"),
};
}
Expand All @@ -1417,6 +1455,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) ++ "'"),
};
}
Expand All @@ -1432,6 +1471,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 {
Expand Down
29 changes: 29 additions & 0 deletions src/zigluau/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1093,3 +1093,32 @@ test {
testing.refAllDecls(Lua);
testing.refAllDecls(Buffer);
}

test "userdata dtor" {
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);
}
}

0 comments on commit f7ae737

Please sign in to comment.