Thanks to visit codestin.com
Credit goes to github.com

Skip to content

A Minimalist Asynchronous Toolkit (AMAST) is a small and efficient C99 library that helps manage complex, event-driven programs. It combines the Actor model with hierarchical state machines to make building real-time systems easier.

License

Notifications You must be signed in to change notification settings

adel-mamin/amast

Repository files navigation

MIT licensed Version Sphynx Visitors

Amast

Introduction

Amast is a minimalist asynchronous toolkit to help developing projects with asynchronous interactions and state machines. Written in C99.

What is it useful for?

Here are several use cases in increasing level of complexity.

Finite state machine (FSM)

The FSM has two states:

stateDiagram-v2
    direction LR

    [*] --> state_a

    state_a --> state_b : B
    state_b --> state_a : A
Loading

Here is the full implementation of the FSM:

#include "amast_config.h"
#include "amast.h"

#define EVT_A AM_EVT_USER
#define EVT_B (AM_EVT_USER + 1)

struct app {
    struct am_fsm fsm;
    /* app data */
} app;

static enum am_rc state_b(struct app *me, const struct am_event *event);

static enum am_rc state_a(struct app *me, const struct am_event *event) {
    switch (event->id) {
    case AM_EVT_ENTRY:
        am_pal_printf("state_a entry\n");
        return AM_HSM_HANDLED();

    case AM_EVT_EXIT:
        am_pal_printf("state_a exit\n");
        return AM_HSM_HANDLED();

    case EVT_B:
        return AM_FSM_TRAN(state_b);
    }
    return AM_FSM_HANDLED();
}

static enum am_rc state_b(struct app *me, const struct am_event *event) {
    switch (event->id) {
    case AM_EVT_ENTRY:
        am_pal_printf("state_b entry\n");
        return AM_HSM_HANDLED();

    case AM_EVT_EXIT:
        am_pal_printf("state_b exit\n");
        return AM_HSM_HANDLED();

    case EVT_A:
        return AM_FSM_TRAN(state_a);
    }
    return AM_FSM_HANDLED();
}

static enum am_rc init(struct app *me, const struct am_event *event) {
    return AM_FSM_TRAN(state_a);
}

int main(void) {
    am_fsm_ctor(&app.fsm, AM_FSM_STATE_CTOR(init));
    am_fsm_init(&app.fsm, /*init_event=*/NULL);
    am_fsm_dispatch(&app.fsm, &(struct am_event){.id = EVT_B});
    am_fsm_dispatch(&app.fsm, &(struct am_event){.id = EVT_A});
    return 0;
}

The console output:

state_a entry
state_a exit
state_b entry
state_b exit
state_a entry

The FSM API can be found here. The FSM documenation is here.

The compiled binary on x86 is about 2.4kB of memory (gcc, -Os, -flto).

Hierarchical state machine (HSM)

The HSM has two sub-states and one superstate.

It demonstrates:

  1. creating of hierarchy of states
  2. behavioral inheritance by handling the event C in superstate
stateDiagram-v2
    direction LR

    [*] --> superstate : init

    state superstate {
        [*] --> substate_a

        substate_a --> substate_b : B
        substate_b --> substate_a : A
    }

    superstate --> substate_b : C
Loading

Here is the full implementation of the HSM:

#include <stdio.h>

#include "amast_config.h"
#include "amast.h"

enum { APP_EVT_A = AM_EVT_USER, APP_EVT_B, APP_EVT_C };

struct app {
    struct am_hsm hsm;
    /* app data */
} app;

static enum am_rc substate_a(struct app *me, const struct am_event *event);
static enum am_rc substate_b(struct app *me, const struct am_event *event);

static enum am_rc superstate(struct app *me, const struct am_event *event) {
    switch (event->id) {
    case AM_EVT_ENTRY:
        am_pal_printf("superstate entry\n");
        return AM_HSM_HANDLED();

    case AM_EVT_EXIT:
        am_pal_printf("superstate exit\n");
        return AM_HSM_HANDLED();

    case AM_EVT_INIT:
        return AM_HSM_TRAN(substate_a);

    case APP_EVT_C:
        return AM_HSM_TRAN(substate_b);
    }
    return AM_HSM_SUPER(am_hsm_top);
}

static enum am_rc substate_a(struct app *me, const struct am_event *event) {
    switch (event->id) {
    case AM_EVT_ENTRY:
        am_pal_printf("substate_a entry\n");
        return AM_HSM_HANDLED();

    case AM_EVT_EXIT:
        am_pal_printf("substate_a exit\n");
        return AM_HSM_HANDLED();

    case APP_EVT_B:
        return AM_HSM_TRAN(substate_b);
    }
    return AM_HSM_SUPER(superstate);
}

static enum am_rc substate_b(struct app *me, const struct am_event *event) {
    switch (event->id) {
    case AM_EVT_ENTRY:
        am_pal_printf("substate_b entry\n");
        return AM_HSM_HANDLED();

    case AM_EVT_EXIT:
        am_pal_printf("substate_b exit\n");
        return AM_HSM_HANDLED();

    case APP_EVT_A:
        return AM_HSM_TRAN(substate_a);
    }
    return AM_HSM_SUPER(superstate);
}

static enum am_rc init(struct app *me, const struct am_event *event) {
    return AM_HSM_TRAN(superstate);
}

int main(void) {
    am_hsm_ctor(&app.hsm, AM_HSM_STATE_CTOR(init));
    am_hsm_init(&app.hsm, /*init_event=*/NULL);
    am_hsm_dispatch(&app.hsm, &(struct am_event){.id = APP_EVT_B});
    am_hsm_dispatch(&app.hsm, &(struct am_event){.id = APP_EVT_A});
    am_hsm_dispatch(&app.hsm, &(struct am_event){.id = APP_EVT_C});
    return 0;
}

The console output:

superstate entry
substate_a entry
substate_a exit
substate_b entry
substate_b exit
substate_a entry
substate_a exit
substate_b entry

The HSM API can be found here. The HSM documenation is here.

The compiled binary on x86 is about 4.0kB of memory (gcc, -Os, -flto).

Active Object

Here is a full implementation of one active object with two states.

It demonstrates:

  1. creating the active object
  2. creating and maintaining a timer
  3. event publishing
  4. creating regular tasks, for blocking calls like sleep and waiting for user input
#include <stdio.h>
#include <stddef.h>
#include <string.h>

#include "amast_config.h"
#include "amast.h"

enum {
    APP_EVT_SWITCH_MODE = AM_EVT_USER,
    APP_EVT_PUB_MAX,
    APP_EVT_TIMER,
};

struct app {
    struct am_ao ao;
    struct am_timer *timer;
    int ticks;
};

/* events are allocated from this memory pool */
static struct am_timer m_event_pool[1] AM_ALIGNED(AM_ALIGN_MAX);

/* event publish/subscribe memory */
static struct am_ao_subscribe_list m_pubsub_list[APP_EVT_PUB_MAX];

/* active object incoming events queue */
static const struct am_event *m_queue[2];

static enum am_rc app_state_a(struct app *me, const struct am_event *event);
static enum am_rc app_state_b(struct app *me, const struct am_event *event);

static enum am_rc app_state_a(struct app *me, const struct am_event *event) {
    switch (event->id) {
    case AM_EVT_ENTRY:
        am_pal_printf("state A\n");
        return AM_HSM_HANDLED();

    case APP_EVT_SWITCH_MODE:
        return AM_HSM_TRAN(app_state_b);
    }
    return AM_HSM_SUPER(am_hsm_top);
}

static enum am_rc app_state_b(struct app *me, const struct am_event *event) {
    switch (event->id) {
    case AM_EVT_ENTRY:
        am_pal_printf("state B\n");
        am_timer_arm_ticks(me->timer, me->ticks, /*interval=*/0);
        return AM_HSM_HANDLED();

    case AM_EVT_EXIT:
        am_timer_disarm(me->timer);
        return AM_HSM_HANDLED();

    case APP_EVT_SWITCH_MODE:
        return AM_HSM_TRAN(app_state_a);

    case APP_EVT_TIMER:
        am_pal_printf("timer\n");
        am_timer_arm_ticks(me->timer, me->ticks, /*interval=*/0);
        return AM_HSM_HANDLED();
    }
    return AM_HSM_SUPER(am_hsm_top);
}

static enum am_rc app_init(struct app *me, const struct am_event *event) {
    am_ao_subscribe(&me->ao, APP_EVT_SWITCH_MODE);
    return AM_HSM_TRAN(app_state_a);
}

static void app_ctor(struct app *me) {
    memset(me, 0, sizeof(*me));
    am_ao_ctor(&me->ao, AM_HSM_STATE_CTOR(app_init));
    me->timer = am_timer_allocate(
        APP_EVT_TIMER, sizeof(*me->timer), AM_PAL_TICK_DOMAIN_DEFAULT, &me->ao
    );
    me->ticks = am_pal_time_get_tick_from_ms(AM_PAL_TICK_DOMAIN_DEFAULT, 1000);
}

static void ticker_task(void *param) {
    am_pal_wait_all_tasks();

    uint32_t now_ticks = am_pal_time_get_tick(AM_PAL_TICK_DOMAIN_DEFAULT);
    while (am_ao_get_cnt() > 0) {
        am_pal_sleep_till_ticks(AM_PAL_TICK_DOMAIN_DEFAULT, now_ticks + 1);
        now_ticks += 1;
        am_timer_tick(AM_PAL_TICK_DOMAIN_DEFAULT);
    }
}

static void input_task(void *param) {
    am_pal_wait_all_tasks();

    int ch;
    while ((ch = getc(stdin)) != EOF) {
        if ('\n' == ch) {
            static struct am_event event = {.id = APP_EVT_SWITCH_MODE};
            am_ao_publish(&event);
        }
    }
}

int main(void) {
    am_ao_state_ctor(/*cfg=*/NULL);
    am_event_pool_add(
        m_event_pool,
        sizeof(m_event_pool),
        sizeof(m_event_pool[0]),
        AM_ALIGNOF(am_timer_t)
    );
    am_ao_init_subscribe_list(m_pubsub_list, AM_COUNTOF(m_pubsub_list));

    struct app m;
    app_ctor(&m);

    am_ao_start(
        &m.ao,
        (struct am_ao_prio){.ao = AM_AO_PRIO_MAX, .task = AM_AO_PRIO_MAX},
        /*queue=*/m_queue,
        /*nqueue=*/AM_COUNTOF(m_queue),
        /*stack=*/NULL,
        /*stack_size=*/0,
        /*name=*/"app",
        /*init_event=*/NULL
    );

    /* ticker thread to feed timers */
    am_pal_task_create(
        "ticker",
        AM_AO_PRIO_MIN,
        /*stack=*/NULL,
        /*stack_size=*/0,
        /*entry=*/ticker_task,
        /*arg=*/NULL
    );

    /* user input controlling thread */
    am_pal_task_create(
        "input",
        AM_AO_PRIO_MIN,
        /*stack=*/NULL,
        /*stack_size=*/0,
        /*entry=*/input_task,
        /*arg=*/&m
    );

    while (am_ao_get_cnt() > 0) {
        am_ao_run_all();
    }

    am_ao_state_dtor();

    return 0;
}

The AO API can be found here. The Event API can be found here. The Timer API can be found here.

The compiled binary on x86 is about 10.6kB of memory (gcc, -Os, -flto).

Architecture Diagram

Architecture Diagram

What Is Inside

Library name Description
ao active object (preemptive and cooperative) (documentation, example)
async async/await (documentation, example)
dlist doubly linked list
event events (documentation)
fsm finite state machine (FSM) (documentation)
hsm hierarchical state machine (HSM) with sub-machines support (documentation, examples)
onesize onesize memory allocator (documentation)
ringbuf ring buffer (documentation, example)
slist singly linked list
timer timers (documentation)

How Big Are Compile Sizes

Some x86-64 size figures to get an idea:

Library name Code size [kB] Data size [kB]
ao_cooperative 3.83 0.57
ao_preemptive 3.77 0.56
dlist 1.29 0.00
event 3.97 0.23
fsm 0.88 0.00
hsm 2.65 0.01
onesize 1.43 0.00
ringbuf 1.39 0.00
slist 1.21 0.00
timer 1.81 0.08

How To Compile For Amast Development

On Linux or WSL:

Install pixi. Run pixi run all.

How To Use The Latest Amast Release

Include

  • amast.h
  • amast_config.h
  • amast.c
  • amast_preemptive.c or amast_cooperative.c

from the latest release to your project.

If you want to use Amast features that require porting, then also add the following port to you project:

  • amast_posix.c
  • amast_libuv.c

If you want to run Amast unit tests, then also include amast_test.h and amast_test.c.

Makefile is available for optional use. Run make test to run the unit tests.

Features, Bugs, etc.

The project uses "Discussions" instead of "Issues".

"Discussions" tab has different discussion groups for "Features" and "Bugs".

For making sure issues are addressed, both me and the community can better evaluate which issues and features are high priority because they can be "upvoted".

How To Contribute

If you find the project useful, then please star it. It helps promoting it.

If you find any bugs, please report them.

License

Amast is open-sourced software licensed under the MIT license.

Star History

Star History Chart

About

A Minimalist Asynchronous Toolkit (AMAST) is a small and efficient C99 library that helps manage complex, event-driven programs. It combines the Actor model with hierarchical state machines to make building real-time systems easier.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Sponsor this project

 

Languages