├── .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 | 
2 | [](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 |
--------------------------------------------------------------------------------