jint icon indicating copy to clipboard operation
jint copied to clipboard

async/await and callback support

Open christianrondeau opened this issue 6 years ago • 58 comments

I was surprised to find no issues nor pull requests to support C#/JS async/await and Task support.

In a perfect world, I could make that transparent and "halt" the calling method (like #192), or use a callback mechanism (which would make scripts much more complex however).

I'd prefer to avoid using return task.Result and friends, so that I don't get into a deadlock eventually.

Before investigating this further, did anyone actually attempt any async/await support in Jint? @sebastienros is this something you'd be interested in supporting, e.g. through the native ECMA-262 async/await (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)? Any failed or partial attempt?

To avoid any confusion, given this C# code:

public async Task<string> loadSomething(string path) {
  return await File.ReadAllTextAsync(path);
}

I'd consider any of these JavaScript equivalents to be correct (in order of preference):

// Pausing (easiest for script writers):
var x = loadSomething('test.txt');
// Async Function (concise)
var x = await loadSomething('test.txt');
// Promises (well known)
var x;
loadSomething('test.txt').then(function(result) { x = result });;
// Callbacks (worst case)
var x;
loadSomething('test.txt', function(result) { x = result });;

Any ideas or feedback on that is welcome; I'd like to avoid spending any time on this if it would not be merged, or if the effort is too large for what I can afford.

christianrondeau avatar Jun 18 '18 20:06 christianrondeau

Can you not convert simple callback into promise using following?

     public class AsyncService {
          public void LoadAsync(JValue input, JValue then, JValue failed) {
                 Task.Run(async () => {
                         try {  
                                 var result = await ..... async processing in C#
                                 then.Invoke( null, result );
                         } catch(Exception ex) {
                                 failed.Invoke(null, ex.ToString());
                         }
                  });
          }      
     }

             loadAsync( input ) : Promise<any> {
                  return new Promise((resolve,reject) => {
                           asyncService.loadAsync( input, resolve, reject);
                  });
             }


     // now you can use loadAsync method with async await

ackava avatar Jun 25 '18 14:06 ackava

@ackava thanks for sharing your ideas. It looks like you are using TypeScript, which would explain why using the Promise constructor, the => syntax and eventually using await would work for you :) Using a transpiler is an idea worth investigating though.

I realize that supporting async/await would pretty much be supporting ES2017, which has it's own open issue: https://github.com/sebastienros/jint/issues/343 - this would probably (I can only guess) solve my issue, depending on how @sebastienros decided to implement "await" for C#-backed functions. For now, the Promise constructor looks like it's not implemented in the 3.0 branch, so I guess we'll have to wait and see. If I had more time, I'd try contribute this back in Jint, but I don't think I'll be able to take the lead on this for some time.

If someone else wonders how to do transpiling in Jint: https://github.com/sebastienros/jint/issues/274 (did not try it yet)

christianrondeau avatar Jun 25 '18 19:06 christianrondeau

Hi everyone, I echo @christianrondeau in the desire for async/await support.

While promises would work, it would be even simpler if the Engine could just "await" a function delegate passed to "SetValue" - if that delegate wrapped an asynchronous method.

Suppose we had the following C# code:

Engine engine = new Engine(cfg => cfg.AllowClr());
Func<string, Task<string>> thisFunc = DoOperation;
engine.SetValue("DoOperation", thisFunc);

//C# method of DoOperation
public async Task<string> DoOperation(string input)
{
   //assume that SomeAsynchronousOperation returns a string...

   return await SomeAsynchronousOperation();
}

The ideal scenario would then be for an "ExecuteAsync" method on the engine like:

engine.ExecuteAsync("DoOperation('my_input');");

...where internally the engine would "await" the function delegate at the time of invocation.

@sebastienros , is this at all possible? If not, are there any other workarounds you might suggest?

Jint is a phenomenal piece of software though in my particular case, I'm dealing with heavy network I/O. As such, async/await support is critical.

ta10212 avatar Aug 16 '18 22:08 ta10212

Has anybody had any progress on this issue that's worth sharing? I'm in a situation where I need to call some async methods.

ellern avatar Nov 12 '18 18:11 ellern

Hello, I've maybe a solution for you, it doesn't use await syntax but you can use then(...) :

C# function to call from JS :

public static Promise<bool> alert(string msg)
        {
            return new Promise<bool>(Task.Run<Task<bool>>(new Func<Task<bool>>(async() => { 
            
                bool isFinished = false;
                Device.BeginInvokeOnMainThread(async () =>
                {
                    await App.Current.MainPage.DisplayAlert("Message", msg, "Ok");
                    isFinished = true;
                });
                while (!isFinished) await Task.Delay(10);
                return true;
            })));
        }

C# Class "Promise" :

public class Promise<T>
    {
        Task<T> tache;
        public Promise(Task<T> tache)
        {
            this.tache = tache;
        }
        public Promise(Task<Task<T>> tache)
        {
            this.tache = tache.Result;
            
        }
        public void then(Action<T> onfinish)
        {
            this.tache.ContinueWith(rep =>
            {
                onfinish.Invoke(rep.Result);
            });
        }

    }

Javascript code :

var repAlert=alert("valeur interne = "+valeur);
	repAlert.then(function(rep){
		console.log("journal ok rep="+rep); // return Boolean value (true)
	});
	console.log("journal ok repAlert="+repAlert); // Return Promise object

The catch function is not implemented but you'll can do with this example...

This code is a part of a Xamarin project

jbourny avatar Nov 29 '18 07:11 jbourny

Bumping this one for a question: Since Jint is an interpreter, are there any reasons that C# methods cannot be async/return Task, but made look synchronously to the user-script?

I have a case with very many small simple scripts, that should call async methods. I'd like to avoid the complexity of introducing await/Promise objects to the user scripts, because none of the scripts have parallelism requiring actual async code.

ChrML avatar Mar 31 '21 19:03 ChrML

You want to be able to do await, then? But do that automatically. One of the purposes of the Task API is to let you run things in the background, after all.

ayende avatar Apr 01 '21 09:04 ayende

You want to be able to do await, then? But do that automatically. One of the purposes of the Task API is to let you run things in the background, after all.

Yes, but the user scripts in my case don't really need it. Still, the host process needs it to avoid blocking the thread.

ChrML avatar Apr 01 '21 10:04 ChrML

That might actually be a good idea to support, so the API isn't async based to the script, but is async for the usage.

ayende avatar Apr 01 '21 11:04 ayende

Promise support has been implemented, but no support for async/await keywords yet.

lahma avatar May 17 '21 16:05 lahma

May I ask what is block on this ? C# support callling any type that implment GetAwater() and result that implement ICriticalNotifyCompletion, and we can use TaskCompletionSource or IValueTaskSource to wrap any type/method that have callback action.

I wasn't see any problem from C# side (both js->c# and C# -> js)

John0King avatar Aug 12 '22 04:08 John0King

@John0King Its been long since I answered, I have created my own JavaScript engine in C# which supports most of features of ES6 through ES2019. You can try and let me know.

ackava avatar Aug 12 '22 05:08 ackava

Jint now has async/await support (Promise-wise). I'm not sure if it answers the need in this issue though? Anyone want to extend the support to interop side (task.GetAwaiter().GetResult() probably)? Or can we close this as "too generic"?

lahma avatar Oct 20 '22 19:10 lahma

so Jint supports native async / await, but doesn't convert c# Task and others to Promise and vice versa yet?

viceice avatar Oct 20 '22 19:10 viceice

Native for .NET doesn't exist yet, only the JS fullfill/reject chaining. Some brave soul could implement wrapping Task into Promise.

lahma avatar Oct 20 '22 19:10 lahma

ok, will try to have a look at it, as that's now the missing blocker for async usage 😅

viceice avatar Oct 20 '22 19:10 viceice

Probably something like adding task with GetAwaiter().GetResult() to event loop and resolving to its result.

lahma avatar Oct 20 '22 19:10 lahma

Pretty please don't do that :) The whole point of await is freeing the thread and allowing the use of synchronization contexts. Really the only desirable implementation will be painful but very valuable : async all the way (TM) meaning ExecuteAsync, AwaitAsync and friends. If you want I can provide more material to back this, we've been through this in the past too :)

christianrondeau avatar Oct 20 '22 20:10 christianrondeau

@christianrondeau got your attention, now waiting for the PR 😉

lahma avatar Oct 20 '22 20:10 lahma

but we need to make sure there are no real parallel tasks, as it's not expected from JavaScript. there should only be one thread at a time, otherwise it would make interoperability much more complicated.

viceice avatar Oct 20 '22 20:10 viceice

I think that would mean adding the tasks to main event loop and blocking and processing with RunAvailableContinuations, even if that sounds horrible, it's JS after all.

lahma avatar Oct 20 '22 20:10 lahma

Haha yeah I'd love to, but I'm also struggling with time :) This being said, the reason I didn't even try is because I don't understand promises internally, and I know it'll be another huge PR with tons of breaking signature changes.

Or duplicate every involved method to avoid the (minimal) overhead of the Task.

JS can be single threaded, but the promise itself can definitely wait on a C# task that is bound to multiple async tasks (Task.WhenAll). What's important is that after the promise completes, control is given back to the synchronous single threaded javascript code (like node).

My idea that I didn't validate yet was to have a duplicated Execute and Evaluate methods as well as a duplicated RunAvailableContinuations (async and not async), and throw in the non async RunAvailableContinuations if a promise is bound to a Task that is not completed. Not sure if it's that simple in practice though.

I guess the very first question to ask is whether you want to offer only async execute and evaluate methods and pay the small overhead, or provide both signatures and potentially have duplicated methods and tests.

christianrondeau avatar Oct 20 '22 20:10 christianrondeau

I can definitely support @viceice and maybe even try a prototype branch, since I'm the one who opened the issue one year ago... And to be honest, I have to do some gymnastics to work around this so I'd definitely still make use of it. Can't promise anything though.

christianrondeau avatar Oct 20 '22 21:10 christianrondeau

I do have interest in performance and solutions supporting it, but I'm afraid that the true async paths and dispatch to pool would lose the benefits just by the overhead of the interpreter.

Of course hard to measure when only sync path available (in Jint). Async shines in high concurrency but I'm unsure if Jint or any JS interpreting runtime will benefit of it.

lahma avatar Oct 20 '22 21:10 lahma

Actually the main value is to use IO ports instead of locking a thread in the ThreadPool (asp.net being the most obvious example). That was also the great selling point of NodeJS when it came out, not concurrency (well, concurrency of the server, not of the script itself).

It's also very true for Jint, as right now loading a file or accessing resources on a blob storage or any remote location (or use SQL etc) will lock the web thread and eventually kill your web server scalability if you use GetAwaiter as a workaround to provide synchronous methods to Jint. So implementing fake async using GetAwaiter would actually be a trap for novice and uninformed developers using Jint, who may discover they suddenly require more machines with idle CPUs to support the same load.

I hope this makes sense, I can try and provide some more thorough explanation and references if you're not convinced yet :)

christianrondeau avatar Oct 20 '22 21:10 christianrondeau

I think a demo using Jint with different approaches would be great. That would clarify the benefits and probably show the benefits. I have no idea what percent of users jump to actual waiting I/O from engine.

lahma avatar Oct 20 '22 21:10 lahma

I'll try and do two things then (I knew this would come back and bite me!)

  1. I'll at least try to see if I can make an incomplete and poorly tested PR to help scope the actual work and changes involved, and;
  2. Make a case for the need to have a good implementation of async

What I cannot do is estimate how many people would benefit, but anyone who runs Jint in a web server and has any kind of file, http, tcp wait will greatly benefit under scale (not under low volume though).

christianrondeau avatar Oct 20 '22 21:10 christianrondeau

Awesome! One thing to note is that we probably cannot have async/await as c# constructs at the heart of runtime (expression and statement evaluation), that would probably break both performance and ability have deeply nested calls (stack grows a lot with async state machines).

lahma avatar Oct 21 '22 04:10 lahma

In theory, I think we wouldn't need to have them deeply nested. The await of JavaScript is really just "pausing" execution until the promise has been resolved, we can await any async promises, and when they are resolved, continue.

That said, that "pause executing when encountering await and return" would be a significant step forward without doing any Task and C# await stuff (that's one way to break down this issue in smaller pieces). Something like:

// Setup a totally normal promise...
Action<JsValue> resolveFunc = null;
engine.SetValue('f', new Func<JsValue>(() => {
  var (promise, resolve, _) = engine.RegisterPromise();
  resolveFunc = resolve;
  return promise;
}));

// Then, we run code with await in it.
engine.Execute("let x = 1; x = await f(); throw new Error(x)");
Assert.That(engine.Wait, Is.True);
// This would actually run until the await statement and return immediately (not blocking).
// The stack and internal state would stay as-is, and Engine would have a flag Wait

// At this point, you cannot do engine.Evaluate("x") because Jint is in Wait state. You could throw or "queue" the additional scripts (throwing sounds sane to me)

// Technically, you could do C# await here but Jint doesn't care, it's just pending.
await Task.Delay(100);

// Then we can resume execution.
resolveFunc(2);
engine.RunAvailableContinuations();
Assert.That(engine.Wait, Is.False);
// This sets x = 2 and throws. Either resolveFunc  runs continuations, or RunAvailableContinuations continues until the next promise.

That wouldn't be the final behavior for users but it would be very very close. The next step would be to add an AsyncPromise, which is a normal promise but that contains a Task<JsValue> function instead of an Action<JsValue>. Then, an eventual EvaluateAsync would simply run the code, and as long as the result is Wait, await the current Promise sequentially until none are left, and the final completion value is returned. But behind the scene, it would use exactly the same behavior as described above.

That first step would be deep in the bowels of Jint and way out of my league though ;)

christianrondeau avatar Oct 21 '22 05:10 christianrondeau

Also pinging @twop to this discussion as he originally wrote the Promise implementation and is painfully aware about the event loop and task internals 😉

lahma avatar Oct 21 '22 08:10 lahma