Open ixjf opened 5 years ago
@ixjf
I was checking this pull request because it looks exactly what I'm looking for in terms of being able to temporarily pause a Lua script.
I do have one question on the implementation though. The ExecutionControlToken was added as the first parameter which as you stated breaks backwards compatibility. What's the downside from putting it at the end and having an overload or optional parameter with a default value so that old calls continue to work alongside the new ones?
It's not possible because some of the async functions take a variable number of arguments at the end, unfortunately.
I can't think of an alternative which doesn't break compatibility, and leaving the existing functions and adding overloads doesn't make sense to me.
I have to note that when associating the same token with multiple scripts, calling PauseExecution from the ScriptExecutionContext in a CLR function binding will stop all of them which are associated. Not sure if this should be the behaviour, but it currently is in order to be able to call PauseExecution on the token from outside those script threads.
Also, at the time I did this I did not think it could be useful to be able to call PauseExecution from outside CLR function bindings, though now I'm reconsidering.
Whoa this is big.
Give me a little more time, sorry.
Thanks for the contribs!!
@xanathar what's the state of this? I've tried the branch and lgtm.
I haven't worked on this for a long time, but besides the doubts I have that I have already presented above, I also need to clean up the pull request. There seem to be a lot of project file changes that probably aren't needed and just happened as I went about modifying the original code.
I feel ashamed for the response time ... sorry π³ (and thanks lofcz for the tag, otherwise I'd have missed this again). I don't think there's plenty of usage for the current async methods as they are quite poor in functionality, so I'm quite fine with breaking that compatibility. It also seems to me that users of the current async API (if any exist) can on-board on the new one with just a trivial change, so, I'd go on. For me it's fine to merge on main as soon as the project files are cleaned up from accidental changes (intentional ones are of course fine).
Thanks for the amazing patch.
Great, thanks :) I think I will need another month before I can clean this up, though, since I'm a little busy with university right now.
@ixjf Thought I'd share, I've been using your changes for a year now on a personal project and they've worked well for me. Thanks for sharing, look forward to seeing the new pull request.
@ixjf Just wanted to check in on this, if you get it clean I will merge it.
I think I got it as clean as it can be. Still a pretty huge commit, though.
@ixjf thanks for cleaning this up! Looking trough the tests and wondering how to wire up async Task<T> F(ScriptExecutionContext ctx, CallbackArguments args)
with this (eg. a delegate of type (Func<ScriptExecutionContext, CallbackArguments, Task<T>>
). Is this possible?
Why do you need the function to return the task? You get a Task
I'm fetching data from DB and I'd like to free the thread while that is being done to avoid thread starvation. In my use case moonsharp scripts are being used as backend for a web application.
So what you want is to be able to pause the task indefinitely when you ask for data from the DB and be able to trigger it to resume when the data is ready?
Essentially I just need to make the whole chain of function calls up to the one fetching data from DB async and awaiting each other. When this function is called the thread can handle other requests rather that waiting (being blocked) for the data to arrive.
moonsharp: -> [implicit await] db.query("...") -> [await] c# method "query" is called -> [current thread is now free to do whatever is needed, we are waiting for the data] -> data arrives -> "query" in c# finishes -> "query" in moonsharp finishes -> script execution continues
Just like callbacks but with current thread able to do other work while callbacks are resolving.
Can you give me some sample code of both Lua and C# land? Easier to visualize what you're trying to do.
C#:
Script script = new Script();
script.Globals["dbfn"] = (Func<ScriptExecutionContext, CallbackArguments, Task<int>>)DbFn;
async Task<int> DbFn(ScriptExecutionContext ctx, CallbackArguments args) {
return await (int)dbcontext.QueryAsync($"select top 1 someIntColumn from Users where id = {(int)args[0].Double}"); // whatever
}
Lua:
n1 = dbfn(1);
n2 = dbfn(2);
n = n1 + n2; -- do whatever with n1,n2 here...
related to: https://github.com/moonsharp-devs/moonsharp/issues/228
When you call dbfn
, do you expect the Lua code to continue on to the next dbfn call? Or what?
Hey everyone, at this point maybe it is better to either bring it to the Discord or bring it to Github Discussions for this repo?
@ixjf I need/expect Lua to pause execution until the value is resolved. On the second line I need to be able to work with n1 already.
Right, you want it to block, just not keep the Lua thread running while waiting for the results from the QueryAsync thread. Got it.
@LimpingNinja I was thinking that I might be able to add this one functionality (if possible) to this pull request. But maybe this is big enough a change already?
@ixjf Let's spawn this off to a different discussion, I need to review this one in context and apply it then we can look at iterative adjustments like this request.
Fair enough.
Thanks for considering this! Would be a game changer for me :)
Right, you want it to block, just not keep the Lua thread running while waiting for the results from the QueryAsync thread. Got it.
@LimpingNinja I was thinking that I might be able to add this one functionality (if possible) to this pull request. But maybe this is big enough a change already?
Where can we follow development of the new async work mentioned here, please? @ixjf
I'm converting our WIP strategy game (which uses MoonSharp for moddev) to using C# async/await for its level loading, web requests etc. This has been going fine but I just realised I need to be able to call async C# methods from within Lua code. The proposal above sounds π¨βπ³ π€ (french chef kiss) perfect! (I had expected I'd have to require shotgunning with coroutines or extra keywords like async and await.)
Thanks
@Arakade exactly, we are in the same boat here ππ»
There is now an implementation of the feature I've requested in comments - https://github.com/Librelancer/moonsharp/commit/2c52d89bdf2161c5324d5dda2fa6245927c943de by @CallumDev Might be a good inspiration for @ixjf in case you'd like to get it upstream.
cc: @Arakade
Fortunately, I find a way to solve these problems above, @lofcz @ixjf
First of all, we should realize that Coroutine is the key point since it makes the lua script to be able to "pause" itself on its own, "sending" something to outside, and "waiting" for something back so that the script resume.
Assuming we have a CLR method which returns a Task
public static async Task<string> ReadFileText( string fileName ){
return await File.ReadAllTextAsync(fileName);
}
And I wish it can be invoked in lua like this below:
local txt = readFileText("C:\\test.txt");
In order to do that in MoonSharp, we have to "send" this Task
It's sound seems like this, basic version:
local txt = coroutine.yield( readFileText("C:\\test.txt") ); -- send the Task object to outside and wait for result.
The coroutine.yield
is good, but I still DONT want to write it in my lua code. therefore, I "hide" it by using SetClrToScriptCustomConversion.
With this conversion, MoonSharp will automaticlly converts the Task object to a YieldRequest which means calling coroutine.yield( task )
in lua.
public class TaskDescriptor
{
public Task<object?> Task { get; private set; }
public bool HasResult { get; private set; }
public static TaskDescriptor Build(Func<Task> taskAction)
{
return new TaskDescriptor
{
Task = System.Threading.Tasks.Task.Run(async () =>
{
await taskAction();
return (object?) null;
}),
HasResult = false
};
}
public static TaskDescriptor Build<T>(Func<Task<T>> taskAction)
{
return new TaskDescriptor
{
Task = System.Threading.Tasks.Task.Run(async () => (object?)await taskAction()),
HasResult = true
};
}
}
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion<TaskDescriptor>((script, task) =>
{
// Important !!!
return DynValue.NewYieldReq(new[]
{
DynValue.FromObject(script, new AnonWrapper<TaskDescriptor>(task))
});
});
public class Demo
{
public static TaskDescriptor Delay(int seconds) {
// wrapper the Task by TaskDescriptor
return TaskDescriptor.Build(async () => await Task.Delay(seconds * 1000));
}
public static TaskDescriptor Read(string path)
{
// wrapper the Task<T> by TaskDescriptor
return TaskDescriptor.Build(async () => await File.ReadAllTextAsync(path));
}
}
public static class ScriptExtension
{
public static async Task<DynValue> DoStringAsync(this Script script, string codeScript, CancellationToken cancellation = default)
{
cancellation.ThrowIfCancellationRequested();
// load the code without running
var code = script.LoadString(codeScript);
// create an coroutine for running the code
var coRoutine = script.CreateCoroutine(code);
coRoutine.Coroutine.AutoYieldCounter = 1000;
DynValue scriptResult;
DynValue? resumeArg = null;
while (true)
{
scriptResult = resumeArg == null ? coRoutine.Coroutine.Resume() : coRoutine.Coroutine.Resume(resumeArg);
resumeArg = null;
if (scriptResult.Type == DataType.YieldRequest) // AutoYieldCounter
{
cancellation.ThrowIfCancellationRequested();
}
else if (scriptResult.Type == DataType.UserData)
{
if (scriptResult.UserData.Descriptor.Type != typeof(AnonWrapper))
{
break;
}
var userData = scriptResult.UserData.Object;
if (userData is not AnonWrapper<TaskDescriptor> wrapper)
{
break;
}
var taskDescriptor = wrapper.Value;
var taskResult = await taskDescriptor.Task;
if (taskDescriptor.HasResult)
{
resumeArg = DynValue.FromObject(script, taskResult);
}
}
else
{
break;
}
}
return scriptResult;
}
}
static async Task Main(string[] args)
{
UserData.RegisterType<Demo>();
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion<TaskDescriptor>((script, task) =>
{
// Important !!!
return DynValue.NewYieldReq(new[]
{
DynValue.FromObject(script, new AnonWrapper<TaskDescriptor>(task))
});
});
try
{
var script = new Script();
script.Globals["demo"] = typeof(Demo);
var r = await script.DoStringAsync(await File.ReadAllTextAsync("TestScript.lua"));
Console.WriteLine(r.ToPrintString());
}
catch (Exception e)
{
Console.WriteLine("The lua script abort with exception. \n{0}", e);
}
}
print("Hello World! This is Lua code script.");
print("Before delay...")
demo.delay(3);
print("After delay")
local content = demo.read("TestScript.lua");
print(content);
return 0;
Thank you @lonverce, I used this code and it worked perfectly for me.
MoonSharp already has an extension for executing scripts asynchronously. However, that extension allows pretty much no control over the script's execution. Currently, the only way to do so is through coroutines, which involves writing a bunch of boilerplate every time, and which also makes it more difficult to implement, say, a C#-land binding that needs to pause the script thread (imagine a 'sleep' function). Coroutines also don't really allow scripts to be run in the background. They are still run in the foreground, but pause every once in a while. In order to actually run them in the background AND be able to control their execution, one has to add more control code (Lua thread running coroutine and checking every x instructions for abort, main thread creating that separate Lua thread and controlling it via a reset event).
Right now, we want to:
Additional functionality could obviously be added, like pausing & then resuming execution from within the main flow of the program.
Having support for this in the library makes sense considering it already has async methods. It is also better because it makes everyone else's code cleaner and requires less boilerplate to be written.
This is implemented as follows:
'ExecutionControlToken' provides control of the execution of a script.
Calling 'Terminate' will raise a ScriptTerminationRequestedException from the thread that is running the Lua script. All exceptions can be caught in the same way as with the existing async methods.
The first three methods are modified from the original MoonSharp. They have an additional parameter in the 1st position, which is an 'ExecutionControlToken'. This 'ExecutionControlToken' becomes associated with the execution of the code specified, and it can be associated with multiple scripts.
... is parameters from the non-async methods.
Because 'ecToken' is added as a first parameter to these async methods, this will break compatibility with current 2.x version.
'PauseExecution' is added to 'ScriptExecutionContext' so that C#-land bindings can pause the script thread. This function responds to abort requests, so any call to 'PauseExecution' won't block the normal flow of the program.
Although async extensions are only supported on .NET 4.0+, 'PauseExecution' works on .NET 3.5 as well. On that platform, it simply calls Thread.Sleep. That is because there is no async support anyway, so the script execution is already blocking the thread. This is simply for uniformity.
Update as of 18/05/2019: I still have some doubts regarding this pull request, particularly about how the following: