Skip to content

Commit 6150f0f

Browse files
authored
Parsing memory fixes (#63)
* updated memory management system to use toAny and toAnyAlloc * performance optimization * small change to see if tests pass now * fixed bug * added stress test * added stress test and fixed bugs causing lua stack overflows * bug fix * removed std.debug.panic
1 parent effbedc commit 6150f0f

File tree

2 files changed

+174
-28
lines changed

2 files changed

+174
-28
lines changed

src/lib.zig

Lines changed: 115 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,21 @@ pub const CWarnFn = switch (lang) {
571571
/// The type of the writer function used by `Lua.dump()`
572572
pub const CWriterFn = *const fn (state: ?*LuaState, buf: ?*const anyopaque, size: usize, data: ?*anyopaque) callconv(.C) c_int;
573573

574+
/// For bundling a parsed value with an arena allocator
575+
/// Copied from std.json.Parsed
576+
pub fn Parsed(comptime T: type) type {
577+
return struct {
578+
arena: *std.heap.ArenaAllocator,
579+
value: T,
580+
581+
pub fn deinit(self: @This()) void {
582+
const allocator = self.arena.child_allocator;
583+
self.arena.deinit();
584+
allocator.destroy(self.arena);
585+
}
586+
};
587+
}
588+
574589
/// A Zig wrapper around the Lua C API
575590
/// Represents a Lua state or thread and contains the entire state of the Lua interpreter
576591
pub const Lua = struct {
@@ -1658,8 +1673,7 @@ pub const Lua = struct {
16581673
}
16591674
}
16601675

1661-
/// Similar to `Lua.setTable()` but does a raw asskdjfal;sdkfjals;dkfj;dk:q
1662-
/// gnment (without metamethods)
1676+
/// Similar to `Lua.setTable()` but does a raw assignment (without metamethods)
16631677
/// See https://www.lua.org/manual/5.4/manual.html#lua_rawset
16641678
pub fn rawSetTable(lua: *Lua, index: i32) void {
16651679
c.lua_rawset(lua.state, index);
@@ -3145,7 +3159,42 @@ pub const Lua = struct {
31453159

31463160
/// Converts the specified index of the lua stack to the specified
31473161
/// type if possible and returns it
3148-
pub fn toAny(lua: *Lua, comptime T: type, index: i32) !T {
3162+
/// Allocates memory if necessary
3163+
pub fn toAnyAlloc(lua: *Lua, comptime T: type, index: i32) !Parsed(T) {
3164+
var parsed = Parsed(T){
3165+
.arena = try lua.allocator().create(std.heap.ArenaAllocator),
3166+
.value = undefined,
3167+
};
3168+
errdefer lua.allocator().destroy(parsed.arena);
3169+
parsed.arena.* = std.heap.ArenaAllocator.init(lua.allocator());
3170+
errdefer parsed.arena.deinit();
3171+
3172+
parsed.value = try lua.toAnyInternal(T, parsed.arena.allocator(), true, index);
3173+
3174+
return parsed;
3175+
}
3176+
3177+
/// Converts the specified index of the lua stack to the specified
3178+
/// type if possible and returns it
3179+
/// Does not allocate any memory, if memory allocation is needed (such as for parsing slices)
3180+
/// use toAnyAlloc
3181+
pub inline fn toAny(lua: *Lua, comptime T: type, index: i32) !T {
3182+
return lua.toAnyInternal(T, null, false, index);
3183+
}
3184+
3185+
/// Converts the specified index of the lua stack to the specified
3186+
/// type if possible and returns it
3187+
/// optional allocator
3188+
fn toAnyInternal(lua: *Lua, comptime T: type, a: ?std.mem.Allocator, comptime allow_alloc: bool, index: i32) !T {
3189+
const stack_size_on_entry = lua.getTop();
3190+
defer {
3191+
if (lua.getTop() != stack_size_on_entry) {
3192+
std.debug.print("Type that filed to parse was: {any}\n", .{T});
3193+
std.debug.print("Expected stack size: {}, Actual Stack Size: {}\n\n", .{ stack_size_on_entry, lua.getTop() });
3194+
@panic("internal parsing error");
3195+
}
3196+
}
3197+
31493198
switch (@typeInfo(T)) {
31503199
.Int => {
31513200
switch (comptime lang) {
@@ -3183,7 +3232,10 @@ pub const Lua = struct {
31833232
}
31843233
} else switch (info.size) {
31853234
.Slice, .Many => {
3186-
return try lua.toSlice(info.child, index);
3235+
if (!allow_alloc) {
3236+
@compileError("toAny cannot allocate memory, try using toAnyAlloc");
3237+
}
3238+
return try lua.toSlice(info.child, a.?, index);
31873239
},
31883240
else => {
31893241
return try lua.toUserdata(info.child, index);
@@ -3194,7 +3246,7 @@ pub const Lua = struct {
31943246
return lua.toBoolean(index);
31953247
},
31963248
.Enum => |info| {
3197-
const string = try lua.toAny([]const u8, index);
3249+
const string = try lua.toAnyInternal([]const u8, a, allow_alloc, index);
31983250
inline for (info.fields) |enum_member| {
31993251
if (std.mem.eql(u8, string, enum_member.name)) {
32003252
return @field(T, enum_member.name);
@@ -3203,14 +3255,13 @@ pub const Lua = struct {
32033255
return error.InvalidEnumTagName;
32043256
},
32053257
.Struct => {
3206-
return try lua.toStruct(T, index);
3258+
return try lua.toStruct(T, a, allow_alloc, index);
32073259
},
32083260
.Optional => {
32093261
if (lua.isNil(index)) {
3210-
lua.pop(1);
32113262
return null;
32123263
} else {
3213-
return try lua.toAny(@typeInfo(T).Optional.child, index);
3264+
return try lua.toAnyInternal(@typeInfo(T).Optional.child, a, allow_alloc, index);
32143265
}
32153266
},
32163267
else => {
@@ -3220,27 +3271,31 @@ pub const Lua = struct {
32203271
}
32213272

32223273
/// Converts a lua array to a zig slice, memory is owned by the caller
3223-
fn toSlice(lua: *Lua, comptime ChildType: type, raw_index: i32) ![]ChildType {
3274+
fn toSlice(lua: *Lua, comptime ChildType: type, a: std.mem.Allocator, raw_index: i32) ![]ChildType {
32243275
const index = lua.absIndex(raw_index);
32253276

32263277
if (!lua.isTable(index)) {
32273278
return error.ValueNotATable;
32283279
}
32293280

32303281
const size = lua.rawLen(index);
3231-
var result = try lua.allocator().alloc(ChildType, size);
3282+
var result = try a.alloc(ChildType, size);
32323283

32333284
for (1..size + 1) |i| {
32343285
_ = try lua.pushAny(i);
32353286
_ = lua.getTable(index);
3236-
result[i - 1] = try lua.toAny(ChildType, -1);
3287+
result[i - 1] = try lua.toAnyInternal(ChildType, a, true, -1);
3288+
lua.pop(1);
32373289
}
32383290

32393291
return result;
32403292
}
32413293

32423294
/// Converts value at given index to a zig struct if possible
3243-
fn toStruct(lua: *Lua, comptime T: type, raw_index: i32) !T {
3295+
fn toStruct(lua: *Lua, comptime T: type, a: ?std.mem.Allocator, comptime allow_alloc: bool, raw_index: i32) !T {
3296+
const stack_size_on_entry = lua.getTop();
3297+
defer std.debug.assert(lua.getTop() == stack_size_on_entry);
3298+
32443299
const index = lua.absIndex(raw_index);
32453300

32463301
if (!lua.isTable(index)) {
@@ -3249,27 +3304,32 @@ pub const Lua = struct {
32493304
std.debug.assert(lua.typeOf(index) == .table);
32503305

32513306
var result: T = undefined;
3307+
32523308
inline for (@typeInfo(T).Struct.fields) |field| {
32533309
const field_name = comptime field.name ++ "";
32543310
_ = lua.pushString(field_name);
3255-
std.debug.assert(lua.typeOf(index) == .table);
3311+
32563312
const lua_field_type = lua.getTable(index);
32573313
if (lua_field_type == .nil) {
32583314
if (field.default_value) |default_value| {
32593315
@field(result, field.name) = @as(*const field.type, @ptrCast(@alignCast(default_value))).*;
32603316
} else {
3317+
lua.pop(1);
32613318
return error.LuaTableMissingValue;
32623319
}
32633320
} else {
3264-
@field(result, field.name) = try lua.toAny(field.type, -1);
3321+
const stack_size_before_call = lua.getTop();
3322+
@field(result, field.name) = try lua.toAnyInternal(field.type, a, allow_alloc, -1);
3323+
std.debug.assert(stack_size_before_call == lua.getTop());
32653324
}
3325+
lua.pop(1); //pop the value off the stack
32663326
}
32673327

32683328
return result;
32693329
}
32703330

3271-
///automatically calls a lua function with the given arguments
3272-
pub fn autoCall(lua: *Lua, comptime ReturnType: type, func_name: [:0]const u8, args: anytype) !ReturnType {
3331+
/// Calls a function and pushes its return value to the top of the stack
3332+
fn autoCallAndPush(lua: *Lua, comptime ReturnType: type, func_name: [:0]const u8, args: anytype) !void {
32733333
if (try lua.getGlobal(func_name) != LuaType.function) return error.InvalidFunctionName;
32743334

32753335
inline for (args) |arg| {
@@ -3278,9 +3338,22 @@ pub const Lua = struct {
32783338

32793339
const num_results = if (ReturnType == void) 0 else 1;
32803340
try lua.protectedCall(args.len, num_results, 0);
3281-
defer lua.setTop(0);
3341+
}
32823342

3283-
return lua.toAny(ReturnType, -1);
3343+
///automatically calls a lua function with the given arguments
3344+
pub fn autoCall(lua: *Lua, comptime ReturnType: type, func_name: [:0]const u8, args: anytype) !ReturnType {
3345+
try lua.autoCallAndPush(ReturnType, func_name, args);
3346+
const result = try lua.toAny(ReturnType, -1);
3347+
lua.setTop(0);
3348+
return result;
3349+
}
3350+
3351+
///automatically calls a lua function with the given arguments
3352+
pub fn autoCallAlloc(lua: *Lua, comptime ReturnType: type, func_name: [:0]const u8, args: anytype) !Parsed(ReturnType) {
3353+
try lua.autoCallAndPush(ReturnType, func_name, args);
3354+
const result = try lua.toAnyAlloc(ReturnType, -1);
3355+
lua.setTop(0);
3356+
return result;
32843357
}
32853358

32863359
//automatically generates a wrapper function
@@ -3296,9 +3369,23 @@ pub const Lua = struct {
32963369
var parameters: std.meta.ArgsTuple(@TypeOf(function)) = undefined;
32973370

32983371
inline for (info.Fn.params, 0..) |param, i| {
3299-
parameters[i] = lua.toAny(param.type.?, (i + 1)) catch |err| {
3300-
lua.raiseErrorStr(@errorName(err), .{});
3301-
};
3372+
const param_info = @typeInfo(param.type.?);
3373+
//only use the overhead of creating the arena allocator if needed
3374+
if (comptime param_info == .Pointer and param_info.Pointer.size != .One) {
3375+
const parsed = lua.toAnyAlloc(param.type.?, (i + 1)) catch |err| {
3376+
lua.raiseErrorStr(@errorName(err), .{});
3377+
};
3378+
3379+
defer parsed.deinit();
3380+
3381+
parameters[i] = parsed.value;
3382+
} else {
3383+
const parsed = lua.toAny(param.type.?, (i + 1)) catch |err| {
3384+
lua.raiseErrorStr(@errorName(err), .{});
3385+
};
3386+
3387+
parameters[i] = parsed;
3388+
}
33023389
}
33033390

33043391
if (@typeInfo(info.Fn.return_type.?) == .ErrorUnion) {
@@ -3332,6 +3419,13 @@ pub const Lua = struct {
33323419
return try lua.toAny(ReturnType, -1);
33333420
}
33343421

3422+
/// get any lua global
3423+
/// can allocate memory
3424+
pub fn getAlloc(lua: *Lua, comptime ReturnType: type, name: [:0]const u8) !Parsed(ReturnType) {
3425+
_ = try lua.getGlobal(name);
3426+
return try lua.toAnyAlloc(ReturnType, -1);
3427+
}
3428+
33353429
///set any lua global
33363430
pub fn set(lua: *Lua, name: [:0]const u8, value: anytype) !void {
33373431
try lua.pushAny(value);

src/tests.zig

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2510,11 +2510,11 @@ test "toAny slice" {
25102510
;
25112511
try lua.doString(program);
25122512
_ = try lua.getGlobal("list");
2513-
const sliced = try lua.toAny([]u32, -1);
2514-
defer lua.allocator().free(sliced);
2513+
const sliced = try lua.toAnyAlloc([]u32, -1);
2514+
defer sliced.deinit();
25152515

25162516
try testing.expect(
2517-
std.mem.eql(u32, &[_]u32{ 1, 2, 3, 4, 5 }, sliced),
2517+
std.mem.eql(u32, &[_]u32{ 1, 2, 3, 4, 5 }, sliced.value),
25182518
);
25192519
}
25202520

@@ -2637,8 +2637,58 @@ test "autoCall" {
26372637
;
26382638

26392639
try lua.doString(program);
2640-
const sum = try lua.autoCall(usize, "add", .{ 1, 2 });
2641-
try std.testing.expect(3 == sum);
2640+
2641+
for (0..100) |_| {
2642+
const sum = try lua.autoCall(usize, "add", .{ 1, 2 });
2643+
try std.testing.expect(3 == sum);
2644+
}
2645+
2646+
for (0..100) |_| {
2647+
const sum = try lua.autoCallAlloc(usize, "add", .{ 1, 2 });
2648+
defer sum.deinit();
2649+
try std.testing.expect(3 == sum.value);
2650+
}
2651+
}
2652+
2653+
test "autoCall stress test" {
2654+
var lua = try Lua.init(&testing.allocator);
2655+
defer lua.deinit();
2656+
2657+
const program =
2658+
\\function add(a, b)
2659+
\\ return a + b
2660+
\\end
2661+
\\
2662+
\\
2663+
\\function KeyBindings()
2664+
\\
2665+
\\ local bindings = {
2666+
\\ {['name'] = 'player_right', ['key'] = 'a'},
2667+
\\ {['name'] = 'player_left', ['key'] = 'd'},
2668+
\\ {['name'] = 'player_up', ['key'] = 'w'},
2669+
\\ {['name'] = 'player_down', ['key'] = 's'},
2670+
\\ {['name'] = 'zoom_in', ['key'] = '='},
2671+
\\ {['name'] = 'zoom_out', ['key'] = '-'},
2672+
\\ {['name'] = 'debug_mode', ['key'] = '/'},
2673+
\\ }
2674+
\\
2675+
\\ return bindings
2676+
\\end
2677+
;
2678+
2679+
try lua.doString(program);
2680+
2681+
const ConfigType = struct {
2682+
name: []const u8,
2683+
key: []const u8,
2684+
shift: bool = false,
2685+
control: bool = false,
2686+
};
2687+
2688+
for (0..100) |_| {
2689+
const sum = try lua.autoCallAlloc([]ConfigType, "KeyBindings", .{});
2690+
defer sum.deinit();
2691+
}
26422692
}
26432693

26442694
test "get set" {
@@ -2667,6 +2717,8 @@ test "array of strings" {
26672717

26682718
try lua.doString(program);
26692719

2670-
const strings = try lua.autoCall([]const []const u8, "strings", .{});
2671-
lua.allocator().free(strings);
2720+
for (0..100) |_| {
2721+
const strings = try lua.autoCallAlloc([]const []const u8, "strings", .{});
2722+
defer strings.deinit();
2723+
}
26722724
}

0 commit comments

Comments
 (0)