Async-Await

Provides a lightweight implementation of asynchronous programming using a custom async-await mechanism in C. It allows a stateful asynchronous execution flow be represented in a linear structured, readable manner.

Overview

The core of the implementation revolves around managing async state in a struct am_async object. The object combined with a set of state-tracking macros allows to mimic async-await behavior.

Useful for the cases when the sequence of states (operations) is strictly pre-defined.

Best effect is achieved when combined with hierarchical state machines - the more powerful concept, which allows to implement more complex interaction of states.

Inspired by:

Async Macros and Functions

The async macros and functions below help create asynchronous functions, manage state, and handle control flow.

Macros

  • AM_ASYNC_BEGIN(me)

    Begins an asynchronous function and initializes the async state. It takes a pointer me to the struct am_async managing the async state.

  • AM_ASYNC_BREAK()

    Marks the end of the async function. This macro resets the async state to the initial value and returns AM_ASYNC_RC_DONE, indicating that the function has completed.

  • AM_ASYNC_END()

    Ends the asynchronous function, ensuring that completed or unexpected states are handled correctly. It internally calls AM_ASYNC_BREAK() to reset the async state.

  • AM_ASYNC_LABEL()

    Sets a “label” in the async function by storing the current line number in the state field. This allows the async function to resume execution from this point when re-entered.

  • AM_ASYNC_AWAIT(cond)

    Awaits a specified condition cond before proceeding with execution. If the condition is not met, the function returns AM_ASYNC_RC_BUSY and can be re-entered later. This allows the async function to wait for external conditions without blocking.

  • AM_ASYNC_YIELD()

    Yields control back to the caller without completing the function, returning AM_ASYNC_RC_BUSY. This enables the async function to be resumed later from this point.

Functions

  • void am_async_init(struct am_async *me)

    Initializes an am_async structure by setting its state field to AM_ASYNC_STATE_INIT. This prepares the structure to be used in an async function.

Enumerations

  • enum am_async_rc

    Defines return codes used in async functions:

    • AM_ASYNC_RC_DONE: Indicates that the async function has completed successfully.

    • AM_ASYNC_RC_BUSY: Indicates that the async function is still busy and should be re-entered later.

Usage Example

The following example demonstrates how to use this async implementation in C.

#include "async.h"

struct my_async {
    struct am_async async;
    int foo;
};

int async_function(struct my_async *me) {
    AM_ASYNC_BEGIN(me);

    /* Await some condition before continuing */
    AM_ASYNC_AWAIT(me->foo);

    /* Yield control back to the caller */
    AM_ASYNC_YIELD();

    if (some_condition) {
        /* Complete the function with AM_ASYNC_RC_DONE */
        AM_ASYNC_BREAK();
    }

    /* Await another condition */
    AM_ASYNC_AWAIT(another_condition());

    /* Complete the function with AM_ASYNC_RC_DONE */
    AM_ASYNC_END();
}

int main() {
    struct my_async me;
    am_async_init(&me);

    while (async_function(&me) == AM_ASYNC_RC_BUSY) {
        /* Perform other work while async function is busy */
    }

    return 0;
}

Notes

  • Avoid using switch-case constructs withing asynchronous function using the macros

  • Keep the variables that should preserve their values across async function calls in a state stored outside of the async function.

  • See test.c for usage examples