Taking inspiration from protothreads, async.h, coroutines.h and async/await as found in python this is an async/await/fawait/event loop implementation for C based on Duff's device.
- It's 100% pure, portable C.
- It uses 96 bytes of memory per state, but grants you seamless nesting abilities, error handling and stack management.
- It's not dependent on an OS.
- It's a bit simpler to understand than other implementations as async state/stack management is fully handled by the lib.
- You can't preserve local variables across function calls, but the library provides a way to store them persistently (see practices)
The API will probably be deprecated because I'm currently rewriting a lot of parts from scratch and taking a different approach for state callbacks.
Don't use the current code to learn good practices or rely on it a lot since stuff might change A LOT.
Types | Description |
---|---|
AsyncCallback | pointer to an async function with signature: async funcname(struct astate *state) |
AsyncCancelCallback | pointer to a cancel function with signature: void funcname(struct astate *state) |
ASYNC_NONE | Type to imply empty function stack(locals) when creating new coro with async_new , typedef for char |
async_error | Enum type with async errors: ASYNC_OK, ASYNC_ENOMEM, ASYNC_ECANCELED, ASYNC_EINVAL_STATE |
s_astate | typedef for async state pointer (struct astate) |
Return type | Function/Macro | Description |
---|---|---|
struct async_event_loop * | async_get_event_loop(void) | Get current event loop |
void | async_set_event_loop(struct async_event_loop *loop) | Set custom event loop |
void | loop->init(void) | Init new event loop |
void | loop->destroy(void) | Destroy inited event loop, cancel and destroy all the tasks inside |
void | loop->run_forever(void) | Block and run event loop until there's no uncompleted tasks |
void | loop->loop_run_until_complete(s_astate main_coro) | Block and run event loop until main_coro completes. Can be called any number of times preserving uncompleted tasks state, frees main_coro if it has no ownership acquired after exiting |
MACRO_BLOCK | async_begin(state) | Mark the beginning of an async subroutine |
MACRO_BLOCK | async_end | Mark the end of an async subroutine |
s_astate | async_create_task(s_astate coro) | Add task to the event loop without blocking current progress, returns NULL and frees coro on failure |
s_astate * | async_create_tasks(size_t n, s_astate *coros)* | Add n tasks from array of states to the event loop without blocking current progress, does nothing and returns NULL if one of the coros is NULL or if there's not enough memory to add them |
MACRO_BLOCK | fawait(s_astate coro){ } | Add task to the event loop and block progress until coro is done executing. Sets async_errno: ASYNC_ECANCELED if coro task was cancelled, ASYNC_ENOMEM and frees coro if there's not enough memory to create task, any custom error code, ASYNC_OK otherwise. Code inside curly braces only executes if coro sets async_errno, on async_errno==ASYNC_OK braces are simply ignored. |
MACRO_BLOCK | async_yield | Yield execution until it's invoked again |
MACRO_BLOCK | await(cond) | Block progress until cond is true |
MACRO_BLOCK | await_while(cond) | Block progress while cond is true |
s_astate | async_new(AsyncCallback func, void *args, T_locals) | Returns a new coro from function (function must follow AsyncCallback signature) with args and stack memory capable to hold type passed to T_locals: int, struct, array, custom type or ASYNC_NONE if you don't need stack memory at all |
s_astate | async_gather(size_t n, s_astate *array_of_coros) | Gathers together few coros to run them in parallel into single coro and returns it. If gather() is cancelled, all submitted coros (that have not completed yet) are also cancelled. |
s_astate | async_vgather(size_t n, ...) | Variadic version of async_gather, expects coros to be passed directly (no need to cleanup them on failure) |
s_astate | async_sleep(double delay) | Block execution for delay seconds, precision differs from platform to platform but you can easily provide own implementation with non-portable timers when needed |
s_astate | async_wait_for(s_astate coro, double timeout) | Wait for the coro to complete with a timeout. Cancel it otherwise and set async_erro to ASYNC_ECANCELED. |
void * | async_alloc(size_t size) | Allocate memory automatically managed by the event loop, no need to free by yourself |
void | async_free(void *ptr) | Free ptr allocated with async_alloc without waiting for event loop to do it for you. Can be useful in long running tasks, but malloc with cancel function is preferable and faster anyway. |
int | int async_free_later_(struct astate *state, void *mem) | returns true if memory block was successfully queued and will be freed later by the event loop |
MACRO_BLOCK | async_cancel(s_astate coro) | Cancels coro . Note, that if coro operates some custom memory not allocated with async_alloc it'll result in a memory leak. |
int | async_cancelled(s_astate coro) | Returns true if coro was cancelled, false otherwise |
MACRO_BLOCK | async_on_cancel(AsyncCancelCallback cancel_func) | Set cancel function to be called on cancellation for current async function. cancel_func must follow AsyncCancelCallback type signature |
MACRO_BLOCK | async_set_on_cancel(s_astate coro, AsyncCancelCallback cancel_func) | Same as function above, but takes coro instead of using current function |
MACRO_BLOCK | async_exit | Terminate the current async subroutine |
int | async_done(state) | Returns true if async subroutine has completed execution, otherwise false |
async_error | async_errno | Macro that expands to the value of the type async_err of the current async function, can be assigned too |
const char * | async_strerror(async_err err) | Returns string representation of async_errno value |
void | async_free_coro_(s_astate coro) | free coro's memory, should be never used manually until dealing with states manually or when creating custom event loop, ignores NULL |
void | async_free_coros_(size_t n, s_astate *coros) | free n coros in array ignoring NULL pointers |
Some future api methods might use su_state as input coro type, explicitly indicating that it steals current ownership, in such case user mustn't access passed s_astate object or should INCREF ownership manually once more before transferring ownership to the method.
Macro | Description |
---|---|
ASYNC_INCREF(coro) | Mark coro as "currently in use", so it won't be deleted automatically. Must be followed by ASYNC_DECREF somewhere else in the async function/cancel function". |
ASYNC_DECREF(coro) | Mark coro as "isn't used anymore". Note, that it won't be deleted immediately if other functions used INCREF on this coro too |
ASYNC_PREPARE_NOARGS(async_callback, state, locals_t, cancel_f, err_label) | Prepare adapter function without allocating memory for args, in case of faulty allocation goto err_label; |
ASYNC_PREPARE(async_callback, state, args_size, locals_t, cancel_f, err_label) | Prepare adapter function |
- How can I return value/values from the function?
Pass pointer to value as args when creating new coroutine with async_new. Then assign to pointer inside function body:
async f(state){
async_begin(state);
int *res = state->args; /* args here is a pointer to int a (can be a pointer to any c object) */
*res = 42;
...
}
...
int a;
fawait(async_new(f, &a, ASYNC_NONE)){
printf("error %s happened\n", async_strerror(async_errno));
}
We can replace int * with, say, struct * if we want multiple return values of different types. Any type pointer is fine.
- What should I do if I need local variables?
Just create a struct with all needed variables and then pass struct as a type when creating new coro. See examples/example.c and short example below.
#include <stdio.h>
#include "async2.h"
struct amain_stack {
int i;
};
async amain(s_astate state) {
struct amain_stack *locals = state->locals;
char *args = state->args;
async_begin(state);
/* Note, that locals->i assignment to 0 is happening inside async_begin block.
* Don't assign them outside! */
for (locals->i = 0; locals->i < 3; locals->i++) {
printf("task %s: iteration №%d\n", args, locals->i + 1);
/* Let other tasks run with yield. Similar to python's await asyncio.sleep(0) usage. */
async_yield;
}
async_end;
}
int main(void) {
struct async_event_loop *loop = async_get_event_loop();
loop->init();
async_create_task(async_new(amain, "task 1", struct amain_stack)); /* Allocate enough memory to hold locals of type struct amain_stack */
async_create_task(async_new(amain, "task 2", struct amain_stack));
loop->run_forever();
loop->destroy();
}
fawait(async_func(10)){
if(async_errno == ASYNC_ENOMEM){
/* Handle memory error... */
}
} else {
/* Coroutine finished without errors */
}
locals->task = async_create_task(coro());
ASYNC_XINCREF(locals->task); /* Acquire ownership for task so it won't be immediately deleted after completion */
/* Do stuff... */
if(locals->task){
await(async_done(locals->task));
if(locals->task->err != ASYNC_OK){
/* Handle task error manually */
}
}
ASYNC_XDECREF(locals->task);
- Always wrap coroutines in such way as asyncio_sleep or asyncio_wait_for so their use will be very simple(no need to use async_new on them as they prepare), such way you can provide custom async functions that can take any arguments, but return type still will be limited to s_astate. ASYNC_PREPARE and ASYNC_PREPARE_NOARGS are pretty helpful for such function wrapping.
- Use underlying dev methods as async_new_coro_ and async_alloc_ in order to overcome userland limitations in creating async functions
- Always wrap all the used tasks with ownership, otherwise you risk to get invalid pointer access. Remember: one INCREF - one DECREF
- Provide cancel functions for your friendly methods, so even cancelled function won't break ownership and coro will be properly deleted.
- Use async_alloc(_) to manage dynamic memory
- As with protothreads, you have to be very careful with switch
statements and manually-created local variables(variables not stored in
locals
) within an async subroutine. Generally best to avoid them. - As with protothreads, you can't make blocking system calls and preserve the async semantics. These must be changed into non-blocking calls that test a condition.
- You can't use setjump and longjump functions amid await because async2 alters usual function flow, so given code is fine:
setjmp(var);
...
longjump(var);
...
await(global == 42);
but this one will be an undefined behavior:
setjmp(var);
...
await(global == 42);
...
longjump(var);
That means no way you can use C exceptions libraries like exceptions4c to wrap fawait or await or yield calls, use async_errno and fawait braces instead.