Skip to content

Commit e9c898c

Browse files
committed
webassembly/asyncio: Support top-level await of asyncio Task and Event.
This change allows doing a top-level await on an asyncio primitive like Task and Event. This feature enables a better interaction and synchronisation between JavaScript and Python, because `api.runPythonAsync` can now be used (called from JavaScript) to await on the completion of asyncio primitives. Signed-off-by: Damien George <[email protected]>
1 parent a053e63 commit e9c898c

File tree

6 files changed

+83
-22
lines changed

6 files changed

+83
-22
lines changed

ports/webassembly/asyncio/core.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ def __next__(self):
5050
# Pause task execution for the given time (integer in milliseconds, uPy extension)
5151
# Use a SingletonGenerator to do it without allocating on the heap
5252
def sleep_ms(t, sgen=SingletonGenerator()):
53-
if cur_task is None:
54-
# Support top-level asyncio.sleep, via a JavaScript Promise.
55-
return jsffi.async_timeout_ms(t)
5653
assert sgen.state is None
5754
sgen.state = ticks_add(ticks(), max(0, t))
5855
return sgen
@@ -69,6 +66,18 @@ def sleep(t):
6966
asyncio_timer = None
7067

7168

69+
class TopLevelCoro:
70+
@staticmethod
71+
def set(resolve, reject):
72+
TopLevelCoro.resolve = resolve
73+
TopLevelCoro.reject = reject
74+
_schedule_run_iter(0)
75+
76+
@staticmethod
77+
def send(value):
78+
TopLevelCoro.resolve()
79+
80+
7281
class ThenableEvent:
7382
def __init__(self, thenable):
7483
self.result = None # Result of the thenable
@@ -122,12 +131,12 @@ def _run_iter():
122131
dt = max(0, ticks_diff(t.ph_key, ticks()))
123132
else:
124133
# No tasks can be woken so finished running
125-
cur_task = None
134+
cur_task = _top_level_task
126135
return
127136

128137
if dt > 0:
129138
# schedule to call again later
130-
cur_task = None
139+
cur_task = _top_level_task
131140
_schedule_run_iter(dt)
132141
return
133142

@@ -198,11 +207,14 @@ def create_task(coro):
198207
return t
199208

200209

210+
# Task used to suspend and resume top-level await.
211+
_top_level_task = Task(TopLevelCoro, globals())
212+
201213
################################################################################
202214
# Event loop wrapper
203215

204216

205-
cur_task = None
217+
cur_task = _top_level_task
206218

207219

208220
class Loop:

ports/webassembly/modjsffi.c

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,6 @@ static mp_obj_t mp_jsffi_to_js(mp_obj_t arg) {
6262
}
6363
static MP_DEFINE_CONST_FUN_OBJ_1(mp_jsffi_to_js_obj, mp_jsffi_to_js);
6464

65-
// *FORMAT-OFF*
66-
EM_JS(void, promise_with_timeout_ms, (double ms, uint32_t * out), {
67-
const ret = new Promise((resolve) => setTimeout(resolve, ms));
68-
proxy_convert_js_to_mp_obj_jsside(ret, out);
69-
});
70-
// *FORMAT-ON*
71-
72-
static mp_obj_t mp_jsffi_async_timeout_ms(mp_obj_t arg) {
73-
uint32_t out[PVN];
74-
promise_with_timeout_ms(mp_obj_get_float_to_d(arg), out);
75-
return proxy_convert_js_to_mp_obj_cside(out);
76-
}
77-
static MP_DEFINE_CONST_FUN_OBJ_1(mp_jsffi_async_timeout_ms_obj, mp_jsffi_async_timeout_ms);
78-
7965
// *FORMAT-OFF*
8066
EM_JS(void, js_get_proxy_js_ref_info, (uint32_t * out), {
8167
let used = 0;
@@ -121,7 +107,6 @@ static const mp_rom_map_elem_t mp_module_jsffi_globals_table[] = {
121107
{ MP_ROM_QSTR(MP_QSTR_JsException), MP_ROM_PTR(&mp_type_JsException) },
122108
{ MP_ROM_QSTR(MP_QSTR_create_proxy), MP_ROM_PTR(&mp_jsffi_create_proxy_obj) },
123109
{ MP_ROM_QSTR(MP_QSTR_to_js), MP_ROM_PTR(&mp_jsffi_to_js_obj) },
124-
{ MP_ROM_QSTR(MP_QSTR_async_timeout_ms), MP_ROM_PTR(&mp_jsffi_async_timeout_ms_obj) },
125110
{ MP_ROM_QSTR(MP_QSTR_mem_info), MP_ROM_PTR(&mp_jsffi_mem_info_obj) },
126111
};
127112
static MP_DEFINE_CONST_DICT(mp_module_jsffi_globals, mp_module_jsffi_globals_table);

ports/webassembly/proxy_c.c

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,12 @@ EM_JS(void, js_then_continue, (int jsref, uint32_t * py_resume, uint32_t * resol
470470
});
471471
// *FORMAT-ON*
472472

473+
EM_JS(void, create_promise, (uint32_t * out_set, uint32_t * out_promise), {
474+
const out_set_js = proxy_convert_mp_to_js_obj_jsside(out_set);
475+
const promise = new Promise(out_set_js);
476+
proxy_convert_js_to_mp_obj_jsside(promise, out_promise);
477+
});
478+
473479
static mp_obj_t proxy_resume_execute(mp_obj_t self_in, mp_obj_t send_value, mp_obj_t throw_value, mp_obj_t resolve, mp_obj_t reject) {
474480
if (throw_value != MP_OBJ_NULL && throw_value != mp_const_none) {
475481
if (send_value == mp_const_none) {
@@ -483,6 +489,9 @@ static mp_obj_t proxy_resume_execute(mp_obj_t self_in, mp_obj_t send_value, mp_o
483489
}
484490
} else {
485491
throw_value = MP_OBJ_NULL;
492+
if (send_value == mp_const_undefined) {
493+
send_value = mp_const_none;
494+
}
486495
}
487496

488497
mp_obj_t ret_value;
@@ -496,7 +505,29 @@ static mp_obj_t proxy_resume_execute(mp_obj_t self_in, mp_obj_t send_value, mp_o
496505
js_then_resolve(out_ret_value, out_resolve);
497506
return mp_const_none;
498507
} else if (ret_kind == MP_VM_RETURN_YIELD) {
499-
// ret_value should be a JS thenable
508+
// If ret_value is None then there has been a top-level await of an asyncio primitive.
509+
// Otherwise, ret_value should be a JS thenable.
510+
511+
if (ret_value == mp_const_none) {
512+
// Waiting on an asyncio primitive to complete, eg a Task or Event.
513+
//
514+
// Completion of this primitive will occur when the asyncio.core._top_level_task
515+
// Task is made runable and its coroutine's send() method is called. Need to
516+
// construct a Promise that resolves when that send() method is called, because
517+
// that will resume the top-level await from the JavaScript side.
518+
//
519+
// This is accomplished via the asyncio.core.TopLevelCoro class and its methods.
520+
mp_obj_t asyncio = mp_import_name(MP_QSTR_asyncio_dot_core, mp_const_none, MP_OBJ_NEW_SMALL_INT(0));
521+
mp_obj_t asyncio_core = mp_load_attr(asyncio, MP_QSTR_core);
522+
mp_obj_t top_level_coro = mp_load_attr(asyncio_core, MP_QSTR_TopLevelCoro);
523+
mp_obj_t top_level_coro_set = mp_load_attr(top_level_coro, MP_QSTR_set);
524+
uint32_t out_set[PVN];
525+
proxy_convert_mp_to_js_obj_cside(top_level_coro_set, out_set);
526+
uint32_t out_promise[PVN];
527+
create_promise(out_set, out_promise);
528+
ret_value = proxy_convert_js_to_mp_obj_cside(out_promise);
529+
}
530+
500531
mp_obj_t py_resume = mp_obj_new_bound_meth(MP_OBJ_FROM_PTR(&resume_obj), self_in);
501532
int ref = mp_obj_jsproxy_get_ref(ret_value);
502533
uint32_t out_py_resume[PVN];

ports/webassembly/qstrdefsport.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
// qstrs specific to this port
22
// *FORMAT-OFF*
33
Q(/lib)
4+
Q(asyncio.core)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Test top-level await on asyncio primitives: Task, Event.
2+
3+
const mp = await (await import(process.argv[2])).loadMicroPython();
4+
5+
await mp.runPythonAsync(`
6+
import asyncio
7+
8+
async def task(event):
9+
print("task set event")
10+
event.set()
11+
print("task sleep")
12+
await asyncio.sleep(0.1)
13+
print("task end")
14+
15+
event = asyncio.Event()
16+
t = asyncio.create_task(task(event))
17+
18+
print("top-level wait event")
19+
await event.wait()
20+
print("top-level wait task")
21+
await t
22+
print("top-level end")
23+
`);
24+
25+
console.log("finished");
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
top-level wait event
2+
task set event
3+
task sleep
4+
top-level wait task
5+
task end
6+
top-level end
7+
finished

0 commit comments

Comments
 (0)