Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for tagged userdata and userdata destructors #45

Merged
merged 2 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
69 changes: 67 additions & 2 deletions src/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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()";
Expand All @@ -2182,5 +2182,70 @@ 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" {
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(*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 expectEqual(1, gc_hits);
lua.gcCollect();
try expectEqual(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(13, tag);

lua.setUserdataTag(-1, 100);
tag = try lua.userdataTag(-1);
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));

// 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));
}