diff --git a/src/Lua/Exceptions.cs b/src/Lua/Exceptions.cs index d5344c18..89f9eecf 100644 --- a/src/Lua/Exceptions.cs +++ b/src/Lua/Exceptions.cs @@ -2,6 +2,7 @@ using Lua.CodeAnalysis.Syntax; using Lua.Internal; using Lua.Runtime; +using System.Diagnostics; using System.Runtime.CompilerServices; namespace Lua; @@ -67,9 +68,14 @@ static string GetMessageWithNearToken(string message, string? nearToken) public class LuaUnDumpException(string message) : Exception(message); -public class LuaRuntimeException : Exception +internal interface ILuaTracebackBuildable { - public LuaRuntimeException(LuaThread? thread, Exception innerException) : base(innerException.Message,innerException) + Traceback? BuildOrGet(); +} + +public class LuaRuntimeException : Exception, ILuaTracebackBuildable +{ + public LuaRuntimeException(LuaThread? thread, Exception innerException) : base(innerException.Message, innerException) { Thread = thread; } @@ -78,7 +84,7 @@ public LuaRuntimeException(LuaThread? thread, LuaValue errorObject) { if (thread != null) { - thread.CurrentException?.Build(); + thread.CurrentException?.BuildOrGet(); thread.ExceptionTrace.Clear(); thread.CurrentException = this; } @@ -96,7 +102,7 @@ public Traceback? LuaTraceback { if (luaTraceback == null) { - Build(); + ((ILuaTracebackBuildable)this).BuildOrGet(); } return luaTraceback; @@ -185,7 +191,7 @@ static string CreateMessage(Traceback traceback, LuaValue errorObject) [MethodImpl(MethodImplOptions.NoInlining)] - internal Traceback? Build() + Traceback? ILuaTracebackBuildable.BuildOrGet() { if (luaTraceback != null) return luaTraceback; if (Thread != null) @@ -247,4 +253,50 @@ public override string ToString() public class LuaAssertionException(LuaThread? traceback, string message) : LuaRuntimeException(traceback, message); -public class LuaModuleNotFoundException(string moduleName) : Exception($"module '{moduleName}' not found"); \ No newline at end of file +public class LuaModuleNotFoundException(string moduleName) : Exception($"module '{moduleName}' not found"); + +public sealed class LuaCancelledException : OperationCanceledException, ILuaTracebackBuildable +{ + Traceback? luaTraceback; + + public Traceback? LuaTraceback + { + get + { + if (luaTraceback == null) + { + ((ILuaTracebackBuildable)this).BuildOrGet(); + } + + return luaTraceback; + } + } + + internal LuaThread? Thread { get; private set; } + + internal LuaCancelledException(LuaThread thread, CancellationToken cancellationToken, Exception? innerException = null) : base("The operation was cancelled during execution on Lua.", innerException, cancellationToken) + { + thread.CurrentException?.BuildOrGet(); + thread.ExceptionTrace.Clear(); + thread.CurrentException = this; + Thread = thread; + } + + + [MethodImpl(MethodImplOptions.NoInlining)] + Traceback? ILuaTracebackBuildable.BuildOrGet() + { + if (luaTraceback != null) return luaTraceback; + + if (Thread != null) + { + var callStack = Thread.ExceptionTrace.AsSpan(); + if (callStack.IsEmpty) return null; + luaTraceback = new Traceback(Thread.State, callStack); + Thread.ExceptionTrace.Clear(); + Thread = null!; + } + + return luaTraceback; + } +} \ No newline at end of file diff --git a/src/Lua/LuaCoroutine.cs b/src/Lua/LuaCoroutine.cs index 1ac4cf34..43eb7807 100644 --- a/src/Lua/LuaCoroutine.cs +++ b/src/Lua/LuaCoroutine.cs @@ -186,9 +186,9 @@ async ValueTask ResumeAsyncCore(LuaStack stack, int argCount, int returnBas { if (IsProtectedMode) { - if (ex is LuaRuntimeException luaRuntimeException) + if (ex is ILuaTracebackBuildable tracebackBuildable) { - traceback = luaRuntimeException.Build(); + traceback = tracebackBuildable.BuildOrGet(); } Volatile.Write(ref status, (byte)LuaThreadStatus.Dead); diff --git a/src/Lua/LuaThread.cs b/src/Lua/LuaThread.cs index e784c6ab..11601350 100644 --- a/src/Lua/LuaThread.cs +++ b/src/Lua/LuaThread.cs @@ -70,8 +70,11 @@ public void Release() internal int LastVersion; internal int CurrentVersion; - internal LuaRuntimeException? CurrentException; + internal ILuaTracebackBuildable? CurrentException; internal readonly ReversedStack ExceptionTrace = new(); + + // internal bool CancelRequested; + // internal CancellationToken CancellationToken; public bool IsRunning => CallStackFrameCount != 0; internal LuaFunction? Hook { get; set; } @@ -131,7 +134,7 @@ void UpdateCurrentVersion(ref FastStackCore callStack) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal LuaThreadAccess PushCallStackFrame(in CallStackFrame frame) { - CurrentException?.Build(); + CurrentException?.BuildOrGet(); CurrentException = null; ref var callStack = ref CoreData!.CallStack; callStack.Push(frame); diff --git a/src/Lua/LuaThreadExtensions.cs b/src/Lua/LuaThreadExtensions.cs index be953850..020ed3fe 100644 --- a/src/Lua/LuaThreadExtensions.cs +++ b/src/Lua/LuaThreadExtensions.cs @@ -13,4 +13,19 @@ public static CoroutineLease RentCoroutine(this LuaThread thread, LuaFunction fu { return new(LuaCoroutine.Create(thread, function, isProtectedMode)); } + + internal static void ThrowIfCancellationRequested(this LuaThread thread, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + Throw(thread, cancellationToken); + } + + return; + + static void Throw(LuaThread thread, CancellationToken cancellationToken) + { + throw new LuaCancelledException(thread, cancellationToken); + } + } } \ No newline at end of file diff --git a/src/Lua/Runtime/LuaStack.cs b/src/Lua/Runtime/LuaStack.cs index 2b403b0f..c38d3075 100644 --- a/src/Lua/Runtime/LuaStack.cs +++ b/src/Lua/Runtime/LuaStack.cs @@ -32,7 +32,7 @@ static void Resize(ref LuaValue[] array, int newSize) if (1000000 < size) { - throw new LuaException("Lua Stack overflow"); + throw new ("Lua Stack overflow"); } Array.Resize(ref array, size); diff --git a/src/Lua/Runtime/LuaThreadAccess.cs b/src/Lua/Runtime/LuaThreadAccess.cs index 03589e6e..1e6ff504 100644 --- a/src/Lua/Runtime/LuaThreadAccess.cs +++ b/src/Lua/Runtime/LuaThreadAccess.cs @@ -53,6 +53,7 @@ public async ValueTask RunAsync(LuaFunction function, int argumentCount, in throw new ArgumentNullException(nameof(function)); } + Thread.ThrowIfCancellationRequested(cancellationToken); var thread = Thread; var varArgumentCount = function.GetVariableArgumentCount(argumentCount); if (varArgumentCount != 0) @@ -78,7 +79,7 @@ public async ValueTask RunAsync(LuaFunction function, int argumentCount, in var access = thread.PushCallStackFrame(frame); LuaFunctionExecutionContext context = new() { Access = access, ArgumentCount = argumentCount, ReturnFrameBase = returnBase, }; - + var callStackTop = thread.CallStackFrameCount; try { if (this.Thread.CallOrReturnHookMask.Value != 0 && !this.Thread.IsInHook) @@ -90,7 +91,7 @@ public async ValueTask RunAsync(LuaFunction function, int argumentCount, in } finally { - this.Thread.PopCallStackFrame(); + this.Thread.PopCallStackFrameUntil(callStackTop-1); } } diff --git a/src/Lua/Runtime/LuaVirtualMachine.Debug.cs b/src/Lua/Runtime/LuaVirtualMachine.Debug.cs index f03f5af3..5afb0eef 100644 --- a/src/Lua/Runtime/LuaVirtualMachine.Debug.cs +++ b/src/Lua/Runtime/LuaVirtualMachine.Debug.cs @@ -47,7 +47,7 @@ static async ValueTask Impl(VirtualMachineExecutionContext context) countHookIsDone = true; } - + context.ThrowIfCancellationRequested(); if (context.Thread.IsLineHookEnabled) { var sourcePositions = prototype.LineInfo; @@ -126,6 +126,7 @@ internal static async ValueTask ExecuteCallHook(LuaFunctionExecutionContext context.Thread.PopCallStackFrameWithStackPop(); } } + context.Thread.ThrowIfCancellationRequested(cancellationToken); { var frame = context.Thread.GetCurrentFrame(); @@ -135,7 +136,7 @@ internal static async ValueTask ExecuteCallHook(LuaFunctionExecutionContext { return r; } - + context.Thread.ThrowIfCancellationRequested(cancellationToken); var top = stack.Count; stack.Push("return"); stack.Push(LuaValue.Nil); diff --git a/src/Lua/Runtime/LuaVirtualMachine.cs b/src/Lua/Runtime/LuaVirtualMachine.cs index 6e4b48dd..edbfac1e 100644 --- a/src/Lua/Runtime/LuaVirtualMachine.cs +++ b/src/Lua/Runtime/LuaVirtualMachine.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Lua.Internal; +// ReSharper disable MethodHasAsyncOverload // ReSharper disable InconsistentNaming @@ -255,6 +256,8 @@ public async ValueTask ExecuteClosureAsyncImpl() { break; } + + ThrowIfCancellationRequested(); } return Thread.Stack.Count - returnFrameBase; @@ -264,6 +267,17 @@ public async ValueTask ExecuteClosureAsyncImpl() pool.TryPush(this); } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ThrowIfCancellationRequested() + { + if (!CancellationToken.IsCancellationRequested) return; + Throw(); + + void Throw() + { + GetThreadWithCurrentPc(this).ThrowIfCancellationRequested(CancellationToken); + } + } } enum PostOperationType @@ -567,6 +581,7 @@ static double ArithmeticOperation(OpCode code, double a, double b) context.Thread.State.CloseUpValues(context.Thread, frameBase + iA - 1); } + context.ThrowIfCancellationRequested(); continue; case OpCode.Eq: Markers.Eq(); @@ -695,6 +710,7 @@ static double ArithmeticOperation(OpCode code, double a, double b) indexRef = index; Unsafe.Add(ref indexRef, 3) = index; stack.NotifyTop(iA + frameBase + 4); + context.ThrowIfCancellationRequested(); continue; } @@ -799,7 +815,7 @@ static void VarArg(VirtualMachineExecutionContext context) catch (Exception e) { context.State.CloseUpValues(context.Thread, context.FrameBase); - if (e is not LuaRuntimeException) + if (e is not (LuaRuntimeException or LuaCancelledException)) { var newException = new LuaRuntimeException(context.Thread, e); context.PopOnTopCallStackFrames(); @@ -1003,6 +1019,7 @@ static bool ToString(ref LuaValue v) static async ValueTask ExecuteBinaryOperationMetaMethod(int target, LuaValue vb, LuaValue vc, VirtualMachineExecutionContext context, OpCode opCode) { + context.ThrowIfCancellationRequested(); var (name, description) = opCode.GetNameAndDescription(); if (vb.TryGetMetamethod(context.State, name, out var metamethod) || vc.TryGetMetamethod(context.State, name, out metamethod)) @@ -1039,12 +1056,14 @@ static async ValueTask ExecuteBinaryOperationMetaMethod(int target, LuaValue vb, await ExecuteCallHook(functionContext, context.CancellationToken); stack.PopUntil(target + 1); context.PostOperation = PostOperationType.DontPop; + context.ThrowIfCancellationRequested(); return; } await func.Func(functionContext, context.CancellationToken); stack.PopUntil(target + 1); context.PostOperation = PostOperationType.DontPop; + context.ThrowIfCancellationRequested(); return; } finally @@ -1060,6 +1079,7 @@ static async ValueTask ExecuteBinaryOperationMetaMethod(int target, LuaValue vb, static bool Call(VirtualMachineExecutionContext context, out bool doRestart) { + context.ThrowIfCancellationRequested(); var instruction = context.Instruction; var RA = instruction.A + context.FrameBase; var newBase = RA + 1; @@ -1119,6 +1139,7 @@ static bool FuncCall(VirtualMachineExecutionContext context, LuaThreadAccess acc var awaiter = task.GetAwaiter(); awaiter.GetResult(); + context.Thread.ThrowIfCancellationRequested(context.CancellationToken); var instruction = context.Instruction; var ic = instruction.C; @@ -1137,8 +1158,9 @@ static bool FuncCall(VirtualMachineExecutionContext context, LuaThreadAccess acc } } - internal static async ValueTask Call(LuaThread thread, int funcIndex, int returnBase, CancellationToken ct) + internal static async ValueTask Call(LuaThread thread, int funcIndex, int returnBase, CancellationToken cancellationToken) { + thread.ThrowIfCancellationRequested(cancellationToken); var stack = thread.Stack; var newBase = funcIndex + 1; var va = stack.Get(funcIndex); @@ -1165,12 +1187,24 @@ internal static async ValueTask Call(LuaThread thread, int funcIndex, int r var functionContext = new LuaFunctionExecutionContext() { Access = access, ArgumentCount = argCount, ReturnFrameBase = returnBase }; if (thread.CallOrReturnHookMask.Value != 0 && !thread.IsInHook) { - await ExecuteCallHook(functionContext, ct); + await ExecuteCallHook(functionContext, cancellationToken); + } + else + { + await func.Func(functionContext, cancellationToken); } - await func.Func(functionContext, ct); + thread.ThrowIfCancellationRequested(cancellationToken); return thread.Stack.Count - funcIndex; } + catch(OperationCanceledException operationCanceledException) + { + if(operationCanceledException is not LuaCancelledException) + { + throw new LuaCancelledException(thread, cancellationToken, operationCanceledException); + } + throw; + } finally { thread.PopCallStackFrame(); @@ -1195,6 +1229,7 @@ static void CallPostOperation(VirtualMachineExecutionContext context) static bool TailCall(VirtualMachineExecutionContext context, out bool doRestart) { + context.ThrowIfCancellationRequested(); var instruction = context.Instruction; var stack = context.Stack; var RA = instruction.A + context.FrameBase; @@ -1259,6 +1294,7 @@ static bool TailCall(VirtualMachineExecutionContext context, out bool doRestart) task.GetAwaiter().GetResult(); + context.ThrowIfCancellationRequested(); if (!context.PopFromBuffer(context.CurrentReturnFrameBase, context.Stack.Count - context.CurrentReturnFrameBase)) { return true; @@ -1270,6 +1306,7 @@ static bool TailCall(VirtualMachineExecutionContext context, out bool doRestart) static bool TForCall(VirtualMachineExecutionContext context, out bool doRestart) { + context.ThrowIfCancellationRequested(); doRestart = false; var instruction = context.Instruction; var stack = context.Stack; @@ -1341,8 +1378,8 @@ static bool TForCall(VirtualMachineExecutionContext context, out bool doRestart) return false; } - var awaiter = task.GetAwaiter(); - awaiter.GetResult(); + task.GetAwaiter().GetResult(); + context.ThrowIfCancellationRequested(); context.Thread.PopCallStackFrame(); TForCallPostOperation(context); return true; @@ -1942,8 +1979,10 @@ static bool ExecuteUnaryOperationMetaMethod(LuaValue vb, VirtualMachineExecution } [MethodImpl(MethodImplOptions.NoInlining)] - internal static async ValueTask ExecuteUnaryOperationMetaMethod(LuaThread thread, LuaValue vb, OpCode opCode, CancellationToken ct) + internal static async ValueTask ExecuteUnaryOperationMetaMethod(LuaThread thread, LuaValue vb, OpCode opCode, CancellationToken cancellationToken) { + thread.ThrowIfCancellationRequested(cancellationToken); + var (name, description) = opCode.GetNameAndDescription(); if (vb.TryGetMetamethod(thread.State, name, out var metamethod)) @@ -1976,11 +2015,14 @@ internal static async ValueTask ExecuteUnaryOperationMetaMethod(LuaThr var functionContext = new LuaFunctionExecutionContext() { Access = access, ArgumentCount = argCount, ReturnFrameBase = newBase }; if (thread.CallOrReturnHookMask.Value != 0 && !thread.IsInHook) { - await ExecuteCallHook(functionContext, ct); + await ExecuteCallHook(functionContext, cancellationToken); + } + else + { + await func.Func(functionContext, cancellationToken); } - - await func.Func(functionContext, ct); + thread.ThrowIfCancellationRequested(cancellationToken); var results = stack.GetBuffer()[newFrame.ReturnBase..]; var result = results.Length == 0 ? default : results[0]; results.Clear(); @@ -2097,8 +2139,10 @@ static bool ExecuteCompareOperationMetaMethod(LuaValue vb, LuaValue vc, } [MethodImpl(MethodImplOptions.NoInlining)] - internal static async ValueTask ExecuteCompareOperationMetaMethod(LuaThread thread, LuaValue vb, LuaValue vc, OpCode opCode, CancellationToken ct) + internal static async ValueTask ExecuteCompareOperationMetaMethod(LuaThread thread, LuaValue vb, LuaValue vc, OpCode opCode, CancellationToken cancellationToken) { + thread.ThrowIfCancellationRequested(cancellationToken); + var (name, description) = opCode.GetNameAndDescription(); bool reverseLe = false; ReCheck: @@ -2133,11 +2177,14 @@ internal static async ValueTask ExecuteCompareOperationMetaMethod(LuaThrea var functionContext = new LuaFunctionExecutionContext() { Access = access, ArgumentCount = argCount, ReturnFrameBase = newBase }; if (thread.CallOrReturnHookMask.Value != 0 && !thread.IsInHook) { - await ExecuteCallHook(functionContext, ct); + await ExecuteCallHook(functionContext, cancellationToken); + } + else + { + await func.Func(functionContext, cancellationToken); } - - await func.Func(functionContext, ct); + thread.ThrowIfCancellationRequested(cancellationToken); var results = stack.GetBuffer()[newFrame.ReturnBase..]; var result = results.Length == 0 ? default : results[0]; results.Clear(); diff --git a/src/Lua/Standard/BasicLibrary.cs b/src/Lua/Standard/BasicLibrary.cs index d0e94610..8558bab1 100644 --- a/src/Lua/Standard/BasicLibrary.cs +++ b/src/Lua/Standard/BasicLibrary.cs @@ -1,6 +1,7 @@ using System.Globalization; using Lua.Internal; using Lua.Runtime; + // ReSharper disable MethodHasAsyncOverloadWithCancellation namespace Lua.Standard; @@ -259,14 +260,17 @@ public async ValueTask PCall(LuaFunctionExecutionContext context, Cancellat catch (Exception ex) { context.Thread.PopCallStackFrameUntil(frameCount); - if (ex is LuaRuntimeException luaEx) - { - luaEx.Forget(); - return context.Return(false, luaEx.ErrorObject); - } - else + switch (ex) { - return context.Return(false, ex.Message); + case LuaCancelledException: + throw; + case OperationCanceledException: + throw new LuaCancelledException(context.Thread,cancellationToken, ex); + case LuaRuntimeException luaEx: + luaEx.Forget(); + return context.Return(false, luaEx.ErrorObject); + default: + return context.Return(false, ex.Message); } } } @@ -565,6 +569,7 @@ public async ValueTask XPCall(LuaFunctionExecutionContext context, Cancella { var thread = context.Thread; thread.PopCallStackFrameUntil(frameCount); + cancellationToken.ThrowIfCancellationRequested(); var access = thread.CurrentAccess; if (ex is LuaRuntimeException luaEx) diff --git a/tests/Lua.Tests/CancellationTest.cs b/tests/Lua.Tests/CancellationTest.cs new file mode 100644 index 00000000..7445cdcd --- /dev/null +++ b/tests/Lua.Tests/CancellationTest.cs @@ -0,0 +1,180 @@ +using Lua.Standard; + +namespace Lua.Tests; + +public class CancellationTest +{ + LuaState state = default!; + + [SetUp] + public void SetUp() + { + state = LuaState.Create(); + state.OpenStandardLibraries(); + + state.Environment["assert"] = new LuaFunction("assert_with_wait", + async (context, ct) => + { + await Task.Delay(1, ct); + var arg0 = context.GetArgument(0); + + if (!arg0.ToBoolean()) + { + var message = "assertion failed!"; + if (context.HasArgument(1)) + { + message = context.GetArgument(1); + } + + throw new LuaAssertionException(context.Thread, message); + } + + return (context.Return(context.Arguments)); + }); + state.Environment["sleep"] = new LuaFunction("sleep", + (context, _) => + { + Thread.Sleep(context.GetArgument(0)); + + return new(context.Return()); + }); + state.Environment["wait"] = new LuaFunction("wait", + async (context, ct) => + { + await Task.Delay(context.GetArgument(0), ct); + return context.Return(); + }); + } + + [Test] + public async Task PCall_WaitTest() + { + var source = """ + local function f(millisec) + wait(millisec) + end + pcall(f, 500) + """; + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.CancelAfter(200); + + try + { + await state.DoStringAsync(source, "@test.lua", cancellationTokenSource.Token); + Assert.Fail("Expected TaskCanceledException was not thrown."); + } + catch (Exception e) + { + Assert.That(e, Is.TypeOf()); + var luaCancelledException = (LuaCancelledException)e; + Assert.That(luaCancelledException.InnerException, Is.TypeOf()); + var luaStackTrace = luaCancelledException.LuaTraceback!.ToString(); + Console.WriteLine(luaStackTrace); + Assert.That(luaStackTrace, Contains.Substring("'wait'")); + Assert.That(luaStackTrace, Contains.Substring("'pcall'")); + } + } + + [Test] + public async Task PCall_SleepTest() + { + var source = """ + local function f(millisec) + sleep(millisec) + end + pcall(f, 500) + """; + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.CancelAfter(250); + + try + { + await state.DoStringAsync(source, "@test.lua", cancellationTokenSource.Token); + Assert.Fail("Expected TaskCanceledException was not thrown."); + } + catch (Exception e) + { + Assert.That(e, Is.TypeOf()); + var luaCancelledException = (LuaCancelledException)e; + Assert.That(luaCancelledException.InnerException, Is.Null); + var luaStackTrace = luaCancelledException.LuaTraceback!.ToString(); + Console.WriteLine(luaStackTrace); + Assert.That(luaStackTrace, Contains.Substring("'sleep'")); + Assert.That(luaStackTrace, Contains.Substring("'pcall'")); + } + } + + [Test] + public async Task ForLoopTest() + { + var source = """ + local ret = 0 + for i = 1, 1000000000 do + ret = ret + i + end + return ret + """; + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.CancelAfter(100); + cancellationTokenSource.Token.Register(() => + { + Console.WriteLine("Cancellation requested"); + }); + try + { + var r = await state.DoStringAsync(source, "@test.lua", cancellationTokenSource.Token); + Console.WriteLine(r[0]); + Assert.Fail("Expected TaskCanceledException was not thrown."); + } + catch (Exception e) + { + Assert.That(e, Is.TypeOf()); + Console.WriteLine(e.StackTrace); + var luaCancelledException = (LuaCancelledException)e; + Assert.That(luaCancelledException.InnerException, Is.Null); + var traceback = luaCancelledException.LuaTraceback; + if (traceback != null) + { + var luaStackTrace = traceback.ToString(); + Console.WriteLine(luaStackTrace); + } + } + } + + [Test] + public async Task GoToLoopTest() + { + var source = """ + local ret = 0 + ::loop:: + ret = ret + 1 + goto loop + return ret + """; + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.CancelAfter(100); + cancellationTokenSource.Token.Register(() => + { + Console.WriteLine("Cancellation requested"); + }); + try + { + var r = await state.DoStringAsync(source, "@test.lua", cancellationTokenSource.Token); + Console.WriteLine(r[0]); + Assert.Fail("Expected TaskCanceledException was not thrown."); + } + catch (Exception e) + { + Assert.That(e, Is.TypeOf()); + Console.WriteLine(e.StackTrace); + var luaCancelledException = (LuaCancelledException)e; + Assert.That(luaCancelledException.InnerException, Is.Null); + var traceback = luaCancelledException.LuaTraceback; + if (traceback != null) + { + var luaStackTrace = traceback.ToString(); + Console.WriteLine(luaStackTrace); + } + } + } +} \ No newline at end of file