├── .astylerc ├── .github └── workflows │ └── ccpp.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── doc ├── mem_layout.png └── scanning.md ├── src ├── gc.c ├── gc.h ├── log.c └── log.h └── test ├── Makefile ├── minunit.h └── test_gc.c /.astylerc: -------------------------------------------------------------------------------- 1 | --style=kr 2 | --pad-oper 3 | --preserve-date 4 | --max-code-length=100 5 | -------------------------------------------------------------------------------- /.github/workflows/ccpp.yml: -------------------------------------------------------------------------------- 1 | name: C/C++ CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | name: ${{ matrix.os }} | ${{ matrix.compiler }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, macos-latest] 12 | compiler: [clang, gcc] 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: make test (${{ matrix.compiler }}) 16 | run: make test CC=${{ matrix.compiler }} 17 | coverage: 18 | name: Coverage 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: Install lcov 23 | run: sudo apt-get install -y lcov 24 | - name: Build & test 25 | run: make CC=gcc 26 | - name: Collect coverage 27 | run: make coverage 28 | - name: Push to Coveralls 29 | uses: coverallsapp/github-action@master 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | path-to-lcov: ./build/test/coverage.info 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Marc Kirchner 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=clang 2 | CFLAGS=-g -Wall -Wextra -pedantic -I./include 3 | LDFLAGS=-g -L./build/src 4 | LDLIBS= 5 | RM=rm 6 | BUILD_DIR=./build 7 | 8 | .PHONY: test 9 | test: 10 | $(MAKE) -C $@ 11 | $(BUILD_DIR)/test/test_gc 12 | 13 | coverage: test 14 | $(MAKE) -C test coverage 15 | 16 | coverage-html: coverage 17 | $(MAKE) -C test coverage-html 18 | 19 | .PHONY: clean 20 | clean: 21 | $(MAKE) -C test clean 22 | 23 | distclean: clean 24 | $(MAKE) -C test distclean 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://github.com/mkirchner/gc/workflows/C/C++%20CI/badge.svg) 2 | [![Coverage Status](https://coveralls.io/repos/github/mkirchner/gc/badge.svg)](https://coveralls.io/github/mkirchner/gc) 3 | 4 | # gc: mark & sweep garbage collection for C 5 | 6 | `gc` is an implementation of a conservative, thread-local, mark-and-sweep 7 | garbage collector. The implementation provides a fully functional replacement 8 | for the standard POSIX `malloc()`, `calloc()`, `realloc()`, and `free()` calls. 9 | 10 | The focus of `gc` is to provide a conceptually clean implementation of 11 | a mark-and-sweep GC, without delving into the depths of architecture-specific 12 | optimization (see e.g. the [Boehm GC][boehm] for such an undertaking). It 13 | should be particularly suitable for learning purposes and is open for all kinds 14 | of optimization (PRs welcome!). 15 | 16 | The original motivation for `gc` is my desire to write [my own LISP][stutter] 17 | in C, entirely from scratch - and that required garbage collection. 18 | 19 | 20 | ### Acknowledgements 21 | 22 | This work would not have been possible without the ability to read the work of 23 | others, most notably the [Boehm GC][boehm], orangeduck's [tgc][tgc] (which also 24 | follows the ideals of being tiny and simple), and [The Garbage Collection 25 | Handbook][garbage_collection_handbook]. 26 | 27 | 28 | ## Table of contents 29 | 30 | * [Table of contents](#table-of-contents) 31 | * [Documentation Overview](#documentation-overview) 32 | * [Quickstart](#quickstart) 33 | * [Download and test](#download-and-test) 34 | * [Basic usage](#basic-usage) 35 | * [Core API](#core-api) 36 | * [Starting, stopping, pausing, resuming and running GC](#starting-stopping-pausing-resuming-and-running-gc) 37 | * [Memory allocation and deallocation](#memory-allocation-and-deallocation) 38 | * [Helper functions](#helper-functions) 39 | * [Basic Concepts](#basic-concepts) 40 | * [Data Structures](#data-structures) 41 | * [Garbage collection](#garbage-collection) 42 | * [Reachability](#reachability) 43 | * [The Mark-and-Sweep Algorithm](#the-mark-and-sweep-algorithm) 44 | * [Finding roots](#finding-roots) 45 | * [Depth-first recursive marking](#depth-first-recursive-marking) 46 | * [Dumping registers on the stack](#dumping-registers-on-the-stack) 47 | * [Sweeping](#sweeping) 48 | 49 | ## Documentation Overview 50 | 51 | * Read the [quickstart](#quickstart) below to see how to get started quickly 52 | * The [concepts](#concepts) section describes the basic concepts and design 53 | decisions that went into the implementation of `gc`. 54 | * Interleaved with the concepts, there are implementation sections that detail 55 | the implementation of the core components, see [hash map 56 | implementation](#data-structures), [dumping registers on the 57 | stack](#dumping-registers-on-the-stack), [finding roots](#finding-roots), and 58 | [depth-first, recursive marking](#depth-first-recursive-marking). 59 | 60 | 61 | ## Quickstart 62 | 63 | ### Download, compile and test 64 | 65 | $ git clone git@github.com:mkirchner/gc.git 66 | $ cd gc 67 | 68 | To compile using the `clang` compiler: 69 | 70 | $ make test 71 | 72 | To use the GNU Compiler Collection (GCC): 73 | 74 | $ make test CC=gcc 75 | 76 | The tests should complete successfully. To create the current coverage report: 77 | 78 | $ make coverage 79 | 80 | 81 | ### Basic usage 82 | 83 | ```c 84 | ... 85 | #include "gc.h" 86 | ... 87 | 88 | 89 | void some_fun() { 90 | ... 91 | int* my_array = gc_calloc(&gc, 1024, sizeof(int)); 92 | for (size_t i=0; i<1024; ++i) { 93 | my_array[i] = 42; 94 | } 95 | ... 96 | // look ma, no free! 97 | } 98 | 99 | int main(int argc, char* argv[]) { 100 | gc_start(&gc, &argc); 101 | ... 102 | some_fun(); 103 | ... 104 | gc_stop(&gc); 105 | return 0; 106 | } 107 | ``` 108 | 109 | ## Core API 110 | 111 | This describes the core API, see `gc.h` for more details and the low-level API. 112 | 113 | ### Starting, stopping, pausing, resuming and running GC 114 | 115 | In order to initialize and start garbage collection, use the `gc_start()` 116 | function and pass a *bottom-of-stack* address: 117 | 118 | ```c 119 | void gc_start(GarbageCollector* gc, void* bos); 120 | ``` 121 | 122 | The bottom-of-stack parameter `bos` needs to point to a stack-allocated 123 | variable and marks the low end of the stack from where [root 124 | finding](#root-finding) (scanning) starts. 125 | 126 | Garbage collection can be stopped, paused and resumed with 127 | 128 | ```c 129 | void gc_stop(GarbageCollector* gc); 130 | void gc_pause(GarbageCollector* gc); 131 | void gc_resume(GarbageCollector* gc); 132 | ``` 133 | 134 | and manual garbage collection can be triggered with 135 | 136 | ```c 137 | size_t gc_run(GarbageCollector* gc); 138 | ``` 139 | 140 | ### Memory allocation and deallocation 141 | 142 | `gc` supports `malloc()`, `calloc()`and `realloc()`-style memory allocation. 143 | The respective function signatures mimick the POSIX functions (with the 144 | exception that we need to pass the garbage collector along as the first 145 | argument): 146 | 147 | ```c 148 | void* gc_malloc(GarbageCollector* gc, size_t size); 149 | void* gc_calloc(GarbageCollector* gc, size_t count, size_t size); 150 | void* gc_realloc(GarbageCollector* gc, void* ptr, size_t size); 151 | ``` 152 | 153 | It is possible to pass a pointer to a destructor function through the 154 | extended interface: 155 | 156 | ```c 157 | void* dtor(void* obj) { 158 | // do some cleanup work 159 | obj->parent->deregister(); 160 | obj->db->disconnect() 161 | ... 162 | // no need to free obj 163 | } 164 | ... 165 | SomeObject* obj = gc_malloc_ext(gc, sizeof(SomeObject), dtor); 166 | ... 167 | ``` 168 | 169 | `gc` supports static allocations that are garbage collected only when the 170 | GC shuts down via `gc_stop()`. Just use the appropriate helper function: 171 | 172 | ```c 173 | void* gc_malloc_static(GarbageCollector* gc, size_t size, void (*dtor)(void*)); 174 | ``` 175 | 176 | Static allocation expects a pointer to a finalization function; just set to 177 | `NULL` if finalization is not required. 178 | 179 | Note that `gc` currently does not guarantee a specific ordering when it 180 | collects static variables, If static vars need to be deallocated in a 181 | particular order, the user should call `gc_free()` on them in the desired 182 | sequence prior to calling `gc_stop()`, see below. 183 | 184 | It is also possible to trigger explicit memory deallocation using 185 | 186 | ```c 187 | void gc_free(GarbageCollector* gc, void* ptr); 188 | ``` 189 | 190 | Calling `gc_free()` is guaranteed to (a) finalize/destruct on the object 191 | pointed to by `ptr` if applicable and (b) to free the memory that `ptr` points to 192 | irrespective of the current scheduling for garbage collection and will also 193 | work if GC has been paused using `gc_pause()` above. 194 | 195 | 196 | ### Helper functions 197 | 198 | `gc` also offers a `strdup()` implementation that returns a garbage-collected 199 | copy: 200 | 201 | ```c 202 | char* gc_strdup (GarbageCollector* gc, const char* s); 203 | ``` 204 | 205 | 206 | ## Basic Concepts 207 | 208 | The fundamental idea behind garbage collection is to automate the memory 209 | allocation/deallocation cycle. This is accomplished by keeping track of all 210 | allocated memory and periodically triggering deallocation for memory that is 211 | still allocated but [unreachable](#reachability). 212 | 213 | Many advanced garbage collectors also implement their own approach to memory 214 | allocation (i.e. replace `malloc()`). This often enables them to layout memory 215 | in a more space-efficient manner or for faster access but comes at the price of 216 | architecture-specific implementations and increased complexity. `gc` sidesteps 217 | these issues by falling back on the POSIX `*alloc()` implementations and keeping 218 | memory management and garbage collection metadata separate. This makes `gc` 219 | much simpler to understand but, of course, also less space- and time-efficient 220 | than more optimized approaches. 221 | 222 | ### Data Structures 223 | 224 | The core data structure inside `gc` is a hash map that maps the address of 225 | allocated memory to the garbage collection metadata of that memory: 226 | 227 | The items in the hash map are allocations, modeled with the `Allocation` 228 | `struct`: 229 | 230 | ```c 231 | typedef struct Allocation { 232 | void* ptr; // mem pointer 233 | size_t size; // allocated size in bytes 234 | char tag; // the tag for mark-and-sweep 235 | void (*dtor)(void*); // destructor 236 | struct Allocation* next; // separate chaining 237 | } Allocation; 238 | ``` 239 | 240 | Each `Allocation` instance holds a pointer to the allocated memory, the size of 241 | the allocated memory at that location, a tag for mark-and-sweep (see below), an 242 | optional pointer to the destructor function and a pointer to the next 243 | `Allocation` instance (for separate chaining, see below). 244 | 245 | The allocations are collected in an `AllocationMap` 246 | 247 | ```c 248 | typedef struct AllocationMap { 249 | size_t capacity; 250 | size_t min_capacity; 251 | double downsize_factor; 252 | double upsize_factor; 253 | double sweep_factor; 254 | size_t sweep_limit; 255 | size_t size; 256 | Allocation** allocs; 257 | } AllocationMap; 258 | ``` 259 | 260 | that, together with a set of `static` functions inside `gc.c`, provides hash 261 | map semantics for the implementation of the public API. 262 | 263 | The `AllocationMap` is the central data structure in the `GarbageCollector` 264 | struct which is part of the public API: 265 | 266 | ```c 267 | typedef struct GarbageCollector { 268 | struct AllocationMap* allocs; 269 | bool paused; 270 | void *bos; 271 | size_t min_size; 272 | } GarbageCollector; 273 | ``` 274 | 275 | With the basic data structures in place, any `gc_*alloc()` memory allocation 276 | request is a two-step procedure: first, allocate the memory through system (i.e. 277 | standard `malloc()`) functionality and second, add or update the associated 278 | metadata to the hash map. 279 | 280 | For `gc_free()`, use the pointer to locate the metadata in the hash map, 281 | determine if the deallocation requires a destructor call, call if required, 282 | free the managed memory and delete the metadata entry from the hash map. 283 | 284 | These data structures and the associated interfaces enable the 285 | management of the metadata required to build a garbage collector. 286 | 287 | 288 | ### Garbage collection 289 | 290 | `gc` triggers collection under two circumstances: (a) when any of the calls to 291 | the system allocation fail (in the hope to deallocate sufficient memory to 292 | fulfill the current request); and (b) when the number of entries in the hash 293 | map passes a dynamically adjusted high water mark. 294 | 295 | If either of these cases occurs, `gc` stops the world and starts a 296 | mark-and-sweep garbage collection run over all current allocations. This 297 | functionality is implemented in the `gc_run()` function which is part of the 298 | public API and delegates all work to the `gc_mark()` and `gc_sweep()` functions 299 | that are part of the private API. 300 | 301 | `gc_mark()` has the task of [finding roots](#finding-roots) and tagging all 302 | known allocations that are referenced from a root (or from an allocation that 303 | is referenced from a root, i.e. transitively) as "used". Once the marking of 304 | is completed, `gc_sweep()` iterates over all known allocations and 305 | deallocates all unused (i.e. unmarked) allocations, returns to `gc_run()` and 306 | the world continues to run. 307 | 308 | 309 | ### Reachability 310 | 311 | `gc` will keep memory allocations that are *reachable* and collect everything 312 | else. An allocation is considered reachable if any of the following is true: 313 | 314 | 1. There is a pointer on the stack that points to the allocation content. 315 | The pointer must reside in a stack frame that is at least as deep in the call 316 | stack as the bottom-of-stack variable passed to `gc_start()` (i.e. `bos` is 317 | the smallest stack address considered during the mark phase). 318 | 2. There is a pointer inside `gc_*alloc()`-allocated content that points to the 319 | allocation content. 320 | 3. The allocation is tagged with `GC_TAG_ROOT`. 321 | 322 | 323 | ### The Mark-and-Sweep Algorithm 324 | 325 | The naïve mark-and-sweep algorithm runs in two stages. First, in a *mark* 326 | stage, the algorithm finds and marks all *root* allocations and all allocations 327 | that are reachable from the roots. Second, in the *sweep* stage, the algorithm 328 | passes over all known allocations, collecting all allocations that were not 329 | marked and are therefore deemed unreachable. 330 | 331 | ### Finding roots 332 | 333 | At the beginning of the *mark* stage, we first sweep across all known 334 | allocations and find explicit roots with the `GC_TAG_ROOT` tag set. 335 | Each of these roots is a starting point for [depth-first recursive 336 | marking](#depth-first-recursive-marking). 337 | 338 | `gc` subsequently detects all roots in the stack (starting from the bottom-of-stack 339 | pointer `bos` that is passed to `gc_start()`) and the registers (by [dumping them 340 | on the stack](#dumping-registers-on-the-stack) prior to the mark phase) and 341 | uses these as starting points for marking as well. 342 | 343 | ### Depth-first recursive marking 344 | 345 | Given a root allocation, marking consists of (1) setting the `tag` field in an 346 | `Allocation` object to `GC_TAG_MARK` and (2) scanning the allocated memory for 347 | pointers to known allocations, recursively repeating the process. 348 | 349 | The underlying implementation is a simple, recursive depth-first search that 350 | scans over all memory content to find potential references: 351 | 352 | ```c 353 | void gc_mark_alloc(GarbageCollector* gc, void* ptr) 354 | { 355 | Allocation* alloc = gc_allocation_map_get(gc->allocs, ptr); 356 | if (alloc && !(alloc->tag & GC_TAG_MARK)) { 357 | alloc->tag |= GC_TAG_MARK; 358 | for (char* p = (char*) alloc->ptr; 359 | p < (char*) alloc->ptr + alloc->size; 360 | ++p) { 361 | gc_mark_alloc(gc, *(void**)p); 362 | } 363 | } 364 | } 365 | ``` 366 | 367 | In `gc.c`, `gc_mark()` starts the marking process by marking the 368 | known roots on the stack via a call to `gc_mark_roots()`. To mark the roots we 369 | do one full pass through all known allocations. We then proceed to dump the 370 | registers on the stack. 371 | 372 | 373 | ### Dumping registers on the stack 374 | 375 | In order to make the CPU register contents available for root finding, `gc` 376 | dumps them on the stack. This is implemented in a somewhat portable way using 377 | `setjmp()`, which stores them in a `jmp_buf` variable right before we mark the 378 | stack: 379 | 380 | ```c 381 | ... 382 | /* Dump registers onto stack and scan the stack */ 383 | void (*volatile _mark_stack)(GarbageCollector*) = gc_mark_stack; 384 | jmp_buf ctx; 385 | memset(&ctx, 0, sizeof(jmp_buf)); 386 | setjmp(ctx); 387 | _mark_stack(gc); 388 | ... 389 | ``` 390 | 391 | The detour using the `volatile` function pointer `_mark_stack` to the 392 | `gc_mark_stack()` function is necessary to avoid the inlining of the call to 393 | `gc_mark_stack()`. 394 | 395 | 396 | ### Sweeping 397 | 398 | After marking all memory that is reachable and therefore potentially still in 399 | use, collecting the unreachable allocations is trivial. Here is the 400 | implementation from `gc_sweep()`: 401 | 402 | ```c 403 | size_t gc_sweep(GarbageCollector* gc) 404 | { 405 | size_t total = 0; 406 | for (size_t i = 0; i < gc->allocs->capacity; ++i) { 407 | Allocation* chunk = gc->allocs->allocs[i]; 408 | Allocation* next = NULL; 409 | while (chunk) { 410 | if (chunk->tag & GC_TAG_MARK) { 411 | /* unmark */ 412 | chunk->tag &= ~GC_TAG_MARK; 413 | chunk = chunk->next; 414 | } else { 415 | total += chunk->size; 416 | if (chunk->dtor) { 417 | chunk->dtor(chunk->ptr); 418 | } 419 | free(chunk->ptr); 420 | next = chunk->next; 421 | gc_allocation_map_remove(gc->allocs, chunk->ptr, false); 422 | chunk = next; 423 | } 424 | } 425 | } 426 | gc_allocation_map_resize_to_fit(gc->allocs); 427 | return total; 428 | } 429 | ``` 430 | 431 | We iterate over all allocations in the hash map (the `for` loop), following every 432 | chain (the `while` loop with the `chunk = chunk->next` update) and either (1) 433 | unmark the chunk if it was marked; or (2) call the destructor on the chunk and 434 | free the memory if it was not marked, keeping a running total of the amount of 435 | memory we free. 436 | 437 | That concludes the mark & sweep run. The stopped world is resumed and we're 438 | ready for the next run! 439 | 440 | 441 | 442 | [naive_mas]: https://en.wikipedia.org/wiki/Tracing_garbage_collection#Naïve_mark-and-sweep 443 | [boehm]: https://www.hboehm.info/gc/ 444 | [stutter]: https://github.com/mkirchner/stutter 445 | [tgc]: https://github.com/orangeduck/tgc 446 | [garbage_collection_handbook]: https://amzn.to/2VdEvjC 447 | -------------------------------------------------------------------------------- /doc/mem_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkirchner/gc/7f6f17c8b3425df6cd27d6f9385265b23034a793/doc/mem_layout.png -------------------------------------------------------------------------------- /doc/scanning.md: -------------------------------------------------------------------------------- 1 | # Finding reachable memory 2 | 3 | The hallmark of a conservative garbage collector is that it does not collect 4 | any memory unless it determines that it is no longer *reachable*. In order for 5 | any allocation to be reachable, there needs to be a pointer in the working 6 | memory of the program that points to said allocation. The working memory is the 7 | BSS (we ignore that on purpose), the CPU registers (we dump those on the stack 8 | before scanning), the stack and all existing (`gc`-managed) allocations on the 9 | heap. 10 | 11 | Scanning means that we test each of these memory locations for a pointer to another 12 | memory location, determining the transitive closure of allocated memory. 13 | Everything that is not in the transitive closure is then collected. 14 | 15 | Note that there are many ways how each of these steps can be optimized but 16 | most of these 17 | optimizations are platfrom/compiler-dependent and therefore out of the scope of 18 | `gc` (at least currently). 19 | 20 | ## Memory layout of a C program 21 | 22 | In order to understand the scanning process, it is necessary to understand the 23 | standard memory layout of a C program: 24 | 25 | 26 | 27 | The key observations for our discussion are 28 | 29 | 1. The stack grows towards *smaller* memory addresses. This is the case for 30 | all mainstream platforms, either by convention or by requirement. 31 | 2. The heap grows upwards 32 | 33 | 34 | There are platforms on which the stack grows towards larger memory addresses 35 | but we're safe to ignore those for the scope of `gc`. 36 | 37 | ## Scanning the stack 38 | 39 | Scanning the stack starts by determining the stack boundaries. The 40 | *bottom-of-stack* pointer, named `bos` refers to the *address of the 41 | lowest stack frame on the stack* (i.e. the highest address in memory). The 42 | *top-of-stack* pointer referes to the highest stack frame on the stack, i.e. 43 | the *lowest* address on the stack. 44 | In other words, we expect `tos` < `bos`. 45 | 46 | ```c 47 | void gc_mark_stack(GarbageCollector* gc) 48 | { 49 | char dummy; 50 | void *tos = (void*) &dummy; 51 | void *bos = gc->bos; 52 | for (char* p = (char*) tos; p <= (char*) bos - PTRSIZE; ++p) { 53 | gc_mark_alloc(gc, *(void**)p); 54 | } 55 | } 56 | ``` 57 | 58 | The code here is straightforward: 59 | 60 | 1. Declare a local variable `dummy` on the stack such that we can use 61 | its address as top-of -stack, ie. `tos = &dummy`. 62 | 2. Get the bottom-of-stack from the `gc` instance. 63 | 3. Iterate over all memory locations between `tos` and `bos` and check 64 | if they contain references to known memory locations (`gc_mark_alloc()` 65 | queries the allocation map and recursively marks the allocations if the 66 | pointed-to memory allocations are a known key in the allocation map). 67 | We do not iterate all the way to `bos` since the last `PTRSIZE-1` bytes 68 | are too short to hold valid pointer addresses. 69 | 70 | That leaves two questions: why are we iterating using a `char*` and 71 | what does `*(void**)p` do? 72 | 73 | ### Stack alignment and `char*` 74 | 75 | The reason why we are using a `char*` to iterate over the stack is because it 76 | allows us to access each byte on the stack. This is simply an (inefficient) 77 | approach to 78 | not having to deal with stack alignment across different platforms and/or 79 | compilers. 80 | 81 | An obvious optimization would be to assume proper stack alignment of pointers 82 | and, starting from `tos`, work out way forward in 4- (for 32 bit systems) or 83 | 8-byte (for 64 bit systems) steps instead of the 1-byte steps afforded by 84 | `char*`. 85 | 86 | ### Deciphering `*(void**)p` 87 | 88 | It's just confusing to read, and we can make it easier by introducing a 89 | `typedef`. Let's define a pointer to a memory location like so: 90 | 91 | ```c 92 | typedef void* MemPtr 93 | ``` 94 | 95 | We can then rewrite `*(void**)p` as 96 | 97 | ```c 98 | `*(MemPtr*)p` 99 | ``` 100 | 101 | making the syntax much less confusing. In detail, `p` is of type `char*`, so 102 | `(MemPtr*)p` is just a cast of the `char` pointer to be a pointer to a `MemPtr` 103 | type. The leftmost asterisk then dereferences to the content of the memory 104 | address pointed to by the `MemPtr*`, which is the content we want to check for 105 | references (i.e. pointers) to known memory locations. 106 | 107 | ## Scanning the heap 108 | 109 | Compared to scanning the stack, scanning the heap is trivial: we just iterate 110 | over all known allocations and check if they contain a pointer to another 111 | (known) allocation: 112 | 113 | ```c 114 | void gc_mark_roots(GarbageCollector* gc) 115 | { 116 | for (size_t i = 0; i < gc->allocs->capacity; ++i) { 117 | Allocation* chunk = gc->allocs->allocs[i]; 118 | while (chunk) { 119 | if (chunk->tag & GC_TAG_ROOT) { 120 | gc_mark_alloc(gc, chunk->ptr); 121 | } 122 | chunk = chunk->next; 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | ## Implementing `gc_mark_alloc()` 129 | 130 | Taking a closer look at `gc_mark_alloc()` reveals that it is really only a loop 131 | that iterates over the memory content of an allocation, attempting to find any 132 | pointers located within: 133 | 134 | ```c 135 | void gc_mark_alloc(GarbageCollector* gc, void* ptr) 136 | { 137 | Allocation* alloc = gc_allocation_map_get(gc->allocs, ptr); 138 | if (alloc && !(alloc->tag & GC_TAG_MARK)) { 139 | alloc->tag |= GC_TAG_MARK; 140 | for (char* p = (char*) alloc->ptr; 141 | p <= (char*) alloc->ptr + alloc->size - PTRSIZE; 142 | ++p) { 143 | gc_mark_alloc(gc, *(void**)p); 144 | } 145 | } 146 | } 147 | 148 | -------------------------------------------------------------------------------- /src/gc.c: -------------------------------------------------------------------------------- 1 | #include "gc.h" 2 | #include "log.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | //#include "primes.h" 9 | 10 | /* 11 | * Set log level for this compilation unit. If set to LOGLEVEL_DEBUG, 12 | * the garbage collector will be very chatty. 13 | */ 14 | #undef LOGLEVEL 15 | #define LOGLEVEL LOGLEVEL_INFO 16 | 17 | /* 18 | * The size of a pointer. 19 | */ 20 | #define PTRSIZE sizeof(char*) 21 | 22 | /* 23 | * Allocations can temporarily be tagged as "marked" an part of the 24 | * mark-and-sweep implementation or can be tagged as "roots" which are 25 | * not automatically garbage collected. The latter allows the implementation 26 | * of global variables. 27 | */ 28 | #define GC_TAG_NONE 0x0 29 | #define GC_TAG_ROOT 0x1 30 | #define GC_TAG_MARK 0x2 31 | 32 | /* 33 | * Support for windows c compiler is added by adding this macro. 34 | * Tested on: Microsoft (R) C/C++ Optimizing Compiler Version 19.24.28314 for x86 35 | */ 36 | #if defined(_MSC_VER) 37 | #define __builtin_frame_address(x) ((void)(x), _AddressOfReturnAddress()) 38 | #endif 39 | 40 | /* 41 | * Define a globally available GC object; this allows all code that 42 | * includes the gc.h header to access a global static garbage collector. 43 | * Convenient for single-threaded code, insufficient for multi-threaded 44 | * use cases. Use the GC_NO_GLOBAL_GC flag to toggle. 45 | */ 46 | #ifndef GC_NO_GLOBAL_GC 47 | GarbageCollector gc; // global GC object 48 | #endif 49 | 50 | 51 | static bool is_prime(size_t n) 52 | { 53 | /* https://stackoverflow.com/questions/1538644/c-determine-if-a-number-is-prime */ 54 | if (n <= 3) 55 | return n > 1; // as 2 and 3 are prime 56 | else if (n % 2==0 || n % 3==0) 57 | return false; // check if n is divisible by 2 or 3 58 | else { 59 | for (size_t i=5; i*i<=n; i+=6) { 60 | if (n % i == 0 || n%(i + 2) == 0) 61 | return false; 62 | } 63 | return true; 64 | } 65 | } 66 | 67 | static size_t next_prime(size_t n) 68 | { 69 | while (!is_prime(n)) ++n; 70 | return n; 71 | } 72 | 73 | /** 74 | * The allocation object. 75 | * 76 | * The allocation object holds all metadata for a memory location 77 | * in one place. 78 | */ 79 | typedef struct Allocation { 80 | void* ptr; // mem pointer 81 | size_t size; // allocated size in bytes 82 | char tag; // the tag for mark-and-sweep 83 | void (*dtor)(void*); // destructor 84 | struct Allocation* next; // separate chaining 85 | } Allocation; 86 | 87 | /** 88 | * Create a new allocation object. 89 | * 90 | * Creates a new allocation object using the system `malloc`. 91 | * 92 | * @param[in] ptr The pointer to the memory to manage. 93 | * @param[in] size The size of the memory range pointed to by `ptr`. 94 | * @param[in] dtor A pointer to a destructor function that should be called 95 | * before freeing the memory pointed to by `ptr`. 96 | * @returns Pointer to the new allocation instance. 97 | */ 98 | static Allocation* gc_allocation_new(void* ptr, size_t size, void (*dtor)(void*)) 99 | { 100 | Allocation* a = (Allocation*) malloc(sizeof(Allocation)); 101 | a->ptr = ptr; 102 | a->size = size; 103 | a->tag = GC_TAG_NONE; 104 | a->dtor = dtor; 105 | a->next = NULL; 106 | return a; 107 | } 108 | 109 | /** 110 | * Delete an allocation object. 111 | * 112 | * Deletes the allocation object pointed to by `a`, but does *not* 113 | * free the memory pointed to by `a->ptr`. 114 | * 115 | * @param a The allocation object to delete. 116 | */ 117 | static void gc_allocation_delete(Allocation* a) 118 | { 119 | free(a); 120 | } 121 | 122 | /** 123 | * The allocation hash map. 124 | * 125 | * The core data structure is a hash map that holds the allocation 126 | * objects and allows O(1) retrieval given the memory location. Collision 127 | * resolution is implemented using separate chaining. 128 | */ 129 | typedef struct AllocationMap { 130 | size_t capacity; 131 | size_t min_capacity; 132 | double downsize_factor; 133 | double upsize_factor; 134 | double sweep_factor; 135 | size_t sweep_limit; 136 | size_t size; 137 | Allocation** allocs; 138 | } AllocationMap; 139 | 140 | /** 141 | * Determine the current load factor of an `AllocationMap`. 142 | * 143 | * Calculates the load factor of the hash map as the quotient of the size and 144 | * the capacity of the hash map. 145 | * 146 | * @param am The allocationo map to calculate the load factor for. 147 | * @returns The load factor of the allocation map `am`. 148 | */ 149 | static double gc_allocation_map_load_factor(AllocationMap* am) 150 | { 151 | return (double) am->size / (double) am->capacity; 152 | } 153 | 154 | static AllocationMap* gc_allocation_map_new(size_t min_capacity, 155 | size_t capacity, 156 | double sweep_factor, 157 | double downsize_factor, 158 | double upsize_factor) 159 | { 160 | AllocationMap* am = (AllocationMap*) malloc(sizeof(AllocationMap)); 161 | am->min_capacity = next_prime(min_capacity); 162 | am->capacity = next_prime(capacity); 163 | if (am->capacity < am->min_capacity) am->capacity = am->min_capacity; 164 | am->sweep_factor = sweep_factor; 165 | am->sweep_limit = (int) (sweep_factor * am->capacity); 166 | am->downsize_factor = downsize_factor; 167 | am->upsize_factor = upsize_factor; 168 | am->allocs = (Allocation**) calloc(am->capacity, sizeof(Allocation*)); 169 | am->size = 0; 170 | LOG_DEBUG("Created allocation map (cap=%ld, siz=%ld)", am->capacity, am->size); 171 | return am; 172 | } 173 | 174 | static void gc_allocation_map_delete(AllocationMap* am) 175 | { 176 | // Iterate over the map 177 | LOG_DEBUG("Deleting allocation map (cap=%ld, siz=%ld)", 178 | am->capacity, am->size); 179 | Allocation *alloc, *tmp; 180 | for (size_t i = 0; i < am->capacity; ++i) { 181 | if ((alloc = am->allocs[i])) { 182 | // Make sure to follow the chain inside a bucket 183 | while (alloc) { 184 | tmp = alloc; 185 | alloc = alloc->next; 186 | // free the management structure 187 | gc_allocation_delete(tmp); 188 | } 189 | } 190 | } 191 | free(am->allocs); 192 | free(am); 193 | } 194 | 195 | static size_t gc_hash(void *ptr) 196 | { 197 | return ((uintptr_t)ptr) >> 3; 198 | } 199 | 200 | static void gc_allocation_map_resize(AllocationMap* am, size_t new_capacity) 201 | { 202 | if (new_capacity <= am->min_capacity) { 203 | return; 204 | } 205 | // Replaces the existing items array in the hash table 206 | // with a resized one and pushes items into the new, correct buckets 207 | LOG_DEBUG("Resizing allocation map (cap=%ld, siz=%ld) -> (cap=%ld)", 208 | am->capacity, am->size, new_capacity); 209 | Allocation** resized_allocs = calloc(new_capacity, sizeof(Allocation*)); 210 | 211 | for (size_t i = 0; i < am->capacity; ++i) { 212 | Allocation* alloc = am->allocs[i]; 213 | while (alloc) { 214 | Allocation* next_alloc = alloc->next; 215 | size_t new_index = gc_hash(alloc->ptr) % new_capacity; 216 | alloc->next = resized_allocs[new_index]; 217 | resized_allocs[new_index] = alloc; 218 | alloc = next_alloc; 219 | } 220 | } 221 | free(am->allocs); 222 | am->capacity = new_capacity; 223 | am->allocs = resized_allocs; 224 | am->sweep_limit = am->size + am->sweep_factor * (am->capacity - am->size); 225 | } 226 | 227 | static bool gc_allocation_map_resize_to_fit(AllocationMap* am) 228 | { 229 | double load_factor = gc_allocation_map_load_factor(am); 230 | if (load_factor > am->upsize_factor) { 231 | LOG_DEBUG("Load factor %0.3g > %0.3g. Triggering upsize.", 232 | load_factor, am->upsize_factor); 233 | gc_allocation_map_resize(am, next_prime(am->capacity * 2)); 234 | return true; 235 | } 236 | if (load_factor < am->downsize_factor) { 237 | LOG_DEBUG("Load factor %0.3g < %0.3g. Triggering downsize.", 238 | load_factor, am->downsize_factor); 239 | gc_allocation_map_resize(am, next_prime(am->capacity / 2)); 240 | return true; 241 | } 242 | return false; 243 | } 244 | 245 | static Allocation* gc_allocation_map_get(AllocationMap* am, void* ptr) 246 | { 247 | size_t index = gc_hash(ptr) % am->capacity; 248 | Allocation* cur = am->allocs[index]; 249 | while(cur) { 250 | if (cur->ptr == ptr) { 251 | return cur; 252 | } 253 | cur = cur->next; 254 | } 255 | return NULL; 256 | } 257 | 258 | static Allocation* gc_allocation_map_put(AllocationMap* am, 259 | void* ptr, 260 | size_t size, 261 | void (*dtor)(void*)) 262 | { 263 | size_t index = gc_hash(ptr) % am->capacity; 264 | LOG_DEBUG("PUT request for allocation ix=%ld", index); 265 | Allocation* alloc = gc_allocation_new(ptr, size, dtor); 266 | Allocation* cur = am->allocs[index]; 267 | Allocation* prev = NULL; 268 | /* Upsert if ptr is already known (e.g. dtor update). */ 269 | while(cur != NULL) { 270 | if (cur->ptr == ptr) { 271 | // found it 272 | alloc->next = cur->next; 273 | if (!prev) { 274 | // position 0 275 | am->allocs[index] = alloc; 276 | } else { 277 | // in the list 278 | prev->next = alloc; 279 | } 280 | gc_allocation_delete(cur); 281 | LOG_DEBUG("AllocationMap Upsert at ix=%ld", index); 282 | return alloc; 283 | 284 | } 285 | prev = cur; 286 | cur = cur->next; 287 | } 288 | /* Insert at the front of the separate chaining list */ 289 | cur = am->allocs[index]; 290 | alloc->next = cur; 291 | am->allocs[index] = alloc; 292 | am->size++; 293 | LOG_DEBUG("AllocationMap insert at ix=%ld", index); 294 | void* p = alloc->ptr; 295 | if (gc_allocation_map_resize_to_fit(am)) { 296 | alloc = gc_allocation_map_get(am, p); 297 | } 298 | return alloc; 299 | } 300 | 301 | 302 | static void gc_allocation_map_remove(AllocationMap* am, 303 | void* ptr, 304 | bool allow_resize) 305 | { 306 | // ignores unknown keys 307 | size_t index = gc_hash(ptr) % am->capacity; 308 | Allocation* cur = am->allocs[index]; 309 | Allocation* prev = NULL; 310 | Allocation* next; 311 | while(cur != NULL) { 312 | next = cur->next; 313 | if (cur->ptr == ptr) { 314 | // found it 315 | if (!prev) { 316 | // first item in list 317 | am->allocs[index] = cur->next; 318 | } else { 319 | // not the first item in the list 320 | prev->next = cur->next; 321 | } 322 | gc_allocation_delete(cur); 323 | am->size--; 324 | } else { 325 | // move on 326 | prev = cur; 327 | } 328 | cur = next; 329 | } 330 | if (allow_resize) { 331 | gc_allocation_map_resize_to_fit(am); 332 | } 333 | } 334 | 335 | 336 | static void* gc_mcalloc(size_t count, size_t size) 337 | { 338 | if (!count) return malloc(size); 339 | return calloc(count, size); 340 | } 341 | 342 | static bool gc_needs_sweep(GarbageCollector* gc) 343 | { 344 | return gc->allocs->size > gc->allocs->sweep_limit; 345 | } 346 | 347 | static void* gc_allocate(GarbageCollector* gc, size_t count, size_t size, void(*dtor)(void*)) 348 | { 349 | /* Allocation logic that generalizes over malloc/calloc. */ 350 | 351 | /* Check if we reached the high-water mark and need to clean up */ 352 | if (gc_needs_sweep(gc) && !gc->paused) { 353 | size_t freed_mem = gc_run(gc); 354 | LOG_DEBUG("Garbage collection cleaned up %lu bytes.", freed_mem); 355 | } 356 | /* With cleanup out of the way, attempt to allocate memory */ 357 | void* ptr = gc_mcalloc(count, size); 358 | size_t alloc_size = count ? count * size : size; 359 | /* If allocation fails, force an out-of-policy run to free some memory and try again. */ 360 | if (!ptr && !gc->paused && (errno == EAGAIN || errno == ENOMEM)) { 361 | gc_run(gc); 362 | ptr = gc_mcalloc(count, size); 363 | } 364 | /* Start managing the memory we received from the system */ 365 | if (ptr) { 366 | LOG_DEBUG("Allocated %zu bytes at %p", alloc_size, (void*) ptr); 367 | Allocation* alloc = gc_allocation_map_put(gc->allocs, ptr, alloc_size, dtor); 368 | /* Deal with metadata allocation failure */ 369 | if (alloc) { 370 | LOG_DEBUG("Managing %zu bytes at %p", alloc_size, (void*) alloc->ptr); 371 | ptr = alloc->ptr; 372 | } else { 373 | /* We failed to allocate the metadata, fail cleanly. */ 374 | free(ptr); 375 | ptr = NULL; 376 | } 377 | } 378 | return ptr; 379 | } 380 | 381 | static void gc_make_root(GarbageCollector* gc, void* ptr) 382 | { 383 | Allocation* alloc = gc_allocation_map_get(gc->allocs, ptr); 384 | if (alloc) { 385 | alloc->tag |= GC_TAG_ROOT; 386 | } 387 | } 388 | 389 | void* gc_malloc(GarbageCollector* gc, size_t size) 390 | { 391 | return gc_malloc_ext(gc, size, NULL); 392 | } 393 | 394 | void* gc_malloc_static(GarbageCollector* gc, size_t size, void(*dtor)(void*)) 395 | { 396 | void* ptr = gc_malloc_ext(gc, size, dtor); 397 | gc_make_root(gc, ptr); 398 | return ptr; 399 | } 400 | 401 | void* gc_make_static(GarbageCollector* gc, void* ptr) 402 | { 403 | gc_make_root(gc, ptr); 404 | return ptr; 405 | } 406 | 407 | void* gc_malloc_ext(GarbageCollector* gc, size_t size, void(*dtor)(void*)) 408 | { 409 | return gc_allocate(gc, 0, size, dtor); 410 | } 411 | 412 | 413 | void* gc_calloc(GarbageCollector* gc, size_t count, size_t size) 414 | { 415 | return gc_calloc_ext(gc, count, size, NULL); 416 | } 417 | 418 | 419 | void* gc_calloc_ext(GarbageCollector* gc, size_t count, size_t size, 420 | void(*dtor)(void*)) 421 | { 422 | return gc_allocate(gc, count, size, dtor); 423 | } 424 | 425 | 426 | void* gc_realloc(GarbageCollector* gc, void* p, size_t size) 427 | { 428 | Allocation* alloc = gc_allocation_map_get(gc->allocs, p); 429 | if (p && !alloc) { 430 | // the user passed an unknown pointer 431 | errno = EINVAL; 432 | return NULL; 433 | } 434 | void* q = realloc(p, size); 435 | if (!q) { 436 | // realloc failed but p is still valid 437 | return NULL; 438 | } 439 | if (!p) { 440 | // allocation, not reallocation 441 | Allocation* alloc = gc_allocation_map_put(gc->allocs, q, size, NULL); 442 | return alloc->ptr; 443 | } 444 | if (p == q) { 445 | // successful reallocation w/o copy 446 | alloc->size = size; 447 | } else { 448 | // successful reallocation w/ copy 449 | void (*dtor)(void*) = alloc->dtor; 450 | gc_allocation_map_remove(gc->allocs, p, true); 451 | gc_allocation_map_put(gc->allocs, q, size, dtor); 452 | } 453 | return q; 454 | } 455 | 456 | void gc_free(GarbageCollector* gc, void* ptr) 457 | { 458 | Allocation* alloc = gc_allocation_map_get(gc->allocs, ptr); 459 | if (alloc) { 460 | if (alloc->dtor) { 461 | alloc->dtor(ptr); 462 | } 463 | free(ptr); 464 | gc_allocation_map_remove(gc->allocs, ptr, true); 465 | } else { 466 | LOG_WARNING("Ignoring request to free unknown pointer %p", (void*) ptr); 467 | } 468 | } 469 | 470 | void gc_start(GarbageCollector* gc, void* bos) 471 | { 472 | gc_start_ext(gc, bos, 1024, 1024, 0.2, 0.8, 0.5); 473 | } 474 | 475 | void gc_start_ext(GarbageCollector* gc, 476 | void* bos, 477 | size_t initial_capacity, 478 | size_t min_capacity, 479 | double downsize_load_factor, 480 | double upsize_load_factor, 481 | double sweep_factor) 482 | { 483 | double downsize_limit = downsize_load_factor > 0.0 ? downsize_load_factor : 0.2; 484 | double upsize_limit = upsize_load_factor > 0.0 ? upsize_load_factor : 0.8; 485 | sweep_factor = sweep_factor > 0.0 ? sweep_factor : 0.5; 486 | gc->paused = false; 487 | gc->bos = bos; 488 | initial_capacity = initial_capacity < min_capacity ? min_capacity : initial_capacity; 489 | gc->allocs = gc_allocation_map_new(min_capacity, initial_capacity, 490 | sweep_factor, downsize_limit, upsize_limit); 491 | LOG_DEBUG("Created new garbage collector (cap=%ld, siz=%ld).", gc->allocs->capacity, 492 | gc->allocs->size); 493 | } 494 | 495 | void gc_pause(GarbageCollector* gc) 496 | { 497 | gc->paused = true; 498 | } 499 | 500 | void gc_resume(GarbageCollector* gc) 501 | { 502 | gc->paused = false; 503 | } 504 | 505 | void gc_mark_alloc(GarbageCollector* gc, void* ptr) 506 | { 507 | Allocation* alloc = gc_allocation_map_get(gc->allocs, ptr); 508 | /* Mark if alloc exists and is not tagged already, otherwise skip */ 509 | if (alloc && !(alloc->tag & GC_TAG_MARK)) { 510 | LOG_DEBUG("Marking allocation (ptr=%p)", ptr); 511 | alloc->tag |= GC_TAG_MARK; 512 | /* Iterate over allocation contents and mark them as well */ 513 | LOG_DEBUG("Checking allocation (ptr=%p, size=%lu) contents", ptr, alloc->size); 514 | for (char* p = (char*) alloc->ptr; 515 | p <= (char*) alloc->ptr + alloc->size - PTRSIZE; 516 | ++p) { 517 | LOG_DEBUG("Checking allocation (ptr=%p) @%lu with value %p", 518 | ptr, p-((char*) alloc->ptr), *(void**)p); 519 | gc_mark_alloc(gc, *(void**)p); 520 | } 521 | } 522 | } 523 | 524 | void gc_mark_stack(GarbageCollector* gc) 525 | { 526 | LOG_DEBUG("Marking the stack (gc@%p) in increments of %ld", (void*) gc, sizeof(char)); 527 | void *tos = __builtin_frame_address(0); 528 | void *bos = gc->bos; 529 | /* The stack grows towards smaller memory addresses, hence we scan tos->bos. 530 | * Stop scanning once the distance between tos & bos is too small to hold a valid pointer */ 531 | for (char* p = (char*) tos; p <= (char*) bos - PTRSIZE; ++p) { 532 | gc_mark_alloc(gc, *(void**)p); 533 | } 534 | } 535 | 536 | void gc_mark_roots(GarbageCollector* gc) 537 | { 538 | LOG_DEBUG("Marking roots%s", ""); 539 | for (size_t i = 0; i < gc->allocs->capacity; ++i) { 540 | Allocation* chunk = gc->allocs->allocs[i]; 541 | while (chunk) { 542 | if (chunk->tag & GC_TAG_ROOT) { 543 | LOG_DEBUG("Marking root @ %p", chunk->ptr); 544 | gc_mark_alloc(gc, chunk->ptr); 545 | } 546 | chunk = chunk->next; 547 | } 548 | } 549 | } 550 | 551 | void gc_mark(GarbageCollector* gc) 552 | { 553 | /* Note: We only look at the stack and the heap, and ignore BSS. */ 554 | LOG_DEBUG("Initiating GC mark (gc@%p)", (void*) gc); 555 | /* Scan the heap for roots */ 556 | gc_mark_roots(gc); 557 | /* Dump registers onto stack and scan the stack */ 558 | void (*volatile _mark_stack)(GarbageCollector*) = gc_mark_stack; 559 | jmp_buf ctx; 560 | memset(&ctx, 0, sizeof(jmp_buf)); 561 | setjmp(ctx); 562 | _mark_stack(gc); 563 | } 564 | 565 | size_t gc_sweep(GarbageCollector* gc) 566 | { 567 | LOG_DEBUG("Initiating GC sweep (gc@%p)", (void*) gc); 568 | size_t total = 0; 569 | for (size_t i = 0; i < gc->allocs->capacity; ++i) { 570 | Allocation* chunk = gc->allocs->allocs[i]; 571 | Allocation* next = NULL; 572 | /* Iterate over separate chaining */ 573 | while (chunk) { 574 | if (chunk->tag & GC_TAG_MARK) { 575 | LOG_DEBUG("Found used allocation %p (ptr=%p)", (void*) chunk, (void*) chunk->ptr); 576 | /* unmark */ 577 | chunk->tag &= ~GC_TAG_MARK; 578 | chunk = chunk->next; 579 | } else { 580 | LOG_DEBUG("Found unused allocation %p (%lu bytes @ ptr=%p)", (void*) chunk, chunk->size, (void*) chunk->ptr); 581 | /* no reference to this chunk, hence delete it */ 582 | total += chunk->size; 583 | if (chunk->dtor) { 584 | chunk->dtor(chunk->ptr); 585 | } 586 | free(chunk->ptr); 587 | /* and remove it from the bookkeeping */ 588 | next = chunk->next; 589 | gc_allocation_map_remove(gc->allocs, chunk->ptr, false); 590 | chunk = next; 591 | } 592 | } 593 | } 594 | gc_allocation_map_resize_to_fit(gc->allocs); 595 | return total; 596 | } 597 | 598 | /** 599 | * Unset the ROOT tag on all roots on the heap. 600 | * 601 | * @param gc A pointer to a garbage collector instance. 602 | */ 603 | void gc_unroot_roots(GarbageCollector* gc) 604 | { 605 | LOG_DEBUG("Unmarking roots%s", ""); 606 | for (size_t i = 0; i < gc->allocs->capacity; ++i) { 607 | Allocation* chunk = gc->allocs->allocs[i]; 608 | while (chunk) { 609 | if (chunk->tag & GC_TAG_ROOT) { 610 | chunk->tag &= ~GC_TAG_ROOT; 611 | } 612 | chunk = chunk->next; 613 | } 614 | } 615 | } 616 | 617 | size_t gc_stop(GarbageCollector* gc) 618 | { 619 | gc_unroot_roots(gc); 620 | size_t collected = gc_sweep(gc); 621 | gc_allocation_map_delete(gc->allocs); 622 | return collected; 623 | } 624 | 625 | size_t gc_run(GarbageCollector* gc) 626 | { 627 | LOG_DEBUG("Initiating GC run (gc@%p)", (void*) gc); 628 | gc_mark(gc); 629 | return gc_sweep(gc); 630 | } 631 | 632 | char* gc_strdup (GarbageCollector* gc, const char* s) 633 | { 634 | size_t len = strlen(s) + 1; 635 | void *new = gc_malloc(gc, len); 636 | 637 | if (new == NULL) { 638 | return NULL; 639 | } 640 | return (char*) memcpy(new, s, len); 641 | } 642 | -------------------------------------------------------------------------------- /src/gc.h: -------------------------------------------------------------------------------- 1 | /* 2 | * gc - A simple mark and sweep garbage collector for C. 3 | */ 4 | 5 | #ifndef __GC_H__ 6 | #define __GC_H__ 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | struct AllocationMap; 13 | 14 | typedef struct GarbageCollector { 15 | struct AllocationMap* allocs; // allocation map 16 | bool paused; // (temporarily) switch gc on/off 17 | void *bos; // bottom of stack 18 | size_t min_size; 19 | } GarbageCollector; 20 | 21 | extern GarbageCollector gc; // Global garbage collector for all 22 | // single-threaded applications 23 | 24 | /* 25 | * Starting, stopping, pausing, resuming and running the GC. 26 | */ 27 | void gc_start(GarbageCollector* gc, void* bos); 28 | void gc_start_ext(GarbageCollector* gc, void* bos, size_t initial_size, size_t min_size, 29 | double downsize_load_factor, double upsize_load_factor, double sweep_factor); 30 | size_t gc_stop(GarbageCollector* gc); 31 | void gc_pause(GarbageCollector* gc); 32 | void gc_resume(GarbageCollector* gc); 33 | size_t gc_run(GarbageCollector* gc); 34 | 35 | /* 36 | * Allocating and deallocating memory. 37 | */ 38 | void* gc_malloc(GarbageCollector* gc, size_t size); 39 | void* gc_malloc_static(GarbageCollector* gc, size_t size, void (*dtor)(void*)); 40 | void* gc_malloc_ext(GarbageCollector* gc, size_t size, void (*dtor)(void*)); 41 | void* gc_calloc(GarbageCollector* gc, size_t count, size_t size); 42 | void* gc_calloc_ext(GarbageCollector* gc, size_t count, size_t size, void (*dtor)(void*)); 43 | void* gc_realloc(GarbageCollector* gc, void* ptr, size_t size); 44 | void gc_free(GarbageCollector* gc, void* ptr); 45 | 46 | /* 47 | * Lifecycle management 48 | */ 49 | void* gc_make_static(GarbageCollector* gc, void* ptr); 50 | 51 | /* 52 | * Helper functions and stdlib replacements. 53 | */ 54 | char* gc_strdup (GarbageCollector* gc, const char* s); 55 | 56 | #endif /* !__GC_H__ */ 57 | -------------------------------------------------------------------------------- /src/log.c: -------------------------------------------------------------------------------- 1 | #include "log.h" 2 | 3 | const char * log_level_strings [] = { "CRIT", "WARN", "INFO", "DEBG", "NONE" }; 4 | 5 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | #ifndef __LOG_H__ 2 | #define __LOG_H__ 3 | 4 | #include 5 | 6 | #define LOGLEVEL LOGLEVEL_DEBUG 7 | 8 | enum { 9 | LOGLEVEL_CRITICAL, 10 | LOGLEVEL_WARNING, 11 | LOGLEVEL_INFO, 12 | LOGLEVEL_DEBUG, 13 | LOGLEVEL_NONE 14 | }; 15 | 16 | extern const char* log_level_strings[]; 17 | 18 | #define log(level, fmt, ...) \ 19 | do { if (level <= LOGLEVEL) fprintf(stderr, "[%s] %s:%s:%d: " fmt "\n", log_level_strings[level], __func__, __FILE__, __LINE__, __VA_ARGS__); } while (0) 20 | 21 | #define LOG_CRITICAL(fmt, ...) log(LOGLEVEL_CRITICAL, fmt, __VA_ARGS__) 22 | #define LOG_WARNING(fmt, ...) log(LOGLEVEL_WARNING, fmt, __VA_ARGS__) 23 | #define LOG_INFO(fmt, ...) log(LOGLEVEL_INFO, fmt, __VA_ARGS__) 24 | #define LOG_DEBUG(fmt, ...) log(LOGLEVEL_DEBUG, fmt, __VA_ARGS__) 25 | 26 | #endif /* !__LOG_H__ */ 27 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | CC=clang 2 | CFLAGS=-g -Wall -Wextra -pedantic -I../include -fprofile-arcs -ftest-coverage 3 | LDFLAGS=-g -L../build/src -L../build/test --coverage 4 | LDLIBS= 5 | RM=rm 6 | BUILD_DIR=../build 7 | 8 | .PHONY: all 9 | all: $(BUILD_DIR)/test/test_gc 10 | 11 | $(BUILD_DIR)/test/%.o: %.c 12 | mkdir -p $(@D) 13 | $(CC) $(CFLAGS) -MMD -c $< -o $@ 14 | 15 | SRCS=test_gc.c ../src/log.c 16 | OBJS=$(SRCS:%.c=$(BUILD_DIR)/test/%.o) 17 | DEPS=$(OBJS:%.o=%.d) 18 | 19 | $(BUILD_DIR)/test/test_gc: $(OBJS) 20 | mkdir -p $(@D) 21 | $(CC) $(LDFLAGS) $(LDLIBS) $^ -o $@ 22 | 23 | coverage: $(BUILD_DIR)/test/test_gc 24 | lcov -b . -d ../build/test/ -c -o ../build/test/coverage-all.info 25 | lcov -b . -r ../build/test/coverage-all.info "*test*" -o ../build/test/coverage.info 26 | 27 | coverage-html: coverage 28 | mkdir -p ../build/test/coverage 29 | genhtml -o ../build/test/coverage ../build/test/coverage.info 30 | open ../build/test/coverage/index.html 31 | 32 | .PHONY: clean 33 | clean: 34 | $(RM) -f $(OBJS) $(DEPS) 35 | 36 | distclean: clean 37 | $(RM) -f $(BUILD_DIR)/test/test_gc 38 | $(RM) -f $(BUILD_DIR)/test/*gcda 39 | $(RM) -f $(BUILD_DIR)/test/*gcno 40 | 41 | -------------------------------------------------------------------------------- /test/minunit.h: -------------------------------------------------------------------------------- 1 | /* 2 | * minunit.h 3 | * 4 | * See: http://www.jera.com/techinfo/jtns/jtn002.html 5 | * 6 | */ 7 | 8 | #ifndef MINUNIT_H 9 | #define MINUNIT_H 10 | 11 | #define mu_assert(test, message) do { if (!(test)) return message; } while (0) 12 | #define mu_run_test(test) do { char *message = test(); tests_run++; \ 13 | if (message) return message; } while (0) 14 | 15 | extern int tests_run; 16 | 17 | #endif /* !MINUNIT_H */ 18 | -------------------------------------------------------------------------------- /test/test_gc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "minunit.h" 5 | 6 | #include "../src/gc.c" 7 | 8 | #define UNUSED(x) (void)(x) 9 | 10 | static size_t DTOR_COUNT = 0; 11 | 12 | static char* test_primes() 13 | { 14 | /* 15 | * Test a few known cases. 16 | */ 17 | mu_assert(!is_prime(0), "Prime test failure for 0"); 18 | mu_assert(!is_prime(1), "Prime test failure for 1"); 19 | mu_assert(is_prime(2), "Prime test failure for 2"); 20 | mu_assert(is_prime(3), "Prime test failure for 3"); 21 | mu_assert(!is_prime(12742382), "Prime test failure for 12742382"); 22 | mu_assert(is_prime(611953), "Prime test failure for 611953"); 23 | mu_assert(is_prime(479001599), "Prime test failure for 479001599"); 24 | return 0; 25 | } 26 | 27 | void dtor(void* ptr) 28 | { 29 | UNUSED(ptr); 30 | DTOR_COUNT++; 31 | } 32 | 33 | static char* test_gc_allocation_new_delete() 34 | { 35 | int* ptr = malloc(sizeof(int)); 36 | Allocation* a = gc_allocation_new(ptr, sizeof(int), dtor); 37 | mu_assert(a != NULL, "Allocation should return non-NULL"); 38 | mu_assert(a->ptr == ptr, "Allocation should contain original pointer"); 39 | mu_assert(a->size == sizeof(int), "Size of mem pointed to should not change"); 40 | mu_assert(a->tag == GC_TAG_NONE, "Annotation should initially be untagged"); 41 | mu_assert(a->dtor == dtor, "Destructor pointer should not change"); 42 | mu_assert(a->next == NULL, "Annotation should initilally be unlinked"); 43 | gc_allocation_delete(a); 44 | free(ptr); 45 | return NULL; 46 | } 47 | 48 | 49 | static char* test_gc_allocation_map_new_delete() 50 | { 51 | /* Standard invocation */ 52 | AllocationMap* am = gc_allocation_map_new(8, 16, 0.5, 0.2, 0.8); 53 | mu_assert(am->min_capacity == 11, "True min capacity should be next prime"); 54 | mu_assert(am->capacity == 17, "True capacity should be next prime"); 55 | mu_assert(am->size == 0, "Allocation map should be initialized to empty"); 56 | mu_assert(am->sweep_limit == 8, "Incorrect sweep limit calculation"); 57 | mu_assert(am->downsize_factor == 0.2, "Downsize factor should not change"); 58 | mu_assert(am->upsize_factor == 0.8, "Upsize factor should not change"); 59 | mu_assert(am->allocs != NULL, "Allocation map must not have a NULL pointer"); 60 | gc_allocation_map_delete(am); 61 | 62 | /* Enforce min sizes */ 63 | am = gc_allocation_map_new(8, 4, 0.5, 0.2, 0.8); 64 | mu_assert(am->min_capacity == 11, "True min capacity should be next prime"); 65 | mu_assert(am->capacity == 11, "True capacity should be next prime"); 66 | mu_assert(am->size == 0, "Allocation map should be initialized to empty"); 67 | mu_assert(am->sweep_limit == 5, "Incorrect sweep limit calculation"); 68 | mu_assert(am->downsize_factor == 0.2, "Downsize factor should not change"); 69 | mu_assert(am->upsize_factor == 0.8, "Upsize factor should not change"); 70 | mu_assert(am->allocs != NULL, "Allocation map must not have a NULL pointer"); 71 | gc_allocation_map_delete(am); 72 | 73 | return NULL; 74 | } 75 | 76 | 77 | static char* test_gc_allocation_map_basic_get() 78 | { 79 | AllocationMap* am = gc_allocation_map_new(8, 16, 0.5, 0.2, 0.8); 80 | 81 | /* Ask for something that does not exist */ 82 | int* five = malloc(sizeof(int)); 83 | Allocation* a = gc_allocation_map_get(am, five); 84 | mu_assert(a == NULL, "Empty allocation map must not contain any allocations"); 85 | 86 | /* Create an entry and query it */ 87 | *five = 5; 88 | a = gc_allocation_map_put(am, five, sizeof(int), NULL); 89 | mu_assert(a != NULL, "Result of PUT on allocation map must be non-NULL"); 90 | mu_assert(am->size == 1, "Expect size of one-element map to be one"); 91 | mu_assert(am->allocs != NULL, "AllocationMap must hold list of allocations"); 92 | Allocation* b = gc_allocation_map_get(am, five); 93 | mu_assert(a == b, "Get should return the same result as put"); 94 | mu_assert(a->ptr == b->ptr, "Pointers must not change between calls"); 95 | mu_assert(b->ptr == five, "Get result should equal original pointer"); 96 | 97 | /* Update the entry and query */ 98 | a = gc_allocation_map_put(am, five, sizeof(int), dtor); 99 | mu_assert(am->size == 1, "Expect size of one-element map to be one"); 100 | mu_assert(a->dtor == dtor, "Setting the dtor should set the dtor"); 101 | b = gc_allocation_map_get(am, five); 102 | mu_assert(b->dtor == dtor, "Failed to persist the dtor update"); 103 | 104 | /* Delete the entry */ 105 | gc_allocation_map_remove(am, five, true); 106 | mu_assert(am->size == 0, "After removing last item, map should be empty"); 107 | Allocation* c = gc_allocation_map_get(am, five); 108 | mu_assert(c == NULL, "Empty allocation map must not contain any allocations"); 109 | 110 | gc_allocation_map_delete(am); 111 | free(five); 112 | return NULL; 113 | } 114 | 115 | 116 | static char* test_gc_allocation_map_put_get_remove() 117 | { 118 | /* Create a few data pointers */ 119 | int** ints = malloc(64*sizeof(int*)); 120 | for (size_t i=0; i<64; ++i) { 121 | ints[i] = malloc(sizeof(int)); 122 | } 123 | 124 | /* Enforce separate chaining by disallowing up/downsizing. 125 | * The pigeonhole principle then states that we need to have at least one 126 | * entry in the hash map that has a separare chain with len > 1 127 | */ 128 | AllocationMap* am = gc_allocation_map_new(32, 32, DBL_MAX, 0.0, DBL_MAX); 129 | Allocation* a; 130 | for (size_t i=0; i<64; ++i) { 131 | a = gc_allocation_map_put(am, ints[i], sizeof(int), NULL); 132 | } 133 | mu_assert(am->size == 64, "Maps w/ 64 elements should have size 64"); 134 | /* Now update all of them with a new dtor */ 135 | for (size_t i=0; i<64; ++i) { 136 | a = gc_allocation_map_put(am, ints[i], sizeof(int), dtor); 137 | } 138 | mu_assert(am->size == 64, "Maps w/ 64 elements should have size 64"); 139 | /* Now delete all of them again */ 140 | for (size_t i=0; i<64; ++i) { 141 | gc_allocation_map_remove(am, ints[i], true); 142 | } 143 | mu_assert(am->size == 0, "Empty map must have size 0"); 144 | /* And delete the entire map */ 145 | gc_allocation_map_delete(am); 146 | 147 | /* Clean up the data pointers */ 148 | for (size_t i=0; i<64; ++i) { 149 | free(ints[i]); 150 | } 151 | free(ints); 152 | 153 | return NULL; 154 | } 155 | 156 | static char* test_gc_allocation_map_cleanup() 157 | { 158 | /* Make sure that the entries in the allocation map get reset 159 | * to NULL when we delete things. This is required for the 160 | * chunk != NULL checks when iterating over the items in the hash map. 161 | */ 162 | DTOR_COUNT = 0; 163 | GarbageCollector gc_; 164 | void *bos = __builtin_frame_address(0); 165 | gc_start_ext(&gc_, bos, 32, 32, 0.0, DBL_MAX, DBL_MAX); 166 | 167 | /* run a few alloc/free cycles */ 168 | int** ptrs = gc_malloc_ext(&gc_, 64*sizeof(int*), dtor); 169 | for (size_t j=0; j<8; ++j) { 170 | for (size_t i=0; i<64; ++i) { 171 | ptrs[i] = gc_malloc(&gc_, i*sizeof(int)); 172 | } 173 | for (size_t i=0; i<64; ++i) { 174 | gc_free(&gc_, ptrs[i]); 175 | } 176 | } 177 | gc_free(&gc_, ptrs); 178 | mu_assert(DTOR_COUNT == 1, "Failed to call destructor for array"); 179 | DTOR_COUNT = 0; 180 | 181 | /* now make sure that all allocation entries are NULL */ 182 | for (size_t i=0; icapacity; ++i) { 183 | mu_assert(gc_.allocs->allocs[i] == NULL, "Deleted allocs should be reset to NULL"); 184 | } 185 | gc_stop(&gc_); 186 | return NULL; 187 | } 188 | 189 | 190 | static char* test_gc_mark_stack() 191 | { 192 | GarbageCollector gc_; 193 | void *bos = __builtin_frame_address(0); 194 | gc_start_ext(&gc_, bos, 32, 32, 0.0, DBL_MAX, DBL_MAX); 195 | gc_pause(&gc_); 196 | 197 | /* Part 1: Create an object on the heap, reference from the stack, 198 | * and validate that it gets marked. */ 199 | int** five_ptr = gc_calloc(&gc_, 2, sizeof(int*)); 200 | gc_mark_stack(&gc_); 201 | Allocation* a = gc_allocation_map_get(gc_.allocs, five_ptr); 202 | mu_assert(a->tag & GC_TAG_MARK, "Heap allocation referenced from stack should be tagged"); 203 | 204 | /* manually reset the tags */ 205 | a->tag = GC_TAG_NONE; 206 | 207 | /* Part 2: Add dependent allocations and check if these allocations 208 | * get marked properly*/ 209 | five_ptr[0] = gc_malloc(&gc_, sizeof(int)); 210 | *five_ptr[0] = 5; 211 | five_ptr[1] = gc_malloc(&gc_, sizeof(int)); 212 | *five_ptr[1] = 5; 213 | gc_mark_stack(&gc_); 214 | a = gc_allocation_map_get(gc_.allocs, five_ptr); 215 | mu_assert(a->tag & GC_TAG_MARK, "Referenced heap allocation should be tagged"); 216 | for (size_t i=0; i<2; ++i) { 217 | a = gc_allocation_map_get(gc_.allocs, five_ptr[i]); 218 | mu_assert(a->tag & GC_TAG_MARK, "Dependent heap allocs should be tagged"); 219 | } 220 | 221 | /* Clean up the tags manually */ 222 | a = gc_allocation_map_get(gc_.allocs, five_ptr); 223 | a->tag = GC_TAG_NONE; 224 | for (size_t i=0; i<2; ++i) { 225 | a = gc_allocation_map_get(gc_.allocs, five_ptr[i]); 226 | a->tag = GC_TAG_NONE; 227 | } 228 | 229 | /* Part3: Now delete the pointer to five_ptr[1] which should 230 | * leave the allocation for five_ptr[1] unmarked. */ 231 | Allocation* unmarked_alloc = gc_allocation_map_get(gc_.allocs, five_ptr[1]); 232 | five_ptr[1] = NULL; 233 | gc_mark_stack(&gc_); 234 | a = gc_allocation_map_get(gc_.allocs, five_ptr); 235 | mu_assert(a->tag & GC_TAG_MARK, "Referenced heap allocation should be tagged"); 236 | a = gc_allocation_map_get(gc_.allocs, five_ptr[0]); 237 | mu_assert(a->tag & GC_TAG_MARK, "Referenced alloc should be tagged"); 238 | mu_assert(unmarked_alloc->tag == GC_TAG_NONE, "Unreferenced alloc should not be tagged"); 239 | 240 | /* Clean up the tags manually, again */ 241 | a = gc_allocation_map_get(gc_.allocs, five_ptr[0]); 242 | a->tag = GC_TAG_NONE; 243 | a = gc_allocation_map_get(gc_.allocs, five_ptr); 244 | a->tag = GC_TAG_NONE; 245 | 246 | gc_stop(&gc_); 247 | return NULL; 248 | } 249 | 250 | 251 | static char* test_gc_basic_alloc_free() 252 | { 253 | /* Create an array of pointers to an int. Then delete the pointer to 254 | * the containing array and check if all the contained allocs are garbage 255 | * collected. 256 | */ 257 | DTOR_COUNT = 0; 258 | GarbageCollector gc_; 259 | void *bos = __builtin_frame_address(0); 260 | gc_start_ext(&gc_, bos, 32, 32, 0.0, DBL_MAX, DBL_MAX); 261 | 262 | int** ints = gc_calloc(&gc_, 16, sizeof(int*)); 263 | Allocation* a = gc_allocation_map_get(gc_.allocs, ints); 264 | mu_assert(a->size == 16*sizeof(int*), "Wrong allocation size"); 265 | 266 | for (size_t i=0; i<16; ++i) { 267 | ints[i] = gc_malloc_ext(&gc_, sizeof(int), dtor); 268 | *ints[i] = 42; 269 | } 270 | mu_assert(gc_.allocs->size == 17, "Wrong allocation map size"); 271 | 272 | /* Test that all managed allocations get tagged if the root is present */ 273 | gc_mark(&gc_); 274 | for (size_t i=0; icapacity; ++i) { 275 | Allocation* chunk = gc_.allocs->allocs[i]; 276 | while (chunk) { 277 | mu_assert(chunk->tag & GC_TAG_MARK, "Referenced allocs should be marked"); 278 | // reset for next test 279 | chunk->tag = GC_TAG_NONE; 280 | chunk = chunk->next; 281 | } 282 | } 283 | 284 | /* Now drop the root allocation */ 285 | ints = NULL; 286 | gc_mark(&gc_); 287 | 288 | /* Check that none of the allocations get tagged */ 289 | size_t total = 0; 290 | for (size_t i=0; icapacity; ++i) { 291 | Allocation* chunk = gc_.allocs->allocs[i]; 292 | while (chunk) { 293 | mu_assert(!(chunk->tag & GC_TAG_MARK), "Unreferenced allocs should not be marked"); 294 | total += chunk->size; 295 | chunk = chunk->next; 296 | } 297 | } 298 | mu_assert(total == 16 * sizeof(int) + 16 * sizeof(int*), 299 | "Expected number of managed bytes is off"); 300 | 301 | size_t n = gc_sweep(&gc_); 302 | mu_assert(n == total, "Wrong number of collected bytes"); 303 | mu_assert(DTOR_COUNT == 16, "Failed to call destructor"); 304 | DTOR_COUNT = 0; 305 | gc_stop(&gc_); 306 | return NULL; 307 | } 308 | 309 | static void _create_static_allocs(GarbageCollector* gc, 310 | size_t count, 311 | size_t size) 312 | { 313 | for (size_t i=0; icapacity; ++i) { 339 | Allocation* chunk = gc_.allocs->allocs[i]; 340 | while (chunk) { 341 | mu_assert(!(chunk->tag & GC_TAG_MARK), "Marked an unused alloc"); 342 | mu_assert(!(chunk->tag & GC_TAG_ROOT), "Unrooting failed"); 343 | total += chunk->size; 344 | n++; 345 | chunk = chunk->next; 346 | } 347 | } 348 | mu_assert(n == N, "Expected number of allocations is off"); 349 | mu_assert(total == N*512, "Expected number of managed bytes is off"); 350 | /* make sure we collect everything */ 351 | collected = gc_sweep(&gc_); 352 | mu_assert(collected == N*512, "Unexpected number of bytes"); 353 | mu_assert(DTOR_COUNT == N, "Failed to call destructor"); 354 | DTOR_COUNT = 0; 355 | gc_stop(&gc_); 356 | return NULL; 357 | } 358 | 359 | static char* test_gc_realloc() 360 | { 361 | GarbageCollector gc_; 362 | void *bos = __builtin_frame_address(0); 363 | gc_start(&gc_, bos); 364 | 365 | /* manually allocate some memory */ 366 | { 367 | void *unmarked = malloc(sizeof(char)); 368 | void *re_unmarked = gc_realloc(&gc_, unmarked, sizeof(char) * 2); 369 | mu_assert(!re_unmarked, "GC should not realloc pointers unknown to it"); 370 | free(unmarked); 371 | } 372 | 373 | /* reallocing NULL pointer */ 374 | { 375 | void *unmarked = NULL; 376 | void *re_marked = gc_realloc(&gc_, unmarked, sizeof(char) * 42); 377 | mu_assert(re_marked, "GC should not realloc NULL pointers"); 378 | Allocation* a = gc_allocation_map_get(gc_.allocs, re_marked); 379 | mu_assert(a->size == 42, "Wrong allocation size"); 380 | } 381 | 382 | /* realloc a valid pointer with same size to enforce same pointer is used*/ 383 | { 384 | int** ints = gc_calloc(&gc_, 16, sizeof(int*)); 385 | ints = gc_realloc(&gc_, ints, 16*sizeof(int*)); 386 | Allocation* a = gc_allocation_map_get(gc_.allocs, ints); 387 | mu_assert(a->size == 16*sizeof(int*), "Wrong allocation size"); 388 | } 389 | 390 | /* realloc with size greater than before */ 391 | { 392 | int** ints = gc_calloc(&gc_, 16, sizeof(int*)); 393 | ints = gc_realloc(&gc_, ints, 42*sizeof(int*)); 394 | Allocation* a = gc_allocation_map_get(gc_.allocs, ints); 395 | mu_assert(a->size == 42*sizeof(int*), "Wrong allocation size"); 396 | } 397 | 398 | gc_stop(&gc_); 399 | return NULL; 400 | } 401 | 402 | static void _create_allocs(GarbageCollector* gc, 403 | size_t count, 404 | size_t size) 405 | { 406 | for (size_t i=0; iresume cycle */ 420 | gc_pause(&gc_); 421 | mu_assert(gc_.paused, "GC should be paused after pausing"); 422 | gc_resume(&gc_); 423 | 424 | /* Avoid dumping the registers on the stack to make test less flaky */ 425 | gc_mark_roots(&gc_); 426 | gc_mark_stack(&gc_); 427 | size_t collected = gc_sweep(&gc_); 428 | 429 | mu_assert(collected == N*8, "Unexpected number of collected bytes in pause/resume"); 430 | gc_stop(&gc_); 431 | return NULL; 432 | } 433 | 434 | static char* duplicate_string(GarbageCollector* gc, char* str) 435 | { 436 | char* copy = (char*) gc_strdup(gc, str); 437 | mu_assert(strncmp(str, copy, 16) == 0, "Strings should be equal"); 438 | return NULL; 439 | } 440 | 441 | char* test_gc_strdup() 442 | { 443 | GarbageCollector gc_; 444 | void *bos = __builtin_frame_address(0); 445 | gc_start(&gc_, bos); 446 | char* str = "This is a string"; 447 | char* error = duplicate_string(&gc_, str); 448 | mu_assert(error == NULL, "Duplication failed"); // cascade minunit tests 449 | size_t collected = gc_run(&gc_); 450 | mu_assert(collected == 17, "Unexpected number of collected bytes in strdup"); 451 | gc_stop(&gc_); 452 | return NULL; 453 | } 454 | 455 | /* 456 | * Test runner 457 | */ 458 | 459 | int tests_run = 0; 460 | 461 | static char* test_suite() 462 | { 463 | printf("---=[ GC tests\n"); 464 | mu_run_test(test_gc_allocation_new_delete); 465 | mu_run_test(test_gc_allocation_map_new_delete); 466 | mu_run_test(test_gc_allocation_map_basic_get); 467 | mu_run_test(test_gc_allocation_map_put_get_remove); 468 | mu_run_test(test_gc_mark_stack); 469 | mu_run_test(test_gc_basic_alloc_free); 470 | mu_run_test(test_gc_allocation_map_cleanup); 471 | mu_run_test(test_gc_static_allocation); 472 | mu_run_test(test_primes); 473 | mu_run_test(test_gc_realloc); 474 | mu_run_test(test_gc_pause_resume); 475 | mu_run_test(test_gc_strdup); 476 | return 0; 477 | } 478 | 479 | int main() 480 | { 481 | char *result = test_suite(); 482 | if (result) { 483 | printf("%s\n", result); 484 | } else { 485 | printf("ALL TESTS PASSED\n"); 486 | } 487 | printf("Tests run: %d\n", tests_run); 488 | return result != 0; 489 | } 490 | 491 | --------------------------------------------------------------------------------