Skip to content

Commit e94fc01

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_KW is for operations with no interrupt: the task tries again on each pass of the event loop. Based on the awaitable in adafruit#11040.
1 parent 68e3527 commit e94fc01

3 files changed

Lines changed: 276 additions & 0 deletions

File tree

‎supervisor/shared/awaitable.c‎

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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/objmodule.h"
12+
#include "py/runtime.h"
13+
#include "py/stream.h"
14+
15+
#if MICROPY_PY_ASYNC_AWAIT
16+
17+
#if MICROPY_PY_ASYNCIO
18+
// The globals of asyncio.core, if it has been imported. Found through sys.modules, because
19+
// mp_asyncio_context in modasyncio.c is left pointing at the old heap after a soft reload.
20+
static mp_map_t *asyncio_core_globals(void) {
21+
mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_loaded_modules_dict).map,
22+
MP_OBJ_NEW_QSTR(MP_QSTR_asyncio_dot_core), MP_MAP_LOOKUP);
23+
return elem == NULL ? NULL : &mp_obj_module_get_globals(elem->value)->map;
24+
}
25+
26+
static mp_obj_t map_get(mp_map_t *map, qstr name) {
27+
mp_map_elem_t *elem = mp_map_lookup(map, MP_OBJ_NEW_QSTR(name), MP_MAP_LOOKUP);
28+
return elem == NULL ? mp_const_none : elem->value;
29+
}
30+
31+
// Make the running asyncio task wait for this awaitable: on the I/O queue until it polls
32+
// readable, or with poll, on the task queue so that it runs again on the next pass.
33+
static void asyncio_wait(mp_obj_t awaitable, bool poll) {
34+
mp_map_t *core = asyncio_core_globals();
35+
if (core == NULL) {
36+
return;
37+
}
38+
mp_obj_t cur_task = map_get(core, MP_QSTR_cur_task);
39+
if (cur_task == mp_const_none) {
40+
return;
41+
}
42+
mp_obj_t dest[4];
43+
if (poll) {
44+
mp_load_method(map_get(core, MP_QSTR__task_queue), MP_QSTR_push, dest);
45+
dest[2] = cur_task;
46+
mp_call_method_n_kw(1, 0, dest);
47+
} else {
48+
mp_load_method(map_get(core, MP_QSTR__io_queue), MP_QSTR__enqueue, dest);
49+
dest[2] = awaitable;
50+
dest[3] = MP_OBJ_NEW_SMALL_INT(0); // wait to read
51+
mp_call_method_n_kw(2, 0, dest);
52+
}
53+
}
54+
#endif
55+
56+
// ---- the async function ------------------------------------------------------
57+
58+
// Store the arguments; they are parsed by start() when the awaitable is first resumed.
59+
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) {
60+
const circuitpy_async_fun_obj_t *fun = MP_OBJ_TO_PTR(self_in);
61+
mp_arg_check_num(n_args, n_kw, fun->n_args_min, MP_OBJ_FUN_ARGS_MAX, true);
62+
size_t n = n_args + 2 * n_kw;
63+
circuitpy_awaitable_obj_t *aw = mp_obj_malloc_var_with_finaliser(circuitpy_awaitable_obj_t,
64+
args, mp_obj_t, n, &circuitpy_awaitable_type);
65+
aw->fun = fun;
66+
aw->context = NULL;
67+
aw->started = false;
68+
aw->finished = false;
69+
aw->n_args = n_args;
70+
aw->n_kw = n_kw;
71+
memcpy(aw->args, args, n * sizeof(mp_obj_t));
72+
return MP_OBJ_FROM_PTR(aw);
73+
}
74+
75+
MP_DEFINE_CONST_OBJ_TYPE(
76+
circuitpy_async_fun_type,
77+
MP_QSTR_function,
78+
MP_TYPE_FLAG_BINDS_SELF,
79+
call, async_fun_call
80+
);
81+
82+
// ---- the awaitable -----------------------------------------------------------
83+
84+
static void awaitable_cancel(circuitpy_awaitable_obj_t *self) {
85+
if (self->started && !self->finished) {
86+
self->finished = true;
87+
self->fun->cancel(self->context, &self->done);
88+
}
89+
}
90+
91+
static mp_obj_t awaitable_iternext(mp_obj_t self_in) {
92+
circuitpy_awaitable_obj_t *self = MP_OBJ_TO_PTR(self_in);
93+
if (self->finished) {
94+
// Awaited again: like a finished generator, return None at once.
95+
MP_STATE_THREAD(stop_iteration_arg) = mp_const_none;
96+
return MP_OBJ_STOP_ITERATION;
97+
}
98+
if (!self->started) {
99+
mp_map_t kw_args;
100+
mp_map_init_fixed_table(&kw_args, self->n_kw, self->args + self->n_args);
101+
CIRCUITPY_ASYNC_FLAG_INIT(&self->done);
102+
// An exception from start() (bad arguments) propagates out of the await.
103+
self->context = self->fun->start(&self->done, self->n_args, self->args, &kw_args);
104+
self->started = true;
105+
}
106+
if (!CIRCUITPY_ASYNC_FLAG_IS_SET(&self->done) && self->fun->poll != NULL && self->fun->poll(self->context)) {
107+
CIRCUITPY_ASYNC_FLAG_SET(&self->done);
108+
}
109+
if (!CIRCUITPY_ASYNC_FLAG_IS_SET(&self->done)) {
110+
#if MICROPY_PY_ASYNCIO
111+
asyncio_wait(self_in, self->fun->poll != NULL);
112+
#endif
113+
return mp_const_none;
114+
}
115+
self->finished = true;
116+
MP_STATE_THREAD(stop_iteration_arg) = self->fun->end(self->context, &self->done);
117+
return MP_OBJ_STOP_ITERATION;
118+
}
119+
120+
static mp_obj_t awaitable_await(mp_obj_t self_in) {
121+
return self_in;
122+
}
123+
static MP_DEFINE_CONST_FUN_OBJ_1(awaitable_await_obj, awaitable_await);
124+
125+
static mp_obj_t awaitable_send(mp_obj_t self_in, mp_obj_t value) {
126+
if (value != mp_const_none) {
127+
mp_raise_TypeError(MP_ERROR_TEXT("can't send non-None value to a just-started generator"));
128+
}
129+
mp_obj_t ret = awaitable_iternext(self_in);
130+
if (ret == MP_OBJ_STOP_ITERATION) {
131+
mp_raise_StopIteration(MP_STATE_THREAD(stop_iteration_arg));
132+
}
133+
return ret;
134+
}
135+
static MP_DEFINE_CONST_FUN_OBJ_2(awaitable_send_obj, awaitable_send);
136+
137+
// throw() is how asyncio cancels a task: stop the operation and raise the exception.
138+
static mp_obj_t awaitable_throw(size_t n_args, const mp_obj_t *args) {
139+
awaitable_cancel(MP_OBJ_TO_PTR(args[0]));
140+
mp_obj_t exc = args[1];
141+
if (n_args > 2 && args[2] != mp_const_none) {
142+
exc = args[2];
143+
}
144+
nlr_raise(mp_make_raise_obj(exc));
145+
}
146+
static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(awaitable_throw_obj, 2, 4, awaitable_throw);
147+
148+
static mp_obj_t awaitable_close(mp_obj_t self_in) {
149+
awaitable_cancel(MP_OBJ_TO_PTR(self_in));
150+
return mp_const_none;
151+
}
152+
static MP_DEFINE_CONST_FUN_OBJ_1(awaitable_close_obj, awaitable_close);
153+
154+
// Readable once the operation has finished.
155+
static mp_uint_t awaitable_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) {
156+
circuitpy_awaitable_obj_t *self = MP_OBJ_TO_PTR(self_in);
157+
if (request != MP_STREAM_POLL) {
158+
*errcode = MP_EINVAL;
159+
return MP_STREAM_ERROR;
160+
}
161+
return CIRCUITPY_ASYNC_FLAG_IS_SET(&self->done) ? (arg & MP_STREAM_POLL_RD) : 0;
162+
}
163+
164+
static const mp_stream_p_t awaitable_stream_p = {
165+
.ioctl = awaitable_ioctl,
166+
};
167+
168+
static const mp_rom_map_elem_t awaitable_locals_dict_table[] = {
169+
{ MP_ROM_QSTR(MP_QSTR___await__), MP_ROM_PTR(&awaitable_await_obj) },
170+
{ MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&awaitable_send_obj) },
171+
{ MP_ROM_QSTR(MP_QSTR_throw), MP_ROM_PTR(&awaitable_throw_obj) },
172+
{ MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&awaitable_close_obj) },
173+
// Runs during GC: an operation still in flight must not write into freed memory.
174+
{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&awaitable_close_obj) },
175+
};
176+
static MP_DEFINE_CONST_DICT(awaitable_locals_dict, awaitable_locals_dict_table);
177+
178+
MP_DEFINE_CONST_OBJ_TYPE(
179+
circuitpy_awaitable_type,
180+
MP_QSTR_awaitable,
181+
MP_TYPE_FLAG_ITER_IS_ITERNEXT,
182+
iter, awaitable_iternext,
183+
protocol, &awaitable_stream_p,
184+
locals_dict, &awaitable_locals_dict
185+
);
186+
187+
#endif // MICROPY_PY_ASYNC_AWAIT

‎supervisor/shared/awaitable.h‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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_KW also uses, for an operation with no interrupt to set *done:
26+
// bool common_hal_<fun_name>_poll(void *context)
27+
// tries to finish the operation and returns whether it has. The awaiting task tries again
28+
// on each pass of the asyncio event loop.
29+
//
30+
// While an operation with an interrupt runs, the awaiting asyncio task waits on asyncio's I/O
31+
// queue: the awaitable is a stream that polls readable once *done is set.
32+
33+
#pragma once
34+
35+
#include "py/obj.h"
36+
#include "supervisor/shared/async_flag.h"
37+
38+
#if MICROPY_PY_ASYNC_AWAIT
39+
40+
typedef void *(*circuitpy_awaitable_start_fn)(circuitpy_async_flag_t *done, size_t n_args,
41+
const mp_obj_t *pos_args, mp_map_t *kw_args);
42+
typedef mp_obj_t (*circuitpy_awaitable_end_fn)(void *context, circuitpy_async_flag_t *done);
43+
typedef void (*circuitpy_awaitable_cancel_fn)(void *context, circuitpy_async_flag_t *done);
44+
typedef bool (*circuitpy_awaitable_poll_fn)(void *context);
45+
46+
// An async function: calling it returns a circuitpy_awaitable_obj_t.
47+
typedef struct {
48+
mp_obj_base_t base;
49+
uint16_t n_args_min;
50+
circuitpy_awaitable_start_fn start;
51+
circuitpy_awaitable_end_fn end;
52+
circuitpy_awaitable_cancel_fn cancel;
53+
circuitpy_awaitable_poll_fn poll; // NULL when an interrupt sets done
54+
} circuitpy_async_fun_obj_t;
55+
56+
extern const mp_obj_type_t circuitpy_async_fun_type;
57+
58+
typedef struct {
59+
mp_obj_base_t base;
60+
const circuitpy_async_fun_obj_t *fun;
61+
void *context; // returned by start()
62+
circuitpy_async_flag_t done;
63+
bool started;
64+
bool 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+
#define CIRCUITPY_DEFINE_ASYNC_FUN_OBJ_KW(obj_name, n_args_min, fun_name) \
73+
const circuitpy_async_fun_obj_t obj_name = { \
74+
{&circuitpy_async_fun_type}, n_args_min, fun_name, \
75+
common_hal_##fun_name##_end, common_hal_##fun_name##_cancel, NULL \
76+
}
77+
78+
#define CIRCUITPY_DEFINE_ASYNC_POLL_FUN_OBJ_KW(obj_name, n_args_min, fun_name) \
79+
const circuitpy_async_fun_obj_t obj_name = { \
80+
{&circuitpy_async_fun_type}, n_args_min, fun_name, \
81+
common_hal_##fun_name##_end, common_hal_##fun_name##_cancel, common_hal_##fun_name##_poll \
82+
}
83+
84+
#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)