用于c代码中正确垃圾回收的静态分析器注释
一般概览
由于Julia的垃圾收集是准确的,因此对于垃圾收集时可能引用的任何值都有正确的根信息是必要的。 这样的地方被称为’safepoints',在函数的本地上下文中,这个概念适用于任何可以递归终止于安全点的函数调用。
在生成的代码中,由于垃圾收集根的放置,这种情况会自动发生(请参阅面向开发人员的LLVM代码生成文档中关于垃圾收集根的章节)。 但是,在C代码中,运行时需要手动报告垃圾回收根。 这是使用以下宏完成的。
// The value assigned to any slot passed as an argument to these // is rooted for the duration of this GC frame. JL_GC_PUSH{1,...,6}(args...) // The values assigned into the size `n` array `rts` are rooted // for the duration of this GC frame. JL_GC_PUSHARGS(rts, n) // Pop a GC frame JL_GC_POP
如果这些宏没有在它们应该在的地方使用,或者应用不正确,则会在没有通知的情况下发生内存损坏。 出于这个原因,在代码的所有相关位置正确添加它们是非常重要的。
为了确保正确使用这些宏,我们使用静态分析(特别是clang静态分析器)。 本文档的其余部分提供了静态分析的概述,并描述了Julia代码库工作所需的支持操作。
静态分析算法
这个想法本身非常简单,尽管实现相当复杂(主要是由于C和C的许多特殊情况和微妙之处++实际上,我们跟踪所有根区域,所有可以是根的值以及所有表达式(分配,内存分配等)。)影响这些值是否为根。 然后,在任何安全点,我们执行符号垃圾收集并毒害所有未根植于该位置的值。 如果随后访问这些值,则返回错误。
Clang静态分析器的工作是构建一个状态图并检查它是否有错误源。 这个图的几个节点是由分析器自己创建的(例如,为了确保执行的顺序),但是上面的定义用我们自己的状态来补充这个图。
静态分析器是进程间的,可以分析函数边界交叉的执行顺序. 但是,静态分析器不是完全递归的,并且对哪些调用应该进行调查做出启发式决策(此外,一些调用需要在程序单元之间进行分析,并且对分析器不可见)。 在我们的情况下,确定正确性需要完整的信息。 在这方面,有必要使用分析所需的所有信息对任何函数调用的原型进行注释,即使这些信息可以作为过程间静态分析的结果获得。
幸运的是,我们可以使用这种过程间分析来确保添加到特定函数的注释是正确的,同时考虑到其实现。
分析器注释
这些注释位于src/support/analyzer_annotations中。h文件。 它们只有在使用分析器时才是活动的,并且它们扩展到无(对于原型注释)或空闲操作(对于像函数这样的注释)。
'JL_NOTSAFEPOINT`
这可能是最常见的注释。 它应该被添加到任何已知无法到达安全垃圾收集点的函数中。 通常,这些函数的安全操作只是算术计算、内存访问和具有注释`JL_NOTSAFEPOINT`或根据其他可用数据不是安全点的函数调用(例如,它可能是c标准库中的函数,在分析器中被硬编码)。
在对使用此属性注释的任何函数的调用中,值可能保持非根。
使用示例:
void jl_get_one() JL_NOTSAFEPOINT {
return 1;
}
jl_value_t *example() {
jl_value_t *val = jl_alloc_whatever();
// This is valid, even though `val` is unrooted, because
// jl_get_one is not a safepoint
jl_get_one();
return val;
}
JL_MAYBE_UNROOTED'/'JL_ROOTS_TEMPORARILY
当’JL_MAYBE_UNROOTED`被注释为函数参数时,这意味着即使它不是根,也可以传递此参数。 在正常的事件过程中,julia ABI确保调用者在将值传递给被调用的对象之前使值根。 但是,某些函数不遵循此ABI规则并允许传入值,即使它们不是根值。 请记住,它不会自动遵循这样的论点将持续存在。 'ROOTS_TEMPORARILY’注释提供了更可靠的保证,即该值不仅可以在传输过程中不再是根,而且还将由被调用方在内部安全点之间保留。
请注意``JL_NOTSAFEPOINT’实际上意味着’JL_MAYBE_UNROOTED`/`JL_ROOTS_TEMPORARILY',因为如果函数不包含安全点,参数是否为root并不重要。
同样重要的是要注意,这些注释在调用方和被调用方都应用。 在调用端,它们删除了对根字符的限制,这通常适用于julia ABI函数。 在被调用方,它们具有相反的效果:这样的参数不被隐含地视为根。
如果这些注释中的任何一个应用于一个函数作为一个整体,它将应用于它的所有参数。 一般来说,这只对参数数量可变的函数是必要的。
使用示例:
JL_DLLEXPORT void JL_NORETURN jl_throw(jl_value_t *e JL_MAYBE_UNROOTED);
jl_value_t *jl_alloc_error();
void example() {
// The return value of the allocation is unrooted. This would normally
// be an error, but is allowed because of the above annotation.
jl_throw(jl_alloc_error());
}
`JL_PROPAGATES_ROOT'
此注释通常在返回一个对象的访问函数中找到,该对象可能是存储在另一个对象中的根。 当应用于函数参数时,此注释告诉分析器此参数的根也应用于函数返回的值。
使用示例:
jl_value_t *jl_svecref(jl_svec_t *t JL_PROPAGATES_ROOT, size_t i) JL_NOTSAFEPOINT;
size_t example(jl_svec_t *svec) {
jl_value_t *val = jl_svecref(svec, 1)
// This is valid, because, as annotated by the PROPAGATES_ROOT annotation,
// jl_svecref propagates the rooted-ness from `svec` to `val`
jl_gc_safepoint();
return jl_unbox_long(val);
}
JL_ROOTING_ARGUMENT'/'JL_ROOTED_ARGUMENT
这本质上是’JL_PROPAGATES_ROOT’的模拟赋值。 将值分配给已为根的另一个值的字段时,分配的值将继承分配给它的值的根。
使用示例:
void jl_svecset(void *t JL_ROOTING_ARGUMENT, size_t i, void *x JL_ROOTED_ARGUMENT) JL_NOTSAFEPOINT
size_t example(jl_svec_t *svec) {
jl_value_t *val = jl_box_long(10000);
jl_svecset(svec, val);
// This is valid, because the annotations imply that the
// jl_svecset propagates the rooted-ness from `svec` to `val`
jl_gc_safepoint();
return jl_unbox_long(val);
}
JL_GC_DISABLED
此注释意味着只有在运行时禁用垃圾回收时才调用此函数。 此类函数最常用于启动期间和垃圾收集器代码本身。 请注意,检查此注释是否符合启用和禁用运行时的调用,因此clang将确定其正确性。 如果实际上没有禁用垃圾收集,这不是禁用某个函数处理的最佳方法(如有必要,使用`ifdef__clang_analyzer__`)。
使用示例:
void jl_do_magic() JL_GC_DISABLED {
// Wildly allocate here with no regard for roots
}
void example() {
int en = jl_gc_enable(0);
jl_do_magic();
jl_gc_enable(en);
}
JL_REQUIRE_ROOTED_SLOT
此注释要求调用方在作为根的槽中传递值(即,分配给此槽的值将是根)。
使用示例:
void jl_do_processing(jl_value_t **slot JL_REQUIRE_ROOTED_SLOT) {
*slot = jl_box_long(1);
// Ok, only, because the slot was annotated as rooting
jl_gc_safepoint();
}
void example() {
jl_value_t *slot = NULL;
JL_GC_PUSH1(&slot;);
jl_do_processing(&slot;);
JL_GC_POP();
}
'JL_GLOBALLY_ROOTED`
此注释意味着此值始终是全局级别的根值。 它可以应用于全局变量的声明(在这种情况下,它适用于这些变量的值或数组的值,如果声明了数组)或函数(在这种情况下,它适用于它们的返回值;例如,这些函数可以是返回全局级别根的私有值的函数)。
使用示例:
extern JL_DLLEXPORT jl_datatype_t *jl_any_type JL_GLOBALLY_ROOTED; jl_ast_context_t *jl_ast_ctx(fl_context_t *fl) JL_GLOBALLY_ROOTED;
JL_ALWAYS_LEAFTYPE
此注释本质上等同于`JL_GLOBALLY_ROOTED',除了只有当值在全局级别为根时才应使用它,因为它们属于最终类型。 将有限类型定义为根类型有其自身的特点。 它们通常通过相应的`TypeName`对象的`cache`字段被制成root,该对象本身通过包含模块被制成root(也就是说,只要对于包含模块来说这是真的,它们就会成为root)。 在一般情况下,我们可以假设有限类型是使用它们的根类型,但将来可能会澄清此属性,因此单独的注释有助于在全局级别证实根字符的原因。
分析器还会自动检测类型的最终性质的检查,并且不会警告沿着这些路径没有垃圾收集根。
JL_DLLEXPORT jl_value_t *jl_apply_array_type(jl_value_t *type, size_t dim) JL_ALWAYS_LEAFTYPE;
JL_GC_PROMISE_ROOTED
这是一个像函数一样的注释。 传递给此注释的任何值都将被视为当前函数范围内的根值。 它适用于分析仪操作不正确或困难的情况。 但是,它不应该被滥用-最好是优化分析器本身。
void example() { jl_value_t *val = jl_alloc_something(); if (some_condition) { // We happen to know for complicated external reasons // that val is rooted under these conditions JL_GC_PROMISE_ROOTED(val); } }