Compare commits

..

3 commits

14 changed files with 301 additions and 274 deletions

6
.gitignore vendored
View file

@ -1,7 +1,9 @@
* *
!.gitignore !.gitignore
!*.c
!*.h
!*.md !*.md
!LICENSE !LICENSE
!Makefile !Makefile
!/src
!/src/*.c
!/include
!/include/*.h

View file

@ -1,47 +1,45 @@
CC = gcc CC = gcc
CFLAGS = -O3 -Wall -Wextra -Wpedantic CFLAGS =
CPPFLAGS = -Iinclude -MMD -MP
SRCS = main.c vm.c gc.c galloc.c SRCDIR = src
OBJS = $(SRCS:.c=.o)
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,%.o,$(SRCS))
EXE = gc EXE = gc
DBDIR = debug DBDIR = debug
DBEXE = $(DBDIR)/$(EXE) DBEXE = $(DBDIR)/$(EXE)
DBOBJS = $(addprefix $(DBDIR)/, $(OBJS)) DBOBJS = $(addprefix $(DBDIR)/, $(OBJS))
DBCFLAGS = -g -O0 -DDEBUG DBCFLAGS = -g -O0 -DDEBUG
REDIR = build REDIR = bin
REEXE = $(REDIR)/$(EXE) REEXE = $(REDIR)/$(EXE)
REOBJS = $(addprefix $(REDIR)/, $(OBJS)) REOBJS = $(addprefix $(REDIR)/, $(OBJS))
RECFLAGS = -O3 -Wall -Wextra -Wpedantic -Werror
.PHONY: all clean debug release prep .PHONY: all clean debug prep release
all: debug release gc all: debug release
gc: $(REEXE)
@cp $< $@
debug: prep $(DBEXE) debug: prep $(DBEXE)
$(DBEXE): $(DBOBJS) $(DBEXE): $(DBOBJS)
$(CC) $(CFLAGS) $(DBCFLAGS) -DTEST -o $@ $^ $(CC) $(CFLAGS) $(DBCFLAGS) -o $@ $^
$(DBDIR)/%.o: %.c $(DBDIR)/%.o: $(SRCDIR)/%.c
$(CC) -c $(CFLAGS) $(DBCFLAGS) -o $@ $< $(CC) -c $(CPPFLAGS) $(CFLAGS) $(DBCFLAGS) -o $@ $<
release: prep $(REEXE) release: prep $(REEXE)
$(REEXE): $(REOBJS) $(REEXE): $(REOBJS)
$(CC) $(CFLAGS) -o $@ $^ $(CC) $(CFLAGS) $(RECFLAGS) -o $@ $^
$(REDIR)/%.o: %.c $(REDIR)/%.o: $(SRCDIR)/%.c
$(CC) -c $(CFLAGS) -o $@ $< $(CC) -c $(CPPFLAGS) $(CFLAGS) $(RECFLAGS) -o $@ $<
prep: prep:
@mkdir -p $(DBDIR) $(REDIR) @mkdir -p $(DBDIR) $(REDIR)
clean: clean:
rm -rf gc $(DBDIR) $(REDIR) rm -rf $(DBDIR) $(REDIR)

View file

@ -1,38 +1,44 @@
# marCsweep # marCsweep
A mark-and-sweep garbage collector with a simple virtual machine to perform A mark-and-sweep garbage collector written in C.
allocations.
Includes a simple virtual machine and a custom `malloc` implementation
for creating, tracing, and freeing garbage.
## Building ## Building
Compiling the project should be relatively easy, even if you aren't familiar Compiling the project should be relatively easy, even if you aren't familiar
with `make`. with `make`.
Dependencies: `git`, `make`, and `gcc`. Compiling with `clang` is also possible, Dependencies: `git`, `make`, and `gcc`.
simply replace "gcc" with "clang" in the first line of the Makefile.
``` ```
git clone https://codeberg.org/andyscott/marCsweep.git git clone https://codeberg.org/andyscott/marCsweep.git
cd marCsweep cd marCsweep
make release make release # or debug
``` ```
That's it! The Makefile will automatically create the build directory and place That's it! Make will create the `bin` directory and place the finished
the compiled executable there. executable there.
Compiling with `clang` is also supported, just replace "gcc" with "clang" in the
first line of the `Makefile`. Also note that if you don't specify a target for
make (i.e. "release" or "debug") both will be built. Debug builds are placed in
the `debug` directory.
## Running ## Running
Once compiled, the program can be run with the following: Once compiled, the program can be run with the following:
``` ```
cd build # or debug cd bin # or debug
./gc ./gc
``` ```
By default the program will output the results of the test cases: The results of the test cases will be output to your terminal:
> test_int_alloc: PASS > test_int_alloc: PASS
test_pair_alloc: PASS test_pair_alloc: PASS
@ -50,3 +56,7 @@ This is a personal project that I am writing in my free time to learn more about
garbage collection. Thank you to [Robert garbage collection. Thank you to [Robert
Nystrom](https://journal.stuffwithstuff.com/2013/12/08/babys-first-garbage-collector/) Nystrom](https://journal.stuffwithstuff.com/2013/12/08/babys-first-garbage-collector/)
for the idea. for the idea.
Sadly, it was this author's idea to extend the project by writing a custom
`malloc`. Perhaps I should have been lazy and used the preexisting definitions,
but where's the fun in that?

44
gc.c
View file

@ -1,44 +0,0 @@
#include <stdlib.h>
#include "gc.h"
#include "vm.h"
void mark(struct garbageObject *obj) {
if (obj->mark)
return;
++obj->mark;
if (obj->type == GARBAGE_PAIR) {
mark(obj->head);
mark(obj->tail);
}
}
void markAll(struct virtualMachine *vm) {
for (int i = 0; i < vm->stackSize; ++i) {
mark(vm->stack[i]);
}
}
void sweep(struct virtualMachine *vm) {
struct garbageObject **obj = &vm->head;
while (*obj) {
if (!(*obj)->mark) {
struct garbageObject *unreachable = *obj;
*obj = unreachable->next;
free(unreachable);
vm->refCount--;
} else {
(*obj)->mark = 0;
obj = &(*obj)->next;
}
}
}
void collect(struct virtualMachine *vm) {
markAll(vm);
sweep(vm);
vm->refMax = vm->refCount * 2 <= STACK_MAX ? vm->refCount * 2 : STACK_MAX;
}

View file

View file

@ -1,5 +1,5 @@
#ifndef GALLOC_H #ifndef GC_ALLOC_H
#define GALLOC_H #define GC_ALLOC_H
#include <stdio.h> #include <stdio.h>
@ -23,4 +23,4 @@ void *galloc(size_t size);
// Frees memory allocated with galloc() // Frees memory allocated with galloc()
void gfree(void *ptr); void gfree(void *ptr);
#endif /* GALLOC_H */ #endif /* GC_ALLOC_H */

View file

107
main.c
View file

@ -1,107 +0,0 @@
#include <assert.h>
#include <stdio.h>
#include "gc.h"
#include "test_gc.h"
#include "vm.h"
void test_int_alloc(void) {
struct virtualMachine *vm = initVM();
pushInt(vm, 100);
pushInt(vm, 1000);
pushInt(vm, 10000);
pushInt(vm, -100000);
assert(vm->refCount == 4 &&
"test_int_alloc: GARBAGE_INT allocation failure\n");
printf("test_int_alloc: PASS\n");
deinitVM(vm);
}
void test_pair_alloc(void) {
struct virtualMachine *vm = initVM();
pushInt(vm, 100);
pushInt(vm, 1000);
pushInt(vm, 10000);
pushInt(vm, -100000);
pushPair(vm);
pushPair(vm);
assert(vm->refCount == 6 &&
"test_pair_alloc: FAILED: GARBAGE_PAIR allocation failure\n");
printf("test_pair_alloc: PASS\n");
deinitVM(vm);
}
void test_obj_count(void) {
struct virtualMachine *vm = initVM();
pushInt(vm, 100);
pushInt(vm, 1000);
pushInt(vm, 10000);
pushInt(vm, -100000);
collect(vm);
assert(vm->refCount == 4 &&
"test_obj_count: FAILED: GC occurred when it shouldn't have\n");
printf("test_obj_count: PASS\n");
deinitVM(vm);
}
void test_nested_pair(void) {
struct virtualMachine *vm = initVM();
pushInt(vm, 100);
pushInt(vm, 1000);
pushPair(vm);
pushInt(vm, 10000);
pushInt(vm, -100000);
pushPair(vm);
pushPair(vm);
collect(vm);
assert(vm->refCount == 7 &&
"test_nested_pair: FAILED: GARBAGE_PAIR allocation failure\n");
printf("test_pair_alloc: PASS\n");
deinitVM(vm);
}
void test_unreachable(void) {
struct virtualMachine *vm = initVM();
pushInt(vm, 100);
pushInt(vm, 1000);
pop(vm);
pop(vm);
collect(vm);
assert(vm->refCount == 0 &&
"test_unreachable: FAILED: 2 GARBAGE_INT should have been freed\n");
printf("test_unreachable: PASS\n");
deinitVM(vm);
}
void test_auto_gc(void) {
struct virtualMachine *vm = initVM();
for (size_t i = 0; i < 505; ++i) {
pushInt(vm, 1);
}
for (size_t i = 0; i < 5; ++i) {
pop(vm);
}
for (size_t i = 0; i < 50; ++i) {
pushInt(vm, 2);
}
assert(vm->refCount == 550 &&
"test_auto_gc: FAILED: 5 references should have been freed\n");
printf("test_auto_gc: PASS\n");
deinitVM(vm);
}
int main(void) {
test_int_alloc();
test_pair_alloc();
test_obj_count();
test_nested_pair();
test_unreachable();
test_auto_gc();
return 0;
}

47
src/gc.c Normal file
View file

@ -0,0 +1,47 @@
#include <stdlib.h>
#include "gc.h"
void mark(struct garbageObject *obj)
{
if (obj->mark)
return;
++obj->mark;
if (obj->type == GARBAGE_PAIR) {
mark(obj->head);
mark(obj->tail);
}
}
void markAll(struct virtualMachine *vm)
{
for (int i = 0; i < vm->stackSize; ++i) {
mark(vm->stack[i]);
}
}
void sweep(struct virtualMachine *vm)
{
struct garbageObject **obj = &vm->head;
while (*obj) {
if (!(*obj)->mark) {
struct garbageObject *unreachable = *obj;
*obj = unreachable->next;
free(unreachable);
vm->refCount--;
} else {
(*obj)->mark = 0;
obj = &(*obj)->next;
}
}
}
void collect(struct virtualMachine *vm)
{
markAll(vm);
sweep(vm);
vm->refMax = vm->refCount * 2 <= STACK_MAX ? vm->refCount * 2 : STACK_MAX;
}

114
src/main.c Normal file
View file

@ -0,0 +1,114 @@
#include <assert.h>
#include <stdio.h>
#include "gc.h"
#include "test_gc.h"
#include "vm.h"
void test_int_alloc(void)
{
struct virtualMachine *vm = initVM();
pushInt(vm, 100);
pushInt(vm, 1000);
pushInt(vm, 10000);
pushInt(vm, -100000);
assert(vm->refCount == 4
&& "test_int_alloc: GARBAGE_INT allocation failure\n");
printf("test_int_alloc: PASS\n");
deinitVM(vm);
}
void test_pair_alloc(void)
{
struct virtualMachine *vm = initVM();
pushInt(vm, 100);
pushInt(vm, 1000);
pushInt(vm, 10000);
pushInt(vm, -100000);
pushPair(vm);
pushPair(vm);
assert(vm->refCount == 6
&& "test_pair_alloc: FAILED: GARBAGE_PAIR allocation failure\n");
printf("test_pair_alloc: PASS\n");
deinitVM(vm);
}
void test_obj_count(void)
{
struct virtualMachine *vm = initVM();
pushInt(vm, 100);
pushInt(vm, 1000);
pushInt(vm, 10000);
pushInt(vm, -100000);
collect(vm);
assert(vm->refCount == 4
&& "test_obj_count: FAILED: GC occurred when it shouldn't have\n");
printf("test_obj_count: PASS\n");
deinitVM(vm);
}
void test_nested_pair(void)
{
struct virtualMachine *vm = initVM();
pushInt(vm, 100);
pushInt(vm, 1000);
pushPair(vm);
pushInt(vm, 10000);
pushInt(vm, -100000);
pushPair(vm);
pushPair(vm);
collect(vm);
assert(vm->refCount == 7
&& "test_nested_pair: FAILED: GARBAGE_PAIR allocation failure\n");
printf("test_pair_alloc: PASS\n");
deinitVM(vm);
}
void test_unreachable(void)
{
struct virtualMachine *vm = initVM();
pushInt(vm, 100);
pushInt(vm, 1000);
pop(vm);
pop(vm);
collect(vm);
assert(
vm->refCount == 0
&& "test_unreachable: FAILED: 2 GARBAGE_INT should have been freed\n");
printf("test_unreachable: PASS\n");
deinitVM(vm);
}
void test_auto_gc(void)
{
struct virtualMachine *vm = initVM();
for (size_t i = 0; i < 505; ++i) {
pushInt(vm, 1);
}
for (size_t i = 0; i < 5; ++i) {
pop(vm);
}
for (size_t i = 0; i < 50; ++i) {
pushInt(vm, 2);
}
assert(vm->refCount == 550
&& "test_auto_gc: FAILED: 5 references should have been freed\n");
printf("test_auto_gc: PASS\n");
deinitVM(vm);
}
int main(void)
{
test_int_alloc();
test_pair_alloc();
test_obj_count();
test_nested_pair();
test_unreachable();
test_auto_gc();
return 0;
}

89
src/vm.c Normal file
View file

@ -0,0 +1,89 @@
#include <stdio.h>
#include <stdlib.h>
#include "gc.h"
#include "vm.h"
struct virtualMachine *initVM(void)
{
struct virtualMachine *vm = malloc(sizeof(struct virtualMachine));
vm->stackSize = 0;
vm->refCount = 0;
vm->refMax = GC_THRESHOLD;
vm->head = NULL;
return vm;
}
void deinitVM(struct virtualMachine *vm)
{
vm->stackSize = 0;
collect(vm);
free(vm);
}
void push(struct virtualMachine *vm, struct garbageObject *value)
{
if (vm->stackSize >= STACK_MAX) {
fprintf(stderr, "ERROR: push(): refusing to overflow the stack!\n");
return;
}
vm->stack[vm->stackSize++] = value;
}
struct garbageObject *pop(struct virtualMachine *vm)
{
if (vm->stackSize < 0) {
fprintf(stderr, "ERROR: pop(): Stack size cannot be negative!");
return NULL;
}
return vm->stack[--vm->stackSize];
}
struct garbageObject *initGarbage(struct virtualMachine *vm,
enum garbageData type)
{
if (vm->refCount >= vm->refMax) {
collect(vm);
}
struct garbageObject *obj = malloc(sizeof(struct garbageObject));
if (obj == NULL) {
fprintf(stderr, "initGarbage(): Allocation failure!");
return obj;
}
obj->type = type;
obj->mark = 0;
obj->next = vm->head;
vm->head = obj;
vm->refCount++;
return obj;
}
void pushInt(struct virtualMachine *vm, int value)
{
struct garbageObject *obj = initGarbage(vm, GARBAGE_INT);
if (obj == NULL) {
fprintf(stderr,
"pushInt(): Unable to create GARBAGE_INT - out of memory?");
}
obj->value = value;
push(vm, obj);
}
struct garbageObject *pushPair(struct virtualMachine *vm)
{
struct garbageObject *obj = initGarbage(vm, GARBAGE_PAIR);
if (obj == NULL) {
fprintf(stderr,
"pushPair(): Unable to create GARBAGE_PAIR - out of memory?");
}
obj->tail = pop(vm);
obj->head = pop(vm);
push(vm, obj);
return obj;
}

82
vm.c
View file

@ -1,82 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include "gc.h"
#include "vm.h"
struct virtualMachine *initVM(void) {
struct virtualMachine *vm = malloc(sizeof(struct virtualMachine));
vm->stackSize = 0;
vm->refCount = 0;
vm->refMax = GC_THRESHOLD;
vm->head = NULL;
return vm;
}
void deinitVM(struct virtualMachine *vm) {
vm->stackSize = 0;
collect(vm);
free(vm);
}
void push(struct virtualMachine *vm, struct garbageObject *value) {
if (vm->stackSize >= STACK_MAX) {
fprintf(stderr, "ERROR: push(): refusing to overflow the stack!\n");
return;
}
vm->stack[vm->stackSize++] = value;
}
struct garbageObject *pop(struct virtualMachine *vm) {
if (vm->stackSize < 0) {
fprintf(stderr, "ERROR: pop(): Stack size cannot be negative!");
return NULL;
}
return vm->stack[--vm->stackSize];
}
struct garbageObject *initGarbage(struct virtualMachine *vm,
enum garbageData type) {
if (vm->refCount >= vm->refMax) {
collect(vm);
}
struct garbageObject *obj = malloc(sizeof(struct garbageObject));
if (obj == NULL) {
fprintf(stderr, "initGarbage(): Allocation failure!");
return obj;
}
obj->type = type;
obj->mark = 0;
obj->next = vm->head;
vm->head = obj;
vm->refCount++;
return obj;
}
void pushInt(struct virtualMachine *vm, int value) {
struct garbageObject *obj = initGarbage(vm, GARBAGE_INT);
if (obj == NULL) {
fprintf(stderr, "pushInt(): Unable to create GARBAGE_INT - out of memory?");
}
obj->value = value;
push(vm, obj);
}
struct garbageObject *pushPair(struct virtualMachine *vm) {
struct garbageObject *obj = initGarbage(vm, GARBAGE_PAIR);
if (obj == NULL) {
fprintf(stderr,
"pushPair(): Unable to create GARBAGE_PAIR - out of memory?");
}
obj->tail = pop(vm);
obj->head = pop(vm);
push(vm, obj);
return obj;
}