├── .editorconfig ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── container-todo.txt ├── container.c ├── container.h └── main.c /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{c, h}] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | project(container LANGUAGES C) 3 | add_executable(${PROJECT_NAME} main.c container.c) 4 | 5 | find_package(unity) 6 | 7 | target_link_libraries( 8 | ${PROJECT_NAME} PRIVATE unity) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tom Hulton-Harrop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Test Driven C handle container experiment 2 | 3 | This is a little experiment to implement a container supporting weak handles in C (and learn the Unity testing library in the process). 4 | 5 | The exercise was to try and attempt to test drive the interface and ensure all functions/behaviours were tested. 6 | 7 | Inspired by Bitsquid/Stringray blog post: [Managing Decoupling Part 4 -- The ID Lookup Table](http://bitsquid.blogspot.com/2011/09/managing-decoupling-part-4-id-lookup.html) 8 | -------------------------------------------------------------------------------- /container-todo.txt: -------------------------------------------------------------------------------- 1 | container todo 2 | 3 | - --add element 4 | - --contain element 5 | - --add multiple elements, size expected 6 | - --remove element, size expected 7 | - --remove element, no longer accessible 8 | -------------------------------------------------------------------------------- /container.c: -------------------------------------------------------------------------------- 1 | #include "container.h" 2 | 3 | #include "stdlib.h" 4 | #include "string.h" 5 | 6 | typedef struct internal_handle_t { 7 | handle_t handle_; // handle itself 8 | int lookup_; // where to look in object arr 9 | int next_; // next internal handle 10 | } internal_handle_t; 11 | 12 | typedef struct container_t { 13 | enum { CAPACITY = 10 }; // small capacity to make testing edge cases easier 14 | int object_ids_[CAPACITY]; // map from object back to handle (synchronized 15 | // with objects_) 16 | object_t objects_[CAPACITY]; // packed objects 17 | internal_handle_t handles_[CAPACITY]; // sparse handles 18 | 19 | int next_; 20 | int size_; 21 | } container_t; 22 | 23 | container_t* container_alloc() { 24 | container_t* container = malloc(sizeof *container); 25 | *container = (container_t) { .size_ = -1 }; 26 | return container; 27 | } 28 | 29 | void container_dealloc(container_t* container) { 30 | free(container); 31 | } 32 | 33 | container_t* container_init(container_t* container) { 34 | for (int i = 0; i < CAPACITY; ++i) { 35 | container->handles_[i].handle_.id_ = i; 36 | container->handles_[i].handle_.gen_ = -1; 37 | container->handles_[i].lookup_ = -1; 38 | container->handles_[i].next_ = i + 1; 39 | container->object_ids_[i] = -1; 40 | } 41 | 42 | container->next_ = 0; 43 | container->size_ = 0; 44 | return container; 45 | } 46 | 47 | int container_size(container_t* container) { 48 | return container->size_; 49 | } 50 | 51 | int container_capacity(container_t* container) { 52 | return CAPACITY; 53 | } 54 | 55 | handle_t container_add(container_t* container) { 56 | const int insert = container->size_++; 57 | if (insert >= CAPACITY) { 58 | // invalid handle 59 | return (handle_t) { .id_ = -1, .gen_ = -1 }; 60 | } 61 | 62 | const int now = container->next_; 63 | container->handles_[now].lookup_ = insert; 64 | handle_t* handle = &container->handles_[now].handle_; 65 | handle->gen_++; 66 | 67 | container->object_ids_[now] = handle->id_; 68 | 69 | container->next_ = container->handles_[now].next_; 70 | 71 | return *handle; 72 | } 73 | 74 | object_t* container_get(container_t* container, handle_t handle) { 75 | if (!container_has(container, handle)) { 76 | return NULL; 77 | } 78 | 79 | return &container->objects_[container->handles_[handle.id_].lookup_]; 80 | } 81 | 82 | bool container_has(container_t* container, handle_t handle) { 83 | if (handle.id_ >= CAPACITY) { 84 | return false; 85 | } 86 | 87 | internal_handle_t ih = container->handles_[handle.id_]; 88 | return handle.gen_ == ih.handle_.gen_ && ih.lookup_ != -1; 89 | } 90 | 91 | bool container_remove(container_t* container, handle_t handle) { 92 | if (!container_has(container, handle)) { 93 | return false; 94 | } 95 | 96 | // find the handle of the last object currently stored and have it 97 | // point to the look-up of the object about to be removed 98 | container->handles_[container->object_ids_[container->size_ - 1]].lookup_ 99 | = container->handles_[handle.id_].lookup_; 100 | // copy the last object stored into the the position of the object 101 | // being removed 102 | container->objects_[container->handles_[handle.id_].lookup_] 103 | = container->objects_[container->size_ - 1]; 104 | // copy the last object id into the position of the object being removed 105 | // (in the parallel object_ids vector) 106 | container->object_ids_[container->handles_[handle.id_].lookup_] 107 | = container->object_ids_[container->size_ - 1]; 108 | 109 | container->handles_[handle.id_].lookup_ = -1; 110 | container->handles_[handle.id_].next_ = container->next_; 111 | container->next_ = handle.id_; 112 | 113 | container->size_--; 114 | 115 | return true; 116 | } 117 | 118 | static int max(int lhs, int rhs) { 119 | return lhs > rhs ? lhs : rhs; 120 | } 121 | 122 | int debug_container_handles(container_t* container, int buffer_size, char buffer[buffer_size]) { 123 | const char* filled_glyph = "[o]"; 124 | const char* empty_glyph = "[x]"; 125 | 126 | const int filled_glyph_len = strlen(filled_glyph); 127 | const int empty_glyph_len = strlen(empty_glyph); 128 | const int required_buffer_size = CAPACITY * max(filled_glyph_len, empty_glyph_len) + 1; 129 | 130 | if (buffer == NULL) { 131 | return required_buffer_size; 132 | } else if (buffer_size < required_buffer_size) { 133 | return -1; 134 | } else { 135 | for (int i = 0; i < CAPACITY; i++) { 136 | const char* glyph = NULL; 137 | if (container->handles_[i].lookup_ == -1) { 138 | glyph = empty_glyph; 139 | } else { 140 | glyph = filled_glyph; 141 | } 142 | 143 | strcat(buffer, glyph); 144 | } 145 | 146 | return 0; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /container.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTAINER_H 2 | #define CONTAINER_H 3 | 4 | #include "stdint.h" 5 | #include "stdbool.h" 6 | 7 | typedef struct handle_t { 8 | int id_; 9 | int gen_; 10 | } handle_t; 11 | 12 | typedef struct object_t { 13 | int value_; 14 | } object_t; 15 | 16 | typedef struct container_t container_t; 17 | 18 | container_t* container_alloc(); 19 | void container_dealloc(container_t* container); 20 | container_t* container_init(container_t* container); 21 | int container_size(container_t* container); 22 | int container_capacity(container_t* container); 23 | handle_t container_add(container_t* container); 24 | object_t* container_get(container_t* container, handle_t handle); 25 | bool container_has(container_t* container, handle_t handle); 26 | bool container_remove(container_t* container, handle_t handle); 27 | 28 | // pass a null pointer initially to have the function return the size of the buffer required 29 | int debug_container_handles(container_t* container, int buffer_size, char buffer[buffer_size]); 30 | 31 | #endif // CONTAINER_H 32 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include "stdint.h" 2 | #include "stdio.h" 3 | 4 | #include "unity.h" 5 | 6 | #include "container.h" 7 | 8 | #include "string.h" 9 | // TEST_FAIL_MESSAGE("oh no"); 10 | 11 | container_t* test_container = NULL; 12 | 13 | static void initContainerWithFiveHandles(handle_t handles[static 5]) { 14 | for (int i = 0; i < 5; ++i) { 15 | handles[i] = container_add(test_container); 16 | } 17 | } 18 | 19 | void setUp(void) { 20 | test_container = container_alloc(); 21 | container_init(test_container); 22 | } 23 | 24 | void tearDown(void) { 25 | container_dealloc(test_container); 26 | } 27 | 28 | void test_canAllocContainer(void) { 29 | container_t* container = container_alloc(); 30 | TEST_ASSERT_NOT_NULL(container); 31 | } 32 | 33 | void test_containerSizeIsZeroAfterInit(void) { 34 | container_t* container = container_alloc(); 35 | container_init(container); 36 | TEST_ASSERT_EQUAL_INT(0, container_size(container)); 37 | } 38 | 39 | void test_initialHandleReturnedIsZero() { 40 | handle_t handle = container_add(test_container); 41 | TEST_ASSERT_EQUAL_INT(0, handle.id_); 42 | } 43 | 44 | void test_containerSizeIsOneAfterSingleAdd() { 45 | container_add(test_container); 46 | TEST_ASSERT_EQUAL_INT(1, container_size(test_container)); 47 | } 48 | 49 | void test_containerSizeGrowsWithConsecutiveAdds() { 50 | handle_t handle1 = container_add(test_container); 51 | handle_t handle2 = container_add(test_container); 52 | handle_t handle3 = container_add(test_container); 53 | 54 | TEST_ASSERT_EQUAL_INT(0, handle1.id_); 55 | TEST_ASSERT_EQUAL_INT(1, handle2.id_); 56 | TEST_ASSERT_EQUAL_INT(2, handle3.id_); 57 | TEST_ASSERT_EQUAL_INT(3, container_size(test_container)); 58 | } 59 | 60 | void test_containerHasAddedHandle() { 61 | handle_t handle = container_add(test_container); 62 | TEST_ASSERT_TRUE(container_has(test_container, handle)); 63 | } 64 | 65 | void test_containerDoesNotHaveHandle() { 66 | handle_t handle = {}; 67 | TEST_ASSERT_FALSE(container_has(test_container, handle)); 68 | } 69 | 70 | void test_cannotAddMoreHandlesThanCapacity() { 71 | const int capacity = container_capacity(test_container); 72 | for (int i = 0; i < capacity; i++) { 73 | container_add(test_container); 74 | } 75 | 76 | TEST_ASSERT_EQUAL_INT(capacity, container_size(test_container)); 77 | handle_t next_handle = container_add(test_container); 78 | TEST_ASSERT_EQUAL_INT(-1, next_handle.gen_); 79 | TEST_ASSERT_EQUAL_INT(-1, next_handle.id_); 80 | } 81 | 82 | void test_removeDecreasesSize() { 83 | const int handle_count = 5; 84 | handle_t handles[handle_count]; 85 | for (int i = 0; i < handle_count; ++i) { 86 | handles[i] = container_add(test_container); 87 | } 88 | 89 | TEST_ASSERT_EQUAL_INT(handle_count, container_size(test_container)); 90 | 91 | container_remove(test_container, handles[2]); 92 | container_remove(test_container, handles[0]); 93 | 94 | TEST_ASSERT_EQUAL_INT(3, container_size(test_container)); 95 | } 96 | 97 | void test_handleReusedAfterRemoval() { 98 | handle_t initial_handle = container_add(test_container); 99 | container_remove(test_container, initial_handle); 100 | handle_t next_handle = container_add(test_container); 101 | TEST_ASSERT_EQUAL(0, next_handle.id_); 102 | TEST_ASSERT_EQUAL(1, next_handle.gen_); 103 | } 104 | 105 | void test_cannotRemoveInvalidHandle() { 106 | handle_t handle = { .id_ = -1, .gen_ = -1 }; 107 | bool removed = container_remove(test_container, handle); 108 | TEST_ASSERT_EQUAL(false, removed); 109 | } 110 | 111 | void test_canRemoveAddedHandle() { 112 | handle_t handle = container_add(test_container); 113 | bool removed = container_remove(test_container, handle); 114 | bool has = container_has(test_container, handle); 115 | TEST_ASSERT_EQUAL(true, removed); 116 | TEST_ASSERT_EQUAL(false, has); 117 | } 118 | 119 | void test_addAndRemoveHandlesReverseOrder() { 120 | const int capacity = container_capacity(test_container); 121 | handle_t handles[capacity]; 122 | 123 | for (int i = 0; i < capacity; i++) { 124 | handles[i] = container_add(test_container); 125 | } 126 | 127 | TEST_ASSERT_EQUAL_INT(capacity, container_size(test_container)); 128 | 129 | bool removed = true; 130 | for (int i = capacity - 1; i >= 0; i--) { 131 | removed &= !!container_remove(test_container, handles[i]); 132 | } 133 | 134 | TEST_ASSERT_EQUAL(true, removed); 135 | TEST_ASSERT_EQUAL_INT(0, container_size(test_container)); 136 | 137 | for (int i = 0; i < capacity; i++) { 138 | TEST_ASSERT_EQUAL(false, container_has(test_container, handles[i])); 139 | } 140 | } 141 | 142 | void test_addAndRemoveHandlesOrdered() { 143 | const int capacity = container_capacity(test_container); 144 | handle_t handles[capacity]; 145 | 146 | for (int i = 0; i < capacity; i++) { 147 | handles[i] = container_add(test_container); 148 | } 149 | 150 | TEST_ASSERT_EQUAL_INT(capacity, container_size(test_container)); 151 | 152 | bool removed = true; 153 | for (int i = 0; i < capacity; ++i) { 154 | removed &= !!container_remove(test_container, handles[i]); 155 | } 156 | 157 | TEST_ASSERT_EQUAL(true, removed); 158 | TEST_ASSERT_EQUAL_INT(0, container_size(test_container)); 159 | 160 | for (int i = 0; i < capacity; i++) { 161 | TEST_ASSERT_EQUAL(false, container_has(test_container, handles[i])); 162 | } 163 | } 164 | 165 | void test_canGetObjectViaHandle() { 166 | handle_t handle = container_add(test_container); 167 | object_t* object = container_get(test_container, handle); 168 | TEST_ASSERT_NOT_NULL(object); 169 | } 170 | 171 | void test_addTwoHandlesAndUpdateObjects() { 172 | handle_t handle_1 = container_add(test_container); 173 | handle_t handle_2 = container_add(test_container); 174 | 175 | { 176 | object_t* object_1 = container_get(test_container, handle_1); 177 | object_t* object_2 = container_get(test_container, handle_2); 178 | 179 | object_1->value_ = 4; 180 | object_2->value_ = 5; 181 | } 182 | 183 | { 184 | object_t* object_1 = container_get(test_container, handle_1); 185 | object_t* object_2 = container_get(test_container, handle_2); 186 | 187 | TEST_ASSERT_EQUAL_INT(4, object_1->value_); 188 | TEST_ASSERT_EQUAL_INT(5, object_2->value_); 189 | } 190 | } 191 | 192 | void test_originalHandleCannotAccessObjectAfterRemoval() { 193 | handle_t handle = container_add(test_container); 194 | container_remove(test_container, handle); 195 | object_t* object = container_get(test_container, handle); 196 | TEST_ASSERT_NULL(object); 197 | } 198 | 199 | void test_objectsRemainPackedAfterRemoval() { 200 | handle_t handles[5]; 201 | initContainerWithFiveHandles(handles); 202 | 203 | container_remove(test_container, handles[2]); 204 | 205 | object_t* begin = container_get(test_container, handles[0]); 206 | object_t* was_end = container_get(test_container, handles[4]); 207 | 208 | TEST_ASSERT_EQUAL_INT(2, was_end - begin); 209 | } 210 | 211 | void test_containerDebugVisualization() { 212 | handle_t handles[5]; 213 | initContainerWithFiveHandles(handles); 214 | 215 | container_remove(test_container, handles[2]); 216 | container_remove(test_container, handles[0]); 217 | 218 | const int buffer_size = debug_container_handles(test_container, 0, NULL); 219 | 220 | char buffer[buffer_size]; 221 | buffer[0] = '\0'; 222 | 223 | debug_container_handles(test_container, buffer_size, buffer); 224 | 225 | char expected_buffer[buffer_size]; 226 | expected_buffer[0] = '\0'; 227 | 228 | for (int i = 0; i < container_capacity(test_container); i++) { 229 | strcat(expected_buffer, "[x]"); 230 | } 231 | 232 | memcpy(expected_buffer, "[x][o][x][o][o]", 15); 233 | 234 | TEST_ASSERT_EQUAL_STRING(expected_buffer, buffer); 235 | } 236 | 237 | void test_containerDebugVisualizationBufferTooSmall() { 238 | handle_t handles[5]; 239 | initContainerWithFiveHandles(handles); 240 | 241 | const int buffer_size = 2; 242 | char buffer[buffer_size]; 243 | buffer[0] = '\0'; 244 | 245 | int result = debug_container_handles(test_container, buffer_size, buffer); 246 | TEST_ASSERT_EQUAL_INT(-1, result); 247 | 248 | int glyph_size = 3; 249 | int expected_size = container_capacity(test_container) * glyph_size + 1 /*null terminator*/; 250 | int required_buffer_size = debug_container_handles(test_container, 0, NULL); 251 | TEST_ASSERT_EQUAL_INT(expected_size, required_buffer_size); 252 | } 253 | 254 | void test_ensureHandlesReaddedInOrder() { 255 | handle_t handles[5]; 256 | initContainerWithFiveHandles(handles); 257 | 258 | const int buffer_size = debug_container_handles(test_container, 0, NULL); 259 | 260 | char expected_buffer[buffer_size]; 261 | expected_buffer[0] = '\0'; 262 | 263 | for (int i = 0; i < container_capacity(test_container); i++) { 264 | strcat(expected_buffer, "[x]"); 265 | } 266 | 267 | for (int i = 0; i < 5; ++i) { 268 | container_remove(test_container, handles[i]); 269 | } 270 | 271 | char buffer[buffer_size]; 272 | buffer[0] = '\0'; 273 | debug_container_handles(test_container, buffer_size, buffer); 274 | TEST_ASSERT_EQUAL_STRING(expected_buffer, buffer); 275 | 276 | handle_t first_new_handle = container_add(test_container); 277 | buffer[0] = '\0'; 278 | debug_container_handles(test_container, buffer_size, buffer); 279 | memcpy(expected_buffer, "[x][x][x][x][o]", 15); 280 | TEST_ASSERT_EQUAL_STRING(expected_buffer, buffer); 281 | 282 | handle_t second_new_handle = container_add(test_container); 283 | buffer[0] = '\0'; 284 | debug_container_handles(test_container, buffer_size, buffer); 285 | memcpy(expected_buffer, "[x][x][x][o][o]", 15); 286 | TEST_ASSERT_EQUAL_STRING(expected_buffer, buffer); 287 | 288 | object_t* begin = container_get(test_container, first_new_handle); 289 | object_t* end = container_get(test_container, second_new_handle); 290 | 291 | // ensure objects are tightly packed 292 | ptrdiff_t size = end - begin; 293 | TEST_ASSERT_EQUAL_INT64(1, size); 294 | } 295 | 296 | int main(void) { 297 | UNITY_BEGIN(); 298 | RUN_TEST(test_canAllocContainer); 299 | RUN_TEST(test_containerSizeIsZeroAfterInit); 300 | RUN_TEST(test_initialHandleReturnedIsZero); 301 | RUN_TEST(test_containerSizeIsOneAfterSingleAdd); 302 | RUN_TEST(test_containerSizeGrowsWithConsecutiveAdds); 303 | RUN_TEST(test_cannotAddMoreHandlesThanCapacity); 304 | RUN_TEST(test_handleReusedAfterRemoval); 305 | RUN_TEST(test_containerHasAddedHandle); 306 | RUN_TEST(test_containerDoesNotHaveHandle); 307 | RUN_TEST(test_removeDecreasesSize); 308 | RUN_TEST(test_cannotRemoveInvalidHandle); 309 | RUN_TEST(test_canRemoveAddedHandle); 310 | RUN_TEST(test_addAndRemoveHandlesReverseOrder); 311 | RUN_TEST(test_addAndRemoveHandlesOrdered); 312 | RUN_TEST(test_canGetObjectViaHandle); 313 | RUN_TEST(test_addTwoHandlesAndUpdateObjects); 314 | RUN_TEST(test_originalHandleCannotAccessObjectAfterRemoval); 315 | RUN_TEST(test_objectsRemainPackedAfterRemoval); 316 | RUN_TEST(test_containerDebugVisualization); 317 | RUN_TEST(test_containerDebugVisualizationBufferTooSmall); 318 | RUN_TEST(test_ensureHandlesReaddedInOrder); 319 | return UNITY_END(); 320 | } 321 | --------------------------------------------------------------------------------