Content-Length: 1115167 | pFad | http://github.com/micropython/micropython/commit/8a3546b3bd71dbc6d79900afbe58767e09b82c3e

CD webassembly: Add JavaScript-based asyncio support. · micropython/micropython@8a3546b · GitHub
Skip to content

Commit 8a3546b

Browse files
committed
webassembly: Add JavaScript-based asyncio support.
This commit adds a significant portion of the existing MicroPython asyncio module to the webassembly port, using parts of the existing asyncio code and some custom JavaScript parts. The key difference to the standard asyncio is that this version uses the JavaScript runtime to do the actual scheduling and waiting on events, eg Promise fulfillment, timeouts, fetching URLs. This implementation does not include asyncio.run(). Instead one just uses asyncio.create_task(..) to start tasks and then returns to the JavaScript. Then JavaScript will run the tasks. The implementation here tries to reuse as much existing asyncio code as possible, and gets all the semantics correct for things like cancellation and asyncio.wait_for. An alternative approach would reimplement Task, Event, etc using JavaScript Promise's. That approach is very difficult to get right when trying to implement cancellation (because it's not possible to cancel a JavaScript Promise). Signed-off-by: Damien George <damien@micropython.org>
1 parent 84d6f8e commit 8a3546b

File tree

11 files changed

+407
-0
lines changed

11 files changed

+407
-0
lines changed

Diff for: ports/webassembly/Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ BUILD ?= build-$(VARIANT)
2222
include ../../py/mkenv.mk
2323
include $(VARIANT_DIR)/mpconfigvariant.mk
2424

25+
# Use the default frozen manifest, variants may override this.
26+
FROZEN_MANIFEST ?= variants/manifest.py
27+
2528
# Qstr definitions (must come before including py.mk).
2629
QSTR_DEFS = qstrdefsport.h
2730

Diff for: ports/webassembly/asyncio/__init__.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# MicroPython asyncio module, for use with webassembly port
2+
# MIT license; Copyright (c) 2024 Damien P. George
3+
4+
from .core import *
5+
from .funcs import wait_for, wait_for_ms, gather
6+
from .event import Event
7+
from .lock import Lock
8+
9+
__version__ = (3, 0, 0)

Diff for: ports/webassembly/asyncio/core.py

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
# MicroPython asyncio module, for use with webassembly port
2+
# MIT license; Copyright (c) 2019-2024 Damien P. George
3+
4+
from time import ticks_ms as ticks, ticks_diff, ticks_add
5+
import sys, js, jsffi
6+
7+
# Import TaskQueue and Task from built-in C code.
8+
from _asyncio import TaskQueue, Task
9+
10+
11+
################################################################################
12+
# Exceptions
13+
14+
15+
class CancelledError(BaseException):
16+
pass
17+
18+
19+
class TimeoutError(Exception):
20+
pass
21+
22+
23+
# Used when calling Loop.call_exception_handler.
24+
_exc_context = {"message": "Task exception wasn't retrieved", "exception": None, "future": None}
25+
26+
27+
################################################################################
28+
# Sleep functions
29+
30+
31+
# "Yield" once, then raise StopIteration
32+
class SingletonGenerator:
33+
def __init__(self):
34+
self.state = None
35+
self.exc = StopIteration()
36+
37+
def __iter__(self):
38+
return self
39+
40+
def __next__(self):
41+
if self.state is not None:
42+
_task_queue.push(cur_task, self.state)
43+
self.state = None
44+
return None
45+
else:
46+
self.exc.__traceback__ = None
47+
raise self.exc
48+
49+
50+
# Pause task execution for the given time (integer in milliseconds, uPy extension)
51+
# Use a SingletonGenerator to do it without allocating on the heap
52+
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)
56+
assert sgen.state is None
57+
sgen.state = ticks_add(ticks(), max(0, t))
58+
return sgen
59+
60+
61+
# Pause task execution for the given time (in seconds)
62+
def sleep(t):
63+
return sleep_ms(int(t * 1000))
64+
65+
66+
################################################################################
67+
# Main run loop
68+
69+
asyncio_timer = None
70+
71+
72+
class ThenableEvent:
73+
def __init__(self, thenable):
74+
self.result = None # Result of the thenable
75+
self.waiting = None # Task waiting on completion of this thenable
76+
thenable.then(self.set)
77+
78+
def set(self, value):
79+
# Thenable/Promise is fulfilled, set result and schedule any waiting task.
80+
self.result = value
81+
if self.waiting:
82+
_task_queue.push(self.waiting)
83+
self.waiting = None
84+
_schedule_run_iter(0)
85+
86+
def remove(self, task):
87+
self.waiting = None
88+
89+
# async
90+
def wait(self):
91+
# Set the calling task as the task waiting on this thenable.
92+
self.waiting = cur_task
93+
# Set calling task's data to this object so it can be removed if needed.
94+
cur_task.data = self
95+
# Wait for the thenable to fulfill.
96+
yield
97+
# Return the result of the thenable.
98+
return self.result
99+
100+
101+
# Ensure the awaitable is a task
102+
def _promote_to_task(aw):
103+
return aw if isinstance(aw, Task) else create_task(aw)
104+
105+
106+
def _schedule_run_iter(dt):
107+
global asyncio_timer
108+
if asyncio_timer is not None:
109+
js.clearTimeout(asyncio_timer)
110+
asyncio_timer = js.setTimeout(_run_iter, dt)
111+
112+
113+
def _run_iter():
114+
global cur_task
115+
excs_all = (CancelledError, Exception) # To prevent heap allocation in loop
116+
excs_stop = (CancelledError, StopIteration) # To prevent heap allocation in loop
117+
while True:
118+
# Wait until the head of _task_queue is ready to run
119+
t = _task_queue.peek()
120+
if t:
121+
# A task waiting on _task_queue; "ph_key" is time to schedule task at
122+
dt = max(0, ticks_diff(t.ph_key, ticks()))
123+
else:
124+
# No tasks can be woken so finished running
125+
cur_task = None
126+
return
127+
128+
if dt > 0:
129+
# schedule to call again later
130+
cur_task = None
131+
_schedule_run_iter(dt)
132+
return
133+
134+
# Get next task to run and continue it
135+
t = _task_queue.pop()
136+
cur_task = t
137+
try:
138+
# Continue running the coroutine, it's responsible for rescheduling itself
139+
exc = t.data
140+
if not exc:
141+
t.coro.send(None)
142+
else:
143+
# If the task is finished and on the run queue and gets here, then it
144+
# had an exception and was not await'ed on. Throwing into it now will
145+
# raise StopIteration and the code below will catch this and run the
146+
# call_exception_handler function.
147+
t.data = None
148+
t.coro.throw(exc)
149+
except excs_all as er:
150+
# Check the task is not on any event queue
151+
assert t.data is None
152+
# This task is done.
153+
if t.state:
154+
# Task was running but is now finished.
155+
waiting = False
156+
if t.state is True:
157+
# "None" indicates that the task is complete and not await'ed on (yet).
158+
t.state = None
159+
elif callable(t.state):
160+
# The task has a callback registered to be called on completion.
161+
t.state(t, er)
162+
t.state = False
163+
waiting = True
164+
else:
165+
# Schedule any other tasks waiting on the completion of this task.
166+
while t.state.peek():
167+
_task_queue.push(t.state.pop())
168+
waiting = True
169+
# "False" indicates that the task is complete and has been await'ed on.
170+
t.state = False
171+
if not waiting and not isinstance(er, excs_stop):
172+
# An exception ended this detached task, so queue it for later
173+
# execution to handle the uncaught exception if no other task retrieves
174+
# the exception in the meantime (this is handled by Task.throw).
175+
_task_queue.push(t)
176+
# Save return value of coro to pass up to caller.
177+
t.data = er
178+
elif t.state is None:
179+
# Task is already finished and nothing await'ed on the task,
180+
# so call the exception handler.
181+
182+
# Save exception raised by the coro for later use.
183+
t.data = exc
184+
185+
# Create exception context and call the exception handler.
186+
_exc_context["exception"] = exc
187+
_exc_context["future"] = t
188+
Loop.call_exception_handler(_exc_context)
189+
190+
191+
# Create and schedule a new task from a coroutine.
192+
def create_task(coro):
193+
if not hasattr(coro, "send"):
194+
raise TypeError("coroutine expected")
195+
t = Task(coro, globals())
196+
_task_queue.push(t)
197+
_schedule_run_iter(0)
198+
return t
199+
200+
201+
################################################################################
202+
# Event loop wrapper
203+
204+
205+
cur_task = None
206+
207+
208+
class Loop:
209+
_exc_handler = None
210+
211+
def create_task(coro):
212+
return create_task(coro)
213+
214+
def close():
215+
pass
216+
217+
def set_exception_handler(handler):
218+
Loop._exc_handler = handler
219+
220+
def get_exception_handler():
221+
return Loop._exc_handler
222+
223+
def default_exception_handler(loop, context):
224+
print(context["message"], file=sys.stderr)
225+
print("future:", context["future"], "coro=", context["future"].coro, file=sys.stderr)
226+
sys.print_exception(context["exception"], sys.stderr)
227+
228+
def call_exception_handler(context):
229+
(Loop._exc_handler or Loop.default_exception_handler)(Loop, context)
230+
231+
232+
def get_event_loop():
233+
return Loop
234+
235+
236+
def current_task():
237+
if cur_task is None:
238+
raise RuntimeError("no running event loop")
239+
return cur_task
240+
241+
242+
def new_event_loop():
243+
global _task_queue
244+
_task_queue = TaskQueue() # TaskQueue of Task instances.
245+
return Loop
246+
247+
248+
# Initialise default event loop.
249+
new_event_loop()

Diff for: ports/webassembly/objjsproxy.c

+20
Original file line numberDiff line numberDiff line change
@@ -474,9 +474,29 @@ static mp_obj_t jsproxy_new_gen(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
474474

475475
/******************************************************************************/
476476

477+
#if MICROPY_PY_ASYNCIO
478+
extern mp_obj_t mp_asyncio_context;
479+
#endif
480+
477481
static mp_obj_t jsproxy_getiter(mp_obj_t self_in, mp_obj_iter_buf_t *iter_buf) {
478482
mp_obj_jsproxy_t *self = MP_OBJ_TO_PTR(self_in);
479483
if (has_attr(self->ref, "then")) {
484+
#if MICROPY_PY_ASYNCIO
485+
// When asyncio is running and the caller here is a task, wrap the JavaScript
486+
// thenable in a ThenableEvent, and get the task to wait on that event. This
487+
// decouples the task from the thenable and allows cancelling the task.
488+
if (mp_asyncio_context != MP_OBJ_NULL) {
489+
mp_obj_t cur_task = mp_obj_dict_get(mp_asyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_cur_task));
490+
if (cur_task != mp_const_none) {
491+
mp_obj_t thenable_event_class = mp_obj_dict_get(mp_asyncio_context, MP_OBJ_NEW_QSTR(MP_QSTR_ThenableEvent));
492+
mp_obj_t thenable_event = mp_call_function_1(thenable_event_class, self_in);
493+
mp_obj_t dest[2];
494+
mp_load_method(thenable_event, MP_QSTR_wait, dest);
495+
mp_obj_t wait_gen = mp_call_method_n_kw(0, 0, dest);
496+
return mp_getiter(wait_gen, iter_buf);
497+
}
498+
}
499+
#endif
480500
return jsproxy_new_gen(self_in, iter_buf);
481501
} else {
482502
return jsproxy_new_it(self_in, iter_buf);

Diff for: ports/webassembly/variants/manifest.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The asyncio package is built from the standard implementation but with the
2+
# core scheduler replaced with a custom scheduler that uses the JavaScript
3+
# runtime (with setTimeout an Promise's) to contrtol the scheduling.
4+
5+
package(
6+
"asyncio",
7+
(
8+
"event.py",
9+
"funcs.py",
10+
"lock.py",
11+
),
12+
base_path="$(MPY_DIR)/extmod",
13+
opt=3,
14+
)
15+
16+
package(
17+
"asyncio",
18+
(
19+
"__init__.py",
20+
"core.py",
21+
),
22+
base_path="$(PORT_DIR)",
23+
opt=3,
24+
)

Diff for: ports/webassembly/variants/pyscript/manifest.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
include("$(PORT_DIR)/variants/manifest.py")
2+
13
require("abc")
24
require("base64")
35
require("collections")

Diff for: tests/ports/webassembly/asyncio_create_task.mjs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Test asyncio.create_task(), and tasks waiting on a Promise.
2+
3+
const mp = await (await import(process.argv[2])).loadMicroPython();
4+
5+
globalThis.p0 = new Promise((resolve, reject) => {
6+
resolve(123);
7+
});
8+
9+
globalThis.p1 = new Promise((resolve, reject) => {
10+
setTimeout(() => {
11+
console.log("setTimeout resolved");
12+
resolve(456);
13+
}, 200);
14+
});
15+
16+
mp.runPython(`
17+
import js
18+
import asyncio
19+
20+
async def task(id, promise):
21+
print("task start", id)
22+
print("task await", id, await promise)
23+
print("task await", id, await promise)
24+
print("task end", id)
25+
26+
print("start")
27+
t1 = asyncio.create_task(task(1, js.p0))
28+
t2 = asyncio.create_task(task(2, js.p1))
29+
print("t1", t1.done(), t2.done())
30+
print("end")
31+
`);
32+
33+
// Wait for p1 to fulfill so t2 can continue.
34+
await globalThis.p1;
35+
36+
// Wait a little longer so t2 can complete.
37+
await new Promise((resolve, reject) => {
38+
setTimeout(resolve, 10);
39+
});
40+
41+
mp.runPython(`
42+
print("restart")
43+
print("t1", t1.done(), t2.done())
44+
`);

Diff for: tests/ports/webassembly/asyncio_create_task.mjs.exp

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
start
2+
t1 False False
3+
end
4+
task start 1
5+
task start 2
6+
task await 1 123
7+
task await 1 123
8+
task end 1
9+
setTimeout resolved
10+
task await 2 456
11+
task await 2 456
12+
task end 2
13+
restart
14+
t1 True True

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/micropython/micropython/commit/8a3546b3bd71dbc6d79900afbe58767e09b82c3e

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy