Skip to content

Commit 6dfe267

Browse files
authored
Add support for Lua.HookFunction and lua.setHook() (#9)
1 parent 55165f4 commit 6dfe267

2 files changed

Lines changed: 129 additions & 7 deletions

File tree

README.md

Lines changed: 3 additions & 3 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`) | 24% coverage (3/12) |
90+
| Debug API (`lua_Debug`) | 32% coverage (4/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].*
@@ -274,7 +274,7 @@ This section describes the current status of Zig language bindings ("the Zig API
274274
| C Type Definition | Available in `zig-luajit` |
275275
|----------------------------|-------------------------------------|
276276
| `lua_Debug` | ☑️ `Lua.DebugInfo` |
277-
| `lua_Hook` | |
277+
| `lua_Hook` | ☑️📢 `Lua.HookFunction` |
278278

279279
| C API Symbol | Available in `zig-luajit` |
280280
|----------------------------|-------------------------------------|
@@ -285,7 +285,7 @@ This section describes the current status of Zig language bindings ("the Zig API
285285
| `lua_getlocal` ||
286286
| `lua_getstack` | ☑️ `lua.getStack()` |
287287
| `lua_getupvalue` ||
288-
| `lua_sethook` ||
288+
| `lua_sethook` | ☑️ `lua.setHook()` |
289289
| `lua_setlocal` ||
290290
| `lua_setupvalue` ||
291291

src/root.zig

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2628,14 +2628,22 @@ pub const Lua = opaque {
26282628
/// Represents different types of events that can be triggered during Lua execution. These events are received by
26292629
/// hooks to inspect or modify the execution of code in the Lua instance.
26302630
pub const HookEventKind = enum(i32) {
2631-
/// Function call event
2631+
/// The hook is called when the interpreter calls a function.
2632+
/// The hook is called just after Lua enters the new function, before the function gets its arguments.
26322633
call = c.LUA_HOOKCALL,
2633-
/// Normal function return event
2634+
2635+
/// The hook is called when the interpreter returns from a function. The hook is called just before Lua leaves
2636+
/// the function. You have no access to the values to be returned by the function.
26342637
ret = c.LUA_HOOKRET,
2635-
/// Line execution event
2638+
2639+
/// The hook is called when the interpreter is about to start the execution of a new line of code, or when it
2640+
/// jumps back in the code (even to the same line). This event only happens while Lua is executing a Lua function.
26362641
line = c.LUA_HOOKLINE,
2637-
/// Instruction count event
2642+
2643+
/// The hook is called after the interpreter executes every count instructions. This event only happens while
2644+
/// Lua is executing a Lua function.
26382645
count = c.LUA_HOOKCOUNT,
2646+
26392647
/// Tail call return event - occurs when Lua is simulating a return from a function that performed a tail call
26402648
tailret = c.LUA_HOOKTAILRET,
26412649

@@ -2660,6 +2668,9 @@ pub const Lua = opaque {
26602668
///
26612669
/// Note: While Lua is running a hook, it disables other calls to hooks. If a hook calls back
26622670
/// Lua to execute a function or a chunk, this execution occurs without any calls to hooks.
2671+
///
2672+
/// Note: throughout the Lua documentation, this debug info is often referred to as an "activation record" and passed as
2673+
/// a parameter named `ar`.
26632674
pub const DebugInfo = extern struct {
26642675
/// The event that triggered the hook. When hooks are called, this field indicates
26652676
/// the specific event type that triggered it
@@ -2902,6 +2913,80 @@ pub const Lua = opaque {
29022913
@memcpy(simplifiedInterface.short_src[0..DebugShortSourceLen], info.short_src[0..DebugShortSourceLen]);
29032914
return simplifiedInterface;
29042915
}
2916+
2917+
/// Hooks allow for Zig code to be invoked as part of the lifecycle of executing Lua code. Hooks may be registered
2918+
/// with the Lua runtime and will be invoked at requested points in time, such as when entering a function.
2919+
///
2920+
/// Whenever a hook is called by the runtime, the `info.event` argument is set to the specific set to the specific
2921+
/// `HookEventKind` that triggered the hook. For certain kinds of hooks, additional information will be set in the
2922+
/// debug info struct:
2923+
///
2924+
/// * For `line` events: the field `currentline` is also set. For other information the hook function should
2925+
/// call `getInfo()`.
2926+
/// * For return events the `info.event` may either be `ret`, indicating a normal function return, or `tailret` which
2927+
/// indicates that Lua is simulating a return from a function that did a tail call. For `tailret` events it is
2928+
/// useless to call `getInfo()`.
2929+
///
2930+
/// While Lua is running a hook, it disables other calls to hooks. Therefore, if a hook calls back Lua to execute a
2931+
/// function or a chunk, this execution occurs **without** any calls to hooks.
2932+
///
2933+
/// From: `typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar);`
2934+
/// Refer to: https://www.lua.org/manual/5.1/manual.html#lua_Hook
2935+
pub const HookFunction = *const fn (lua: *Lua, info: *Lua.DebugInfo) callconv(.c) void;
2936+
2937+
/// A flag enumeration ("mask" or "bitmask") used to register a callback function ("hook") on certain points in the
2938+
/// execution lifecycle.
2939+
pub const HookMask = packed struct(u32) {
2940+
/// Used as the `mask` parameter to `setHook()` in order to disable a hook.
2941+
pub const disabled: HookMask = .{};
2942+
2943+
/// Indicates that the hook function should be invoked when the Lua runtime calls a function.
2944+
on_call: bool = false,
2945+
2946+
/// Indicates that the hook function should be invoked when the Lua runtime returns from a function (or performs
2947+
/// tail call recursion).
2948+
on_return: bool = false,
2949+
2950+
/// Indicates that the hook function should be invoked when the Lua runtime moves to execute the next line of
2951+
/// a function.
2952+
on_line: bool = false,
2953+
2954+
/// Indicates that the hook function should be invoked after executing `count` instructions.
2955+
on_count: bool = false,
2956+
2957+
/// Unused and ignored.
2958+
__padding: u28 = undefined,
2959+
};
2960+
2961+
/// Sets the debugging hook function. Argument `f` is the hook function. `mask` specifies on which events the hook
2962+
/// will be called.
2963+
///
2964+
/// Events include:
2965+
/// - Call hook: Called when the interpreter calls a function, just after Lua enters the new function
2966+
/// - Return hook: Called when the interpreter returns from a function, just before Lua leaves the function
2967+
/// - Line hook: Called when starting execution of a new line of code or jumping back in code
2968+
/// - Count hook: Called after executing every `count` instructions
2969+
///
2970+
/// Example:
2971+
/// ```zig
2972+
/// const lua: *Lua = ...;
2973+
/// const Example = struct {
2974+
/// fn exHook(l: *Lua, info: *Lua.DebugInfo) callconv(.c) void {
2975+
/// ...
2976+
/// }
2977+
/// };
2978+
/// lua.setHook(Example.exHook, .{ .on_call = true, .on_return = true }, 0);
2979+
/// ```
2980+
///
2981+
/// A hook is disabled by setting `mask` to `HookMask.disabled` (the empty mask `.{}`).
2982+
///
2983+
/// From: `int lua_sethook(lua_State *L, lua_Hook f, int mask, int count);`
2984+
/// Refer to: https://www.lua.org/manual/5.1/manual.html#lua_sethook
2985+
/// Stack Behavior: `[-0, +0, -]`
2986+
pub fn setHook(lua: *Lua, f: HookFunction, mask: HookMask, count: i32) void {
2987+
const res = c.lua_sethook(asState(lua), @ptrCast(f), @bitCast(mask), count);
2988+
assert(1 == res);
2989+
}
29052990
};
29062991

29072992
test "Lua can be initialized with an allocator" {
@@ -5536,3 +5621,40 @@ test "getStack() can be used to inspect the call stack" {
55365621
try std.testing.expect(lua.isString(2));
55375622
try std.testing.expectEqualStrings("bar", try lua.toLString(-1));
55385623
}
5624+
5625+
test "setHook() can be used register a callback to intercept function calls" {
5626+
const lua = try Lua.init(std.testing.allocator);
5627+
defer lua.deinit();
5628+
5629+
const T = struct {
5630+
fn hook(l: *Lua, info: *Lua.DebugInfo) callconv(.c) void {
5631+
std.testing.expectEqual(Lua.HookEventKind.call, info.event) catch unreachable;
5632+
5633+
if (!l.getInfo("nSLufl", info)) {
5634+
return l.raiseErrorFormat("Could not get info.", .{});
5635+
}
5636+
5637+
std.testing.expect(info.name == null) catch unreachable;
5638+
std.testing.expectEqualStrings("Lua\x00", info.what.?[0..4]) catch unreachable;
5639+
std.testing.expectEqualStrings("[string \"function bar()...\"]\x00", info.short_src[0..29]) catch unreachable;
5640+
std.testing.expectEqual(1, info.currentline) catch unreachable;
5641+
std.testing.expectEqual(0, info.nups) catch unreachable;
5642+
std.testing.expectEqual(1, info.linedefined) catch unreachable;
5643+
std.testing.expectEqual(3, info.lastlinedefined) catch unreachable;
5644+
}
5645+
};
5646+
5647+
const expected_source =
5648+
\\function bar()
5649+
\\ return 1
5650+
\\end
5651+
;
5652+
try lua.doString(expected_source);
5653+
5654+
lua.setHook(T.hook, .{ .on_call = true }, 0);
5655+
try std.testing.expectEqual(Lua.Type.function, lua.getGlobal("bar"));
5656+
try lua.callProtected(0, Lua.MultipleReturn, 1);
5657+
try std.testing.expectEqual(1, lua.getTop());
5658+
try std.testing.expect(lua.isInteger(-1));
5659+
try std.testing.expectEqual(1, try lua.toIntegerStrict(-1));
5660+
}

0 commit comments

Comments
 (0)