模拟范围垃圾收集的生命周期约束

时间:2018-03-08 22:00:28

标签: garbage-collection rust lifetime

我和朋友一起定义一个安全的公共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一样长(或更长)的数据。 GcRc类似,但支持周期。您需要使用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

的数据

以下代码必须失败,因为msggc_scope的持续时间不长(或更长)

let gc_scope = GcScope::new();
let a: Gc<&string>;
{
    let msg = String::from("msg");
    a = gc.alloc(&msg).unwrap();
}

必须可以分配多个Gcgc_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));

到目前为止的解决方案

自动终身/终身标记

auto-lifetime branch

上实施

此解决方案的灵感来自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> {
        // ...
    }
}

内/外生命期

inner-outer branch

上实施

此实施尝试通过将GcGcScope的生命周期相关联来解决上一个问题。 过度约束并阻止创建周期。这违反了最后一个属性。

要相对于Gc约束GcScope,我会介绍两个生命周期:'innerGcScope的生命周期,结果是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> {
        // ...
    }

    // ...
}

关闭(上下文管理)

with branch

上实施

另一个想法是不让用户使用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> {
        // ...
    }
}

所有事物的生命长度都超过selfgc_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包装器添加元数据和行为以正确支持借用。

灵活性

我可以满足以下要求来获得解决方案:

  • 不安全的代码
  • 夜间特写
  • API更改(请参阅我的with方法)。重要的是我可以创建一个临时区域,在那里我可以操作垃圾收集的值,并且在此之后它们都被丢弃了。这些垃圾收集值需要能够访问范围之外的更长寿命(但不是静态)变量。

The repositoryscoped-gc/src/lib.rs(编译失败)中进行了一些测试scoped-gc/src/test.rs

我找到了一个解决方案,我会在编辑后将其发布。

1 个答案:

答案 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> {
        // ...
    }

    // ...
}

返回的GcGcScope一样长,分配的值必须比当前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_dangleDrop功能。

具体而言,我的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