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

Dem*_*gos 6 garbage-collection lifetime rust

我正在与一位朋友合作,为“范围内”垃圾收集器的生命周期定义一个安全的公共 API。生命周期要么过度受限,正确的代码无法编译,要么生命周期太宽松,可能允许无效行为。在尝试了多种方法之后,我们仍然无法找到正确的 API。这尤其令人沮丧,因为 Rust 的生命周期可以帮助避免这种情况下的错误,但现在它看起来很顽固。

范围垃圾收集

我正在实现一个 ActionScript 解释器并且需要一个垃圾收集器。我研究了rust-gc但它不适合我的需要。主要原因是它要求垃圾收集的值具有静态生存期,因为 GC 状态是线程局部静态变量。我需要获取到动态创建的主机对象的垃圾收集绑定。避免全局变量的另一个原因是,我更容易处理多个独立的垃圾收集范围、控制它们的内存限制或序列化它们。

作用域垃圾收集器类似于typed-arena。您可以使用它来分配值,一旦垃圾收集器被删除,它们就会全部被释放。不同之处在于,您还可以在其生命周期内触发垃圾收集,它将清理无法访问的数据(并且不限于单一类型)。

我已经实现了一个工作实现(使用范围标记和扫描GC),但该接口尚不能安全使用。

这是我想要的用法示例:

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`)
    }
}
Run Code Online (Sandbox Code Playgroud)

终生特性

基本直觉是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
Run Code Online (Sandbox Code Playgroud)

Gc不能包含寿命短于其寿命的数据GcScope

下面的代码一定会失败,因为msg它的寿命不会像gc_scope

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

必须可以分配多个Gc(不排除gc_scope

以下代码必须编译

let gc_scope = GcScope::new();

let a = gc_scope.alloc(String::from("a"));
let b = gc_scope.alloc(String::from("b"));
Run Code Online (Sandbox Code Playgroud)

必须可以分配包含生命周期长于的引用的值gc_scope

以下代码必须编译

let msg = String::from("msg");
let gc_scope = GcScope::new();
let a: Gc<&str> = gc_scope.alloc(&msg).unwrap();
Run Code Online (Sandbox Code Playgroud)

必须可以创建 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));
Run Code Online (Sandbox Code Playgroud)

到目前为止的解决方案

自动寿命/寿命标签

在分支上实施auto-lifetime

该解决方案的灵感来自于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>,
}
Run Code Online (Sandbox Code Playgroud)

我将其称为自动生命周期,因为这些方法永远不会将这些结构生命周期与它们接收的引用的生命周期混合在一起。

这是 gc_scope.alloc 的实现:

impl<'gc> GcScope<'gc> {
    // ...
    pub fn alloc<T: Trace + 'gc>(&self, value: T) -> Result<Gc<'gc, T>, GcAllocErr> {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

内部/外部生命周期

在分支上实施inner-outer

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

    // ...
}
Run Code Online (Sandbox Code Playgroud)

闭包(上下文管理)

在分支上实施with

另一个想法是不让用户GcScope手动创建GcScope::new,而是公开一个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> {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个显示违反第一个属性的用法示例:

let message = GcScope::with(|scope| {
    scope
        .alloc(NamedObject {
            name: String::from("Hello, World!"),
        })
        .unwrap()
});
println!("{}", message.name);
Run Code Online (Sandbox Code Playgroud)

我想要什么

据我了解,alloc我想要的签名是:

impl<'gc> GcScope<'gc> {
    pub fn alloc<T: Trace + 'gc>(&'gc self, value: T) -> Result<Gc<'gc, T>, GcAllocErr> {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

self一切事物的寿命都与( )一样长或更长gc_scope。但这会在最简单的测试中失败:

fn test_gc() {
    let scope: GcScope = GcScope::new();
    scope.alloc(String::from("Hello, World!")).unwrap();
}
Run Code Online (Sandbox Code Playgroud)

原因

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`)
    }
}
Run Code Online (Sandbox Code Playgroud)

我不知道这里发生了什么。游乐场链接

编辑:正如 IRC 上向我解释的那样,这是因为我实现了Droprequire &mut self,但scope已经以只读模式借用了。

概述

这是我的库的主要组件的快速概述。 GcScope包含 aRefCell到其可变状态。&mut self这被引入为不需要,alloc因为它“锁定”了 gc_scope 并违反了属性 3:分配多个值。这种可变状态是GcState。它跟踪所有分配的值。这些值存储为 的只进链接列表GcBox。这GcBox是堆分配的,包含带有一些元数据的实际值(有多少活动Gc指针将其作为根,以及用于检查该值是否可从根访问的布尔标志(请参阅rust-gc)。此处的值必须超过生存期它gc_scope因此GcBox使用生命周期,然后GcState必须使用生命周期以及GcScope:这始终是相同的生命周期,意味着“长于”。具有(内部可变性)和生命周期gc_scope的事实可能是我不能的原因让我的一生都发挥作用(这会导致不变性?)。GcScopeRefCell

Gc是指向某些分配的数据的智能指针gc_scope。您只能通过gc_scope.alloc或克隆它来获取它。 GcRefCell很可能没问题,它只是一个RefCell添加元数据和行为以正确支持借用的包装器。

灵活性

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

  • 不安全代码
  • 夜间功能
  • API 更改(例如,参见我的with方法)。重要的是我可以创建一个临时区域,在其中可以操作垃圾收集的值,并且在此之后它们都会被删除。这些垃圾收集的值需要能够访问作用域之外的寿命较长(但不是静态)的变量。

存储库scoped-gc/src/lib.rs在(compile-fail) as中有一些测试scoped-gc/src/test.rs

我找到了解决方案,编辑后我会发布它。

Dem*_*gos 4

这是迄今为止我在 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
Run Code Online (Sandbox Code Playgroud)

我不理解这个错误,因为我不清楚为什么scope要借用,借用多长时间,或者为什么在范围结束时不再需要借用。

原因是在分配值期间,scope在分配值的持续时间内, 是不可改变地借用的。现在的问题是,作用域包含一个实现“Drop”的状态对象:use的自定义实现drop&mut self-> 当值已经被不可变地借用时,不可能获得 drop 的可变借用。

理解 drop 的要求&mut self以及它与不可变借用不相容可以解决这个问题。

事实证明,上述问题中描述的内外方法具有正确的生命周期alloc

impl<'outer> GcScope<'outer> {
    // ...

    pub fn alloc<'inner, T: Trace + 'outer>(
        &'inner self,
        value: T,
    ) -> Result<Gc<'inner, T>, GcAllocErr> {
        // ...
    }

    // ...
}
Run Code Online (Sandbox Code Playgroud)

返回的值的Gc生存时间必须长于GcScope当前的值,并且分配的值的生存时间必须长于当前的值GcScope。正如问题中提到的,该解决方案的问题在于它不支持循环值。

循环值无法工作不是因为 的生命周期,alloc而是因为习惯drop。删除drop允许所有测试通过(但泄漏内存)。

这个解释很有趣:

的生命周期alloc表示分配值的属性。分配的值不能比它们的寿命长GcScope,但它们的内容的寿命必须等于或长于GcScope。创建循环时,该值受到这两个约束:它被分配,因此它的生存期必须与 一样长或比 短,GcScope但也被另一个分配的值引用,因此它的生存期必须与 一样长或更长GcScope。因此,只有一种解决方案:分配的值必须与其作用域一样长。

这意味着 的生命周期GcScope和它分配的值是完全相同的。当两个生命周期相同时,Rust 不保证 drop 的顺序。发生这种情况的原因是,drop实现可能会尝试相互访问,并且由于没有顺序,所以它是不安全的(该值可能已经被释放)。

Rustonomicon 的 Drop Check 章节对此进行了解释。

在我们的例子中,drop垃圾收集状态的实现不会取消引用分配的值(恰恰相反,它会释放它们的内存),因此 Rust 编译器过于谨慎,阻止我们实现drop.

幸运的是,Nomicon 还解释了如何解决这些具有相同生命周期的值检查。may_dangle解决方案是在实现的生命周期参数上使用该属性Drop。这是不稳定的属性,需要启用generic_param_attrsdropck_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
    }
}
Run Code Online (Sandbox Code Playgroud)

我添加了以下几行lib.rs

#![feature(generic_param_attrs)]
#![feature(dropck_eyepatch)]
Run Code Online (Sandbox Code Playgroud)

您可以阅读有关这些功能的更多信息:

如果您想仔细看看,我更新了我的库scoped-gc,修复了这个问题。