jsrt-dotnet icon indicating copy to clipboard operation
jsrt-dotnet copied to clipboard

Using async/await features to simulate callbacks

Open frabert opened this issue 9 years ago • 22 comments

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?

frabert avatar Jan 17 '16 19:01 frabert

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.

robpaveza avatar Jan 21 '16 19:01 robpaveza

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...)

frabert avatar Jan 21 '16 19:01 frabert

The async host function is here and the invocation is here.

What are the troubles you're having with my lib?

robpaveza avatar Jan 21 '16 19:01 robpaveza

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!

frabert avatar Jan 21 '16 20:01 frabert

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.

robpaveza avatar Jan 21 '16 20:01 robpaveza

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 avatar Jan 23 '16 16:01 frabert

@frabert Can you share a sample project? I can't repro.

robpaveza avatar Jan 25 '16 23:01 robpaveza

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

frabert avatar Jan 26 '16 17:01 frabert

@robpaveza Any news on this one?

frabert avatar Jan 30 '16 16:01 frabert

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 .

robpaveza avatar Jan 30 '16 19:01 robpaveza

No problem!

frabert avatar Jan 30 '16 22:01 frabert

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.

robpaveza avatar Feb 02 '16 05:02 robpaveza

OK, thanks for looking into this! I'll see what I can find out

frabert avatar Feb 02 '16 06:02 frabert

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...

frabert avatar Feb 02 '16 18:02 frabert

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"...

frabert avatar Feb 03 '16 18:02 frabert