Amast is a minimalist asynchronous toolkit to help developing projects with asynchronous interactions and state machines. Written in C99.
Here are several use cases in increasing level of complexity.
The FSM has two states:
stateDiagram-v2
direction LR
[*] --> state_a
state_a --> state_b : B
state_b --> state_a : A
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).
The HSM has two sub-states and one superstate.
It demonstrates:
- creating of hierarchy of states
- 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
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).
Here is a full implementation of one active object with two states.
It demonstrates:
- creating the active object
- creating and maintaining a timer
- event publishing
- 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).
| 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) |
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 |
On Linux or WSL:
Install pixi.
Run pixi run all.
Include
amast.hamast_config.hamast.camast_preemptive.coramast_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.camast_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.
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".
If you find the project useful, then please star it. It helps promoting it.
If you find any bugs, please report them.
Amast is open-sourced software licensed under the MIT license.