【发布时间】:2018-08-17 09:38:22
【问题描述】:
我正在与一位朋友合作,为“作用域”垃圾收集器的生命周期定义一个安全的公共 API。生命周期要么受到过度约束,正确的代码无法编译,要么生命周期太松,它们可能允许无效行为。在尝试了多种方法之后,我们仍然无法获得正确的 API。这尤其令人沮丧,因为 Rust 的生命周期可以帮助避免这种情况下的错误,但现在它看起来很顽固。
作用域垃圾回收
我正在实现一个 ActionScript 解释器并且需要一个垃圾收集器。我研究了rust-gc,但它不适合我的需要。主要原因是它要求垃圾收集的值具有a static lifetime,因为 GC 状态是线程局部静态变量。我需要将垃圾收集绑定到动态创建的主机对象。避免使用全局变量的另一个原因是我更容易处理多个独立的垃圾收集范围、控制它们的内存限制或序列化它们。
作用域垃圾收集器类似于typed-arena。您可以使用它来分配值,并且一旦丢弃垃圾收集器,它们就会全部释放。不同的是,你还可以在其生命周期内触发垃圾回收,它会清理无法访问的数据(并且不限于单一类型)。
我有a working implementation implemented (mark & sweep GC with scopes),但界面还不能安全使用。
这是我想要的用法示例:
pub struct RefNamedObject<'a> {
pub name: &'a str,
pub other: Option<Gc<'a, GcRefCell<NamedObject<'a>>>>,
}
fn main() {
// Initialize host settings: in our case the host object will be replaced by a string
// In this case it lives for the duration of `main`
let host = String::from("HostConfig");
{
// Create the garbage-collected scope (similar usage to `TypedArena`)
let gc_scope = GcScope::new();
// Allocate a garbage-collected string: returns a smart pointer `Gc` for this data
let a: Gc<String> = gc_scope.alloc(String::from("a")).unwrap();
{
let b = gc_scope.alloc(String::from("b")).unwrap();
}
// Manually trigger garbage collection: will free b's memory
gc_scope.collect_garbage();
// Allocate data and get a Gc pointer, data references `host`
let host_binding: Gc<RefNamed> = gc_scope
.alloc(RefNamedObject {
name: &host,
other: None,
})
.unwrap();
// At the end of this block, gc_scope is dropped with all its
// remaining values (`a` and `host_bindings`)
}
}
生命周期属性
基本直觉是Gc 只能包含与对应的GcScope 一样长(或更长)的数据。 Gc 类似于 Rc 但支持循环。您需要使用Gc<GcRefCell<T>> 来改变值(类似于Rc<RefCell<T>>)。
以下是我的 API 的生命周期必须满足的属性:
Gc 的寿命不能超过它的 GcScope
以下代码必须失败,因为a 比gc_scope 更有效:
let a: Gc<String>;
{
let gc_scope = GcScope::new();
a = gc_scope.alloc(String::from("a")).unwrap();
}
// This must fail: the gc_scope was dropped with all its values
println("{}", *a); // Invalid
Gc 不能包含比 GcScope 更短的数据
以下代码必须失败,因为msg 的寿命不如gc_scope 长(或更长)
let gc_scope = GcScope::new();
let a: Gc<&string>;
{
let msg = String::from("msg");
a = gc.alloc(&msg).unwrap();
}
必须可以分配多个Gc(gc_scope 上不排除)
以下代码必须编译
let gc_scope = GcScope::new();
let a = gc_scope.alloc(String::from("a"));
let b = gc_scope.alloc(String::from("b"));
必须可以分配包含生命周期长于gc_scope 的引用的值
以下代码必须编译
let msg = String::from("msg");
let gc_scope = GcScope::new();
let a: Gc<&str> = gc_scope.alloc(&msg).unwrap();
必须可以创建 Gc 指针的循环(这就是重点)
与Rc<Refcell<T>> 模式类似,您可以使用Gc<GcRefCell<T>> 来改变值并创建循环:
// The lifetimes correspond to my best solution so far, they can change
struct CircularObj<'a> {
pub other: Option<Gc<'a, GcRefCell<CircularObj<'a>>>>,
}
let gc_scope = GcScope::new();
let n1 = gc_scope.alloc(GcRefCell::new(CircularObj { other: None }));
let n2 = gc_scope.alloc(GcRefCell::new(CircularObj {
other: Some(Gc::clone(&n1)),
}));
n1.borrow_mut().other = Some(Gc::clone(&n2));
目前的解决方案
自动生命周期/生命周期标记
此解决方案的灵感来自neon 的句柄。
这允许任何有效的代码编译(并允许我测试我的实现)但是太松并且允许无效代码。 它允许Gc 比创建它的gc_scope 寿命更长。 (违反第一个属性)
这里的想法是我将一个生命周期 'gc 添加到我的所有结构中。这个想法是这个生命周期代表“gc_scope 的生命周期”。
// A smart pointer for `T` valid during `'gc`
pub struct Gc<'gc, T: Trace + 'gc> {
pub ptr: NonNull<GcBox<T>>,
pub phantom: PhantomData<&'gc T>,
pub rooted: Cell<bool>,
}
我称之为自动生命周期,因为这些方法从不将这些结构生命周期与它们接收的引用的生命周期混合。
这是 gc_scope.alloc 的实现:
impl<'gc> GcScope<'gc> {
// ...
pub fn alloc<T: Trace + 'gc>(&self, value: T) -> Result<Gc<'gc, T>, GcAllocErr> {
// ...
}
}
内部/外部生命周期
此实现尝试通过将Gc 与GcScope 的生命周期相关联来解决上一个问题。 它被过度约束并阻止了循环的创建。这违反了最后一个属性。
为了约束Gc 相对于它的GcScope,我引入了两个生命周期:'inner 是GcScope 的生命周期,结果是Gc<'inner, T>。 'outer 表示比 'inner 更长的生命周期,用于分配的值。
这是分配签名:
impl<'outer> GcScope<'outer> {
// ...
pub fn alloc<'inner, T: Trace + 'outer>(
&'inner self,
value: T,
) -> Result<Gc<'inner, T>, GcAllocErr> {
// ...
}
// ...
}
闭包(上下文管理)
在with branch上实现
另一个想法是不要让用户使用GcScope::new 手动创建GcScope,而是公开一个函数GcScope::with(executor),提供对gc_scope 的引用。闭包executor 对应于gc_scope。到目前为止,它要么阻止使用外部引用,要么允许将数据泄漏到外部Gc 变量(第一个和第四个属性)。
这是分配签名:
impl<'gc> GcScope<'gc> {
// ...
pub fn alloc<T: Trace + 'gc>(&self, value: T) -> Result<Gc<'gc, T>, GcAllocErr> {
// ...
}
}
这是一个显示违反第一个属性的用法示例:
let message = GcScope::with(|scope| {
scope
.alloc(NamedObject {
name: String::from("Hello, World!"),
})
.unwrap()
});
println!("{}", message.name);
我想要什么
据我了解,我想要的alloc 签名是:
impl<'gc> GcScope<'gc> {
pub fn alloc<T: Trace + 'gc>(&'gc self, value: T) -> Result<Gc<'gc, T>, GcAllocErr> {
// ...
}
}
一切都比self(gc_scope)活得更长或更久。但这会因最简单的测试而失败:
fn test_gc() {
let scope: GcScope = GcScope::new();
scope.alloc(String::from("Hello, World!")).unwrap();
}
原因
error[E0597]: `scope` does not live long enough
--> src/test.rs:50:3
|
50 | scope.alloc(String::from("Hello, World!")).unwrap();
| ^^^^^ borrowed value does not live long enough
51 | }
| - `scope` dropped here while still borrowed
|
= note: values in a scope are dropped in the opposite order they are created
我不知道这里会发生什么。 Playground link
编辑:正如在 IRC 上向我解释的那样,这是因为我实现了需要 &mut self 的 Drop,但 scope 已经以只读模式借用。
概述
这里是我的库的主要组件的快速概览。
GcScope 包含一个 RefCell 到其可变状态。这被引入不需要&mut self 用于alloc,因为它“锁定”了 gc_scope 并违反了属性 3:分配多个值。
这个可变状态是GcState。它跟踪所有分配的值。这些值存储为GcBox 的只进链表。这个GcBox 是堆分配的并且包含带有一些元数据的实际值(有多少活动的Gc 指针将它作为它们的根和一个用于检查该值是否可以从根访问的布尔标志(参见rust-gc) . 这里的值必须超过它的gc_scope 所以GcBox 使用一个生命周期,然后GcState 必须使用一个生命周期以及GcScope:这总是相同的生命周期,意思是“比gc_scope 更长” .GcScope 具有RefCell(内部可变性)和生命周期这一事实可能是我无法让生命周期工作的原因(它会导致不变性?)。
Gc 是指向某些gc_scope 分配数据的智能指针。您只能通过gc_scope.alloc 或克隆它来获取它。
GcRefCell 很可能没问题,它只是一个 RefCell 包装器,添加了元数据和行为以正确支持借用。
灵活性
我可以满足以下要求以获得解决方案:
- 不安全的代码
- 夜间功能
- API 更改(参见例如我的
with方法)。重要的是我可以创建一个临时区域,我可以在其中操作垃圾收集的值,并且在此之后将它们全部丢弃。这些垃圾收集的值需要能够访问范围之外的寿命更长(但不是静态)的变量。
The repository 在scoped-gc/src/lib.rs(编译失败)中有一些测试为scoped-gc/src/test.rs。
我找到了解决方案,我会在编辑后发布。
【问题讨论】:
标签: garbage-collection rust lifetime