jsrt-dotnet
jsrt-dotnet copied to clipboard
Using async/await features to simulate callbacks
How would one use c# async features to simulate js-style callbacks? (see: node.js fs API )
Imagine I have the following C# method:
async Task<string> ReadFileAsync(string path)
{
return await DoWhatever(path);
}
How would I register a js function which takes a string and a callback as arguments and calls it asynchronously?
I have started this by adding the branch HostObjectBridgeV2. You can create a non-named async function that corresponds to a Task; when it is called, it returns a Promise. You can use JavaScript to convert the Promise to a callback-style function.
This is great! Any samples? EDIT: sorry, just watched the branche. I will try to implement something similar ( I am having some troubles making your lib work, so I have to rely on the standard implementation...)
The async host function is here and the invocation is here.
What are the troubles you're having with my lib?
Thanks. Unfortunately I am not near a PC as of now, so I don't remember exactly, but I had to try compiling chakracore for different archs and configurations and still I got unbalanced stacks, if I recall correctly. Will report when I get home again!
Unbalanced stacks should be addressed with recent pushes to master.
On Jan 21, 2016, at 12:01 PM, Francesco Bertolaccini [email protected] wrote:
Thanks. Unfortunately I am not near a PC as of now, so I don't remember exactly, but I had to try compiling chakracore for different archs and configurations and still I got unbalanced stacks, if I recall correctly. Will report when I get home again!
— Reply to this email directly or view it on GitHub.
I just tried the method you implemented, but async functions never seem to get past the first await
instruction...
public static async Task<JavaScriptValue> Sum(JavaScriptEngine callingEngine, JavaScriptValue thisValue, IEnumerable<JavaScriptValue> args)
{
int[] values = args.Select(jsv => callingEngine.Converter.ToInt32(jsv)).ToArray();
await Task.Delay(5000); // I can't get past here. It also won't work if I change Task.Delay with something else, like my original method
using (var ctx = callingEngine.AcquireContext())
{
return callingEngine.Converter.FromInt32(values.Sum());
}
}
EDIT: It actually won't work even if I remove all await statements EDIT2: Actually, the culprit is AcquireContext(), which never gets executed...
@frabert Can you share a sample project? I can't repro.
While I was creating a suitable test project I was able to found the proper issue :grin:
The script throws a JsErrorWrongThread
error.
This is the source: https://gist.github.com/frabert/89d1fd801b1761fbb377
@robpaveza Any news on this one?
Hey Francesco, sorry, not yet. I'm in the process of moving out of state right now so I've been tied up. Next week for sure! On Sat, Jan 30, 2016 at 8:08 AM Francesco Bertolaccini < [email protected]> wrote:
@robpaveza https://github.com/robpaveza Any news on this one?
— Reply to this email directly or view it on GitHub https://github.com/robpaveza/jsrt-dotnet/issues/2#issuecomment-177220672 .
No problem!
OK @frabert, I've been able to repro this. It's caused by a thread switch during an await
.
This is unfortunately a nontrivial problem to solve. It's related to the threading model of the host application. I can probably come up with a good solution for it when in a Windows Forms, WPF, or UAP scenario (read: when there is a non-null SynchronizationContext.Current
). However, the API for that solution would probably be different than when it is null, such as in a console application.
I have to give this some thought. I'll welcome any PRs that you might submit dealing with this root cause.
OK, thanks for looking into this! I'll see what I can find out
Hmm I think I might have stepped on something I don't understand, intimately related to Chakra. Here's what I've been doing. Since I am writing a game, I already have a game loop which I can use to manage the async tasks. This way I am not really doing async stuff, but at least it's single thread and inherently thread-safe. This is how I implemented it:
class LoopingSynchronizationContext : SynchronizationContext
{
Queue<KeyValuePair<SendOrPostCallback, object>> queue = new Queue<KeyValuePair<SendOrPostCallback, object>>();
public override void Post(SendOrPostCallback d, object state)
{
if (d == null) throw new ArgumentNullException("d");
queue.Enqueue(new KeyValuePair<SendOrPostCallback, object>(d, state));
}
public override void Send(SendOrPostCallback d, object state)
{
throw new NotSupportedException("Synchronously sending is not supported.");
}
public void Loop()
{
if(queue.Count != 0)
{
var pair = queue.Dequeue();
pair.Key(pair.Value);
}
}
}
As you can see, it just enqueues tasks and executes them in a sequential manner, with no spawning of threads. Then the program:
static void Main(string[] args)
{
var sync = new LoopingSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(sync);
Thread.CurrentThread.Name = "hello";
var factory = new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext());
using (var runtime = new JavaScriptRuntime())
using (var engine = runtime.CreateEngine())
using (var context = engine.AcquireContext())
{
var obj = engine.CreateObject();
// I modified the CreateFunction method so that it could take a custom TaskFactory from which
// to create tasks.
obj.SetPropertyByName("sum", engine.CreateFunction(JSMethods.Sum, factory));
obj.SetPropertyByName("print", engine.CreateFunction(JSMethods.Print));
engine.GlobalObject.SetPropertyByName("obj", obj);
var fn = engine.EvaluateScriptText(
@"(() =>
{
obj.sum(1,2,3,4).then(obj.print);
obj.print('hello world');
})()");
fn.Invoke(Enumerable.Empty<JavaScriptValue>());
while (true)
{
sync.Loop();
}
}
}
I checked the thread name in all the spawned tasks and it was always the 'hello', i.e. the right one! Yet, it still throws the JsErrorWrongThread thing. I think I might need some insights on where else are thread spawned...
Hey! I found (a part of?) the problem! In the Sum
function I incorrectly convert the integers to objects before acquiring the context. I still don't understand why it launches a WrongThread error instead of something like "no context"...