我正在用C语言编写一个Lisp解释器。每个 Lisp 对象都由一个带有type
字段的struct LispObject *
表示,以指示它是否是 int、symbol、缺点等。我已经将全局环境实现为包含名称和值对的哈希表。
LispObject
始终使用malloc
动态分配。每当创建新对象时,都会将其添加到弱引用列表中。当垃圾回收器运行时,它会标记可从全局环境访问的所有对象,然后扫描弱引用并释放未标记的对象。
保护全球环境免受垃圾回收很容易。我坚持的是如何保护本地 Lisp 对象。需要明确的是,我还没有实现Lisp函数。我要问的是如何保护LispObject *
类型的局部 C 变量。例如,eval
是一个 C 函数,它采用LispObject *
表达式,应用计算规则并返回LispObject *
值。我需要保护eval
(以及其他处理 Lisp 对象的 C 函数)中的局部LispObject *
变量免受垃圾回收,直到函数返回。
最干净的方法是什么?有没有办法标记可从 C 调用堆栈访问的任何LispObject
?
我已经考虑过实现一个单独的堆栈,仅用于存储不应该进行垃圾回收的本地 Lisp 对象,但这感觉很笨拙,因为这样局部LispObject *
变量就存储在 C 调用堆栈和垃圾回收堆栈上,我必须手动推送和弹出对象才能调用 C 函数。理想情况下,当 Lisp 对象存在于本地作用域中时,它们将自动受到保护,然后在超出作用域时自动失去该保护。
完整代码:https://notabug.org/jtherrmann/lisp-in-c
我假设您的GC是一个精确的GC。首先需要定义何时可能调用 GC。一种常见的方案是让每个分配例程可能调用 GC。
您需要编写一个例程来扫描调用堆栈以查找本地根。因此,您需要有一台将这些局部变量注册到GC的机器。换句话说,你应该显式解释器的调用堆栈(或采用一些延续传递样式的方法)。
一种可能性可能是将您的本地帧显式为某些struct
。例如,查看 Ocaml 运行时的作用(阅读其部分 §20.5 与垃圾收集器和谐相处)或我的旧(未维护)Qish GC。例如,您可以采用约定,即每个本地解释器帧都在某个_
局部变量(struct
)中并使用它。在我的 bismon 项目中,我会编写一些几乎等效的东西(在预处理器扩展之后),对于一个 C 例程,crout
有一个指针参数a
和两个本地指针b
和c
void crout(struct callingframe_st *cf, LispObject*a) {
struct mycallframe_st {
struct callingframe_st* from;
int nbloc;
LispObject* aa;
LispObject* bb;
LispObject* cc;
} _;
memset(&_, 0, sizeof(_));
_.from = cf;
_.nbloc = 3; // the current frame has 3 locals: aa, bb, cc
_.aa = a;
#define a _.aa
#define b _.bb
#define c _.cc
然后是crout
的正文。它将(struct callingframe_st*)(&_)
传递给适当的例程。最后,一定要#undef a
等... 从分配例程调用的 GC 必须将(struct callingframe_st *)(&_)
作为参数(提供当前调用帧)。
所以当然,假设你的b_cons
可以间接调用你的GC,应该声明为
LispObject* b_cons(struct callingframe_st*cf,
LispObject * car, LispObject * cdr);
否则,您需要定义何时调用 GC。
您需要了解垃圾回收的工作原理(以及精确和保守 GC 之间的区别)。我强烈建议阅读GC手册,或者至少阅读Paul Wilson的旧单处理器垃圾收集技术论文。你可以采用所有例程都遵循A-normal表单样式的约定(所以你永远不会直接用Cf(g(x),h(x,y))
编码,所有f
,g
,h
也许做对象分配)。
您也可以使用一些现有的精确 GC,例如 Ravenbrook MPS。
否则,请使用一些保守的GC,如Boehm的GC。
还要查看具有一些GC的现有自由软件解释器的源代码。
另请阅读 Queinnec 的Lisp In Small Pieces一书
我必须手动推送和弹出对象才能调用 C 函数。
这可能是一个好主意(但随后您需要重写大部分代码,并且实际上可以定义自己的字节码机制)。看看Lua、Nim、Ocaml字节码解释器或Emacs Elisp解释器在做什么。
为了完成,您可以考虑(这真的很难,我不建议走这条路,因为这需要多年的工作)编写一些 GCC 插件来生成和/或添加临时调用帧元数据和/或生成呼叫帧相关代码以帮助您精确 GC。这真的很难。IIRC,CLASP正在做类似的事情(在Clang之上,而不是GCC)。
不要忘记垃圾回收是一个全程序的事情。