Skip to content

Commit 4ba8f02

Browse files
committed
Add awaitable C functions
CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj, n_args_min, fun) defines a method that returns an awaitable. As with a CPython coroutine, its arguments are stored and only parsed when it is awaited. While an operation runs, the task waits on asyncio's I/O queue: the awaitable is a stream that polls readable once the operation's done flag is set, possibly from an interrupt. CIRCUITPY_DEFINE_ASYNC_POLL_FUN_OBJ_1 is for a method that takes only self and has no interrupt: the task tries again on each pass of the event loop. Based on the awaitable in adafruit#11040.
1 parent a15a83e commit 4ba8f02

3 files changed

Lines changed: 256 additions & 0 deletions

File tree

‎supervisor/shared/awaitable.c‎

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
#include <string.h>
8+
9+
#include "supervisor/shared/awaitable.h"
10+
#include "py/mperrno.h"
11+
#include "py/runtime.h"
12+
#include "py/stream.h"
13+
14+
#if MICROPY_PY_ASYNC_AWAIT
15+
16+
#if MICROPY_PY_ASYNCIO
17+
// Make the running asyncio task wait for this awaitable: on the I/O queue until it polls
18+
// readable, or with poll, on the task queue so that it runs again on the next pass.
19+
// asyncio.core is found through sys.modules, because mp_asyncio_context in modasyncio.c is left
20+
// pointing at the old heap after a soft reload.
21+
static void asyncio_wait(mp_obj_t awaitable, bool poll) {
22+
mp_map_elem_t *core = mp_map_lookup(&MP_STATE_VM(mp_loaded_modules_dict).map,
23+
MP_OBJ_NEW_QSTR(MP_QSTR_asyncio_dot_core), MP_MAP_LOOKUP);
24+
if (core == NULL) {
25+
return;
26+
}
27+
mp_obj_t cur_task = mp_load_attr(core->value, MP_QSTR_cur_task);
28+
if (cur_task == mp_const_none) {
29+
return;
30+
}
31+
mp_obj_t dest[4];
32+
if (poll) {
33+
mp_load_method(mp_load_attr(core->value, MP_QSTR__task_queue), MP_QSTR_push, dest);
34+
dest[2] = cur_task;
35+
mp_call_method_n_kw(1, 0, dest);
36+
} else {
37+
mp_load_method(mp_load_attr(core->value, MP_QSTR__io_queue), MP_QSTR__enqueue, dest);
38+
dest[2] = awaitable;
39+
dest[3] = MP_OBJ_NEW_SMALL_INT(0); // wait to read
40+
mp_call_method_n_kw(2, 0, dest);
41+
}
42+
}
43+
#endif
44+
45+
// ---- the async function ------------------------------------------------------
46+
47+
// Store the arguments; they are parsed by start() when the awaitable is first resumed.
48+
static mp_obj_t async_fun_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
49+
const circuitpy_async_fun_obj_t *fun = MP_OBJ_TO_PTR(self_in);
50+
mp_arg_check_num_sig(n_args, n_kw, fun->sig);
51+
size_t n = n_args + 2 * n_kw;
52+
circuitpy_awaitable_obj_t *aw = mp_obj_malloc_var_with_finaliser(circuitpy_awaitable_obj_t,
53+
args, mp_obj_t, n, &circuitpy_awaitable_type);
54+
aw->fun = fun;
55+
aw->context = NULL;
56+
aw->state = CIRCUITPY_AWAITABLE_NEW;
57+
aw->n_args = n_args;
58+
aw->n_kw = n_kw;
59+
memcpy(aw->args, args, n * sizeof(mp_obj_t));
60+
return MP_OBJ_FROM_PTR(aw);
61+
}
62+
63+
MP_DEFINE_CONST_OBJ_TYPE(
64+
circuitpy_async_fun_type,
65+
MP_QSTR_function,
66+
MP_TYPE_FLAG_BINDS_SELF,
67+
call, async_fun_call
68+
);
69+
70+
// ---- the awaitable -----------------------------------------------------------
71+
72+
static void awaitable_cancel(circuitpy_awaitable_obj_t *self) {
73+
if (self->state == CIRCUITPY_AWAITABLE_RUNNING) {
74+
self->state = CIRCUITPY_AWAITABLE_FINISHED;
75+
self->fun->cancel(self->context, &self->done);
76+
}
77+
}
78+
79+
static mp_obj_t awaitable_iternext(mp_obj_t self_in) {
80+
circuitpy_awaitable_obj_t *self = MP_OBJ_TO_PTR(self_in);
81+
if (self->state == CIRCUITPY_AWAITABLE_FINISHED) {
82+
// Awaited again: like a finished generator, return None at once.
83+
MP_STATE_THREAD(stop_iteration_arg) = mp_const_none;
84+
return MP_OBJ_STOP_ITERATION;
85+
}
86+
if (self->state == CIRCUITPY_AWAITABLE_NEW) {
87+
mp_map_t kw_args;
88+
mp_map_init_fixed_table(&kw_args, self->n_kw, self->args + self->n_args);
89+
CIRCUITPY_ASYNC_FLAG_INIT(&self->done);
90+
// An exception from start() (bad arguments) propagates out of the await.
91+
self->context = self->fun->start(&self->done, self->n_args, self->args, &kw_args);
92+
self->state = CIRCUITPY_AWAITABLE_RUNNING;
93+
}
94+
if (!CIRCUITPY_ASYNC_FLAG_IS_SET(&self->done) && self->fun->poll != NULL && self->fun->poll(self->context)) {
95+
CIRCUITPY_ASYNC_FLAG_SET(&self->done);
96+
}
97+
if (!CIRCUITPY_ASYNC_FLAG_IS_SET(&self->done)) {
98+
#if MICROPY_PY_ASYNCIO
99+
asyncio_wait(self_in, self->fun->poll != NULL);
100+
#endif
101+
return mp_const_none;
102+
}
103+
self->state = CIRCUITPY_AWAITABLE_FINISHED;
104+
MP_STATE_THREAD(stop_iteration_arg) = self->fun->end(self->context, &self->done);
105+
return MP_OBJ_STOP_ITERATION;
106+
}
107+
108+
// A value sent in is ignored, as by CPython's awaitables written in C.
109+
static mp_obj_t awaitable_send(mp_obj_t self_in, mp_obj_t value) {
110+
mp_obj_t ret = awaitable_iternext(self_in);
111+
if (ret == MP_OBJ_STOP_ITERATION) {
112+
mp_raise_StopIteration(MP_STATE_THREAD(stop_iteration_arg));
113+
}
114+
return ret;
115+
}
116+
static MP_DEFINE_CONST_FUN_OBJ_2(awaitable_send_obj, awaitable_send);
117+
118+
// throw() is how asyncio cancels a task: stop the operation and raise the exception.
119+
static mp_obj_t awaitable_throw(mp_obj_t self_in, mp_obj_t exc) {
120+
awaitable_cancel(MP_OBJ_TO_PTR(self_in));
121+
nlr_raise(mp_make_raise_obj(exc));
122+
}
123+
static MP_DEFINE_CONST_FUN_OBJ_2(awaitable_throw_obj, awaitable_throw);
124+
125+
static mp_obj_t awaitable_close(mp_obj_t self_in) {
126+
awaitable_cancel(MP_OBJ_TO_PTR(self_in));
127+
return mp_const_none;
128+
}
129+
static MP_DEFINE_CONST_FUN_OBJ_1(awaitable_close_obj, awaitable_close);
130+
131+
// Readable once the operation has finished.
132+
static mp_uint_t awaitable_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) {
133+
circuitpy_awaitable_obj_t *self = MP_OBJ_TO_PTR(self_in);
134+
if (request != MP_STREAM_POLL) {
135+
*errcode = MP_EINVAL;
136+
return MP_STREAM_ERROR;
137+
}
138+
return CIRCUITPY_ASYNC_FLAG_IS_SET(&self->done) ? (arg & MP_STREAM_POLL_RD) : 0;
139+
}
140+
141+
static const mp_stream_p_t awaitable_stream_p = {
142+
.ioctl = awaitable_ioctl,
143+
};
144+
145+
static const mp_rom_map_elem_t awaitable_locals_dict_table[] = {
146+
{ MP_ROM_QSTR(MP_QSTR___await__), MP_ROM_PTR(&mp_identity_obj) },
147+
{ MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&awaitable_send_obj) },
148+
{ MP_ROM_QSTR(MP_QSTR_throw), MP_ROM_PTR(&awaitable_throw_obj) },
149+
{ MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&awaitable_close_obj) },
150+
// Runs during GC: an operation still in flight must not write into freed memory.
151+
{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&awaitable_close_obj) },
152+
};
153+
static MP_DEFINE_CONST_DICT(awaitable_locals_dict, awaitable_locals_dict_table);
154+
155+
MP_DEFINE_CONST_OBJ_TYPE(
156+
circuitpy_awaitable_type,
157+
MP_QSTR_coroutine,
158+
MP_TYPE_FLAG_ITER_IS_ITERNEXT,
159+
iter, awaitable_iternext,
160+
protocol, &awaitable_stream_p,
161+
locals_dict, &awaitable_locals_dict
162+
);
163+
164+
#endif // MICROPY_PY_ASYNC_AWAIT

‎supervisor/shared/awaitable.h‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// This file is part of the CircuitPython project: https://circuitpython.org
2+
//
3+
// SPDX-FileCopyrightText: Copyright (c) 2025 Scott Shawcroft for Adafruit Industries
4+
//
5+
// SPDX-License-Identifier: MIT
6+
7+
// Awaitable functions implemented in C.
8+
//
9+
// CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, fun_name) defines obj_name, a callable
10+
// that takes the same arguments as an MP_DEFINE_CONST_FUN_OBJ_KW function but returns an
11+
// awaitable. A call only checks the number of positional arguments; as with a coroutine in
12+
// CPython, nothing runs and the arguments are parsed only when the awaitable is awaited. Then:
13+
//
14+
// void *fun_name(circuitpy_async_flag_t *done, size_t n_args, const mp_obj_t *pos_args,
15+
// mp_map_t *kw_args)
16+
// parses the arguments, starts the operation and returns a context. The operation sets
17+
// *done, possibly from an interrupt, once it has finished.
18+
// mp_obj_t common_hal_<fun_name>_end(void *context, circuitpy_async_flag_t *done)
19+
// finishes the operation once *done is set and returns the result of the await.
20+
// void common_hal_<fun_name>_cancel(void *context, circuitpy_async_flag_t *done)
21+
// stops the operation, or finishes it if *done is already set. It must not allocate: it
22+
// may run during GC. done tells which operation is meant, if the context has started
23+
// another one since.
24+
//
25+
// CIRCUITPY_DEFINE_ASYNC_POLL_FUN_OBJ_1(obj_name, fun_name), for a method that takes only self and
26+
// has no interrupt to set *done, also uses:
27+
// bool common_hal_<fun_name>_poll(void *context)
28+
// tries to finish the operation and returns whether it has. The awaiting task tries again
29+
// on each pass of the asyncio event loop.
30+
//
31+
// While an operation with an interrupt runs, the awaiting asyncio task waits on asyncio's I/O
32+
// queue: the awaitable is a stream that polls readable once *done is set.
33+
34+
#pragma once
35+
36+
#include "py/obj.h"
37+
#include "supervisor/shared/async_flag.h"
38+
39+
#if MICROPY_PY_ASYNC_AWAIT
40+
41+
typedef void *(*circuitpy_awaitable_start_fn)(circuitpy_async_flag_t *done, size_t n_args,
42+
const mp_obj_t *pos_args, mp_map_t *kw_args);
43+
typedef mp_obj_t (*circuitpy_awaitable_end_fn)(void *context, circuitpy_async_flag_t *done);
44+
typedef void (*circuitpy_awaitable_cancel_fn)(void *context, circuitpy_async_flag_t *done);
45+
typedef bool (*circuitpy_awaitable_poll_fn)(void *context);
46+
47+
// An async function: calling it returns a circuitpy_awaitable_obj_t.
48+
typedef struct {
49+
mp_obj_base_t base;
50+
uint32_t sig; // as made by MP_OBJ_FUN_MAKE_SIG
51+
circuitpy_awaitable_start_fn start;
52+
circuitpy_awaitable_end_fn end;
53+
circuitpy_awaitable_cancel_fn cancel;
54+
circuitpy_awaitable_poll_fn poll; // NULL when an interrupt sets done
55+
} circuitpy_async_fun_obj_t;
56+
57+
extern const mp_obj_type_t circuitpy_async_fun_type;
58+
59+
typedef struct {
60+
mp_obj_base_t base;
61+
const circuitpy_async_fun_obj_t *fun;
62+
void *context; // returned by start()
63+
circuitpy_async_flag_t done;
64+
uint8_t state; // CIRCUITPY_AWAITABLE_NEW, _RUNNING or _FINISHED
65+
uint16_t n_args;
66+
uint16_t n_kw;
67+
mp_obj_t args[]; // positional arguments, then keyword name/value pairs
68+
} circuitpy_awaitable_obj_t;
69+
70+
extern const mp_obj_type_t circuitpy_awaitable_type;
71+
72+
enum { CIRCUITPY_AWAITABLE_NEW, CIRCUITPY_AWAITABLE_RUNNING, CIRCUITPY_AWAITABLE_FINISHED };
73+
74+
#define CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, fun_name) \
75+
const circuitpy_async_fun_obj_t obj_name = { \
76+
{&circuitpy_async_fun_type}, MP_OBJ_FUN_MAKE_SIG(n_args_min, MP_OBJ_FUN_ARGS_MAX, true), \
77+
fun_name, common_hal_##fun_name##_end, common_hal_##fun_name##_cancel, NULL \
78+
}
79+
80+
#define CIRCUITPY_DEFINE_ASYNC_POLL_FUN_OBJ_1(obj_name, fun_name) \
81+
const circuitpy_async_fun_obj_t obj_name = { \
82+
{&circuitpy_async_fun_type}, MP_OBJ_FUN_MAKE_SIG(1, 1, false), \
83+
fun_name, common_hal_##fun_name##_end, common_hal_##fun_name##_cancel, \
84+
common_hal_##fun_name##_poll \
85+
}
86+
87+
#endif // MICROPY_PY_ASYNC_AWAIT

‎supervisor/supervisor.mk‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ ifeq ($(CIRCUITPY_LIB_TLSF),1)
2727
SRC_SUPERVISOR += lib/tlsf/tlsf.c
2828
endif
2929

30+
# Awaitable C functions, for the async_* modules.
31+
ifeq ($(CIRCUITPY_ASYNC_SPI),1)
32+
SRC_SUPERVISOR += supervisor/shared/awaitable.c
33+
endif
34+
3035
# For tlsf
3136
CFLAGS += -D_DEBUG=0
3237

0 commit comments

Comments
 (0)