Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,15 @@ This code can resume the execution of the Lua script after waiting with await, a

`state.SetHook(hook, mask, count)` installs a hook for that Lua thread. The mask combines `c` (call), `r` (return), and `l` (line); a positive `count` enables instruction events even with an empty mask. The callback receives the event name and a line number (`nil` for non-line events). Return values, including `false`, are ignored. `state.SetHook(null, "")` removes the hook.

A hook can call `SetHook` to replace or remove itself or change its interval. Setting a hook resets the instruction counter, so subsequent count events use the new interval. Hooks are suppressed while the callback executes, including Lua code it invokes.
A hook can call `SetHook` to replace or remove itself or change its interval. Setting a hook resets the instruction counter, so subsequent count events use the new interval. Hooks on that thread are suppressed while the callback executes, including Lua code it invokes on the same thread.

Host hook settings are copied when `CreateThread`, `CreateCoroutine`, `coroutine.create`, or `coroutine.wrap` creates a child. The child starts a fresh instruction counter and uses the same callback with its own `context.State`; use that state to inspect or reconfigure the executing thread. Parent and child settings are independent: later changes do not update existing children, and a child created before hook installation remains unconfigured. Hook suppression is also independent, so a new child can fire hooks even when created inside a parent hook. Lua `debug.sethook` callbacks remain thread-local, as in [Lua 5.2](https://www.lua.org/source/5.2/ldblib.c.html#hookf); a child inherits their mask/count metadata without running the parent's Lua callback. Scripts given access to `debug.sethook` can still replace or remove a host hook.

Count events measure Lua instructions, as in [Lua 5.2](https://www.lua.org/manual/5.2/manual.html#lua_sethook). They do not fire during a C# host function's own execution or while awaiting it. If that host function calls back into Lua, the nested Lua instructions can trigger hooks. A timeout checked by a hook therefore cannot interrupt a blocking C# function. Long-running host functions should check the supplied `CancellationToken` or pass it to cancellable operations. A host-caught exception or cancellation from a hook does not prevent hooks from running on later executions using the same state.

Hook-based memory sampling is approximate: one Lua instruction or host operation can allocate substantially before the next event. `GC.GetTotalMemory(false)` reports the managed heap, not memory attributable to one Lua state, so it cannot enforce a strict per-state memory limit.

A `LuaRuntimeException` thrown by a hook is a Lua error that `pcall` can catch; it is not an uncatchable execution limit.
A `LuaRuntimeException` thrown by a hook is a Lua error that `pcall` or `coroutine.resume` can catch; it is not an uncatchable execution limit.

## Coroutines

Expand Down
8 changes: 5 additions & 3 deletions README_JA.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,15 @@ print "goodbye!"

`state.SetHook(hook, mask, count)`で、そのLuaスレッドにフックを設定できます。`mask`には`c`(呼び出し)、`r`(戻り)、`l`(行)を組み合わせます。正の`count`を指定すると、空のmaskでも命令数に応じたイベントが有効になります。コールバックにはイベント名と行番号(行イベント以外では`nil`)が渡されます。`false`を含む戻り値は無視されます。`state.SetHook(null, "")`でフックを解除できます。

フック内で`SetHook`を呼び、自身を置き換えたり解除したり、間隔を変更したりできます。設定時に命令カウンターがリセットされ、その後のcountイベントは新しい間隔を使います。コールバックの実行中は、そこから呼び出したLuaコードも含め、フックは再帰的に発火しません。
フック内で`SetHook`を呼び、自身を置き換えたり解除したり、間隔を変更したりできます。設定時に命令カウンターがリセットされ、その後のcountイベントは新しい間隔を使います。コールバックの実行中は、そこから同じスレッドで呼び出したLuaコードも含め、そのスレッドのフックは再帰的に発火しません。

`CreateThread`、`CreateCoroutine`、`coroutine.create`、`coroutine.wrap`で子を作ると、作成元のホストフック設定がコピーされます。子の命令カウンターは初期値から始まり、同じコールバックに子自身の`context.State`が渡されます。実行中のスレッドを調べたり再設定したりする場合は、このstateを使ってください。親と子の設定は独立しており、作成後の変更は既存の子へ反映されません。フック設定前に作成した子にも後から反映されません。フックの再入抑制もスレッドごとに独立するため、親のフック内で作成した子ではフックが発火できます。Lua側の`debug.sethook`のコールバックは[Lua 5.2](https://www.lua.org/source/5.2/ldblib.c.html#hookf)と同様にスレッド固有です。子はmask/countの情報を引き継ぎますが、親のLuaコールバックを呼びません。また、`debug.sethook`を公開すると、スクリプトからホストフックを置き換えたり解除したりできます。

countイベントが数えるのは[Lua 5.2](https://www.lua.org/manual/5.2/manual.html#lua_sethook)と同様にLuaの命令です。C#ホスト関数自体の実行中や、そのawait中には発火しません。ホスト関数からLuaを呼び出した場合は、そのLua命令によって発火できます。そのため、フックで確認するタイムアウトでは、実行中のC#関数を強制中断できません。長時間動くホスト関数では、渡された`CancellationToken`を確認するか、キャンセル可能な処理に渡してください。フックからの例外やキャンセルをホスト側で捕捉した後も、同じstateの次の実行ではフックが動作します。

フックによるメモリ使用量の確認は近似的なものです。次のイベントまでに、1つのLua命令やホスト処理が大量のメモリを確保する場合があります。また、`GC.GetTotalMemory(false)`は特定のLua stateの使用量ではなくマネージドヒープの値なので、stateごとの厳密なメモリ上限には使えません。

フックが投げた`LuaRuntimeException`はLuaのエラーとして`pcall`で捕捉できます。そのため、捕捉できない強制的な実行上限にはなりません。
フックが投げた`LuaRuntimeException`はLuaのエラーとして`pcall`や`coroutine.resume`で捕捉できます。そのため、捕捉できない強制的な実行上限にはなりません。

## コルーチン

Expand Down Expand Up @@ -656,4 +658,4 @@ Lua-CSharpはC#で実装されているため.NETのGCに依存しています

## ライセンス

このライブラリは[MITライセンス](LICENSE)の下で提供されています。
このライブラリは[MITライセンス](LICENSE)の下で提供されています。
58 changes: 47 additions & 11 deletions src/Lua/LuaState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,40 @@ public static LuaState Create(LuaPlatform platform)
return LuaGlobalState.Create(platform).MainThread;
}

internal static LuaState CreateCoroutine(
LuaGlobalState globalState,
LuaFunction function,
bool isProtectedMode = false
)
/// <summary>Creates a thread sharing this state's globals and copying its host hook settings.</summary>
public LuaState CreateThread()
{
return new(globalState, function, isProtectedMode);
var thread = new LuaState(GlobalState);
thread.InheritHook(this);
return thread;
}

public LuaState CreateThread()
/// <summary>Creates a coroutine sharing this state's globals and copying its host hook settings.</summary>
public LuaState CreateCoroutine(LuaFunction function, bool isProtectedMode = false)
{
return new(GlobalState);
var thread = new LuaState(GlobalState, function, isProtectedMode);
thread.InheritHook(this);
return thread;
}

public LuaState CreateCoroutine(LuaFunction function, bool isProtectedMode = false)
void InheritHook(LuaState parent)
{
return new(GlobalState, function, isProtectedMode);
HookMask = parent.HookMask;
BaseHookCount = parent.BaseHookCount;
IsLuaDebugHook = parent.IsLuaDebugHook;
// Lua's debug library registers callbacks per thread. Its mask/count metadata
// is inherited, but there is no Lua callback registered for the new thread.
if (IsLuaDebugHook)
{
return;
}

Hook = parent.Hook;
IsLineHookEnabled = parent.IsLineHookEnabled;
CallOrReturnHookMask = parent.CallOrReturnHookMask;
// lua_newthread copies settings, not the parent's in-progress counter or hook suppression.
HookCount = BaseHookCount > 0 ? (long)BaseHookCount + 1 : 0;
LastPc = -1;
}

public LuaThreadStatus GetStatus()
Expand Down Expand Up @@ -187,6 +204,8 @@ public void Release()
public bool IsRunning => CallStackFrameCount != 0;
public bool IsCoroutine => coroutine != null;
internal LuaFunction? Hook { get; set; }
internal bool IsLuaDebugHook;
internal byte HookMask;

public LuaFunction? CoroutineFunction => coroutine?.Function;
public bool CanResume => GetStatus() == LuaThreadStatus.Suspended;
Expand Down Expand Up @@ -422,7 +441,10 @@ public LuaTable GetCurrentEnvironment()
/// <param name="count">A positive Lua instruction interval, independent of the mask. Zero disables count events.</param>
/// <remarks>
/// The callback can call SetHook to replace or remove itself or change the interval.
/// Setting the hook resets the count interval; callbacks do not recursively trigger hooks.
/// Setting the hook resets the count interval; callbacks do not recursively trigger hooks on the same thread.
/// New threads and coroutines copy host hook settings with an independent instruction counter.
/// Later changes affect only this thread. Hooks installed by Lua's debug.sethook do not copy their callback.
/// A shared callback should use its context.State to inspect or reconfigure the executing thread.
/// Count events track Lua instructions, not C# execution or time spent awaiting host functions.
/// A host function that calls back into Lua can trigger count events for that Lua execution.
/// Long-running host functions must cooperate with the execution's CancellationToken to stop promptly.
Expand All @@ -434,6 +456,8 @@ public void SetHook(LuaFunction? hook, string mask, int count = 0)
HookCount = 0;
BaseHookCount = 0;
Hook = null;
HookMask = 0;
IsLuaDebugHook = false;
IsLineHookEnabled = false;
IsCallHookEnabled = false;
IsReturnHookEnabled = false;
Expand All @@ -453,6 +477,18 @@ public void SetHook(LuaFunction? hook, string mask, int count = 0)
}

Hook = hook;
HookMask = (byte)(
(IsCallHookEnabled ? 1 : 0)
| (IsReturnHookEnabled ? 2 : 0)
| (IsLineHookEnabled ? 4 : 0)
);
IsLuaDebugHook = false;
}

internal void SetHookFromLua(LuaFunction? hook, string mask, int count)
{
SetHook(hook, mask, count);
IsLuaDebugHook = hook is not null;
}

internal void DumpStackValues()
Expand Down
4 changes: 2 additions & 2 deletions src/Lua/Standard/CoroutineLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ CancellationToken cancellationToken
)
{
var arg0 = context.GetArgument<LuaFunction>(0);
return new(context.Return(LuaState.CreateCoroutine(context.State.GlobalState, arg0, true)));
return new(context.Return(context.State.CreateCoroutine(arg0, true)));
}

public ValueTask<int> Resume(
Expand Down Expand Up @@ -82,7 +82,7 @@ CancellationToken cancellationToken
)
{
var arg0 = context.GetArgument<LuaFunction>(0);
var state = LuaState.CreateCoroutine(context.State.GlobalState, arg0, false);
var state = context.State.CreateCoroutine(arg0, false);
return new(context.Return(new CSharpClosure("wrap", [state], WrapResume)));
}

Expand Down
12 changes: 6 additions & 6 deletions src/Lua/Standard/DebugLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ CancellationToken cancellationToken
var hook = context.GetArgumentOrDefault<LuaFunction?>(argOffset);
var mask = context.GetArgumentOrDefault<string?>(argOffset + 1) ?? "";
var count = context.GetArgumentOrDefault<int>(argOffset + 2);
state.SetHook(hook, mask, count);
state.SetHookFromLua(hook, mask, count);
if (hook is null)
{
return 0;
Expand Down Expand Up @@ -521,17 +521,17 @@ CancellationToken cancellationToken
)
{
var state = GetLuaThread(context, out _);
if (state.Hook is null)
if (state.Hook is null && !state.IsLuaDebugHook)
{
return new(context.Return(LuaValue.Nil, LuaValue.Nil, LuaValue.Nil));
}

return new(
context.Return(
state.Hook,
(state.IsCallHookEnabled ? "c" : "")
+ (state.IsReturnHookEnabled ? "r" : "")
+ (state.IsLineHookEnabled ? "l" : ""),
state.Hook is null ? LuaValue.Nil : state.Hook,
((state.HookMask & 1) != 0 ? "c" : "")
+ ((state.HookMask & 2) != 0 ? "r" : "")
+ ((state.HookMask & 4) != 0 ? "l" : ""),
state.BaseHookCount
)
);
Expand Down
Loading
Loading