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 authored and natecraddock committed Jan 12, 2024
1 parent 3337d7e commit a147cbc
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 0 deletions.
55 changes: 55 additions & 0 deletions src/libluau.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1420,13 +1463,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 @@ -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) ++ "'"),
};
}
Expand All @@ -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 {
Expand Down
65 changes: 65 additions & 0 deletions src/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

0 comments on commit a147cbc

Please sign in to comment.