From 68033aba9710cbb5f8ddd41d08ad5f1d7b652015 Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Tue, 9 Aug 2016 02:48:31 -0500 Subject: [PATCH] Updated documentation - core api in equeue.h - porting api in equeue_tick/mutex/sema.h - README documentation - internal code documentation --- README.md | 184 +++++++++++++++++++++++++++++++++++++++++++++---- equeue.c | 63 ++++++++++++----- equeue.h | 124 ++++++++++++++++++++++----------- equeue_mutex.h | 19 +++-- equeue_sema.h | 27 ++++++-- equeue_tick.h | 10 ++- 6 files changed, 342 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index cf52940..18bc12a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## The equeue library ## -The equeue library provides a composable event queue implementation -that acts as a drop in scheduler and event framework. +The equeue library is designed as a simple but powerful library for scheduling +events on composable event queues. ``` c #include "equeue.h" @@ -16,25 +16,185 @@ int main() { equeue_t queue; equeue_create(&queue, 32*EQUEUE_EVENT_SIZE); - // events are simple callbacks + // events can be simple callbacks equeue_call(&queue, print, "called immediately"); equeue_call_in(&queue, 2000, print, "called in 2 seconds"); equeue_call_every(&queue, 1000, print, "called every 1 seconds"); - // events are executed when dispatch is called + // events are executed in equeue_dispatch equeue_dispatch(&queue, 3000); print("called after 3 seconds"); - // dispatch can be called in an infinite loop + equeue_destroy(&queue); +} +``` + +The equeue library can be used as a normal event loop, or it can be +backgrounded on a single hardware timer or even another event loop. +The equeue library is both thread and irq safe, and provides functions +for easily composing multiple queues. + +The equeue library can act as a drop-in scheduler, provide synchronization +between multiple threads, or just act as a mechanism for moving events +out of interrupt contexts. + +## Documentation ## + +Unless it is elaborated, the in-depth documentation of the specific functions +can be found in [equeue.h](equeue.h). + +The core of the equeue library is the `equeue_t` type which represents a +single event queue, and the `equeue_dispath` function which runs the equeue, +providing the context for executing events. + +On top of this, `equeue_call`, `equeue_call_in`, and `equeue_call_every` +provide an easy method of posting events to be executed in the context +of the `equeue_dispatch` function. + +``` c +#include "equeue.h" +#include "game.h" + +equeue_t queue; +struct game game; + +// button_isr may be in interrupt context +void button_isr(void) { + equeue_call(&queue, game_button_update, &game); +} + +// a simple user-interface framework +int main() { + equeue_create(&queue, 4096); + game_create(&game); + + // call game_screen_udpate at 60 Hz + equeue_call_every(&queue, 1000/60, game_screen_update, &game); + + // dispatch forever + equeue_dispatch(&queue, -1); +} +``` + +In addition to simple events, an event can be manually allocated with +`equeue_alloc` and posted with `equeue_post` to allow passing an arbitrary +amount of data to the execution of the event. This memory is allocated out +of the equeue's buffer, and dynamic memory can be completely avoided. + +The equeue allocator is designed to minimize jitter in interrupt contexts as +well as avoid memory fragmentation on small devices. The allocator achieves +both constant-runtime and zero-fragmentation for fixed-size events, however +grows linearly as the quantity of different sized allocations increases. + +``` c +#include "equeue.h" + +equeue_t queue; + +// arbitrary data can be moved to a different context +int enet_callback(void *buffer, int size) { + if (size > 512) { + size = 512; + } + + void *event = equeue_alloc(&queue, 512); + memcpy(event, buffer, size); + equeue_post(&queue, event); + + return size; +} +``` + +Additionally, in-flight events can be cancelled with `equeue_cancel`. Events +are given unique ids on post, allowing safe cancellation of expired events. + +``` c +#include "equeue.h" + +equeue_t queue; +int sonar_value; +int sonar_timeout_id; + +void sonar_isr(int value) { + equeue_cancel(&queue, sonar_timeout_id); + sonar_value = value; +} + +void sonar_timeout(void *) { + sonar_value = -1; +} + +void sonar_read(void) { + sonar_timeout_id = equeue_call_in(&queue, 300, sonar_timeout, 0); + sonar_start(); +} +``` + +From an architectural standpoint, event queues easily align with module +boundaries, where internal state can be implicitly synchronized through +event registration. Multiple modules can easily use event queues running +in separate threads. + +Alternatively, multiple event queues can be easily composed through the +`equeue_chain` function, which allows multiple event queues to share the +context of a single `equeue_dispatch` call. + +``` c +#include "equeue.h" + +// run a simultaneous localization and mapping loop in one queue +struct slam { + equeue_t queue; +}; + +void slam_create(struct slam *s, equeue_t *target) { + equeue_create(&s->queue, 4096); + equeue_chain(&s->queue, target); + equeue_call_every(&s->queue, 100, slam_filter); +} + +// run a sonar with it's own queue +struct sonar { + equeue_t equeue; + struct slam *slam; +}; + +void sonar_create(struct sonar *s, equeue_t *target) { + equeue_create(&s->queue, 64); + equeue_chain(&s->queue, target); + equeue_call_in(&s->queue, 5, sonar_update, s); +} + +// although the above is perfectly synchronized, we can run these +// modules on a single event queue +int main() { + equeue_t queue; + equeue_create(&queue, 1024); + + struct sonar s1, s2, s3; + sonar_create(&s1, &queue); + sonar_create(&s2, &queue); + sonar_create(&s3, &queue); + + struct slam slam; + slam_create(&slam, &queue); + + // dispatches events for all of the modules equeue_dispatch(&queue, -1); } ``` -The equeue library can be used for a normal event loops, however it also -supports composition and multithreaded environments. More information on -the idea behind composable event loops -[here](https://gist.github.com/geky/4969d940f1bd5596bdc10e79093e2553). +## Platform ## + +The equeue library has a minimal porting layer that is flexible depending +on the requirements of the underlying platform. Platform specific declarations +and more information can be found in the following files: + +- [equeue_tick](equeue_tick.h) - millisecond counter +- [equeue_mutex](equeue_mutex.h) - non-recursive mutex +- [equeue_sema](equeue_sema.h) - binary semaphore + ## Tests ## @@ -59,9 +219,3 @@ make prof | tee results.txt cat results.txt | make prof ``` -## Porting ## - -The events library requires a small porting layer: -- [equeue_tick](equeue_tick.h) - monotonic counter -- [equeue_mutex](equeue_mutex.h) - non-recursive mutex -- [equeue_sema](equeue_sema.h) - binary semaphore diff --git a/equeue.c b/equeue.c index 161a627..ab0e32e 100644 --- a/equeue.c +++ b/equeue.c @@ -10,7 +10,31 @@ #include +// calculate the relative-difference between absolute times while +// correctly handling overflow conditions +static inline int equeue_tickdiff(unsigned a, unsigned b) { + return (int)(a - b); +} + +// calculate the relative-difference between absolute times, but +// also clamp to zero, resulting in only non-zero values. +static inline int equeue_clampdiff(unsigned a, unsigned b) { + int diff = equeue_tickdiff(a, b); + return ~(diff >> (8*sizeof(int)-1)) & diff; +} + +// Increment the unique id in an event, hiding the event from cancel +static inline void equeue_incid(equeue_t *q, struct equeue_event *e) { + e->id += 1; + if (e->id >> (8*sizeof(int)-1 - q->npw2)) { + e->id = 1; + } +} + + +// equeue lifetime management int equeue_create(equeue_t *q, size_t size) { + // dynamically allocate the specified buffer void *buffer = malloc(size); if (!buffer) { return -1; @@ -22,6 +46,7 @@ int equeue_create(equeue_t *q, size_t size) { } int equeue_create_inplace(equeue_t *q, size_t size, void *buffer) { + // setup queue around provided buffer q->buffer = buffer; q->allocated = 0; @@ -43,6 +68,7 @@ int equeue_create_inplace(equeue_t *q, size_t size, void *buffer) { q->background.update = 0; q->background.timer = 0; + // initialize platform resources int err; err = equeue_sema_create(&q->eventsema); if (err < 0) { @@ -72,23 +98,28 @@ void equeue_destroy(equeue_t *q) { } } + // notify background timer if (q->background.update) { q->background.update(q->background.timer, -1); } + // clean up platform resources + memory equeue_mutex_destroy(&q->memlock); equeue_mutex_destroy(&q->queuelock); equeue_sema_destroy(&q->eventsema); free(q->allocated); } + // equeue chunk allocation functions static struct equeue_event *equeue_mem_alloc(equeue_t *q, size_t size) { + // add event overhead size += sizeof(struct equeue_event); size = (size + sizeof(void*)-1) & ~(sizeof(void*)-1); equeue_mutex_lock(&q->memlock); + // check if a good chunk is available for (struct equeue_event **p = &q->chunks; *p; p = &(*p)->next) { if ((*p)->size >= size) { struct equeue_event *e = *p; @@ -104,6 +135,7 @@ static struct equeue_event *equeue_mem_alloc(equeue_t *q, size_t size) { } } + // otherwise allocate a new chunk out of the slab if (q->slab.size >= size) { struct equeue_event *e = (struct equeue_event *)q->slab.data; q->slab.data += size; @@ -122,6 +154,7 @@ static struct equeue_event *equeue_mem_alloc(equeue_t *q, size_t size) { static void equeue_mem_dealloc(equeue_t *q, struct equeue_event *e) { equeue_mutex_lock(&q->memlock); + // stick chunk into list of chunks struct equeue_event **p = &q->chunks; while (*p && (*p)->size < e->size) { p = &(*p)->next; @@ -139,7 +172,6 @@ static void equeue_mem_dealloc(equeue_t *q, struct equeue_event *e) { equeue_mutex_unlock(&q->memlock); } -// equeue allocation functions void *equeue_alloc(equeue_t *q, size_t size) { struct equeue_event *e = equeue_mem_alloc(q, size); if (!e) { @@ -163,35 +195,23 @@ void equeue_dealloc(equeue_t *q, void *p) { equeue_mem_dealloc(q, e); } -// equeue scheduling functions -static inline int equeue_tickdiff(unsigned a, unsigned b) { - return (int)(a - b); -} - -static inline int equeue_clampdiff(unsigned a, unsigned b) { - int diff = equeue_tickdiff(a, b); - return ~(diff >> (8*sizeof(int)-1)) & diff; -} - -static inline void equeue_incid(equeue_t *q, struct equeue_event *e) { - e->id += 1; - if (e->id >> (8*sizeof(int)-1 - q->npw2)) { - e->id = 1; - } -} +// equeue scheduling functions static int equeue_enqueue(equeue_t *q, struct equeue_event *e, unsigned ms) { + // setup event and hash local id with buffer offset for unique id int id = (e->id << q->npw2) | ((unsigned char *)e - q->buffer); e->target = equeue_tick() + ms; e->generation = q->generation; equeue_mutex_lock(&q->queuelock); + // find the event slot struct equeue_event **p = &q->queue; while (*p && equeue_tickdiff((*p)->target, e->target) < 0) { p = &(*p)->next; } + // insert at head in slot if (*p && (*p)->target == e->target) { e->next = (*p)->next; if (e->next) { @@ -212,6 +232,7 @@ static int equeue_enqueue(equeue_t *q, struct equeue_event *e, unsigned ms) { *p = e; e->ref = p; + // notify background timer if ((q->background.update && q->background.active) && (q->queue == e && !e->sibling)) { q->background.update(q->background.timer, ms); @@ -223,6 +244,7 @@ static int equeue_enqueue(equeue_t *q, struct equeue_event *e, unsigned ms) { } static struct equeue_event *equeue_unqueue(equeue_t *q, int id) { + // decode event from unique id and check that the local id matches struct equeue_event *e = (struct equeue_event *) &q->buffer[id & ((1 << q->npw2)-1)]; @@ -232,6 +254,7 @@ static struct equeue_event *equeue_unqueue(equeue_t *q, int id) { return 0; } + // clear the event and check if already in-flight e->cb = 0; e->period = -1; @@ -241,6 +264,7 @@ static struct equeue_event *equeue_unqueue(equeue_t *q, int id) { return 0; } + // disentangle from queue if (e->sibling) { e->sibling->next = e->next; if (e->sibling->next) { @@ -265,6 +289,7 @@ static struct equeue_event *equeue_unqueue(equeue_t *q, int id) { static struct equeue_event *equeue_dequeue(equeue_t *q, unsigned target) { equeue_mutex_lock(&q->queuelock); + // find all expired events and mark a new generation q->generation += 1; if (equeue_tickdiff(q->tick, target) <= 0) { q->tick = target; @@ -285,6 +310,7 @@ static struct equeue_event *equeue_dequeue(equeue_t *q, unsigned target) { equeue_mutex_unlock(&q->queuelock); + // reverse and flatten each slot to match insertion order struct equeue_event **tail = &head; struct equeue_event *ess = head; while (ess) { @@ -407,6 +433,7 @@ void equeue_dispatch(equeue_t *q, int ms) { } } + // event functions void equeue_event_delay(void *p, int ms) { struct equeue_event *e = (struct equeue_event*)p - 1; @@ -423,6 +450,7 @@ void equeue_event_dtor(void *p, void (*dtor)(void *)) { e->dtor = dtor; } + // simple callbacks struct ecallback { void (*cb)(void*); @@ -470,6 +498,7 @@ int equeue_call_every(equeue_t *q, int ms, void (*cb)(void*), void *data) { return equeue_post(q, ecallback_dispatch, e); } + // backgrounding void equeue_background(equeue_t *q, void (*update)(void *timer, int ms), void *timer) { diff --git a/equeue.h b/equeue.h index f3aa41b..1a9d9c7 100644 --- a/equeue.h +++ b/equeue.h @@ -11,7 +11,7 @@ extern "C" { #endif -// System specific files +// Platform specific files #include "equeue_tick.h" #include "equeue_mutex.h" #include "equeue_sema.h" @@ -20,11 +20,11 @@ extern "C" { #include -// Definition of the minimum size of an event -// This size fits the events created in the event_call set of functions. +// The minimum size of an event +// This size is garunteed to fit events created by event_call #define EQUEUE_EVENT_SIZE (sizeof(struct equeue_event) + 2*sizeof(void*)) -// Event/queue structures +// Internal event structure struct equeue_event { unsigned size; uint8_t id; @@ -42,6 +42,7 @@ struct equeue_event { // data follows }; +// Event queue structure typedef struct equeue { struct equeue_event *queue; unsigned tick; @@ -70,84 +71,121 @@ typedef struct equeue { } equeue_t; -// Queue operations +// Queue lifetime operations // -// Creation results in negative value on failure. +// Creates and destroys an event queue. The event queue either allocates a +// buffer of the specified size with malloc or uses a user provided buffer +// if constructed with equeue_create_inplace. +// +// If the event queue creation fails, equeue_create returns a negative, +// platform-specific error code. int equeue_create(equeue_t *queue, size_t size); int equeue_create_inplace(equeue_t *queue, size_t size, void *buffer); void equeue_destroy(equeue_t *queue); // Dispatch events // -// Executes any callbacks enqueued for the specified time in milliseconds, -// or forever if ms is negative +// Executes events until the specified milliseconds have passed. If ms is +// negative, equeue_dispatch will dispatch events indefinitely or until +// equeue_break is called on this queue. +// +// When called with a finite timeout, the equeue_dispatch function is garunteed +// to terminate. When called with a timeout of 0, the equeue_dispatch does not +// wait and is irq safe. void equeue_dispatch(equeue_t *queue, int ms); -// Break a running event loop +// Break out of a running event loop // -// Shuts down an unbounded event loop. Already pending events may finish -// executing, but the queue will not continue looping indefinitely. +// Forces the specified event queue's dispatch loop to terminate. Pending +// events may finish executing, but no new events will be executed. void equeue_break(equeue_t *queue); // Simple event calls // -// Passed callback will be executed in the associated equeue's -// dispatch call with the data pointer passed unmodified +// The specified callback will be executed in the context of the event queue's +// dispatch loop. When the callback is executed depends on the call function. // // equeue_call - Immediately post an event to the queue // equeue_call_in - Post an event after a specified time in milliseconds -// equeue_call_every - Post an event periodically in milliseconds +// equeue_call_every - Post an event periodically every milliseconds // -// These calls will result in 0 if no memory is available, otherwise they -// will result in a unique identifier that can be passed to equeue_cancel. +// All equeue_call functions are irq safe and can act as a mechanism for +// moving events out of irq contexts. +// +// The return value is a unique id that represents the posted event and can +// be passed to equeue_cancel. If there is not enough memory to allocate the +// event, equeue_call returns an id of 0. int equeue_call(equeue_t *queue, void (*cb)(void *), void *data); int equeue_call_in(equeue_t *queue, int ms, void (*cb)(void *), void *data); int equeue_call_every(equeue_t *queue, int ms, void (*cb)(void *), void *data); -// Events with queue handled blocks of memory +// Allocate memory for events +// +// The equeue_alloc function allocates an event that can be manually dispatched +// with equeue_post. The equeue_dealloc function may be used to free an event +// that has not been posted. Once posted, an event's memory is managed by the +// event queue and should not be deallocated. // -// Argument to equeue_post must point to a result of a equeue_alloc call -// and the associated memory is automatically freed after the event -// is dispatched. +// Both equeue_alloc and equeue_dealloc are irq safe. // -// equeue_alloc will result in null if no memory is available -// or the requested size is less than the size passed to equeue_create. +// The equeue allocator is designed to minimize jitter in interrupt contexts as +// well as avoid memory fragmentation on small devices. The allocator achieves +// both constant-runtime and zero-fragmentation for fixed-size events, however +// grows linearly as the quantity of different sized allocations increases. +// +// The equeue_alloc function returns a pointer to the event's allocated memory +// and acts as a handle to the underlying event. If there is not enough memory +// to allocate the event, equeue_alloc returns null. void *equeue_alloc(equeue_t *queue, size_t size); void equeue_dealloc(equeue_t *queue, void *event); // Configure an allocated event -// -// equeue_event_delay - Millisecond delay before posting an event -// equeue_event_period - Millisecond period to repeatedly post an event +// +// equeue_event_delay - Millisecond delay before dispatching an event +// equeue_event_period - Millisecond period for repeating dispatching an event // equeue_event_dtor - Destructor to run when the event is deallocated void equeue_event_delay(void *event, int ms); void equeue_event_period(void *event, int ms); void equeue_event_dtor(void *event, void (*dtor)(void *)); -// Post an allocted event to the event queue +// Post an event onto the event queue +// +// The equeue_post function takes a callback and a pointer to an event +// allocated by equeue_alloc. The specified callback will be executed in the +// context of the event queue's dispatch loop with the allocated event +// as its argument. // -// Argument to equeue_post must point to a result of a equeue_alloc call -// and the associated memory is automatically freed after the event -// is dispatched. +// The equeue_post function is irq safe and can act as a mechanism for +// moving events out of irq contexts. // -// This call results in an unique identifier that can be passed to -// equeue_cancel. +// The return value is a unique id that represents the posted event and can +// be passed to equeue_cancel. int equeue_post(equeue_t *queue, void (*cb)(void *), void *event); -// Cancel events that are in flight +// Cancel an in-flight event // -// Every equeue_call function returns a non-negative identifier on success -// that can be used to cancel an in-flight event. If the event has already -// been dispatched or does not exist, no error occurs. Note, this can not -// stop a currently executing event -void equeue_cancel(equeue_t *queue, int event); +// Attempts to cancel an event referenced by the unique id returned from +// equeue_call or equeue_post. It is safe to call equeue_cancel after an event +// has already been dispatched. +// +// The equeue_cancel function is irq safe. +// +// If called while the event queue's dispatch loop is active, equeue_cancel +// does not garuntee that the event will not not execute after it returns as +// the event may have already begun executing. +void equeue_cancel(equeue_t *queue, int id); // Background an event queue onto a single-shot timer // // The provided update function will be called to indicate when the queue // should be dispatched. A negative timeout will be passed to the update -// function when the timer is no longer needed. A null update function -// will disable the existing timer. +// function when the timer is no longer needed. +// +// Passing a null update function disables the existing timer. +// +// The equeue_background function allows an event queue to take advantage +// of hardware timers or even other event loops, allowing an event queue to +// be effectively backgrounded. void equeue_background(equeue_t *queue, void (*update)(void *timer, int ms), void *timer); @@ -155,8 +193,12 @@ void equeue_background(equeue_t *queue, // // After chaining a queue to a target, calling equeue_dispatch on the // target queue will also dispatch events from this queue. The queues -// will use their own buffers and events are handled independently. -// A null queue as the target will unchain this queue. +// use their own buffers and events must be managed independently. +// +// Passing a null queue as the target will unchain the existing queue. +// +// The equeue_chain function allows multiple equeues to be composed, sharing +// the context of a dispatch loop while still being managed independtly. void equeue_chain(equeue_t *queue, equeue_t *target); diff --git a/equeue_mutex.h b/equeue_mutex.h index 6064baa..cf2e893 100644 --- a/equeue_mutex.h +++ b/equeue_mutex.h @@ -12,11 +12,13 @@ extern "C" { #endif -// Mutex type +// Platform mutex type // -// If this type is safe in interrupt contexts, then -// the associated event queue will also be safe in -// interrupt contexts. +// The equeue library requires at minimum a non-recursive mutex that is +// safe in interrupt contexts. The mutex section is help for a bounded +// amount of time, so simply disabling interrupts is acceptable +// +// If irq safety is not required, a regular blocking mutex can be used. #if defined(__unix__) #include typedef pthread_mutex_t equeue_mutex_t; @@ -25,7 +27,14 @@ typedef unsigned equeue_mutex_t; #endif -// Mutex operations +// Platform mutex operations +// +// The equeue_mutex_create and equeue_mutex_destroy manage the lifetime +// of the mutex. On error, equeue_mutex_create should return a negative +// error code. +// +// The equeue_mutex_lock and equeue_mutex_unlock lock and unlock the +// underlying mutex. int equeue_mutex_create(equeue_mutex_t *mutex); void equeue_mutex_destroy(equeue_mutex_t *mutex); void equeue_mutex_lock(equeue_mutex_t *mutex); diff --git a/equeue_sema.h b/equeue_sema.h index 80c0b60..134a242 100644 --- a/equeue_sema.h +++ b/equeue_sema.h @@ -14,10 +14,17 @@ extern "C" { #include -// Semaphore type +// Platform semaphore type // -// Optimal implementation is a binary semaphore, -// however a regular semaphore is sufficient. +// The equeue library requires a binary semaphore type that can be safely +// signaled from interrupt contexts and from inside a equeue_mutex section. +// +// The equeue_signal_wait is relied upon by the equeue library to sleep the +// processor between events. Spurious wakeups have no negative-effects. +// +// A counting semaphore will also work, however may cause the event queue +// dispatch loop to run unnecessarily. For that matter, equeue_signal_wait +// may even be implemented as a single return statement. #if defined(__unix__) #include typedef sem_t equeue_sema_t; @@ -30,7 +37,19 @@ typedef bool equeue_sema_t; #endif -// Semaphore operations +// Platform semaphore operations +// +// The equeue_sema_create and equeue_sema_destroy manage the lifetime +// of the semaphore. On error, equeue_sema_create should return a negative +// error code. +// +// The equeue_sema_signal marks a semaphore as signalled such that the next +// equeue_sema_wait will return true. +// +// The equeue_sema_wait waits for a semaphore to be signalled or returns +// immediately if equeue_sema_signal had been called since the last +// equeue_sema_wait. The equeue_sema_wait returns true if it detected that +// equeue_sema_signal had been called. int equeue_sema_create(equeue_sema_t *sema); void equeue_sema_destroy(equeue_sema_t *sema); void equeue_sema_signal(equeue_sema_t *sema); diff --git a/equeue_tick.h b/equeue_tick.h index f28705d..914e916 100644 --- a/equeue_tick.h +++ b/equeue_tick.h @@ -12,10 +12,14 @@ extern "C" { #endif -// Monotonic tick +// Platform millisecond counter // -// Returns a tick that is incremented every millisecond, -// must intentionally overflow to 0 after 2^32-1 +// Return a tick that represents the number of milliseconds that have passed +// since an arbitrary point in time. The granularity does not need to be at +// the millisecond level, however the accuracy of the equeue library is +// limited by the accuracy of this tick. +// +// Must intentionally overflow to 0 after 2^32-1 unsigned equeue_tick(void);