Coroutines in Lua are supported out of the box. Really, as long as you don’t exclude the coroutines module intentionally (see sandboxing), they are supported for free. There are a lot of caveats (which incidentally apply also to the original Lua implementation) and are discussed in the “Caveats” section below.
Use any Lua coroutine tutorial to work with them.
Coroutines can be created with the script CreateCoroutine method, which accepts a DynValue which must be a function.
string code = @"
return function()
local x = 0
while true do
x = x + 1
coroutine.yield(x)
end
end
";
// Load the code and get the returned function
Script script = new Script();
DynValue function = script.DoString(code);
// Create the coroutine in C#
DynValue coroutine = script.CreateCoroutine(function);
// Resume the coroutine forever and ever..
while (true)
{
DynValue x = coroutine.Coroutine.Resume();
Console.WriteLine("{0}", x);
}
It’s possible to invoke coroutines as if they were an iterator:
string code = @"
return function()
local x = 0
while true do
x = x + 1
coroutine.yield(x)
if (x > 5) then
return 7
end
end
end
";
// Load the code and get the returned function
Script script = new Script();
DynValue function = script.DoString(code);
// Create the coroutine in C#
DynValue coroutine = script.CreateCoroutine(function);
// Loop the coroutine
string ret = "";
foreach (DynValue x in coroutine.Coroutine.AsTypedEnumerable())
{
ret = ret + x.ToString();
}
Assert.AreEqual("1234567", ret);
And here we come to the most important section of all the coroutines stuff. Just like in the original Lua, it’s not possible to yield out of nested calls.
In particular, in MoonSharp if you Call a script inside a C# function called from Lua, you can’t use yield to resume to a coroutine external to the C# call.
There is a way out: returning a TailCallRequest DynValue:
return DynValue.NewTailCallReq(luafunction, arg1, arg2...);
It’s also possible to specify a continuation - a piece of function which will be called after the execution of the tail call completes.
99% of the time this is probably overkill - not even the Lua standard library handles callbacks+yield correctly in the vast majority of the cases. But if you plan to implement APIs like load, pcall or coroutine.resume by yourself, this is needed.
As an aside, in some corner cases MoonSharp handles yielding in a different way (better in every case I tried so far but who knows) than standard Lua. For example, tostring() supports yielding __tostring metamethods without raising an error.
In MoonSharp it’s possible to have a coroutine suspended even if it does not call coroutine.yield
.
This can be useful, for example, if one wants to non-destructively limit the amount of CPU time dedicated to a script, but there are some caveats which are important to maintain script consistency.
Let’s start with an example:
string code = @"
function fib(n)
if (n == 0 or n == 1) then
return 1;
else
return fib(n - 1) + fib(n - 2);
end
end
";
// Load the code and get the returned function
Script script = new Script(CoreModules.None);
script.DoString(code);
// get the function
DynValue function = script.Globals.Get("fib");
// Create the coroutine in C#
DynValue coroutine = script.CreateCoroutine(function);
// Set the automatic yield counter every 10 instructions.
// 10 is likely too small! Use a much bigger value in your code to avoid interrupting too often!
coroutine.Coroutine.AutoYieldCounter = 10;
int cycles = 0;
DynValue result = null;
// Cycle until we get that the coroutine has returned something useful and not an automatic yield..
for (result = coroutine.Coroutine.Resume(8);
result.Type == DataType.YieldRequest;
result = coroutine.Coroutine.Resume())
{
cycles += 1;
}
// Check the values of the operation
Assert.AreEqual(DataType.Number, result.Type);
Assert.AreEqual(34, result.Number);
The steps are simple:
AutoYieldCounter
to some number greater than 0. 1000 is a good starting point, tune from there. This is the number of instructions which will be executed before yielding to the caller.coroutine.Coroutine.Resume(...)
with the proper argsDataType.YieldRequest
, call coroutine.Coroutine.Resume()
with no args.Now on to the caveats: