Coroutines in Lua provide cooperative multitasking, allowing you to pause and resume function execution. SolarSharp’s Coroutine class provides full support for Lua coroutines with additional C# integration.
Coroutines are functions that can be suspended and resumed. Unlike threads, they don’t run concurrently - only one coroutine executes at a time, and they explicitly yield control:
function countdown() for i = 3, 1, -1 do print(i) coroutine.yield() end print("Liftoff!")endco = coroutine.create(countdown)coroutine.resume(co) -- Prints: 3coroutine.resume(co) -- Prints: 2coroutine.resume(co) -- Prints: 1coroutine.resume(co) -- Prints: Liftoff!
using SolarSharp.Interpreter.DataTypes;Script script = new Script();// Load a Lua functionLuaValue func = script.LoadString(@" return function() for i = 1, 3 do print(i) coroutine.yield() end end");LuaValue luaFunc = script.Call(func);// Create coroutine from Lua functionLuaValue coroutine = script.CreateCoroutine(luaFunc);Console.WriteLine(coroutine.Type); // DataType.Thread
script.DoString(@" function myTask() print('Starting...') coroutine.yield() print('Continuing...') coroutine.yield() print('Done!') end co = coroutine.create(myTask)");LuaValue co = script.Globals.Get("co");Coroutine coroutine = co.Coroutine;
public enum CoroutineState{ NotStarted, // Created but never resumed Suspended, // Yielded and can be resumed Running, // Currently executing Dead // Finished execution}
Coroutine co = script.CreateCoroutine(func).Coroutine;Console.WriteLine(co.State); // CoroutineState.NotStartedco.Resume();Console.WriteLine(co.State); // CoroutineState.Suspended or Dead
script.DoString(@" function echo() while true do local msg = coroutine.yield() if msg then print('Received: ' .. msg) end end end co = coroutine.create(echo)");Coroutine co = script.Globals.Get("co").Coroutine;// First resume starts the coroutineco.Resume();// Subsequent resumes pass values to yieldco.Resume("Hello"); // Prints: Received: Helloco.Resume("World"); // Prints: Received: World
script.DoString(@" function producer() local i = 0 while i < 5 do i = i + 1 coroutine.yield(i * 10) end end co = coroutine.create(producer)");Coroutine co = script.Globals.Get("co").Coroutine;while (co.State != CoroutineState.Dead){ LuaValue value = co.Resume(); if (value.Type == DataType.Number) Console.WriteLine(value.Number); // 10, 20, 30, 40, 50}
The coroutine.wrap function creates a function that resumes the coroutine:
script.DoString(@" function task() for i = 1, 3 do coroutine.yield(i) end end wrapped = coroutine.wrap(task) print(wrapped()) -- 1 print(wrapped()) -- 2 print(wrapped()) -- 3");
script.DoString(@" function range(n) return coroutine.wrap(function() for i = 1, n do coroutine.yield(i) end end) end for i in range(5) do print(i) -- 1, 2, 3, 4, 5 end");
script.DoString(@" function countdown() for i = 5, 1, -1 do coroutine.yield(i) end end co = coroutine.create(countdown)");Coroutine co = script.Globals.Get("co").Coroutine;// Iterate as LuaValue enumerableforeach (LuaValue value in co.AsTypedEnumerable()){ Console.WriteLine(value.Number); // 5, 4, 3, 2, 1}
Script script = new Script();LuaValue func = script.LoadString(@" return function() for i = 1, 3 do coroutine.yield(i) end end");LuaValue luaFunc = script.Call(func);// Create and use coroutineLuaValue co1 = script.CreateCoroutine(luaFunc);Coroutine coroutine1 = co1.Coroutine;while (coroutine1.State != CoroutineState.Dead) coroutine1.Resume();// Recycle the dead coroutineLuaValue co2 = script.RecycleCoroutine(coroutine1, luaFunc);Coroutine coroutine2 = co2.Coroutine;// Use the recycled coroutinewhile (coroutine2.State != CoroutineState.Dead) coroutine2.Resume();
Only recycle coroutines that are in the Dead state and were created as CoroutineType.Coroutine.
Script script = new Script();script.Globals["callback"] = (Func<LuaValue, LuaValue>)((func) =>{ // This will fail - cannot yield through CLR code return script.Call(func);});script.DoString(@" function task() callback(function() coroutine.yield() -- ERROR: attempt to yield across CLR boundary end) end co = coroutine.create(task)");try{ Coroutine co = script.Globals.Get("co").Coroutine; co.Resume();}catch (ScriptRuntimeException ex){ Console.WriteLine(ex.Message); // "attempt to yield across a CLR-call boundary"}
using SolarSharp.Interpreter.Debugging;Coroutine co = script.Globals.Get("co").Coroutine;// Resume and yieldco.Resume();if (co.State == CoroutineState.Suspended){ // Get the call stack StackFrame[] stack = co.GetStackTrace(skip: 0); foreach (StackFrame frame in stack) { Console.WriteLine($" at {frame.FunctionName} in {frame.Source}:{frame.Line}"); }}
if (co.State == CoroutineState.Suspended || co.State == CoroutineState.NotStarted){ co.Resume();}else if (co.State == CoroutineState.Dead){ Console.WriteLine("Coroutine has finished");}
Use enumerables for iteration patterns
// Good - clean iterationforeach (LuaValue value in coroutine.AsTypedEnumerable()){ ProcessValue(value);}// Less clean - manual loopwhile (coroutine.State != CoroutineState.Dead){ LuaValue value = coroutine.Resume(); ProcessValue(value);}
Avoid yielding across CLR boundaries
// Bad - will failscript.Globals["dangerousCall"] = (Action)(() =>{ script.DoString("coroutine.yield()");});// Good - keep yield paths pure Luascript.DoString(@" function safeYield() coroutine.yield() end");
Recycle coroutines for better performance
// Pool of coroutinesQueue<Coroutine> deadCoroutines = new Queue<Coroutine>();void ReturnCoroutine(Coroutine co){ if (co.State == CoroutineState.Dead) deadCoroutines.Enqueue(co);}Coroutine GetCoroutine(LuaValue func){ if (deadCoroutines.Count > 0) { Coroutine recycled = deadCoroutines.Dequeue(); return script.RecycleCoroutine(recycled, func).Coroutine; } return script.CreateCoroutine(func).Coroutine;}
script.DoString(@" function producer() for i = 1, 10 do coroutine.yield(i * i) end end co = coroutine.create(producer)");Coroutine producer = script.Globals.Get("co").Coroutine;foreach (LuaValue value in producer.AsTypedEnumerable()){ Console.WriteLine($"Consumed: {value.Number}");}