Skip to content

Commit a3918d1

Browse files
committed
Rework upvalues to use Upvalues(T) wrapper type
Replaces the previous closure API with a unified approach where functions declare upvalues using the Upvalues(T) wrapper as the first parameter. Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
1 parent c69428c commit a3918d1

File tree

7 files changed

+135
-84
lines changed

7 files changed

+135
-84
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file.
1212
- Handles both stack buffer (small strings) and dynamic allocation (large strings)
1313
- Variadic arguments support with `Varargs` iterator for functions accepting variable number of arguments
1414
- `Varargs.raiseError()` method for throwing descriptive type validation errors
15-
- Closure support with `Table.setClosure()` for Lua-style upvalues
15+
- **BREAKING**: `setClosure` Lua closures must use `Upvalues(T)` as first parameter
1616
- Canonical Zig iterator pattern for table iteration with `Table.iterator()`
1717
- Metatable management APIs:
1818
- `Lua.createMetaTable()` for flexible metatable creation without global registration

CLAUDE.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,22 +101,29 @@ The library supports custom memory allocators through `Lua.init()`:
101101
- All Lua memory operations (allocation, reallocation, freeing) go through the custom allocator
102102

103103
### Closure Upvalues (`setClosure`)
104-
The `setClosure` method accepts upvalues that can be either a single value or a tuple:
105-
- **Single value**: Pass directly without wrapping: `setClosure("func", value, fn)`
106-
- **Multiple values**: Pass as tuple: `setClosure("func", .{val1, val2}, fn)`
107-
- **No upvalues**: Pass empty tuple: `setClosure("func", .{}, fn)`
104+
The `setClosure` method creates Lua C closures using the `Upvalues(T)` wrapper type:
105+
- Functions must use `Upvalues(T)` as their first parameter
106+
- **Single upvalue**: `setClosure("func", value, fn)` where fn takes `Upvalues(T)`
107+
- **Multiple upvalues**: Use tuple `setClosure("func", .{val1, val2}, fn)` where fn takes `Upvalues(struct{T1, T2})`
108108

109109
Examples:
110110
```zig
111-
// Single upvalue - pass directly
112-
try table.setClosure("getValue", &state, getValueFn);
113-
try table.setClosure("addFive", 5, addFn);
111+
// Single upvalue
112+
fn getValue(upv: Upvalues(*State), key: []const u8) i32 {
113+
return upv.value.getGlobal(key);
114+
}
115+
try table.setClosure("getValue", &state, getValue);
114116
115-
// Multiple upvalues - use tuple
116-
try table.setClosure("transform", .{ 2.0, 10.0 }, transformFn);
117+
fn addFive(upv: Upvalues(i32), x: i32) i32 {
118+
return x + upv.value;
119+
}
120+
try table.setClosure("addFive", 5, addFive);
117121
118-
// No upvalues
119-
try table.setClosure("helper", .{}, helperFn);
122+
// Multiple upvalues - use tuple
123+
fn transform(upv: Upvalues(struct { f32, f32 }), x: f32) f32 {
124+
return x * upv.value[0] + upv.value[1];
125+
}
126+
try table.setClosure("transform", .{ 2.0, 10.0 }, transform);
120127
```
121128

122129
## Development Patterns

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,11 @@ pub fn main() !void {
146146
const table = lua.createTable(.{});
147147
defer table.deinit();
148148
149-
fn getGlobal(lua_ptr: *Lua, key: []const u8) !i32 {
150-
return try lua_ptr.globals().get(key, i32) orelse 0;
149+
fn getGlobal(upv: Lua.Upvalues(*Lua), key: []const u8) !i32 {
150+
return try upv.value.globals().get(key, i32) orelse 0;
151151
}
152152
const lua_ptr = @constCast(&lua);
153-
try table.setClosure("getGlobal", .lua_ptr, getGlobal);
153+
try table.setClosure("getGlobal", lua_ptr, getGlobal);
154154
try lua.globals().set("funcs", table);
155155
try lua.globals().set("myValue", @as(i32, 123));
156156
@@ -236,9 +236,9 @@ pub fn main() !void {
236236
Efficient string building using Luau's StrBuf API with automatic memory management.
237237

238238
```zig
239-
fn buildGreeting(lua: *Lua, name: []const u8, age: i32) !Lua.StrBuf {
239+
fn buildGreeting(upv: Lua.Upvalues(*Lua), name: []const u8, age: i32) !Lua.StrBuf {
240240
var buf: Lua.StrBuf = undefined;
241-
buf.init(lua);
241+
buf.init(upv.value);
242242
buf.addString("Hello, ");
243243
buf.addLString(name);
244244
buf.addString("! You are ");

examples/guided_tour.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,9 +632,9 @@ pub fn main() !void {
632632

633633
// Return StrBuf from Zig functions
634634
const formatMessage = struct {
635-
fn call(l: *luaz.Lua, name: []const u8, age: i32) !luaz.Lua.StrBuf {
635+
fn call(upv: luaz.Lua.Upvalues(*luaz.Lua), name: []const u8, age: i32) !luaz.Lua.StrBuf {
636636
var b: luaz.Lua.StrBuf = undefined;
637-
b.init(l);
637+
b.init(upv.value);
638638
b.addLString(name);
639639
b.addString(" is ");
640640
try b.add(age);

src/lua.zig

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -393,35 +393,59 @@ pub const Lua = struct {
393393
self.state().pop(1); // Pop table
394394
}
395395

396-
/// Sets a function in the table as a closure with upvalues.
396+
/// Sets a function with upvalues in the table as a Lua C closure.
397397
///
398-
/// Similar to `set(key, func)` but allows creating Lua closures with captured values.
398+
/// Similar to `set(key, func)` but allows creating closures with captured values.
399399
/// Upvalues are values that are captured and accessible to the function.
400-
/// The function must accept the upvalues as its first parameter.
400+
/// The function must accept upvalues as its first parameter using the `Upvalues(T)` wrapper type.
401401
///
402402
/// WARNING: When using light user data pointers as upvalues, the user is responsible
403403
/// for ensuring the pointer remains valid for the lifetime of the closure.
404404
///
405405
/// Parameters:
406406
/// - `key`: The table key where the closure will be stored
407407
/// - `upvalues`: Values to be captured as upvalues (single value or tuple)
408-
/// - `func`: A Zig function that accepts upvalues as its first parameter
408+
/// - `func`: A Zig function with `Upvalues(T)` as its first parameter
409409
///
410410
/// Examples:
411411
/// ```zig
412-
/// // Single upvalue
413-
/// fn add(increment: i32, x: i32) i32 { return x + increment; }
414-
/// try table.setClosure("add5", .{5}, add);
412+
/// fn add(upv: Upvalues(i32), x: i32) i32 {
413+
/// return upv.value + x;
414+
/// }
415+
/// try table.setClosure("add5", 5, add);
415416
///
416-
/// // Multiple upvalues (tuple)
417-
/// fn scaledAdd(upvals: struct { f32, f32 }, x: f32) f32 {
418-
/// return x * upvals[0] + upvals[1];
417+
/// fn transform(upv: Upvalues(struct { scale: f32, offset: f32 }), x: f32) f32 {
418+
/// return x * upv.value.scale + upv.value.offset;
419419
/// }
420-
/// try table.setClosure("transform", .{ 2.0, 10.0 }, scaledAdd);
420+
/// try table.setClosure("scale2add10", .{ .scale = 2.0, .offset = 10.0 }, transform);
421421
/// ```
422422
///
423423
/// Errors: `Error.OutOfMemory` if stack allocation fails
424424
pub fn setClosure(self: Table, key: anytype, upvalues: anytype, func: anytype) !void {
425+
const FuncType = @TypeOf(func);
426+
const func_info = @typeInfo(FuncType);
427+
428+
if (func_info != .@"fn") {
429+
@compileError("Third parameter must be a function");
430+
}
431+
432+
const arg_tuple = std.meta.ArgsTuple(FuncType);
433+
const arg_fields = std.meta.fields(arg_tuple);
434+
435+
if (arg_fields.len == 0) {
436+
@compileError("Function must have at least one parameter (Upvalues)");
437+
}
438+
439+
const FirstParamType = arg_fields[0].type;
440+
const first_param_info = @typeInfo(FirstParamType);
441+
442+
if (first_param_info != .@"struct" or
443+
!@hasDecl(FirstParamType, "is_upvalues") or
444+
!FirstParamType.is_upvalues)
445+
{
446+
@compileError("First parameter of the function must be an Upvalues type");
447+
}
448+
425449
const upvalues_info = @typeInfo(@TypeOf(upvalues));
426450
const upvalue_count = if (upvalues_info == .@"struct" and upvalues_info.@"struct".is_tuple)
427451
upvalues_info.@"struct".fields.len
@@ -443,12 +467,7 @@ pub const Lua = struct {
443467
}
444468

445469
// Create the closure with upvalues
446-
const FuncType = @TypeOf(func);
447-
const arg_fields = std.meta.fields(std.meta.ArgsTuple(FuncType));
448-
if (arg_fields.len == 0) {
449-
@compileError("Closure function must have at least one parameter for upvalues");
450-
}
451-
const trampoline: State.CFunction = stack.createFunc(self.ref.lua, func, arg_fields[0].type);
470+
const trampoline: State.CFunction = stack.createFunc(self.ref.lua, func);
452471
self.state().pushCClosureK(trampoline, @typeName(@TypeOf(func)), @intCast(upvalue_count), null);
453472

454473
self.state().setTable(-3); // Set table[key] = closure and pop key and value
@@ -1071,6 +1090,25 @@ pub const Lua = struct {
10711090
}
10721091
};
10731092

1093+
/// Generic wrapper for upvalues passed to Lua C closure functions.
1094+
///
1095+
/// This type enables functions to receive upvalues in a type-safe manner when registered
1096+
/// as Lua C closures with `table.setClosure()`. The upvalues are automatically injected
1097+
/// when the function is called from Lua.
1098+
///
1099+
/// `Upvalues(T)` must be used as the first parameter of the function.
1100+
///
1101+
/// See `Table.setClosure()` documentation for usage examples.
1102+
pub fn Upvalues(comptime T: type) type {
1103+
return struct {
1104+
value: T,
1105+
1106+
/// Marker field to distinguish from regular types
1107+
pub const is_upvalues = true;
1108+
pub const UpvalueType = T;
1109+
};
1110+
}
1111+
10741112
/// Variadic arguments iterator for functions accepting variable number of arguments from Lua.
10751113
///
10761114
/// This type enables Zig functions to accept any number of arguments from Lua

src/stack.zig

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ pub fn push(lua: Lua, value: anytype) void {
285285
return;
286286
}
287287

288-
const trampoline = createFunc(lua, value, null);
288+
const trampoline = createFunc(lua, value);
289289
lua.state.pushCFunction(trampoline, @typeName(T));
290290
},
291291
else => {
@@ -327,25 +327,21 @@ pub fn pushResult(lua: Lua, result: anytype) c_int {
327327
}
328328

329329
/// Creates a Lua C function from a Zig function.
330-
/// If ClosureType is provided, creates a closure requiring `pushCClosure`.
331-
pub fn createFunc(_: Lua, value: anytype, comptime ClosureType: ?type) State.CFunction {
330+
/// Automatically detects Upvalues type in first parameter for closure functions.
331+
pub fn createFunc(_: Lua, value: anytype) State.CFunction {
332332
const T = @TypeOf(value);
333333
const arg_tuple = std.meta.ArgsTuple(T);
334334
const arg_fields = std.meta.fields(arg_tuple);
335335

336-
// Detect if first parameter should be treated as upvalues
337-
const has_upvalues = ClosureType != null;
338-
339-
// Compile-time validation for closures
340-
if (has_upvalues) {
341-
if (arg_fields.len == 0) {
342-
@compileError("Closure function must have at least one parameter for upvalues");
343-
}
344-
if (arg_fields[0].type != ClosureType.?) {
345-
@compileError("First parameter type (" ++ @typeName(arg_fields[0].type) ++
346-
") must match ClosureType (" ++ @typeName(ClosureType.?) ++ ")");
336+
// Detect if first parameter is an Upvalues type
337+
const first_param_is_upvalues = if (arg_fields.len > 0) blk: {
338+
const FirstParamType = arg_fields[0].type;
339+
const type_info = @typeInfo(FirstParamType);
340+
if (type_info == .@"struct" and @hasDecl(FirstParamType, "is_upvalues")) {
341+
break :blk FirstParamType.is_upvalues;
347342
}
348-
}
343+
break :blk false;
344+
} else false;
349345

350346
// Validate Varargs is only used as the last parameter
351347
inline for (arg_fields, 0..) |field, i| {
@@ -365,24 +361,28 @@ pub fn createFunc(_: Lua, value: anytype, comptime ClosureType: ?type) State.CFu
365361
var args: arg_tuple = undefined;
366362

367363
// Handle first parameter - upvalues or regular argument
368-
const arg_start_idx = if (has_upvalues) blk: {
369-
// First parameter is upvalues - populate from upvalue indices
370-
const CT = ClosureType.?;
371-
const ct_info = @typeInfo(CT);
364+
const arg_start_idx = if (first_param_is_upvalues) blk: {
365+
// First parameter is Upvalues type - populate from upvalue indices
366+
const FirstParamType = arg_fields[0].type;
367+
const UpvalueType = FirstParamType.UpvalueType;
368+
const upvalue_info = @typeInfo(UpvalueType);
372369

373-
if (ct_info == .@"struct" and ct_info.@"struct".is_tuple) {
370+
if (upvalue_info == .@"struct" and upvalue_info.@"struct".is_tuple) {
374371
// Multiple upvalues as tuple
375-
const upvalues_fields = std.meta.fields(CT);
376-
inline for (upvalues_fields, 0..) |field, i| {
372+
var upvalue_tuple: UpvalueType = undefined;
373+
const upvalue_fields = std.meta.fields(UpvalueType);
374+
inline for (upvalue_fields, 0..) |field, i| {
377375
const idx = State.upvalueIndex(@intCast(i + 1));
378-
args[0][i] = toValue(l, field.type, idx) orelse
376+
upvalue_tuple[i] = toValue(l, field.type, idx) orelse
379377
l.state.typeError(idx, @typeName(field.type));
380378
}
379+
args[0] = FirstParamType{ .value = upvalue_tuple };
381380
} else {
382381
// Single upvalue
383382
const idx = State.upvalueIndex(1);
384-
args[0] = toValue(l, CT, idx) orelse
385-
l.state.typeError(idx, @typeName(CT));
383+
const upvalue = toValue(l, UpvalueType, idx) orelse
384+
l.state.typeError(idx, @typeName(UpvalueType));
385+
args[0] = FirstParamType{ .value = upvalue };
386386
}
387387
break :blk 1; // Start stack args from index 1, skip upvalue parameter
388388
} else blk: {
@@ -984,7 +984,8 @@ test "StrBuf pointer fixup on value copy" {
984984
// need to be fixed to point to the new buffer location.
985985

986986
const TestFunction = struct {
987-
fn buildMessage(l: *Lua, name: []const u8, age: i32) !Lua.StrBuf {
987+
fn buildMessage(upv: Lua.Upvalues(*Lua), name: []const u8, age: i32) !Lua.StrBuf {
988+
const l = upv.value;
988989
var buf: Lua.StrBuf = undefined;
989990
buf.init(l);
990991
buf.addLString(name);

src/tests.zig

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,45 +1415,47 @@ test "function clone" {
14151415
try expect(result2 == 30);
14161416
}
14171417

1418-
fn closureAdd5(n: i32, x: i32) i32 {
1419-
return x + n;
1418+
fn closureAdd5(upv: Lua.Upvalues(i32), x: i32) i32 {
1419+
return x + upv.value;
14201420
}
14211421

1422-
fn closureTransform(upv: struct { f32, f32 }, x: f32) f32 {
1423-
return x * upv[0] + upv[1];
1422+
fn closureTransform(upv: Lua.Upvalues(struct { f32, f32 }), x: f32) f32 {
1423+
return x * upv.value[0] + upv.value[1];
14241424
}
14251425

1426-
fn closureOptAdd(thresh: i32, x: i32, y: ?i32) i32 {
1426+
fn closureOptAdd(upv: Lua.Upvalues(i32), x: i32, y: ?i32) i32 {
1427+
const thresh = upv.value;
14271428
return if (x > thresh) x + (y orelse 0) else x;
14281429
}
14291430

1430-
fn closureMultiply(cfg: Lua.Table, x: i32) !i32 {
1431+
fn closureMultiply(upv: Lua.Upvalues(Lua.Table), x: i32) !i32 {
1432+
const cfg = upv.value;
14311433
const m = try cfg.get("mult", i32) orelse 1;
14321434
return x * m;
14331435
}
14341436

1435-
fn closureConstant(val: i32) i32 {
1436-
return val;
1437+
fn closureConstant(upv: Lua.Upvalues(i32)) i32 {
1438+
return upv.value;
14371439
}
14381440

1439-
fn closureSumAll(base: i32, a: ?i32, b: ?i32) i32 {
1440-
return base + (a orelse 0) + (b orelse 0);
1441+
fn closureSumAll(upv: Lua.Upvalues(i32), a: ?i32, b: ?i32) i32 {
1442+
return upv.value + (a orelse 0) + (b orelse 0);
14411443
}
14421444

1443-
fn closureSingle(increment: i32, x: i32) i32 {
1444-
return x + increment;
1445+
fn closureSingle(upv: Lua.Upvalues(i32), x: i32) i32 {
1446+
return x + upv.value;
14451447
}
14461448

14471449
const ClosureCounter = struct {
14481450
count: i32,
14491451

1450-
fn increment(self: *@This(), amount: i32) i32 {
1451-
self.count += amount;
1452-
return self.count;
1452+
fn increment(upv: Lua.Upvalues(*ClosureCounter), amount: i32) i32 {
1453+
upv.value.count += amount;
1454+
return upv.value.count;
14531455
}
14541456

1455-
fn getValue(self: *const @This()) i32 {
1456-
return self.count;
1457+
fn getValue(upv: Lua.Upvalues(*ClosureCounter)) i32 {
1458+
return upv.value.count;
14571459
}
14581460
};
14591461

@@ -1551,8 +1553,8 @@ test "metatable with closure function and table attachment" {
15511553
// Step 2: Set function with upvalue (i32 = 4) and one parameter
15521554
// The function returns sum of upvalue + passed parameter
15531555
const AddFunc = struct {
1554-
fn add(upvalue: i32, param: i32) i32 {
1555-
return upvalue + param;
1556+
fn add(upv: Lua.Upvalues(i32), param: i32) i32 {
1557+
return upv.value + param;
15561558
}
15571559
};
15581560

@@ -1826,7 +1828,8 @@ test "StrBuf integration with Table operations" {
18261828
}
18271829

18281830
// Define a Zig function that builds and returns StrBuf by pointer
1829-
fn makeMsg(l: *Lua, name: []const u8, value: i32) !Lua.StrBuf {
1831+
fn makeMsg(upv: Lua.Upvalues(*Lua), name: []const u8, value: i32) !Lua.StrBuf {
1832+
const l = upv.value;
18301833
var buf: Lua.StrBuf = undefined;
18311834
buf.init(l);
18321835
buf.addString("Hello ");
@@ -1855,7 +1858,8 @@ test "StrBuf returned from Zig functions" {
18551858
}
18561859

18571860
// Test function that returns StrBuf as part of a tuple
1858-
fn makeMsgTuple(l: *Lua, name: []const u8, value: i32) !struct { Lua.StrBuf, i32 } {
1861+
fn makeMsgTuple(upv: Lua.Upvalues(*Lua), name: []const u8, value: i32) !struct { Lua.StrBuf, i32 } {
1862+
const l = upv.value;
18591863
var buf: Lua.StrBuf = undefined;
18601864
buf.init(l);
18611865
buf.addString("Tuple: ");
@@ -1886,7 +1890,8 @@ test "StrBuf returned in tuple from Zig functions" {
18861890
}
18871891

18881892
// Test function that builds a large StrBuf to force dynamic allocation
1889-
fn makeLargeMsg(l: *Lua, count: i32) !Lua.StrBuf {
1893+
fn makeLargeMsg(upv: Lua.Upvalues(*Lua), count: i32) !Lua.StrBuf {
1894+
const l = upv.value;
18901895
var buf: Lua.StrBuf = undefined;
18911896
buf.init(l);
18921897

0 commit comments

Comments
 (0)