我和朋友一起定义一个安全的公共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();
与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生存的时间&#34;。
// 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:
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
,并用于分配的值。
这是alloc签名:
impl<'outer> GcScope<'outer> {
// ...
pub fn alloc<'inner, T: Trace + 'outer>(
&'inner self,
value: T,
) -> Result<Gc<'inner, T>, GcAllocErr> {
// ...
}
// ...
}
另一个想法是不让用户使用GcScope
手动创建GcScope::new
,而是公开提供GcScope::with(executor)
引用的函数gc_scope
。结束executor
对应gc_scope
。到目前为止,它要么阻止使用外部引用,要么允许将数据泄漏到外部Gc
变量(第一和第四属性)。
这是alloc签名:
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上所解释的那样,这是因为我实施Drop
需要&mut self
,但scope
已经借用了唯一的模式。
以下是我的库的主要组件的快速概述。
GcScope
在其可变状态中包含RefCell
。这引入了&mut self
不需要alloc
,因为它已锁定&#34; gc_scope和违反属性3:分配多个值。
这种可变状态是GcState
。它跟踪所有分配的值。这些值存储为GcBox
的仅向前链接列表。这个GcBox
是堆分配的,包含一些元数据的实际值(有多少活动Gc
指针将它作为根,一个布尔标志用于检查是否可以从根访问该值(参见rust-gc)。此处的值必须比其gc_scope
更长,因此GcBox
使用生命周期,然后GcState
必须使用生命周期以及GcScope
:这总是与gc_scope
&#34;相同的生命周期意义。GcScope
具有RefCell
(内部可变性)和生命周期的事实可能就是为什么我不能让我的一生工作(它导致不变性?)。
Gc
是指向某些gc_scope
分配数据的智能指针。您只能通过gc_scope.alloc
或通过克隆来获取它。
GcRefCell
很可能很好,只是RefCell
包装器添加元数据和行为以正确支持借用。
我可以满足以下要求来获得解决方案:
with
方法)。重要的是我可以创建一个临时区域,在那里我可以操作垃圾收集的值,并且在此之后它们都被丢弃了。这些垃圾收集值需要能够访问范围之外的更长寿命(但不是静态)变量。 The repository在scoped-gc/src/lib.rs
(编译失败)中进行了一些测试scoped-gc/src/test.rs
。
我找到了一个解决方案,我会在编辑后将其发布。
答案 0 :(得分:2)
到目前为止,这是我与Rust一生中遇到的最困难的问题之一,但我设法找到了解决方案。感谢panicbit和mbrubeck在IRC帮助过我。
帮助我前进的是对我在问题末尾发布的错误的解释:
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
我不理解这个错误,因为我不清楚为什么scope
被借用,多长时间,或为什么它不再需要在范围的末尾借用。
原因是在分配值期间,scope
在分配值的持续时间内被不可避免地借用。现在的问题是范围包含实现“删除”的状态对象: drop
的自定义实现使用&mut self
- &gt;在价值已经不可避免地借入的情况下,不可能获得可变借款。
了解丢弃需要&mut self
并且它与不可变借用不兼容解锁了这种情况。
事实证明,上述问题中描述的内外方法与alloc
具有正确的生命周期:
impl<'outer> GcScope<'outer> {
// ...
pub fn alloc<'inner, T: Trace + 'outer>(
&'inner self,
value: T,
) -> Result<Gc<'inner, T>, GcAllocErr> {
// ...
}
// ...
}
返回的Gc
与GcScope
一样长,分配的值必须比当前GcScope
长。正如问题所述,此解决方案的问题在于它不支持循环值。
循环值无法工作,不是因为alloc
的生命周期,而是由于自定义drop
。删除drop
允许所有测试通过(但泄漏的内存)。
解释非常有趣:
alloc
的生命周期表示已分配值的属性。分配的值不能超过GcScope
,但其内容的有效期必须长于GcScope
。在创建一个循环时,该值受这两个约束的约束:它被分配,因此必须长于或短于GcScope
,但也由另一个分配的值引用,因此它必须长于{{1 }}。因此,只有一个解决方案:分配的值必须与其范围一样长 。
这意味着GcScope
的生命周期及其分配的值完全相同。 当两个生命周期相同时,Rust不保证滴剂的顺序。发生这种情况的原因是GcScope
实现可能会尝试相互访问,因为没有排序它会不安全(值可能已经被释放)。
Drop Check chapter of the Rustonomicon中解释了这一点。
在我们的例子中,收集的垃圾状态的drop
实现并没有取消引用分配的值(恰恰相反,它释放了它们的内存)所以Rust编译器过于谨慎,因为阻止我们实现{ {1}}。
幸运的是,Nomicon还解释了如何使用相同的生命周期来解决这些值的检查。解决方案是在drop
实现的lifetime参数上使用drop
属性。
这是一个不稳定的属性,需要启用may_dangle
和Drop
功能。
具体而言,我的generic_param_attrs
实施变为:
dropck_eyepatch
我在drop
添加了以下几行:
unsafe impl<'gc> Drop for GcState<'gc> {
fn drop(&mut self) {
// Free all the values allocated in this scope
// Might require changes to make sure there's no use after free
}
}
您可以阅读有关这些功能的更多信息:
如果您想仔细查看它,我已使用此问题的修复程序更新了我的库scoped-gc。