Skip to content

Commit 144f97e

Browse files
authored
Add support for lua.getLocal() and lua.setLocal() (#11)
1 parent 71def45 commit 144f97e

File tree

2 files changed

+196
-13
lines changed

2 files changed

+196
-13
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ you.
8787
|-----------------------------|---------------------------------------|
8888
| Lua C API (`lua_*`) | 🎉 100% coverage<sup>†</sup> (92/92) |
8989
| Auxilary Library (`luaL_*`) | 🤩 100% coverage (48/48) |
90-
| Debug API (`lua_Debug`) | 56% coverage (7/12) |
90+
| Debug API (`lua_Debug`) | 72% coverage (9/12) |
9191
| LuaJIT Extensions | *No plans to implement.* |
9292

9393
*†: Coroutine yield/resume is not yet part of the public `zig-luajit` Zig API, see [#6][ISSUE-6].*
@@ -279,14 +279,14 @@ This section describes the current status of Zig language bindings ("the Zig API
279279
| C API Symbol | Available in `zig-luajit` |
280280
|----------------------------|-------------------------------------|
281281
| `lua_getinfo` | ☑️ `lua.getInfo()` |
282+
| `lua_getstack` | ☑️ `lua.getStack()` |
282283
| `lua_gethookcount` | ☑️ `lua.getHookCount()` |
283284
| `lua_gethookmask` | ☑️ `lua.getHookMask()` |
284285
| `lua_gethook` | ☑️ `lua.getHook()` |
285-
| `lua_getlocal` ||
286-
| `lua_getstack` | ☑️ `lua.getStack()` |
287-
| `lua_getupvalue` ||
288286
| `lua_sethook` | ☑️ `lua.setHook()` |
289-
| `lua_setlocal` ||
287+
| `lua_getlocal` | ☑️ `lua.getLocal()` |
288+
| `lua_setlocal` | ☑️ `lua.setLocal()` |
289+
| `lua_getupvalue` ||
290290
| `lua_setupvalue` ||
291291

292292

src/root.zig

Lines changed: 191 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ pub const Lua = opaque {
6161
return @ptrCast(str.ptr);
6262
}
6363

64+
fn asSlice(str: ?[*:0]const u8) ?[:0]const u8 {
65+
if (str) |s| {
66+
return std.mem.sliceTo(s, 0);
67+
} else {
68+
return null;
69+
}
70+
}
71+
6472
fn asCFn(f: CFunction) ?*const fn (?*c.lua_State) callconv(.c) c_int {
6573
return @ptrCast(f);
6674
}
@@ -926,11 +934,8 @@ pub const Lua = opaque {
926934
/// Stack Behavior: `[-0, +1, m]`
927935
pub fn pushFString(lua: *Lua, comptime format: [:0]const u8, args: anytype) [:0]const u8 {
928936
const string: ?[*:0]const u8 = @call(.auto, c.lua_pushfstring, .{ asState(lua), format.ptr } ++ args);
929-
if (string) |s| {
930-
// NOTE: This seems dangerous. I don't really like this solution, but it doesn't look like there is any other option.
931-
// We are making a strong assumption that Lua returns a well-behaved zero-terminated string.
932-
const len = std.mem.indexOfSentinel(u8, 0, s);
933-
return s[0..len :0];
937+
if (asSlice(string)) |str| {
938+
return str;
934939
} else {
935940
std.debug.panic("Received unexpected NULL response from lua.pushFString(\"{s}, ...\")", .{format});
936941
}
@@ -2463,9 +2468,8 @@ pub const Lua = opaque {
24632468
assert(pattern.len > 0); // gsub(string, "", replacement) causes an infinite loop -- avoid using the empty string as a pattern.
24642469

24652470
const str: ?[*:0]const u8 = c.luaL_gsub(asState(lua), @ptrCast(string.ptr), @ptrCast(pattern.ptr), @ptrCast(replacement.ptr));
2466-
if (str) |s| {
2467-
const len = std.mem.indexOfSentinel(u8, 0, s);
2468-
return s[0..len :0];
2471+
if (asSlice(str)) |s| {
2472+
return s;
24692473
} else {
24702474
lua.raiseErrorFormat(
24712475
"gsub('%s', '%s', '%s') returned null instead of the replaced string.",
@@ -3017,6 +3021,52 @@ pub const Lua = opaque {
30173021
pub fn getHookMask(lua: *Lua) HookMask {
30183022
return @bitCast(c.lua_gethookmask(asState(lua)));
30193023
}
3024+
3025+
/// Gets information about a local variable of a given activation record. The parameter `info` must have been
3026+
/// filled by a previous call to `getStack()` or given as an argument to a hook.
3027+
///
3028+
/// The `index` selects which local variable to inspect (1 is the first parameter or active local variable,
3029+
/// and so on, until the last active local variable).
3030+
///
3031+
/// When the `index` is valid, pushes the variable's value onto the stack and returns its name. Variable names
3032+
/// starting with '(' (such as the name `(*temporary)`) represent internal variables (loop control variables,
3033+
/// temporaries, and C function locals).
3034+
///
3035+
/// When the `index` is negative or greater than the number of active local variables, this function returns
3036+
/// `null` and nothing is pushed onto the stack.
3037+
///
3038+
/// From: `const char *lua_getlocal(lua_State *L, lua_Debug *ar, int n);`
3039+
/// Refer to: https://www.lua.org/manual/5.1/manual.html#lua_getlocal
3040+
/// Stack Behavior: `[-0, +(0|1), -]`
3041+
pub fn getLocal(lua: *Lua, info: *Lua.DebugInfo, index: i32) ?[:0]const u8 {
3042+
lua.skipIndexValidation(
3043+
index,
3044+
"getLocal() is well-behaved and defined to return null when the index is not valid.",
3045+
);
3046+
3047+
const str: ?[*:0]const u8 = @ptrCast(c.lua_getlocal(asState(lua), @ptrCast(info), index));
3048+
return asSlice(str);
3049+
}
3050+
3051+
/// Sets the value of a local variable during hook execution. The parameter `info` must have been filled by a
3052+
/// previous call to `getStack()` or given as an argument to a hook.
3053+
///
3054+
/// Always pops the value at the top of the stack.
3055+
/// * When the `index` is valid, assigns the value at the top of the stack to the variable at the given index
3056+
/// * When the `index` is negative or greater than the number of local variables, this function returns `null`.
3057+
///
3058+
/// From: `const char *lua_setlocal(lua_State *L, lua_Debug *ar, int n);`
3059+
/// Refer to: https://www.lua.org/manual/5.1/manual.html#lua_setlocal
3060+
/// Stack Behavior: `[-1, +0, -]`
3061+
pub fn setLocal(lua: *Lua, info: *Lua.DebugInfo, index: i32) ?[:0]const u8 {
3062+
lua.skipIndexValidation(
3063+
index,
3064+
"setLocal() is well-behaved and defined to return null when the index is not valid.",
3065+
);
3066+
3067+
const str: ?[*:0]const u8 = @ptrCast(c.lua_setlocal(asState(lua), @ptrCast(info), index));
3068+
return asSlice(str);
3069+
}
30203070
};
30213071

30223072
test "Lua can be initialized with an allocator" {
@@ -5755,3 +5805,136 @@ test "getHookCount() can be used check the current hook function count" {
57555805
lua.setHook(T.hook, Lua.HookMask.call_and_return, 42);
57565806
try std.testing.expectEqual(e2, lua.getHookCount());
57575807
}
5808+
5809+
test "getLocal and setLocal can inspect and modify local variables" {
5810+
const lua = try Lua.init(std.testing.allocator);
5811+
defer lua.deinit();
5812+
5813+
const T = struct {
5814+
var locals_data = std.ArrayList(struct {
5815+
name: []const u8,
5816+
value: i32,
5817+
}).init(std.testing.allocator);
5818+
5819+
fn hookLocalVars(l: *Lua, info: *Lua.DebugInfo) callconv(.c) void {
5820+
if (info.event != Lua.HookEventKind.line or info.currentline != 4) {
5821+
// Restrict the hook to only execute on line 3 where we can modify `y`
5822+
return;
5823+
}
5824+
5825+
if (!l.getInfo("l", info)) {
5826+
return l.raiseErrorFormat("Could not get debug info.", .{});
5827+
}
5828+
5829+
var i: i32 = 1;
5830+
while (true) {
5831+
const name = l.getLocal(info, i);
5832+
if (name == null) {
5833+
break;
5834+
}
5835+
5836+
const value = l.toIntegerStrict(-1) catch -1;
5837+
locals_data.append(.{
5838+
.name = std.mem.sliceTo(name.?, 0),
5839+
.value = @intCast(value),
5840+
}) catch unreachable;
5841+
5842+
l.pop(1);
5843+
i += 1;
5844+
}
5845+
5846+
if (l.getLocal(info, 2)) |name| {
5847+
l.pop(1);
5848+
l.pushInteger(42);
5849+
const before = l.getTop();
5850+
const modified_name = l.setLocal(info, 2);
5851+
std.testing.expectEqual(before - 1, l.getTop()) catch unreachable;
5852+
std.testing.expectEqualStrings(name, modified_name.?) catch unreachable;
5853+
}
5854+
}
5855+
5856+
fn cleanup() void {
5857+
locals_data.deinit();
5858+
}
5859+
};
5860+
defer T.cleanup();
5861+
5862+
const test_code =
5863+
\\function test_locals()
5864+
\\ local x = 10
5865+
\\ local y = 20
5866+
\\ return x + y -- Should return 10 + 42 = 52 after our hook runs
5867+
\\end
5868+
;
5869+
try lua.doString(test_code);
5870+
5871+
lua.setHook(T.hookLocalVars, .{ .on_line = true }, 0);
5872+
try std.testing.expectEqual(Lua.Type.function, lua.getGlobal("test_locals"));
5873+
try lua.callProtected(0, 1, 0);
5874+
5875+
try std.testing.expect(lua.isInteger(-1));
5876+
try std.testing.expectEqual(52, try lua.toIntegerStrict(-1)); // Should be 52 because we modified `y` from 20 to 42
5877+
5878+
try std.testing.expect(T.locals_data.items.len >= 2); // The variables x and y must be found, there may be other temporary variables as well
5879+
try std.testing.expectEqualStrings("x", T.locals_data.items[0].name);
5880+
try std.testing.expectEqual(10, T.locals_data.items[0].value);
5881+
try std.testing.expectEqualStrings("y", T.locals_data.items[1].name);
5882+
try std.testing.expectEqual(20, T.locals_data.items[1].value);
5883+
}
5884+
5885+
test "getLocal() and setLocal() should return null for invalid indices" {
5886+
const lua = try Lua.init(std.testing.allocator);
5887+
defer lua.deinit();
5888+
5889+
const T = struct {
5890+
var hook_executed = false;
5891+
5892+
fn edgeCaseHook(l: *Lua, info: *Lua.DebugInfo) callconv(.c) void {
5893+
if (info.event != Lua.HookEventKind.line or info.currentline != 3) {
5894+
return;
5895+
}
5896+
5897+
hook_executed = true;
5898+
5899+
if (!l.getInfo("l", info)) {
5900+
return l.raiseErrorFormat("Could not get debug info.", .{});
5901+
}
5902+
5903+
var before = l.getTop();
5904+
const out_of_bounds = l.getLocal(info, 9999);
5905+
std.testing.expect(out_of_bounds == null) catch unreachable;
5906+
std.testing.expectEqual(before, l.getTop()) catch unreachable; // Stack should not change when index is invalid.
5907+
5908+
before = l.getTop();
5909+
const negative_index = l.getLocal(info, -10);
5910+
std.testing.expect(negative_index == null) catch unreachable;
5911+
std.testing.expectEqual(before, l.getTop()) catch unreachable; // Stack should not change when index is invalid.
5912+
5913+
l.pushInteger(100);
5914+
before = l.getTop();
5915+
const out_of_bounds_set_result = l.setLocal(info, 9999);
5916+
std.testing.expect(out_of_bounds_set_result == null) catch unreachable;
5917+
std.testing.expectEqual(before - 1, l.getTop()) catch unreachable; // Stack should always be popped even if index is invalid
5918+
5919+
l.pushInteger(100);
5920+
before = l.getTop();
5921+
const negative_set_result = l.setLocal(info, -10);
5922+
std.testing.expect(negative_set_result == null) catch unreachable;
5923+
std.testing.expectEqual(before - 1, l.getTop()) catch unreachable; // Stack should always be popped even if index is invalid
5924+
}
5925+
};
5926+
5927+
const test_code =
5928+
\\function test_edge_cases()
5929+
\\ local x = 10
5930+
\\ return x
5931+
\\end
5932+
;
5933+
try lua.doString(test_code);
5934+
5935+
lua.setHook(T.edgeCaseHook, .{ .on_line = true }, 0);
5936+
try std.testing.expectEqual(Lua.Type.function, lua.getGlobal("test_edge_cases"));
5937+
try lua.callProtected(0, 1, 0);
5938+
5939+
try std.testing.expect(T.hook_executed);
5940+
}

0 commit comments

Comments
 (0)