aboutsummaryrefslogtreecommitdiff
path: root/memstack
diff options
context:
space:
mode:
Diffstat (limited to 'memstack')
-rw-r--r--memstack/CMakeLists.txt30
-rw-r--r--memstack/README.md15
-rw-r--r--memstack/include/memstack.h66
-rw-r--r--memstack/src/memstack.c119
-rw-r--r--memstack/test/memstack_test.c165
-rw-r--r--memstack/test/test.h185
6 files changed, 580 insertions, 0 deletions
diff --git a/memstack/CMakeLists.txt b/memstack/CMakeLists.txt
new file mode 100644
index 0000000..9ad1aa1
--- /dev/null
+++ b/memstack/CMakeLists.txt
@@ -0,0 +1,30 @@
1cmake_minimum_required(VERSION 3.5)
2
3project(memstack)
4
5set(CMAKE_C_STANDARD 23)
6set(CMAKE_C_STANDARD_REQUIRED On)
7set(CMAKE_C_EXTENSIONS Off)
8
9# Library
10
11add_library(memstack
12 src/memstack.c)
13
14target_include_directories(memstack PUBLIC
15 include)
16
17target_link_libraries(memstack PRIVATE
18 cassert)
19
20target_compile_options(memstack PRIVATE -Wall -Wextra)
21
22# Test
23
24add_executable(memstack_test
25 test/memstack_test.c)
26
27target_link_libraries(memstack_test
28 memstack)
29
30target_compile_options(memstack_test PRIVATE -DUNIT_TEST -DNDEBUG -Wall -Wextra)
diff --git a/memstack/README.md b/memstack/README.md
new file mode 100644
index 0000000..7eb950e
--- /dev/null
+++ b/memstack/README.md
@@ -0,0 +1,15 @@
1# Mempool
2
3A memory pool of fixed-sized blocks with O(1) allocation and deallocation.
4
5Each block in the pool is tagged with a boolean value that indicates whether the
6block is free or in use, as well as a pointer to the next free/used block.
7Blocks thus form two lists, a free list and a used list. The allocator
8maintains the two lists when allocating/deallocating blocks.
9
10The allocator's internal data is stored separately from the block data so that
11the data remains tightly packed.
12
13Free blocks in the mempool always remain zeroed out for convenience of
14programming and debugging. If the application's data structures are valid when
15zeroed out, then they do not need to be explicitly initialized.
diff --git a/memstack/include/memstack.h b/memstack/include/memstack.h
new file mode 100644
index 0000000..93cd2e6
--- /dev/null
+++ b/memstack/include/memstack.h
@@ -0,0 +1,66 @@
1/*
2 * Stack-based allocator.
3 */
4#pragma once
5
6#include <stddef.h>
7#include <stdint.h>
8
9/// Stack-based allocator.
10typedef struct memstack {
11 size_t capacity; // Total size available.
12 uint8_t* base; // Base pointer to memory.
13 uint8_t* watermark; // Pointer to next free area of memory.
14 bool owned; // True if memory is owned by the memstack.
15 bool trap; // Whether to trap when allocating beyond capacity.
16} memstack;
17
18/// Create a stack-based allocator.
19///
20/// `stack` may be user-provided or null.
21/// - If null, the allocator malloc()s the memory for them.
22/// - If given, `stack` must be at least `capacity` bytes.
23///
24/// The memory is zeroed out for convenience.
25bool memstack_make(memstack*, size_t capacity, void* memory);
26
27/// Destroy the stack.
28///
29/// If the allocator owns the memory, then this function frees it.
30void memstack_del(memstack*);
31
32/// Clear the stack.
33void memstack_clear(memstack*);
34
35/// Return the top of the stack.
36size_t memstack_watermark(const memstack*);
37
38/// Set the top of the stack.
39void memstack_set_watermark(memstack*, size_t watermark);
40
41/// Allocate a new block.
42///
43/// Return null if the block does not fit in the remaining memory.
44///
45/// When there is no space left in the stack, allocation can either trap
46/// (default) or gracefully return null. Call mem_enable_traps() to toggle this
47/// behaviour.
48///
49/// Newly allocated blocks are conveniently zeroed out.
50void* memstack_alloc(memstack*, size_t bytes);
51
52/// Allocate a new aligned block.
53///
54/// An alignment of 0 is allowed for convenience and has the same effect as 1.
55///
56/// Has the same properties as memstack_alloc().
57void* memstack_alloc_aligned(memstack*, size_t bytes, size_t alignment);
58
59/// Return the stack's used size in bytes.
60size_t memstack_size(const memstack*);
61
62/// Return the stack's total capacity in bytes.
63size_t memstack_capacity(const memstack*);
64
65/// Set whether to trap when attempting to allocate beyond capacity.
66void memstack_enable_traps(memstack*, bool);
diff --git a/memstack/src/memstack.c b/memstack/src/memstack.c
new file mode 100644
index 0000000..84131ef
--- /dev/null
+++ b/memstack/src/memstack.c
@@ -0,0 +1,119 @@
1#include "memstack.h"
2
3#include <cassert.h>
4
5#include <stdlib.h>
6#include <string.h>
7
8static bool is_pow2_or_0(size_t x) { return (x & (x - 1)) == 0; }
9
10/// Align the given address to the next address that is a multiple of the
11/// alignment. If the given address is already aligned, return the address.
12static uint8_t* align(uint8_t* address, size_t alignment) {
13 assert(is_pow2_or_0(alignment));
14 const size_t mask = alignment - 1;
15 return (uint8_t*)(((uintptr_t)address + mask) & ~mask);
16}
17
18bool memstack_make(memstack* stack, size_t capacity, void* memory) {
19 assert(stack);
20 assert(capacity >= 1);
21
22 // Allocate memory if not user-provided.
23 uint8_t* stack_memory = memory;
24 if (!stack_memory) {
25 stack_memory = calloc(1, capacity);
26 if (stack_memory == nullptr) {
27 return false;
28 }
29 }
30 assert(stack_memory);
31
32 stack->capacity = capacity;
33 stack->base = stack_memory;
34 stack->watermark = stack_memory;
35 stack->owned = (stack_memory != memory);
36 stack->trap = true;
37
38 return true;
39}
40
41void memstack_del(memstack* stack) {
42 assert(stack);
43
44 if (stack->owned && (stack->base != nullptr)) {
45 free(stack->base);
46 stack->base = nullptr;
47 stack->owned = false;
48 }
49
50 stack->capacity = 0;
51 stack->watermark = stack->base;
52}
53
54void memstack_clear(memstack* stack) {
55 assert(stack);
56
57 stack->watermark = stack->base;
58 memset(stack->base, 0, stack->capacity);
59}
60
61size_t memstack_watermark(const memstack* stack) {
62 assert(stack);
63 return stack->watermark - stack->base;
64}
65
66void memstack_set_watermark(memstack* stack, size_t watermark) {
67 assert(stack);
68 const bool fits = (watermark < stack->capacity);
69 if (stack->trap && !fits) {
70 FAIL("memstack watermark update failed, bad watermark");
71 }
72 assert(fits);
73 stack->watermark = stack->base + watermark;
74}
75
76void* memstack_alloc(memstack* stack, size_t bytes) {
77 assert(stack);
78
79 if ((memstack_size(stack) + bytes) > stack->capacity) {
80 if (stack->trap) {
81 FAIL("memstack allocation failed, increase the stack's capacity.");
82 }
83 return nullptr; // Block does not fit in remaining memory.
84 }
85
86 // Allocate the block.
87 uint8_t* const block = stack->watermark;
88 stack->watermark += bytes;
89 assert(memstack_size(stack) <= stack->capacity);
90
91 return block;
92}
93
94void* memstack_alloc_aligned(memstack* stack, size_t bytes, size_t alignment) {
95 assert(stack);
96
97 uint8_t* const new_watermark = align(stack->watermark, alignment);
98 assert(new_watermark >= stack->watermark);
99 assert((size_t)(new_watermark - stack->base) <= stack->capacity);
100 stack->capacity -= (new_watermark - stack->watermark);
101 stack->watermark = new_watermark;
102
103 return memstack_alloc(stack, bytes);
104}
105
106size_t memstack_size(const memstack* stack) {
107 assert(stack);
108 return stack->watermark - stack->base;
109}
110
111size_t memstack_capacity(const memstack* stack) {
112 assert(stack);
113 return stack->capacity;
114}
115
116void memstack_enable_traps(memstack* stack, bool enable) {
117 assert(stack);
118 stack->trap = enable;
119}
diff --git a/memstack/test/memstack_test.c b/memstack/test/memstack_test.c
new file mode 100644
index 0000000..5308be3
--- /dev/null
+++ b/memstack/test/memstack_test.c
@@ -0,0 +1,165 @@
1#include "memstack.h"
2
3#include "test.h"
4
5#define NUM_INTS 10
6#define CAPACITY (NUM_INTS * sizeof(int))
7
8// Create and destroy a statically-backed stack.
9TEST_CASE(memstack_create) {
10 int memory[CAPACITY];
11
12 memstack stack = {0};
13 memstack_make(&stack, CAPACITY, memory);
14 memstack_del(&stack);
15}
16
17// Create and destroy a dynamically-backed stack.
18TEST_CASE(mem_create_dyn) {
19 memstack stack = {0};
20 memstack_make(&stack, CAPACITY, nullptr);
21 memstack_del(&stack);
22}
23
24// Allocate all N ints.
25TEST_CASE(memstack_allocate_until_full) {
26 memstack stack = {0};
27 memstack_make(&stack, CAPACITY, nullptr);
28
29 for (int i = 0; i < NUM_INTS; ++i) {
30 const int* block = memstack_alloc(&stack, sizeof(int));
31 TEST_TRUE(block != nullptr);
32 }
33
34 TEST_TRUE(memstack_size(&stack) == CAPACITY);
35
36 memstack_del(&stack);
37}
38
39// Allocate all N ints, then free them.
40TEST_CASE(memstack_fill_then_free) {
41 memstack stack = {0};
42 memstack_make(&stack, CAPACITY, nullptr);
43
44 int* blocks[NUM_INTS] = {nullptr};
45 for (int i = 0; i < NUM_INTS; ++i) {
46 blocks[i] = memstack_alloc(&stack, sizeof(int));
47 TEST_TRUE(blocks[i] != nullptr);
48 }
49
50 memstack_clear(&stack);
51
52 TEST_EQUAL(memstack_size(&stack), 0);
53
54 memstack_del(&stack);
55}
56
57// Attempt to allocate blocks past the maximum stack size.
58// The stack should handle the failed allocations gracefully.
59TEST_CASE(memstack_allocate_beyond_max_size) {
60 memstack stack = {0};
61 memstack_make(&stack, CAPACITY, nullptr);
62 memstack_enable_traps(&stack, false);
63
64 // Fully allocate the stack.
65 for (int i = 0; i < NUM_INTS; ++i) {
66 TEST_TRUE(memstack_alloc(&stack, sizeof(int)) != nullptr);
67 }
68
69 // Past the end.
70 for (int i = 0; i < NUM_INTS; ++i) {
71 TEST_EQUAL(memstack_alloc(&stack, sizeof(int)), nullptr);
72 }
73
74 TEST_TRUE(memstack_size(&stack) == CAPACITY);
75
76 memstack_del(&stack);
77}
78
79// Free blocks should always remain zeroed out.
80// This tests the invariant right after creating the stack.
81TEST_CASE(memstack_zero_free_blocks_after_creation) {
82 memstack stack = {0};
83 memstack_make(&stack, CAPACITY, nullptr);
84
85 for (int i = 0; i < NUM_INTS; ++i) {
86 const int* block = memstack_alloc(&stack, sizeof(int));
87 TEST_TRUE(block != nullptr);
88 TEST_EQUAL(*block, 0);
89 }
90
91 memstack_del(&stack);
92}
93
94// Free blocks should always remain zeroed out.
95// This tests the invariant after clearing the stack and allocating a new block.
96TEST_CASE(memstack_zero_free_block_after_free) {
97 memstack stack = {0};
98 memstack_make(&stack, CAPACITY, nullptr);
99
100 for (int i = 0; i < NUM_INTS; ++i) {
101 const int* block = memstack_alloc(&stack, sizeof(int));
102 TEST_TRUE(block != nullptr);
103 TEST_EQUAL(*block, 0);
104 }
105
106 memstack_clear(&stack);
107
108 for (int i = 0; i < NUM_INTS; ++i) {
109 const int* block = memstack_alloc(&stack, sizeof(int));
110 TEST_TRUE(block != nullptr);
111 TEST_EQUAL(*block, 0);
112 }
113
114 memstack_del(&stack);
115}
116
117// Aligned allocations should be properly aligned.
118TEST_CASE(memstack_alloc_aligned) {
119 memstack stack = {0};
120 memstack_make(&stack, CAPACITY, nullptr);
121
122 // -1 because the base address of the memory storage might be unaligned.
123 for (int i = 0; i < NUM_INTS - 1; ++i) {
124 const int* block =
125 memstack_alloc_aligned(&stack, sizeof(int), alignof(int));
126 TEST_TRUE(block != nullptr);
127 TEST_EQUAL(*block, 0);
128 TEST_EQUAL((uintptr_t)block % alignof(int), 0);
129 }
130
131 memstack_del(&stack);
132}
133
134// Get and set the watermark.
135TEST_CASE(memstack_watermark) {
136 memstack stack = {0};
137 memstack_make(&stack, CAPACITY, nullptr);
138
139 // Allocate N/2 ints.
140 for (int i = 0; i < NUM_INTS / 2; ++i) {
141 const int* block = memstack_alloc(&stack, sizeof(int));
142 TEST_TRUE(block != nullptr);
143 }
144
145 const size_t watermark = memstack_watermark(&stack);
146
147 // Allocate the remaining N/2 ints.
148 for (int i = 0; i < NUM_INTS / 2; ++i) {
149 const int* block = memstack_alloc(&stack, sizeof(int));
150 TEST_TRUE(block != nullptr);
151 }
152
153 // Now reset the watermark halfway through.
154 memstack_set_watermark(&stack, watermark);
155
156 // Allocate the remaining N/2 ints (again).
157 for (int i = 0; i < NUM_INTS / 2; ++i) {
158 const int* block = memstack_alloc(&stack, sizeof(int));
159 TEST_TRUE(block != nullptr);
160 }
161
162 memstack_del(&stack);
163}
164
165int main() { return 0; }
diff --git a/memstack/test/test.h b/memstack/test/test.h
new file mode 100644
index 0000000..fd8dc22
--- /dev/null
+++ b/memstack/test/test.h
@@ -0,0 +1,185 @@
1// SPDX-License-Identifier: MIT
2#pragma once
3
4#ifdef UNIT_TEST
5
6#include <stdbool.h>
7#include <stdio.h>
8#include <stdlib.h>
9#include <string.h>
10
11#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || \
12 defined(__NetBSD__) || defined(__OpenBSD__)
13#define USE_SYSCTL_FOR_ARGS 1
14// clang-format off
15#include <sys/types.h>
16#include <sys/sysctl.h>
17// clang-format on
18#include <unistd.h> // getpid
19#endif
20
21struct test_file_metadata;
22
23struct test_failure {
24 bool present;
25 const char *message;
26 const char *file;
27 int line;
28};
29
30struct test_case_metadata {
31 void (*fn)(struct test_case_metadata *, struct test_file_metadata *);
32 struct test_failure failure;
33 const char *name;
34 struct test_case_metadata *next;
35};
36
37struct test_file_metadata {
38 bool registered;
39 const char *name;
40 struct test_file_metadata *next;
41 struct test_case_metadata *tests;
42};
43
44struct test_file_metadata __attribute__((weak)) * test_file_head;
45
46#define SET_FAILURE(_message) \
47 metadata->failure = (struct test_failure) { \
48 .message = _message, .file = __FILE__, .line = __LINE__, .present = true, \
49 }
50
51#define TEST_EQUAL(a, b) \
52 do { \
53 if ((a) != (b)) { \
54 SET_FAILURE(#a " != " #b); \
55 return; \
56 } \
57 } while (0)
58
59#define TEST_TRUE(a) \
60 do { \
61 if (!(a)) { \
62 SET_FAILURE(#a " is not true"); \
63 return; \
64 } \
65 } while (0)
66
67#define TEST_STREQUAL(a, b) \
68 do { \
69 if (strcmp(a, b) != 0) { \
70 SET_FAILURE(#a " != " #b); \
71 return; \
72 } \
73 } while (0)
74
75#define TEST_CASE(_name) \
76 static void __test_h_##_name(struct test_case_metadata *, \
77 struct test_file_metadata *); \
78 static struct test_file_metadata __test_h_file; \
79 static struct test_case_metadata __test_h_meta_##_name = { \
80 .name = #_name, \
81 .fn = __test_h_##_name, \
82 }; \
83 static void __attribute__((constructor(101))) __test_h_##_name##_register(void) { \
84 __test_h_meta_##_name.next = __test_h_file.tests; \
85 __test_h_file.tests = &__test_h_meta_##_name; \
86 if (!__test_h_file.registered) { \
87 __test_h_file.name = __FILE__; \
88 __test_h_file.next = test_file_head; \
89 test_file_head = &__test_h_file; \
90 __test_h_file.registered = true; \
91 } \
92 } \
93 static void __test_h_##_name( \
94 struct test_case_metadata *metadata __attribute__((unused)), \
95 struct test_file_metadata *file_metadata __attribute__((unused)))
96
97extern void __attribute__((weak)) (*test_h_unittest_setup)(void);
98/// Run defined tests, return true if all tests succeeds
99/// @param[out] tests_run if not NULL, set to whether tests were run
100static inline void __attribute__((constructor(102))) run_tests(void) {
101 bool should_run = false;
102#ifdef USE_SYSCTL_FOR_ARGS
103 int mib[] = {
104 CTL_KERN,
105#if defined(__NetBSD__) || defined(__OpenBSD__)
106 KERN_PROC_ARGS,
107 getpid(),
108 KERN_PROC_ARGV,
109#else
110 KERN_PROC,
111 KERN_PROC_ARGS,
112 getpid(),
113#endif
114 };
115 char *arg = NULL;
116 size_t arglen;
117 sysctl(mib, sizeof(mib) / sizeof(mib[0]), NULL, &arglen, NULL, 0);
118 arg = malloc(arglen);
119 sysctl(mib, sizeof(mib) / sizeof(mib[0]), arg, &arglen, NULL, 0);
120#else
121 FILE *cmdlinef = fopen("/proc/self/cmdline", "r");
122 char *arg = NULL;
123 int arglen;
124 fscanf(cmdlinef, "%ms%n", &arg, &arglen);
125 fclose(cmdlinef);
126#endif
127 for (char *pos = arg; pos < arg + arglen; pos += strlen(pos) + 1) {
128 if (strcmp(pos, "--unittest") == 0) {
129 should_run = true;
130 break;
131 }
132 }
133 free(arg);
134
135 if (!should_run) {
136 return;
137 }
138
139 if (&test_h_unittest_setup) {
140 test_h_unittest_setup();
141 }
142
143 struct test_file_metadata *i = test_file_head;
144 int failed = 0, success = 0;
145 while (i) {
146 fprintf(stderr, "Running tests from %s:\n", i->name);
147 struct test_case_metadata *j = i->tests;
148 while (j) {
149 fprintf(stderr, "\t%s ... ", j->name);
150 j->failure.present = false;
151 j->fn(j, i);
152 if (j->failure.present) {
153 fprintf(stderr, "failed (%s at %s:%d)\n", j->failure.message,
154 j->failure.file, j->failure.line);
155 failed++;
156 } else {
157 fprintf(stderr, "passed\n");
158 success++;
159 }
160 j = j->next;
161 }
162 fprintf(stderr, "\n");
163 i = i->next;
164 }
165 int total = failed + success;
166 fprintf(stderr, "Test results: passed %d/%d, failed %d/%d\n", success, total,
167 failed, total);
168 exit(failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
169}
170
171#else
172
173#include <stdbool.h>
174
175#define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void)
176
177#define TEST_EQUAL(a, b) \
178 (void)(a); \
179 (void)(b)
180#define TEST_TRUE(a) (void)(a)
181#define TEST_STREQUAL(a, b) \
182 (void)(a); \
183 (void)(b)
184
185#endif