c语言 - 如何保护解释器的本机调用堆栈免受垃圾回收?



我正在用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和两个本地指针bc

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))编码,所有fgh也许做对象分配)。

您也可以使用一些现有的精确 GC,例如 Ravenbrook MPS。

否则,请使用一些保守的GC,如Boehm的GC。

还要查看具有一些GC的现有自由软件解释器的源代码。

另请阅读 Queinnec 的Lisp In Small Pieces一书


我必须手动推送和弹出对象才能调用 C 函数。

这可能是一个好主意(但随后您需要重写大部分代码,并且实际上可以定义自己的字节码机制)。看看Lua、Nim、Ocaml字节码解释器或Emacs Elisp解释器在做什么。


为了完成,您可以考虑(这真的很难,我不建议走这条路,因为这需要多年的工作)编写一些 GCC 插件来生成和/或添加临时调用帧元数据和/或生成呼叫帧相关代码以帮助您精确 GC。这真的很难。IIRC,CLASP正在做类似的事情(在Clang之上,而不是GCC)。


不要忘记垃圾回收是一个全程序的事情。

最新更新