@@ -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
30223072test "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