├── .gitignore ├── deref ├── build.sh ├── main.c └── readme.md ├── hash-map ├── .vscode │ ├── c_cpp_properties.json │ ├── launch.json │ └── tasks.json ├── Makefile ├── inc │ └── hash.h ├── readme.md └── src │ ├── hash.c │ └── main.c ├── readme.md ├── state-machine ├── build.sh ├── machine-impl.h ├── machine.c ├── machine.h ├── main.c └── readme.md ├── stretchy-buffers ├── build.sh ├── main.c ├── readme.md ├── stretchy-buffer.c └── stretchy-buffer.h └── thread-fifo ├── build.sh ├── fifo.c ├── fifo.h ├── main.c └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | 4 | -------------------------------------------------------------------------------- /deref/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | mkdir -p obj 6 | mkdir -p bin 7 | 8 | LINK_FLAGS="" 9 | COMPILER_FLAGS="-g" 10 | 11 | for FILE in *.c; do 12 | gcc $COMPILER_FLAGS $FILE -o ./obj/$FILE.o -c; 13 | done 14 | 15 | gcc -o bin/main $LINK_FLAGS obj/*.o 16 | -------------------------------------------------------------------------------- /deref/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void printValue(uint32_t x) { 5 | printf("The value is: %d\n", x); 6 | } 7 | 8 | int main() { 9 | uint32_t a = 42; 10 | 11 | // Let's say we have a pointer 12 | uint32_t* pa = &a; 13 | 14 | // We can dereference it the normal way 15 | printValue(*pa); 16 | 17 | // But we can also reference it as if it were an array 18 | printValue(pa[0]); 19 | 20 | // In the case of the array method, it's basically syntactic sugar for this: 21 | printValue(*(pa + 0)); 22 | 23 | // What you might not realise is that since this syntax is just an addition, 24 | // you can actually do this: 25 | printValue(0[pa]); 26 | 27 | // This leads to some *interesting* opportunities for confusing code 28 | printValue((~0)[pa-=~0]); 29 | 30 | return 0; 31 | } 32 | -------------------------------------------------------------------------------- /deref/readme.md: -------------------------------------------------------------------------------- 1 | # Creative Dereferencing 2 | 3 | I'll just let the code speak for itself on this one 4 | 5 | ```C 6 | #include 7 | #include 8 | 9 | void printValue(uint32_t x) { 10 | printf("The value is: %d\n", x); 11 | } 12 | 13 | int main() { 14 | uint32_t a = 42; 15 | 16 | // Let's say we have a pointer 17 | uint32_t* pa = &a; 18 | 19 | // We can dereference it the normal way 20 | printValue(*pa); 21 | 22 | // But we can also reference it as if it were an array 23 | printValue(pa[0]); 24 | 25 | // In the case of the array method, it's basically syntactic sugar for this: 26 | printValue(*(pa + 0)); 27 | 28 | // What you might not realise is that since this syntax is just an addition, 29 | // you can actually do this: 30 | printValue(0[pa]); 31 | 32 | // This leads to some *interesting* opportunities for confusing code 33 | printValue((~0)[pa-=~0]); 34 | 35 | return 0; 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /hash-map/.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**", 7 | "${workspaceFolder}/inc" 8 | ], 9 | "defines": [], 10 | "compilerPath": "/usr/bin/gcc", 11 | "cStandard": "gnu17", 12 | "cppStandard": "gnu++14", 13 | "intelliSenseMode": "linux-gcc-x64" 14 | } 15 | ], 16 | "version": 4 17 | } -------------------------------------------------------------------------------- /hash-map/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "(gdb) Launch", 5 | "type": "cppdbg", 6 | "request": "launch", 7 | "program": "${workspaceFolder}/build/main", 8 | "preLaunchTask": "make", 9 | "args": [], 10 | "stopAtEntry": false, 11 | "cwd": "${fileDirname}", 12 | "environment": [], 13 | "externalConsole": false, 14 | "MIMode": "gdb", 15 | "setupCommands": [ 16 | { 17 | "description": "Enable pretty-printing for gdb", 18 | "text": "-enable-pretty-printing", 19 | "ignoreFailures": true 20 | }, 21 | { 22 | "description": "Set Disassembly Flavor to Intel", 23 | "text": "-gdb-set disassembly-flavor intel", 24 | "ignoreFailures": true 25 | } 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /hash-map/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "make", 8 | "type": "shell", 9 | "command": "make" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /hash-map/Makefile: -------------------------------------------------------------------------------- 1 | CC = gcc 2 | MKDIR=mkdir -p 3 | RM=rm -rf 4 | 5 | INC_DIR=inc 6 | SRC_DIR=src 7 | OBJ_DIR=obj 8 | BUILD_DIR=build 9 | 10 | IFLAGS=-I inc 11 | CFLAGS=-g 12 | 13 | SRC_FILES=$(wildcard src/*.c) 14 | OBJ_FILES:=$(patsubst $(SRC_DIR)/%, $(OBJ_DIR)/%, $(patsubst %.c, %.o, $(SRC_FILES))) 15 | 16 | EXE=main 17 | 18 | .PHONY: all clean 19 | 20 | # all: 21 | # echo $(OBJ_FILES) 22 | all: $(BUILD_DIR)/$(EXE) 23 | 24 | $(BUILD_DIR)/$(EXE) : $(OBJ_FILES) 25 | $(MKDIR) $(BUILD_DIR) 26 | $(CC) $^ -o $@ 27 | 28 | $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c 29 | $(MKDIR) $(OBJ_DIR) 30 | $(CC) $(IFLAGS) $(CFLAGS) -c $< -o $@ 31 | 32 | clean: 33 | $(RM) $(OBJ_DIR)/* $(BUILD_DIR)/* -------------------------------------------------------------------------------- /hash-map/inc/hash.h: -------------------------------------------------------------------------------- 1 | #ifndef HASH_H 2 | #define HASH_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct hm_node_t { 9 | // Regular linked list stuff 10 | struct hm_node_t* next; 11 | 12 | // Store a pointer to the item data 13 | void* item; 14 | 15 | // Store the key so we can properly retrieve items 16 | char key[0]; 17 | } hm_node_t; 18 | 19 | typedef struct { 20 | // Configurable number of buckets 21 | size_t numBuckets; 22 | // Pointer to a yet-to-be-created array of buckets 23 | hm_node_t** buckets; 24 | } hashmap_t; 25 | 26 | // The actual hashing function. This takes a string key, and the number of buckets 27 | // to make life easier. The actual bucketing part could also happen outside of the 28 | // hash function 29 | size_t hash_function(const char* ptr, size_t buckets); 30 | 31 | // Allocate and initialise a hashmap with a fixed bucketSize 32 | hashmap_t* hm_create(const size_t buckets); 33 | 34 | bool hm_remove(hashmap_t* hm, const char* key); 35 | void* hm_get(hashmap_t* hm, const char* key); 36 | void hm_set(hashmap_t* hm, const char* key, void* item); 37 | void hm_free(hashmap_t* hm); 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /hash-map/readme.md: -------------------------------------------------------------------------------- 1 | # Hashmap Data Structure From Scratch 2 | 3 | *Note: This implementation draws a lot from the far more in-depth [blog post put together by Andrei Ciobanu](https://www.andreinc.net/2021/10/02/implementing-hash-tables-in-c-part-1). Be sure to check that out if you want to see how these ideas are taken the next level!* 4 | 5 | Hashmaps, sometimes known as *hash tables*, *associative arrays*, or *dictionaries*, allow mapping a given value to a **key** that represents that value. These are fundamental data structures in almost every language, and are invaluable when solving certain kinds of problems. 6 | 7 | In this project, in order to keep it simple and focussed, the hashmap implementation will have string keys, and arbitrary types as values. Data will be stored in the map by reference, meaning it's the user's responsibility to allocate the memory for any given item. With a little tweaking, you can easily write an implementation where keys can be any kind of data, and primitive values can be stored directly. 8 | 9 | ## Mechanism 10 | 11 | The way they actually operate is a lot like a regular array. In an array, you can store a fixed number of elements of the same type, using a numerical index to select the individual items. The basis of the hashmap in this project is also an array. The string key is transformed into an index by running it through a hashing function (hence the name hashmap). 12 | 13 | Hashing functions are an entire can of worms in and of themselves - ranging from simple techniques like adding all of the characters modulo the size of the array, to much more complex techniques where mathematical/cryptographic properties are taken into account. 14 | 15 | The biggest challenge when selecting a hashing function is that different keys can end up producing the same hash, which is known as a collision. Collisions are par for the course in a hashmap, but some functions will do a much better job at reducing the number of collisions that occur. In a sense, what you're looking for is for in a hashing function is generating a seemingly random index, where the randomness has a uniform distribution (every index is as likely as any other). 16 | 17 | Without going into too much theory, this is the implementation of the hash function (found in src/hash.c): 18 | 19 | ```C 20 | #define DJB2_INIT 5381 21 | size_t hash_function(const char* ptr, const size_t buckets) { 22 | size_t hash = DJB2_INIT; 23 | 24 | uint8_t byte; 25 | while ((byte = *ptr++)) { 26 | hash = (size_t)(((hash << 5) + hash) + byte); 27 | } 28 | 29 | return hash % buckets; 30 | } 31 | ``` 32 | 33 | This function is called DJB2, and it works by starting out with an initial seed value (`DJB2_INIT`), and and iteratively modifying this value with each byte of the key, mixing up the hash each time. Let's focus on the line that's doing the hard work and remove some of the noise: 34 | 35 | ```C 36 | ((hash << 5) + hash) + byte 37 | ``` 38 | 39 | Each new `hash` value is the result of this line. First, the existing value is mixed in with itself by left shifting the current value and then adding the value. Another way of thinking about this operation just: 40 | 41 | ```C 42 | hash * 33 + byte 43 | ``` 44 | 45 | Left shifting is really just multiplication by a power of 2 (`2^5 == 32, + 1 = 33`). Why 33 you ask? Well, it seems to work the best! Apparently no formal explanation has ever really be given as to why. Same thing for the initialisation value `5381` - it just works well. I know, it was unsatisfying for me too. 46 | 47 | In the end, whatever the result of all of this was is reduced to a fixed range with the modulo operator. 48 | 49 | ## The rest of the owl 50 | 51 | The hash function is the key here (no pun intended), but it's not all there is. We need a structure to represent the hash map itself, which looks like this (found in inc/hash.h): 52 | 53 | ```C 54 | typedef struct { 55 | // Configurable number of buckets 56 | size_t numBuckets; 57 | // Pointer to a yet-to-be-created array of buckets 58 | hm_node_t** buckets; 59 | } hashmap_t; 60 | ``` 61 | 62 | It holds two properties: a number of buckets, and the buckets themselves. A bucket is a space that represents a given hash. When we create a hashmap, we might give ourselves 100 buckets. This means there can only be 100 individual hashes. Colliding hashes map into the same bucket. 63 | 64 | The user actually creates a `hashmap_t` by calling `hm_create`, which looks like this (found in src/hash.c): 65 | 66 | ```C 67 | hashmap_t* hm_create(const size_t buckets) { 68 | hashmap_t* hm = malloc(sizeof(hashmap_t)); 69 | hm->numBuckets = buckets; 70 | hm->buckets = malloc(sizeof(hm_node_t*) * buckets); 71 | 72 | return hm; 73 | } 74 | ``` 75 | 76 | Two allocations take place. The first is reserving enough space to store the `hashmap_t` structure itself. The second is space for a contiguous array of `hm_node_t*` buckets. A bucket has the type `hm_node_t`, which is defined like so: 77 | 78 | ```C 79 | typedef struct hm_node_t { 80 | // Regular linked list stuff 81 | struct hm_node_t* next; 82 | 83 | // Store a pointer to the item data 84 | void* item; 85 | 86 | // Store the key so we can properly retrieve items 87 | char key[0]; 88 | } hm_node_t; 89 | ``` 90 | 91 | It's a linked list! Well, in this implementation it is anyway - there are other ways of doing this. As well as storing a pointer to the next node in the linked list, it also stores a (void) pointer to the item (the value associated with a key), and the key itself. A tricky note here is that the key is stored as a 0-element array of characters - meaning it apparently takes up no space in this struct. This is done because we don't know the size of the key until the user places data into the hashmap. When they do, we can allocate enough memory for this struct, plus as much as we need for the key. Since the key is the last member of the struct, it won't clobber any other items. 92 | 93 | So why a linked list? Because keys can collide with each other. When we go to retrieve an item by key, we have to hash the key to get the bucket index. Then we get the first node in the bucket and compare the stored key with the key the user gave us. If the key doesn't match, we go to the next node in the list and check that, until we're out of nodes. The implementation of `hm_get` looks something like this (found in src/hash.c): 94 | 95 | ```C 96 | void* hm_get(hashmap_t* hm, const char* key) { 97 | // Get the hash of the key 98 | size_t hash = hash_function(key, hm->numBuckets); 99 | 100 | // Attempt to find the node 101 | hm_node_t* node = hm->buckets[hash]; 102 | 103 | while (node) { 104 | if (strncmp(node->key, key, HM_MAX_KEY_LENGTH) == 0) { 105 | // This is the item we're looking for 106 | return node->item; 107 | } 108 | node = node->next; 109 | } 110 | 111 | // We didn't find the key we were looking for 112 | return NULL; 113 | } 114 | ``` 115 | 116 | The remaining operations, `hm_set` and `hm_remove` work in a very similar way; First get the hash, and then traverse the nodes until the one matching the key is found. Then the data can be updated in the case of `hm_set`, or the node spliced out in the case of `hm_remove`. In both cases, of course, the the lookup may fail if the key wasn't in the hashmap in the first place. A little extra code makes sure these are properly handled too. You can see the full details in src/hash.c - it's about 100 lines with the comments and extra spacing stripped out. 117 | 118 | ## Improvements 119 | 120 | There are a bunch of improvements and optimistations to be made, and you can find more information about that over at [Andrei's blog](https://www.andreinc.net/2021/10/02/implementing-hash-tables-in-c-part-1). 121 | 122 | An immediate and simple improvement would be use [stretchy buffers AKA vectors](../stretchy-buffers/readme.md) instead of linked lists to store collections of items in the buckets. This can have a big impact on cache locality when often used keys/buckets are accessed - especially when the number of buckets is small. 123 | 124 | -------------------------------------------------------------------------------- /hash-map/src/hash.c: -------------------------------------------------------------------------------- 1 | #include "hash.h" 2 | #include 3 | #include 4 | 5 | #ifndef HM_MAX_KEY_LENGTH 6 | #define HM_MAX_KEY_LENGTH 512 7 | #endif // HM_MAX_KEY_LENGTH 8 | 9 | #define DJB2_INIT 5381 10 | size_t hash_function(const char* ptr, const size_t buckets) { 11 | size_t hash = DJB2_INIT; 12 | 13 | uint8_t byte; 14 | while ((byte = *ptr++)) { 15 | hash = (size_t)(((hash << 5) + hash) + byte); 16 | } 17 | 18 | return hash % buckets; 19 | } 20 | 21 | hashmap_t* hm_create(const size_t buckets) { 22 | hashmap_t* hm = malloc(sizeof(hashmap_t)); 23 | hm->numBuckets = buckets; 24 | hm->buckets = malloc(sizeof(hm_node_t*) * buckets); 25 | 26 | return hm; 27 | } 28 | 29 | void hm_set(hashmap_t* hm, const char* key, void* item) { 30 | // Get the hash of the key 31 | size_t hash = hash_function(key, hm->numBuckets); 32 | 33 | // Attempt to find the node 34 | hm_node_t* prevNode = NULL; 35 | hm_node_t* node = hm->buckets[hash]; 36 | 37 | while (node) { 38 | if (strncmp(node->key, key, HM_MAX_KEY_LENGTH) == 0) { 39 | // Replace the value at this node 40 | node->item = item; 41 | return; 42 | } 43 | prevNode = node; 44 | node = node->next; 45 | } 46 | 47 | // We didn't find our key, malloc a new node and insert it 48 | size_t keyLength = strnlen(key, HM_MAX_KEY_LENGTH-1) + 1; 49 | hm_node_t* newNode = malloc(sizeof(hm_node_t) + keyLength); 50 | newNode->item = item; 51 | newNode->next = NULL; 52 | 53 | // Copy the key into the node directly 54 | strncpy(newNode->key, key, keyLength); 55 | newNode->key[keyLength] = '\0'; 56 | 57 | // If there was a collision on the hash, we should have a pointer to the last node 58 | // which we can attach our new node to 59 | if (prevNode) { 60 | prevNode->next = newNode; 61 | return; 62 | } 63 | 64 | // Otherwise, we will simply insert this node at the hash position 65 | hm->buckets[hash] = newNode; 66 | } 67 | 68 | bool hm_remove(hashmap_t* hm, const char* key) { 69 | // Get the hash of the key 70 | size_t hash = hash_function(key, hm->numBuckets); 71 | 72 | // Attempt to find the node 73 | hm_node_t* prevNode = NULL; 74 | hm_node_t* node = hm->buckets[hash]; 75 | 76 | while (node) { 77 | if (strncmp(node->key, key, HM_MAX_KEY_LENGTH) == 0) { 78 | // We need a reference to the next node 79 | hm_node_t* next = node->next; 80 | 81 | // This node might be the first in the list. If it's not, link the previous node 82 | // to the next 83 | if (prevNode) { 84 | prevNode->next = next; 85 | } else { 86 | // Otherwise, this is the first node in the list 87 | hm->buckets[hash] = next; 88 | } 89 | 90 | // Free the old node 91 | free(node); 92 | 93 | // We're done 94 | return true; 95 | } 96 | prevNode = node; 97 | node = node->next; 98 | } 99 | 100 | // We didn't find the key we were looking for 101 | return false; 102 | } 103 | 104 | void* hm_get(hashmap_t* hm, const char* key) { 105 | // Get the hash of the key 106 | size_t hash = hash_function(key, hm->numBuckets); 107 | 108 | // Attempt to find the node 109 | hm_node_t* node = hm->buckets[hash]; 110 | 111 | while (node) { 112 | if (strncmp(node->key, key, HM_MAX_KEY_LENGTH) == 0) { 113 | // This is the item we're looking for 114 | return node->item; 115 | } 116 | node = node->next; 117 | } 118 | 119 | // We didn't find the key we were looking for 120 | return NULL; 121 | } 122 | 123 | void hm_free(hashmap_t* hm) { 124 | if (!hm) return; 125 | 126 | // Pointers for iterating through all the nodes as we free them 127 | hm_node_t* node; 128 | hm_node_t* next; 129 | 130 | // Work through every bucket, freeing the nodes 131 | for (size_t i = 0; i < hm->numBuckets; i++) { 132 | node = hm->buckets[i]; 133 | 134 | while (node) { 135 | next = node->next; 136 | free(node); 137 | node = next; 138 | } 139 | } 140 | 141 | // After we've freed all the nodes, we can free the hashmap struct itself 142 | free(hm); 143 | } 144 | -------------------------------------------------------------------------------- /hash-map/src/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "hash.h" 3 | 4 | void printGetResult(hashmap_t* hm, const char* key); 5 | 6 | // Hash map will only have 10 buckets 7 | #define BUCKET_SIZE 10 8 | 9 | // Define some keys to use for testing 10 | #define KEY1 "Some key" 11 | #define KEY2 "Another key" 12 | #define KEY3 "Key number 3....." // This key has the same hash as KEY0 with 10 buckets 13 | #define KEY4 "Key number 4" 14 | 15 | int main() { 16 | hashmap_t* hm = hm_create(BUCKET_SIZE); 17 | 18 | uint32_t value1 = 0xdeadbeef; 19 | uint32_t value2 = 0xc0decafe; 20 | uint32_t value3 = 0xf00dbabe; 21 | uint32_t value4 = 0xfeedc0de; 22 | 23 | hm_set(hm, KEY1, &value1); 24 | hm_set(hm, KEY2, &value2); 25 | hm_set(hm, KEY3, &value3); 26 | hm_set(hm, KEY4, &value4); 27 | 28 | printGetResult(hm, KEY1); 29 | printGetResult(hm, KEY2); 30 | printGetResult(hm, KEY3); 31 | printGetResult(hm, KEY4); 32 | printGetResult(hm, "Not here!"); 33 | 34 | printf(" [i] Removing key '%s'\n", KEY1); 35 | hm_remove(hm, KEY1); 36 | printGetResult(hm, KEY1); 37 | printGetResult(hm, KEY3); 38 | 39 | printf(" [i] Setting new value for key '%s'\n", KEY1); 40 | value1 = 0x123; 41 | hm_set(hm, KEY1, &value1); 42 | printGetResult(hm, KEY1); 43 | printGetResult(hm, KEY3); 44 | 45 | hm_free(hm); 46 | 47 | return 0; 48 | } 49 | 50 | void printGetResult(hashmap_t* hm, const char* key) { 51 | uint32_t* valuePtr; 52 | valuePtr = hm_get(hm, key); 53 | if (valuePtr) { 54 | printf("[%s]: %x\n", key, *valuePtr); 55 | } else { 56 | printf("[%s]: Key not found!\n", key); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Tiny C Projects 2 | 3 | This repo contains tiny projects that explore a concept, pattern, or idea in the most minimal form, while still being generally applicable. 4 | 5 | They aren't intended for drop-in production use, and aren't intended to cover every possible use/edge case. Each project should contain a `readme.md` explaining the idea and motivation. 6 | 7 | ## Projects 8 | 9 | - [State Machine](./state-machine): A simple library for implementing event driven state machines 10 | - [FIFO Queue](./thread-fifo): A circular fifo buffer and accompanying operations that can be used to communicate between threads 11 | - [Stretchy Buffers (aka Vectors)](./stretchy-buffers): An implementation of automatically resizable array that does not have a predefined or fixed size 12 | - [Creative Dereferencing](./deref/): A mini exploration of some of the interesting ways you can dereference a pointer 13 | - [Hashmap Data Structure From Scratch](./hash-map/): An implementation of a Hashmap data structure, with string keys and arbitrarily typed values. 14 | -------------------------------------------------------------------------------- /state-machine/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | mkdir -p obj 6 | mkdir -p bin 7 | 8 | LINK_FLAGS="" 9 | COMPILER_FLAGS="-g" 10 | 11 | for FILE in *.c; do 12 | gcc $COMPILER_FLAGS $FILE -o ./obj/$FILE.o -c; 13 | done 14 | 15 | gcc -o bin/main $LINK_FLAGS obj/*.o 16 | -------------------------------------------------------------------------------- /state-machine/machine-impl.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_MACHINE_IMPL_H 2 | #define INC_MACHINE_IMPL_H 3 | 4 | typedef enum { 5 | NoEvent = -1, 6 | EventA, 7 | EventB, 8 | EventC 9 | } TransitionEvent; 10 | 11 | typedef enum { 12 | StateA, 13 | StateB, 14 | StateC, 15 | } MachineState; 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /state-machine/machine.c: -------------------------------------------------------------------------------- 1 | #include "machine.h" 2 | 3 | bool StateMachine_run(State_t* currentState, TransitionEvent_t event, Transition_t* transitions, size_t numTransitions) { 4 | for (int i = 0; i < numTransitions; i++) { 5 | if (transitions[i].state == *currentState && transitions[i].event == event) { 6 | return transitions[i].transitionLogic(currentState) >= 0; 7 | } 8 | } 9 | 10 | return false; 11 | } 12 | -------------------------------------------------------------------------------- /state-machine/machine.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_STATE_MACHINE_H 2 | #define INC_STATE_MACHINE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | typedef int32_t TransitionEvent_t; 9 | typedef int32_t State_t; 10 | 11 | typedef TransitionEvent_t (*NextEventFn)(const State_t); 12 | typedef int32_t (*TransitionFn)(State_t*); 13 | 14 | typedef struct { 15 | State_t state; 16 | TransitionEvent_t event; 17 | TransitionFn transitionLogic; 18 | } Transition_t; 19 | 20 | bool StateMachine_run(State_t* currentState, TransitionEvent_t event, Transition_t* transitions, size_t numTransitions); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /state-machine/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "machine.h" 3 | #include "machine-impl.h" 4 | 5 | static int32_t gotoStateA(State_t* state) { 6 | *state = StateA; 7 | return StateA; 8 | } 9 | 10 | static int32_t gotoStateB(State_t* state) { 11 | *state = StateB; 12 | return StateB; 13 | } 14 | 15 | static int32_t gotoStateC(State_t* state) { 16 | *state = StateC; 17 | return StateC; 18 | } 19 | 20 | Transition_t transitions[] = { 21 | { StateA, EventA, &gotoStateB }, 22 | { StateB, EventB, &gotoStateC }, 23 | { StateC, EventC, &gotoStateA }, 24 | }; 25 | #define NUM_TRANSITIONS 3 26 | 27 | int counter = 0; 28 | 29 | TransitionEvent_t getNextEvent(const State_t currentState) { 30 | if (counter < 2) { 31 | ++counter; 32 | return NoEvent; 33 | } 34 | 35 | if (counter == 2) { 36 | ++counter; 37 | return EventA; 38 | } 39 | 40 | if (counter < 4) { 41 | ++counter; 42 | return NoEvent; 43 | } 44 | 45 | if (counter == 4) { 46 | ++counter; 47 | return EventB; 48 | } 49 | 50 | if (counter == 5) { 51 | ++counter; 52 | return EventC; 53 | } 54 | 55 | counter = 0; 56 | return NoEvent; 57 | } 58 | 59 | State_t currentState = StateA; 60 | 61 | int main() { 62 | TransitionEvent_t event; 63 | for (int i = 0; i < 10; i++) { 64 | event = getNextEvent(currentState); 65 | printf("State (%d) Event (%d) -> ", currentState, event); 66 | 67 | if (StateMachine_run(¤tState, event, transitions, NUM_TRANSITIONS)) { 68 | printf("State (%d)\n", currentState); 69 | } else { 70 | printf("No transition\n"); 71 | } 72 | } 73 | 74 | return 0; 75 | } 76 | -------------------------------------------------------------------------------- /state-machine/readme.md: -------------------------------------------------------------------------------- 1 | # State Machine 2 | 3 | The idea with this little project is to build a minimal example of a reusable state machine interface, flexible enough to cater to event driven systems (in the interrupt sense) or simply logic running in a loop. 4 | 5 | ## The "Library" Side 6 | 7 | `machine.h` defines an interface a simple interface, where states and events are aliases for `int32_t`, as well as a typedef for a state transition struct (`Transition_t`), and a single function `StateMachine_run` that will be in charge of orchestrating transitions. 8 | 9 | A `Transition_t` holds a relevant state, an event that it is sensitive to, and a function pointer that can be called when those conditions are met which can optionally change the machines state. 10 | 11 | `machine.c` holds the implementation for `StateMachine_run`, whose signature is: 12 | 13 | ```C 14 | bool StateMachine_run(State_t* currentState, TransitionEvent_t event, Transition_t* transitions, size_t numTransitions); 15 | ``` 16 | 17 | It's not much more complex than looping over the transition table, checking if the conditions match for the current state and the event, then running transition logic function. A little note here is that the transition logic function returns an `int32_t` (in most cases probably just the state it transitioned to if a transition occurred), but a negative value can signify that no transition happened. `StateMachine_run` itself returns `bool`, which is basically an indication of "was there a transition at all?" This is purely convention and can safely be ignored if you happened to implement states with different semantics, but seems useful enough to leave in. 18 | 19 | ## The "User" Side 20 | 21 | A user can include those two files in their project, and then the only thing they would need to do is to define some kind of schema for the states and events, and build a transition table. There is a `machine-impl.h` file, which I've set up some enums for states and events, but it's really just illustrative. `main.c` shows the rest - some functions that handle state transition logic, a transition table, a function to generate events, and some code to exercise the state machine through some transitions. 22 | -------------------------------------------------------------------------------- /stretchy-buffers/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | mkdir -p obj 6 | mkdir -p bin 7 | 8 | LINK_FLAGS="" 9 | COMPILER_FLAGS="-g" 10 | 11 | for FILE in *.c; do 12 | gcc $COMPILER_FLAGS $FILE -o ./obj/$FILE.o -c; 13 | done 14 | 15 | gcc -o bin/main $LINK_FLAGS obj/*.o 16 | -------------------------------------------------------------------------------- /stretchy-buffers/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "stretchy-buffer.h" 3 | 4 | typedef struct { 5 | uint32_t x; 6 | uint32_t y; 7 | uint32_t z; 8 | } Point3d; 9 | 10 | void floatExample() { 11 | float* buf = NULL; 12 | 13 | sb_pushback(buf, 1.2f); 14 | sb_pushback(buf, 2.4f); 15 | sb_pushback(buf, 3.14f); 16 | sb_pushback(buf, -37.1f); 17 | sb_pushback(buf, 55.0f); 18 | 19 | printf("Capacity: %d Length: %d\n", sb_capacity(buf), sb_length(buf)); 20 | 21 | for (size_t i = 0; i < sb_length(buf); i++) { 22 | printf("%d: %f\n", i, buf[i]); 23 | } 24 | } 25 | 26 | void pointExample() { 27 | sb_create(Point3d, points); 28 | 29 | Point3d temp = { 1, 2, 3 }; 30 | 31 | sb_pushback(points, temp); 32 | 33 | temp.x = 55; 34 | temp.y = 75; 35 | sb_pushback(points, temp); 36 | 37 | temp.x = -1; 38 | temp.y = -2; 39 | temp.z = -3; 40 | sb_pushback(points, temp); 41 | 42 | Point3d p = *sb_pop(points); 43 | printf("popped: %d %d %d\n", p.x, p.y, p.z); 44 | 45 | for (size_t i = 0; i < sb_length(points); i++) { 46 | printf("p%d: %d %d %d\n", i, points[i].x, points[i].y, points[i].z); 47 | } 48 | 49 | printf("Capacity: %d, Length = %d\n", sb_capacity(points), sb_length(points)); 50 | } 51 | 52 | int main() { 53 | pointExample(); 54 | 55 | return 0; 56 | } 57 | -------------------------------------------------------------------------------- /stretchy-buffers/readme.md: -------------------------------------------------------------------------------- 1 | # Stretchy Buffer aka Vectors 2 | 3 | Stretchy buffers behave like arrays, except you don't need to specify their capacity up front, and they automatically expand as you add more items. In this way, they are a lot like the `vector` types you'd find in C++. 4 | 5 | I heard about stretchy buffers from [Bitwise](https://github.com/pervognsen/bitwise), Per Vognesen's series about creating a computing platform from the ground up, though [Sean Barret](https://nothings.org/) seems to be the originator of the pattern. This implementation is pretty much identical to what Per shows on Bitwise - so if you want a little more nuance and explanation, check out the video where he codes it up. 6 | 7 | The beauty of stretchy buffers is that, to the user, they appear to just be regular arrays accessed using the familar `array[index]` syntax. 8 | 9 | ```C 10 | float* buf = NULL; // Empty buffer pointing to nothing 11 | 12 | // Push some data 13 | sb_pushback(buf, 3.14f); 14 | sb_pushback(buf, 1.0f); 15 | sb_pushback(buf, -10.0f); 16 | 17 | for (size_t i = 0; i < sb_length(buf); i++) { 18 | // Access values just like a regular array 19 | printf("%d: %f\n", i, buf[i]); 20 | } 21 | 22 | // -> 0: 3.140000 23 | // -> 1: 1.000000 24 | // -> 2: -10.000000 25 | ``` 26 | 27 | ## The API 28 | 29 | `stretchy-buffers.h` defines the basis of all the operations. The public API includes the following "functions": 30 | 31 | - `sb_length(bufferPtr)` 32 | - `sb_capacity(bufferPtr)` 33 | - `sb_pushback(bufferPtr, item)` 34 | - `sb_pop(bufferPtr)` 35 | - `sb_free(bufferPtr)` 36 | - `sb_create(itemType, name)` 37 | 38 | I put "functions" in quotes there because all of these functions are in fact just macros. There are also a few private "functions", and these all begin with `sb__` - and of course, these shouldn't be used by the end user. 39 | 40 | ## The core idea 41 | 42 | The core idea here is that you really do just have a regular array, only there is a little bit of extra data (the "header") just before the array in memory. That header contains the current capacity (as a `size_t`) and the current length (also a `size_t`). Whenever we want to, say, insert an element using `sb_pushback(bufferPtr, item)`, what happens is we access the header struct (`SBHeader`) just before the array and check if we have enough space for the new item. If we do, great - we simply write the value into the array at index `sb_length(bufferPtr)` and then increment the length. If we don't, we perform a reallocation of the total buffer (including the header), and then insert the element in the same way. 43 | 44 | The `SBHeader` struct looks like this: 45 | 46 | ```C 47 | typedef struct { 48 | // The actual header information 49 | size_t length; 50 | size_t capacity; 51 | // A named reference to the start of the data (takes no actual memory) 52 | uint8_t data[0]; 53 | } SBHeader; 54 | ``` 55 | 56 | Depending on your viewpoint, you might see that 0-length data array as a pretty clever way to give a name to the space where the users data *will* be, or as an absolute abomination that has no place in a codebase. I'm not making any value judgements here, but I will say that I've seen worse things in my life. 57 | 58 | So where does this header actually come from? As previously mentioned, when we use an operation like `sb_pushback`, we're checking if we have the capacity for the new element. The `sb_capacity` macro looks like this: 59 | 60 | ```C 61 | #define sb_capacity(b) ((b) ? sb__header(b)->capacity : 0) 62 | ``` 63 | 64 | There is an implicit `NULL` check happening here in the ternary, which will evaluate to `0` if the buffer hasn't yet been initialised. `sb_length` is defined in exactly the same way. The result is that when the current capacity is checked, it will evaluate to false in both the case where the buffer is initialised but there isn't space, and the case where the buffer wasn't initialised to begin with. When not enough space is available, a call is made to the only real function in the API: `void* sb__grow(void* data, size_t itemSize)` 65 | 66 | ```C 67 | void* sb__grow(void* data, size_t itemSize) { 68 | SBHeader* header; 69 | 70 | // Double the previous capacity (or set it to 1 if this is init) 71 | size_t newCapacity = MAX(1, sb_capacity(data) * 2); 72 | size_t newSize = offsetof(SBHeader, data) + newCapacity * itemSize; 73 | 74 | // The buffer isn't yet initialised 75 | if (data == NULL) { 76 | header = malloc(newSize); 77 | header->capacity = newCapacity; 78 | header->length = 0; 79 | } else { 80 | // The buffer has been previously allocated, reallocate more space 81 | header = realloc(sb__header(data), newSize); 82 | header->capacity = newCapacity; 83 | } 84 | 85 | // Give the user back their data 86 | return &header->data; 87 | } 88 | ``` 89 | 90 | This function does the heavy lifting. First it calculates what the new capacity will be. You can use various growth strategies here, but this one doubles the size of the buffer whenever space runs out. Next it calculates the size of the new buffer - and these are two different things. For one thing, this size includes the header, but it also needs to account for the size of each item. If the buffer is still NULL, it gets `malloc`d for the first time, otherwise it is `realloc`d. And of course, the *actual* pointer that was obtained through `malloc` was the header itself, so that's what we need to pass. But the user shouldn't be exposed to that, so we return a pointer to the data itself. 91 | 92 | You can take a closer look at [stretchy-buffer.h](./stretchy-buffer.h) to see how all of the operations are defined. -------------------------------------------------------------------------------- /stretchy-buffers/stretchy-buffer.c: -------------------------------------------------------------------------------- 1 | #include "stretchy-buffer.h" 2 | 3 | // Every good C program needs to redefine MAX for the billionth time 4 | #define MAX(a, b) ((a) < (b) ? (b) : (a)) 5 | 6 | void* sb__grow(void* data, size_t itemSize) { 7 | SBHeader* header; 8 | 9 | // Double the previous capacity (or set it to 1 if this is init) 10 | size_t newCapacity = MAX(1, sb_capacity(data) * 2); 11 | size_t newSize = offsetof(SBHeader, data) + newCapacity * itemSize; 12 | 13 | // The buffer isn't yet initialised 14 | if (data == NULL) { 15 | header = malloc(newSize); 16 | header->capacity = newCapacity; 17 | header->length = 0; 18 | } else { 19 | // The buffer has been previously allocated, reallocate more space 20 | header = realloc(sb__header(data), newSize); 21 | header->capacity = newCapacity; 22 | } 23 | 24 | // Give the user back their data 25 | return &header->data; 26 | } 27 | -------------------------------------------------------------------------------- /stretchy-buffers/stretchy-buffer.h: -------------------------------------------------------------------------------- 1 | #ifndef INC_STRETCHY_BUFFER_H 2 | #define INC_STRETCHY_BUFFER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | typedef struct { 10 | // The actual header information 11 | size_t length; 12 | size_t capacity; 13 | // A named reference to the start of the data (takes no actual memory) 14 | uint8_t data[0]; 15 | } SBHeader; 16 | 17 | // The only real function in the API, used to initialise and grow the buffer 18 | void* sb__grow(void* data, size_t itemSize); 19 | 20 | // Convert a pointer to a regular buffer to a pointer to a header 21 | #define sb__header(b) ((SBHeader*)((uint8_t*)b - offsetof(SBHeader, data))) 22 | 23 | // Check if n items can fit inside the buffer without a resize 24 | #define sb__fits(b, n) (sb_length(b) + n <= sb_capacity(b)) 25 | 26 | // Determine if the buffer needs to be resized, then resize if needed 27 | #define sb__fit(b, n) (sb__fits(b, n) ? 0 : ((b) = (sb__grow((b), sizeof(*(b)))))) 28 | 29 | // Get the current length 30 | #define sb_length(b) ((b) ? sb__header(b)->length : 0) 31 | 32 | // Get the current capacity 33 | #define sb_capacity(b) ((b) ? sb__header(b)->capacity : 0) 34 | 35 | // Push an item 36 | #define sb_pushback(b, item) (sb__fit(b, 1), b[sb_length(b)] = (item), sb__header(b)->length++) 37 | 38 | // Pop an item 39 | #define sb_pop(b) (sb_length(b) > 0 ? (sb__header(b)->length--, &b[sb_length(b)]) : 0) 40 | 41 | // Free the stretchy buffer 42 | #define sb_free(b) ((b) ? free(sb__header(b)) : 0) 43 | 44 | // Convenience function for creating a stretchy buffer, where you can't forget to assign NULL 45 | #define sb_create(type, name) type* name = NULL; 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /thread-fifo/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | mkdir -p obj 6 | mkdir -p bin 7 | 8 | LINK_FLAGS="-lpthread -lrt" 9 | 10 | for FILE in *.c; do 11 | gcc $FILE -o ./obj/$FILE.o -c; 12 | done 13 | 14 | gcc -o bin/main $LINK_FLAGS obj/*.o 15 | -------------------------------------------------------------------------------- /thread-fifo/fifo.c: -------------------------------------------------------------------------------- 1 | #include "fifo.h" 2 | 3 | void fifo_init(fifo_t* fifo) { 4 | fifo->head = 0; 5 | fifo->tail = 0; 6 | fifo->size = 0; 7 | sem_init(&fifo->mutex, 1, 1); 8 | } 9 | 10 | bool fifo_full(fifo_t* fifo) { 11 | sem_wait(&fifo->mutex); 12 | bool result = fifo->size >= FIFO_QUEUE_SIZE; 13 | sem_post(&fifo->mutex); 14 | return result; 15 | } 16 | 17 | bool fifo_empty(fifo_t* fifo) { 18 | sem_wait(&fifo->mutex); 19 | bool result = fifo->size == 0; 20 | sem_post(&fifo->mutex); 21 | return result; 22 | } 23 | 24 | fifo_event_t* fifo_pull(fifo_t* fifo) { 25 | sem_wait(&fifo->mutex); 26 | 27 | // If there's nothing to pull, return nullptr 28 | if (fifo->size == 0) { 29 | sem_post(&fifo->mutex); 30 | return NULL; 31 | } 32 | 33 | size_t head = fifo->head; 34 | fifo_event_t* event = &fifo->queue[head]; 35 | 36 | if (head == FIFO_QUEUE_SIZE - 1) { 37 | fifo->head = 0; 38 | } else { 39 | ++(fifo->head); 40 | } 41 | 42 | --(fifo->size); 43 | 44 | sem_post(&fifo->mutex); 45 | 46 | return event; 47 | } 48 | 49 | fifo_event_t* fifo_peek(fifo_t* fifo) { 50 | sem_wait(&fifo->mutex); 51 | 52 | // If there's nothing to pull, return nullptr 53 | if (fifo->size == 0) { 54 | sem_post(&fifo->mutex); 55 | return NULL; 56 | } 57 | 58 | fifo_event_t* event = &fifo->queue[fifo->head]; 59 | 60 | sem_post(&fifo->mutex); 61 | 62 | return event; 63 | } 64 | 65 | bool fifo_push(fifo_t* fifo, uint32_t event_type, void* data) { 66 | sem_wait(&fifo->mutex); 67 | 68 | if (fifo->size >= FIFO_QUEUE_SIZE) { 69 | sem_post(&fifo->mutex); 70 | return false; 71 | } 72 | 73 | size_t tail = fifo->tail; 74 | fifo->queue[tail].data = data; 75 | fifo->queue[tail].event_type = event_type; 76 | 77 | if (tail == FIFO_QUEUE_SIZE - 1) { 78 | fifo->tail = 0; 79 | } else { 80 | ++(fifo->tail); 81 | } 82 | 83 | ++(fifo->size); 84 | 85 | sem_post(&fifo->mutex); 86 | 87 | return true; 88 | } 89 | -------------------------------------------------------------------------------- /thread-fifo/fifo.h: -------------------------------------------------------------------------------- 1 | #ifndef FIFO_H 2 | #define FIFO_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifndef FIFO_QUEUE_SIZE 10 | #define FIFO_QUEUE_SIZE 128 11 | #endif 12 | 13 | typedef struct { 14 | uint32_t event_type; 15 | void* data; 16 | } fifo_event_t; 17 | 18 | typedef struct { 19 | sem_t mutex; 20 | fifo_event_t queue[FIFO_QUEUE_SIZE]; 21 | size_t head; 22 | size_t tail; 23 | size_t size; 24 | } fifo_t; 25 | 26 | void fifo_init(fifo_t* fifo); 27 | bool fifo_full(fifo_t* fifo); 28 | bool fifo_empty(fifo_t* fifo); 29 | fifo_event_t* fifo_pull(fifo_t* fifo); 30 | fifo_event_t* fifo_peek(fifo_t* fifo); 31 | bool fifo_push(fifo_t* fifo, uint32_t event_type, void* data); 32 | 33 | #endif // MAIN_H 34 | -------------------------------------------------------------------------------- /thread-fifo/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "fifo.h" 5 | 6 | void print_fifo_info(fifo_t* fifo) { 7 | printf("Length: %d\nEmpty: %d\nFull: %d\n", fifo->size, fifo_empty(fifo), fifo_full(fifo)); 8 | printf("Head: %d\nTail: %d\n", fifo->head, fifo->tail); 9 | printf("-----------------\n"); 10 | } 11 | 12 | void print_event_info(fifo_event_t* event) { 13 | if (event != NULL) { 14 | printf("Event Type: %d\nData Address: 0x%p\n", event->event_type, event->data); 15 | } else { 16 | printf("[!] Pulled from an empty fifo!\n"); 17 | } 18 | } 19 | 20 | void test_basics() { 21 | fifo_t fifo; 22 | fifo_t* p_fifo = &fifo; 23 | fifo_event_t* out; 24 | 25 | printf("> Init\n"); 26 | fifo_init(p_fifo); 27 | print_fifo_info(p_fifo); 28 | 29 | printf("> Pull from empty\n"); 30 | out = fifo_pull(p_fifo); 31 | print_event_info(out); 32 | print_fifo_info(p_fifo); 33 | 34 | printf("> Peek from empty\n"); 35 | out = fifo_peek(p_fifo); 36 | print_event_info(out); 37 | print_fifo_info(p_fifo); 38 | 39 | printf("> Push event 1\n"); 40 | fifo_push(p_fifo, 1, NULL); 41 | print_fifo_info(p_fifo); 42 | 43 | printf("> Peek at event 1\n"); 44 | out = fifo_peek(p_fifo); 45 | print_event_info(out); 46 | print_fifo_info(p_fifo); 47 | 48 | printf("> Push event 2\n"); 49 | fifo_push(p_fifo, 2, p_fifo); 50 | print_fifo_info(p_fifo); 51 | 52 | printf("> Push event 3\n"); 53 | fifo_push(p_fifo, 3, &out); 54 | print_fifo_info(p_fifo); 55 | 56 | printf("> Pull event 1\n"); 57 | out = fifo_pull(p_fifo); 58 | print_event_info(out); 59 | print_fifo_info(p_fifo); 60 | 61 | printf("> Push event 4\n"); 62 | fifo_push(p_fifo, 4, NULL); 63 | print_fifo_info(p_fifo); 64 | 65 | printf("> Pull event 2\n"); 66 | out = fifo_pull(p_fifo); 67 | print_event_info(out); 68 | print_fifo_info(p_fifo); 69 | 70 | printf("> Pull event 3\n"); 71 | out = fifo_pull(p_fifo); 72 | print_event_info(out); 73 | print_fifo_info(p_fifo); 74 | 75 | printf("> Pull event 4\n"); 76 | out = fifo_pull(p_fifo); 77 | print_event_info(out); 78 | print_fifo_info(p_fifo); 79 | 80 | printf("> Pull from empty\n"); 81 | out = fifo_pull(p_fifo); 82 | print_event_info(out); 83 | print_fifo_info(p_fifo); 84 | } 85 | 86 | fifo_t a; 87 | fifo_t b; 88 | 89 | void* thread_a(void* arg) { 90 | printf("[a] Starting\n"); 91 | printf("[a] Sleeping 2 seconds...\n"); 92 | sleep(2); 93 | printf("[a] Pushing event to b...\n"); 94 | fifo_push(&b, 1, NULL); 95 | 96 | printf("[a] Waiting for response\n"); 97 | while (fifo_empty(&a)) {} 98 | fifo_event_t* out = fifo_pull(&a); 99 | printf("[a] Got event %d\n", out->event_type); 100 | 101 | pthread_exit(NULL); 102 | } 103 | 104 | void* thread_b(void* arg) { 105 | printf("[b] Starting\n"); 106 | printf("[b] Waiting for response\n"); 107 | while (fifo_empty(&b)) {} 108 | fifo_event_t* out = fifo_pull(&b); 109 | printf("[b] Got event %d\n", out->event_type); 110 | 111 | printf("[b] Sleeping 2 seconds...\n"); 112 | sleep(2); 113 | printf("[b] Pushing event to a...\n"); 114 | fifo_push(&a, 2, NULL); 115 | 116 | pthread_exit(NULL); 117 | } 118 | 119 | void test_threads() { 120 | fifo_init(&a); 121 | fifo_init(&b); 122 | 123 | pthread_t ta; 124 | pthread_t tb; 125 | 126 | pthread_create(&ta, NULL, thread_a, NULL); 127 | pthread_create(&tb, NULL, thread_b, NULL); 128 | 129 | pthread_join(ta, NULL); 130 | pthread_join(tb, NULL); 131 | } 132 | 133 | int main() { 134 | test_basics(); 135 | 136 | return 0; 137 | } 138 | -------------------------------------------------------------------------------- /thread-fifo/readme.md: -------------------------------------------------------------------------------- 1 | # FIFO For Thread Communication and Synchronisation 2 | 3 | A minimal implementation of a circular fifo buffer that can be used for communication and synchronisation between threads, or just a buffer between data producers and consumers. 4 | 5 | Put simply, a fifo is a buffer with two key operations - sometimes called `push` and `pull` (but also `put` and `get`, as well as many other similar names). It stands for "First in, first out", which indicates that the order that you put items into the queue is the order that you take them out again. This is in contrast to a stack which has a lifo arrangement - "Last in, first out". 6 | 7 | Fifos can be found in both software and hardware, concrete and ad-hoc. One of their super powers is syncing up data producers and consumers, especially when they produce and consume at different rates. 8 | 9 | They typically come with two further operations for determining whether the queue is empty or full. Sometimes (especially in hardware), they also have an "almost full" and "almost empty". 10 | 11 | ## fifo.h 12 | 13 | `fifo.h` contains the interface, containing a typedef for `fifo_t`, which holds the state and data of the queue, as well as a typedef for `fifo_event_t` - which is the type of the items that are pushed into the queue. 14 | 15 | The event type uses a `uint32_t` type to *tag* item, as well as a void pointer to data - allowing the user to attach whatever data to events they like. 16 | 17 | The `fifo_t` type contains a mutex which has to be acquired by all of the operations for thread safety. The queue size can be configured with a `#define FIFO_QUEUE_SIZE` before including `fifo.h`, or via compiler flags. The queue itself is a circular buffer, which means that pushing or pulling from the queue does not necessitate shifting all of the elements. 18 | 19 | 6 functions are defined for interacting with a fifo: 20 | 21 | - `fifo_init` - for initialising the `fifo_t` data structure 22 | - `fifo_full` - for checking if the queue is full 23 | - `fifo_empty` - for checking if the queue is empty 24 | - `fifo_pull` - for pulling an item from the queue 25 | - `fifo_peek` - for accessing the next item in the queue without consuming it 26 | - `fifo_push` - for placing an item into the queue 27 | 28 | ## Patterns 29 | 30 | This library can be used to pass data between two threads, and force synchronisation points. `main.c` contains a small example of two threads and two corresponding fifos - one where thread `A` pushes messages to thread `B`, and one where thread `B` pushes messages to thread `A`. 31 | 32 | If thread `A` is producing data slower than thread `B` wants to consume it, `B` can do something like: 33 | 34 | ```C 35 | #define WAIT_FOR_DATA(fifoPtr) (while (fifo_empty(fifoPtr)) {}) 36 | 37 | // ... 38 | // In the thread b code 39 | 40 | fifo_event_t* ev; 41 | 42 | while (1) { 43 | // Wait until A has data 44 | WAIT_FOR_DATA(&fifo_a_to_b); 45 | 46 | // Get the next event 47 | ev = fifo_pull(&fifo_a_to_b); 48 | 49 | // Do something with ev 50 | } 51 | ``` 52 | --------------------------------------------------------------------------------