• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Coroutines and promises
2The document describes single-threaded coroutine model.
3
4## Classes
5All the classes below are runtime classes.
6
7### Coroutine
8![](coroutine.png)
9
10Coroutine class contains state and the associated Promise object (which will be fulfilled by the entrypoint method's return value).
11
12The state should contain information about coroutine's call stack and other neccessary information to suspend and resume execution.
13When runtime creates a new coroutine new stack is created and coroutine's entrypoint is called on the new stack.
14
15#### Fields
16* promise - the promise associated with the coroutine and returned by 'launch' instruction.
17* state - the coroutine's state needed to suspend and resume the coroutine.
18
19### JobQueue
20![](job_queue.png)
21
22JobQueue interface is used to process promises asynchronously.
23When runtime executes `await` operator runtime puts the promise and the coroutine into the queue and suspends the coroutine.
24When the promise gets processed the waiting coroutine is scheduled again.
25
26### CoroutineManager
27![](coroutine_manager.png)
28
29CoroutineManager manages switching between coroutines. It responsible for suspending the current coroutine and choosing the next coroutine for execution.
30When control is returned from the coroutine entrypoint function, runtime calls `Terminate` for the coroutine.
31
32#### Fields
33* job_queue - an instance of JobQueue to process promises asynchronously.
34* wait_list - a list of waiting coroutines.
35* ready_to_run_list - the list of coroutines which can be scheduled if the current coroutine gets suspended.
36* running_coroutine - the running coroutine.
37
38#### Methods
39* `void Launch(Method entrypoint, List arguments)`
40    * Create an instance of a coroutine with empty stack
41    * Create a promise
42    * Suspend the current coroutine
43    * Put the current coroutine to the front `ready_to_run_list`
44    * Set `running_coroutine` to the new coroutine
45    * Invoke `entrypoint` on the new stack
46* `void Schedule()`
47    * Suspend the current coroutine
48    * Swap the head of `ready_to_run_list` and `current_coroutine`
49    * Resume `current_coroutine`
50* `void Unblock(Coroutine coro)`
51    * Remove the `coro` from the `wait_list`
52    * Put the coro to the front of `ready_to_run_list`
53* `void Await(Promise promise)`
54    * Suspend the current coroutine
55    * Put `promise` argument and the current coroutine to the `job_queue`
56    * Put the current coroutine to the end of `wait_list`
57    * Resume the coroutine from the front of `ready_to_run_list`
58* `void Terminate(Coroutine coro)`
59    * Resolve the promise by return value or reject it if there is an exception
60    * Delete coroutine `coro`
61    * Resume the coroutine from the front of `ready_to_run_list`
62* `void Suspend(Coroutine coro)`
63    * Save current execution state (hw registers, stack pointer, Thread structure) into the `coro`'s context
64* `void Resume(Coroutine coro)`
65    * Set the coroutine `coro` to `running_coroutine`
66    * Restore the coroutine state (hw registers, stack pointer, Thread structure) from coroutine's context
67
68## Coroutine awaiting
69The promise returned by `launch` instruction is used to await the coroutine.
70`await` function applied to the promise suspends execution of the current coroutine even if the promise is fulfilled.
71The promise is put into the JobQueue and the coroutine is put into `wait_list` until the promise gets processed. When the JobQueue processes the promise CoroutineManager schedules the coroutine for execution.
72
73## Coroutine life cycle
74Runtime launches a new coroutine when it executes `launch` instruction. The steps runtime executes are the following:
75
76* Create a new instance of Coroutine
77* Create a new Promise object and set it to the coroutine
78* Create new stack for the coroutine
79* Invoke entrypoint method on the new stack
80
81If the entrypoint method has `@MainThread` or `@Async` annotation the coroutine is always executed in the same thread where the instruction is executing.
82Else the coroutine may be executed in a different thread (implementation dependent).
83
84When the coroutine returns from the entrypoint method the return value is used to fulfill the Promise object if there is no pending exception.
85In case there is a pending exception, the Promise object is rejected and the exception is stored to the Promise object.
86
87The main coroutine is created in a special way during runtime initialization. Runtime doesn't create a new stack for it but uses the existing one.
88Entrypoint method for the main coroutine is specified in the command line arguments.
89
90![](coro_seq.png)
91
92During initialization runtime creates an instance of CoroutineManager and the main coroutine. The main coroutine starts executing `main` function.
93Suppose the main function executes a *launch foo()* instruction i.e. start an asynchronous operation. At this moment CoroutineManager creates an
94instance of coroutine and a Promise. Next CoroutineManager suspends the main coroutine and puts it to `ready_to_run_list`.
95CoroutineManager invokes new coroutine's entrypoint method *foo* on the new stack.
96After the coroutine finished its promise is automatically resolved by entrypoint's method return value.
97At this moment CoroutineManager schedules the next ready to run coroutine (the main coroutine).
98
99## Bytecode representation of async/await constructions
100Below is description of ETS constructions and the corresponding bytecode structures.
101
102| ETS operator | ETS bytecode | Comment |
103| :-- | :-- | :------ |
104|```async <function declaration>``` | ```.function @name@ <ets.annotation=ets.Async>``` | Frontend adds @Async runtime annotation if the source function is declared as `async`. |
105| `fn(args)` | `launch fn_method_id, args` | `launch` bytecode instruction is used to calls async functions. |
106| `await promise`; | `Coroutine.Await(promise)` | `await` is translated into call of the `await` native function implemented in runtime. |
107
108## Interaction with JS runtime
109#### JSMicroJobQueue
110Class JSMicroJobQueue, implementation of JobQueue interface, interacts with micro job queue in JS VM.
111Also this class should track associations between ETS promises and their counterparts in JS world.
112
113![](jsmicro_job_queue.png)
114
115`promise_map` map stores references to ETS and JS Promise objects. The references should be GC roots for both GCs.
116If ETS GC moves an ETS promise it should update the reference in the map. The same is for JS GC.
117
118Implementation of `put` gets the associated JS instance of Promise (or creates a new one) and connects ETS promise to JS instance using `then` callback.
119When the callback is called (i.e. the promise is processed) the corresponding coroutine should be scheduled for execution.
120
121```
122class JSMicroJobQueue: public JobInterface {
123public:
124    virtual void put(Promise promise, Coroutine coro) override {
125        auto js_env = GetJSEnv();
126        // Get associated JS Promise instance
127        js_value js_promise = getJSPromise(promise);
128        if (js_promise == undefined) {
129            js_promise = js_env.create_promise();
130            associate(promise, js_promise)
131        }
132        js_promise.then([&promise, &coro] (js_value value){
133            promise.resolve(unwrap(value));
134            CoroutineManager.Unblock(coro);
135            CoroutineManager.Schedule();
136        });
137        js_env.PushMicroTask(js_promise);
138        CoroutineManager.Await(coro, promise);
139    }
140};
141```
142
143#### Async function calling
144Calling an async function should launch a coroutine.
145In case JS engine calls an ETS function then control should pass through JS2ETS C++ bridge.
146The C++ bridge should check if the callee has @Async annotation and launch a new coroutine.
147Below is a pseudocode of such bridge.
148```
149js_value JS2ETSEntrypoint(Napi::CallbackInfo *info) {
150    auto js_env = info->GetEnv();
151    auto callee = info->GetCallee(); // js callee function object
152    ets_env ets = GetEtsEnv();
153    // get name of function.
154    const char *method_name = js_env.GetProperty(fn, "name");
155    // return the method which corresponds to the JS function
156    Method *method = ets_env.resolve(method_name);
157    if (method->IsAsync()) { // check @Async annotation
158        // for async function we start a coroutine
159        ets_value ets_promise = ets_env.launch(method, proxy(args));
160        // wrap ETS promise (result) to JS promise
161        js_value js_promise = js_env.new_promise();
162        JSMicroJobQueue.map(ets_promise, js_promise);
163        // connect ETS promise with JS promise
164        ets_promise.then([&js_promise] (ets_object value) {
165            js_promise.resolve(wrap(value));
166        });
167        ets_promise.catch([&js_promise] (ets_object value) {
168            js_promise.reject(wrap(value));
169        });
170        return js_promise;
171    }
172}
173```
174
175#### Handling Promise objects returned from JS code
176If ETS code should await the Promise instance returned by a JS function the JS object should be wrapped into
177ETS object and the objects should be associated.
178Wrapping code should look as follow:
179
180```
181ets_object JsPromiseToEtsPromise(js_value js_promise) {
182    js_env js_env = get_js_env();
183    ets_env ets = get_ets_env();
184
185    ets_object ets_promise = ets.create_promise();
186    JSMicroJobQueue.map(ets_promise, js_promise);
187    return ets_promise;
188}
189```
190
191## Example
192
193### TS function calls ETS function.
194Typescript source code:
195```typescript
196async function bar(): Promise<string> {
197    print("enter bar");
198    return "exit bar";
199}
200
201async function foo(): Promise<string> {
202    print("enter foo");
203    print(await bar());
204    return "exit foo";
205}
206```
207
208Translated ETS code is the same except `@Async` annotation is added to `foo` and `bar`.
209
210JS code executed by JS VM:
211```javascript
212async function main() {
213    print("enter main");
214    print(await foo());
215    print("exit main");
216}
217
218main();
219```
220
221Expected output is:
222```
223enter main
224enter foo
225enter bar
226exit bar
227exit foo
228exit main
229```
230
231Execution and state:
232
233`>` - means already executed line
234`>>` - means the current line
235
236<table cellpadding=10 style="border-spacing: 0px">
237<tr bgcolor="#ABB2B9">
238<th>#</th>
239<th>
240Current function
241</th>
242<th>CoroutineManager's state<br>(before the current instruction)</th>
243<th>Output</th>
244<th>Comment</th>
245</tr>
246<tr>
247<td>1.</td>
248<td>
249<pre>
250>>  function main() {
251        print("enter main");
252        print(await foo());
253        print("exit main");
254    }
255</pre>
256</td>
257<td>
258running_coroutine=main<br>
259ready_to_run=[]<br>
260job_queue=[]<br>
261</td>
262<td></td>
263<td>There is only main coroutine exists.</td>
264</tr>
265<tr bgcolor="#EAECEE">
266<td>2.</td>
267<td>
268<pre>
269> function main() {
270>       print("enter main");
271>>      print(await >>foo());
272        print("exit main");
273    }
274</pre>
275</td>
276<td>
277running_coroutine=main<br>
278ready_to_run=[]<br>
279job_queue=[]<br>
280</td>
281<td>enter&nbsp;main</td>
282<td>
283`foo` is an ETS function. From JS point of view `foo` has native entrypoint `foo_impl`.
284</td>
285</tr>
286<tr>
287<td>3.</td>
288<td>
289<pre>
290> js_value foo_impl(Napi::CallbackInfo &info) {
291>   auto js_env = info.GetEnv();
292>   auto callee = info.GetCallee(); // js callee function object (foo)
293>   ets_env ets = GetEtsEnv();
294>   // get name of function. Should return "foo"
295>   const char *method_name = js_env.GetProperty(fn, "name");
296>   // return the method which corresponds to "foo" function
297>   Method *method = ets_env.resolve(method_name);
298>   if (method->IsAsync()) { // check @Async annotation
299>     // for async function we start a coroutine
300>>    ets_value ets_promise = ets_env.launch(method, proxy(args));
301      // wrap ETS promise (result) to JS promise
302      js_value js_promise = js_env.new_promise();
303      // connect ETS promise with JS promise
304      ets_promise.then([&js_promise] (ets_object value) {
305          js_promise.resolve(wrap(value));
306      });
307      ets_promise.catch([&js_promise] (ets_object value) {
308          js_promise.reject(wrap(value));
309      });
310      return js_promise;
311    }
312  }
313</pre>
314</td>
315<td>
316running_coroutine=main<br>
317ready_to_run=[]<br>
318job_queue=[]<br>
319</td>
320<td>
321enter&nbsp;main
322</td>
323<td>
324`foo_impl` is a C++ bridge between JS and ETS. It converts JS values into ETS values, resolves the callee function.
325Since the callee is an async function the bridge creates a coroutine.
326</td>
327</tr>
328<tr bgcolor="#EAECEE">
329<td>4.</td>
330<td>
331<pre>
332  @Async
333> function foo(): String {
334>     print("enter foo");
335>>    print(await >>bar());
336      return "exit foo";
337  }
338</pre>
339</td>
340<td>
341running_coroutine=foo<br>
342ready_to_run=[main]<br>
343job_queue=[]<br>
344</td>
345<td>
346enter&nbsp;main<br>
347enter&nbsp;foo<br>
348</td>
349<td>
350Launching `foo` leads to put the `main` coroutine to the beginning of `ready_to_run_list`.<br>
351Since `bar` is an async function a coroutine will be created.
352</td>
353</tr>
354<tr>
355<td>5.</td>
356<td>
357<pre>
358  @Async
359> function bar(): String {
360>     print("enter bar");
361>     return "exit bar";
362>>}
363</pre>
364</td>
365<td>
366running_coroutine=bar<br>
367ready_to_run=[foo,main]<br>
368job_queue=[]<br>
369</td>
370<td>
371enter&nbsp;main<br>
372enter&nbsp;foo<br>
373enter&nbsp;bar<br>
374</td>
375<td>
376Launching `bar` coroutine ejects `foo` coroutine to the beginning of `ready_to_run_list`.<br>
377When `bar` is finised it resolves the associated promise and the coroutine gets terminated.
378Runtime resumes `foo` coroutine.
379</td>
380</tr>
381<tr bgcolor="#EAECEE">
382<td>6.</td>
383<td>
384<pre>
385  @Async
386> function foo(): String {
387>     print("enter foo");
388>>    print(>>await bar());
389      return "exit foo";
390  }
391</pre>
392</td>
393<td>
394running_coroutine=foo<br>
395ready_to_run=[main]<br>
396job_queue=[]<br>
397</td>
398<td>
399enter&nbsp;main<br>
400enter&nbsp;foo<br>
401enter&nbsp;bar<br>
402</td>
403<td>
404Awaiting `bar`'s promise leads to call of MicroJobQueue.
405</td>
406</tr>
407<tr>
408<td>7.</td>
409<td>
410<pre>
411> virtual void MicroJobQueue::put(Promise promise, Coroutine coro) override {
412>     auto js_env = GetJSEnv();
413>     js_value js_promise = js_env.create_promise();
414>     js_promise.then([&promise, &coro] (js_value value){
415          promise.resolve(unwrap(value));
416          CoroutineManager.Unblock(coro);
417          CoroutineManager.Schedule();
418>     });
419>     js_env.PushMicroTask(js_promise);
420>>    CoroutineManager.Await(coro, promise);
421  }
422</pre>
423</td>
424<td>
425running_coroutine=foo<br>
426ready_to_run=[main]<br>
427job_queue=[bar's&nbsp;promise]<br>
428</td>
429<td>
430enter&nbsp;main<br>
431enter&nbsp;foo<br>
432enter&nbsp;bar<br>
433</td>
434<td>
435The method creates a JS mirror of bar's promise and adds it to JS engine's MicroJobQueue.<br>
436`await` method will put `foo` coroutine to `wait_list`. The main coroutine will be resumed in `foo_impl`.
437</td>
438</tr>
439<tr bgcolor="#EAECEE">
440<td>8.</td>
441<td>
442<pre>
443> js_value foo_impl(Napi::CallbackInfo &info) {
444>   auto js_env = info.GetEnv();
445>   auto callee = info.GetCallee(); // js callee function object (foo)
446>   ets_env ets = GetEtsEnv();
447>   // get name of function. Should return "foo"
448>   const char *method_name = js_env.GetProperty(fn, "name");
449>   // return the method which corresponds to "foo" function
450>   Method *method = ets_env.resolve(method_name);
451>   if (method->IsAsync()) { // check @Async annotation
452>     // for async function we start a coroutine
453>     ets_value ets_promise = ets_env.launch(method, proxy(args));
454>     // wrap ETS promise (result) to JS promise
455>     js_value js_promise = js_env.new_promise();
456>     // connect ETS promise with JS promise
457>     ets_promise.then([&js_promise] (ets_object value) {
458          js_promise.resolve(wrap(value));
459>     });
460>     ets_promise.catch([&js_promise] (ets_object value) {
461          js_promise.reject(wrap(value));
462>     });
463>     return js_promise;
464    }
465  }
466</pre>
467</td>
468<td>
469running_coroutine=main<br>
470ready_to_run=[]<br>
471wait_list=[foo]<br>
472job_queue=[bar's&nbsp;promise]<br>
473</td>
474<td>
475enter&nbsp;main<br>
476enter&nbsp;foo<br>
477enter&nbsp;bar<br>
478</td>
479<td>
480Runtime resumes the main coroutine in `foo_impl`. The returned ETS promise is wrapped into JS promise and
481the promises are connected. I.e. resolving/rejecting of ETS promise leads to resolving/rejecting JS promise.
482</td>
483</tr>
484<tr>
485<td>9.</td>
486<td>
487<pre>
488> function main() {
489>       print("enter main");
490>>      print(>>await foo());
491        print("exit main");
492    }
493</pre>
494</td>
495<td>
496running_coroutine=main<br>
497ready_to_run=[]<br>
498wait_list=[foo]<br>
499job_queue=[bar's&nbsp;promise]<br>
500</td>
501<td>
502enter&nbsp;main<br>
503enter&nbsp;foo<br>
504enter&nbsp;bar<br>
505</td>
506<td>
507JS runtime puts the promise returned by `foo` into job queue and gets suspended.
508</td>
509</tr>
510<tr bgcolor="#EAECEE">
511<td>10.</td>
512<td>
513<pre>
514>  main();
515>> process_micro_job_queue();
516</pre>
517</td>
518<td>
519running_coroutine=main<br>
520ready_to_run=[]<br>
521wait_list=[foo]<br>
522job_queue=[bar's&nbsp;promise,foo's&nbsp;promise]<br>
523</td>
524<td>
525enter&nbsp;main<br>
526enter&nbsp;foo<br>
527enter&nbsp;bar<br>
528</td>
529<td>
530When the main function is finished JS runtime starts processing micro job queue.
531The first promise in the queue is bar's promise. Since it is already resolved JS runtime
532calls its 'then' method which resumes `foo` coroutine.
533</td>
534</tr>
535<tr>
536<td>11.</td>
537<td>
538<pre>
539> virtual void MicroJobQueue::put(Promise promise, Coroutine coro) override {
540>     auto js_env = GetJSEnv();
541>     js_value js_promise = js_env.create_promise();
542>     js_promise.then([&promise, &coro] (js_value value){
543>         promise.resolve(unwrap(value));
544>         CoroutineManager.Unblock(coro);
545>>        CoroutineManager.Schedule();
546>     });
547>     js_env.PushMicroTask(js_promise);
548>     CoroutineManager.Await(coro, promise);
549  }
550</pre>
551</td>
552<td>
553running_coroutine=main<br>
554ready_to_run=[foo]<br>
555wait_list=[]<br>
556job_que=[foo's&nbsp;promise]<br>
557</td>
558<td>
559enter&nbsp;main<br>
560enter&nbsp;foo<br>
561enter&nbsp;bar<br>
562</td>
563<td>
564MicroJobQueue calls `then` method for `bar` promise. The method schedules `foo` coroutine.<br>
565The main coroutine is ejected into `ready_to_run_list` and `foo` continues execution.
566</td>
567</tr>
568<tr bgcolor="#EAECEE">
569<td>12.</td>
570<td>
571<pre>
572  @Async
573> function foo(): String {
574>     print("enter foo");
575>     print(await bar());
576>     return "exit foo";
577>>}
578</pre>
579</td>
580<td>
581running_coroutine=foo<br>
582ready_to_run=[main]<br>
583wait_list=[]<br>
584job_queue=[foo's&nbsp;promise]<br>
585</td>
586<td>
587enter&nbsp;main<br>
588enter&nbsp;foo<br>
589enter&nbsp;bar<br>
590exit&nbsp;bar<br>
591</td>
592<td>
593`foo` coroutine is terminated and runtime resolves its promise by the returned value.
594Then runtime resumes the main coroutine.
595</td>
596</tr>
597<tr>
598<td>13.</td>
599<td>
600<pre>
601>  main();
602>> process_micro_job_queue();
603</pre>
604</td>
605<td>
606running_coroutine=main<br>
607ready_to_run=[]<br>
608wait_list=[]<br>
609job_queue=[foo's&nbsp;promise]<br>
610</td>
611<td>
612enter&nbsp;main<br>
613enter&nbsp;foo<br>
614enter&nbsp;bar<br>
615exit&nbsp;bar<br>
616</td>
617<td>
618JS engine continues processing micro job queue. The next task is foo's promise which already resolved.
619</td>
620</tr>
621<tr bgcolor="#EAECEE">
622<td>14.</td>
623<td>
624<pre>
625> function main() {
626>       print("enter main");
627>       print(await foo());
628>       print("exit main");
629>>  }
630</pre>
631</td>
632<td>
633running_coroutine=main<br>
634ready_to_run=[]<br>
635wait_list=[]<br>
636job_queue=[]<br>
637</td>
638<td>
639enter&nbsp;main<br>
640enter&nbsp;foo<br>
641enter&nbsp;bar<br>
642exit&nbsp;bar<br>
643exit&nbsp;foo<br>
644exit&nbsp;main<br>
645</td>
646<td>
647After processing foo's promise the main function gets resumed and finishes its execution.
648</td>
649</tr>
650<tr>
651</table>
652