Crate dumpster

source ·
Expand description

A cycle-tracking concurrent garbage collector with an easy-to-use API.

Most garbage collectors are tracing garbage collectors, meaning that they keep track of a set of roots which are directly accessible from the stack, and then use those roots to find the set of all accessible allocations. However, because Rust does not allow us to hook into when a value is moved, it’s quite difficult to detect when a garbage-collected value stops being a root.

dumpster takes a different approach. It begins by using simple reference counting, then automatically detects cycles. Allocations are freed when their reference count reaches zero or when they are only accessible via their descendants.

Garbage-collected pointers can be created and destroyed in O(1) amortized time, but destroying a garbage-collected pointer may take O(r), where r is the number of existing garbage-collected references, on occasion. However, the cleanups that require O(r) performance are performed once every O(1/r) times a reference is dropped, yielding an amortized O(1) runtime.

Why should you use this crate?

In short, dumpster offers a great mix of usability, performance, and flexibility.

  • dumpster’s API is a drop-in replacement for std’s reference-counted shared allocations (Rc and Arc).
  • It’s very performant and has builtin implementations of both thread-local and concurrent garbage collection.
  • There are no restrictions on the reference structure within a garbage-collected allocation (references may point in any way you like).
  • It’s trivial to make a custom type collectable using the provided derive macros.
  • You can even store ?Sized data in a garbage-collected pointer!

Module structure

dumpster contains 3 core modules: the root (this module), as well as sync and unsync. sync contains an implementation of thread-safe garbage-collected pointers, while unsync contains an implementation of thread-local garbage-collected pointers which cannot be shared across threads. Thread-safety requires some synchronization overhead, so for a single-threaded application, it is recommended to use unsync.

The project root contains common definitions across both sync and unsync. Types which implement Collectable can immediately be used in unsync, but in order to use sync’s garbage collector, the types must also implement Sync.

Examples

If your code is meant to run as a single thread, or if your data doesn’t need to be shared across threads, you should use unsync::Gc to store your allocations.

use dumpster::unsync::Gc;
use std::cell::Cell;

let my_gc = Gc::new(Cell::new(0451));

let other_gc = my_gc.clone(); // shallow copy
other_gc.set(512);

assert_eq!(my_gc.get(), 512);

For data which is shared across threads, you can use sync::Gc with the exact same API.

use dumpster::sync::Gc;
use std::sync::Mutex;

let my_shared_gc = Gc::new(Mutex::new(25));
let other_shared_gc = my_shared_gc.clone();

std::thread::scope(|s| {
    s.spawn(move || {
        *other_shared_gc.lock().unwrap() = 35;
    });
});

println!("{}", *my_shared_gc.lock().unwrap());

It’s trivial to use custom data structures with the provided derive macro.

use dumpster::{unsync::Gc, Collectable};
use std::cell::RefCell;

#[derive(Collectable)]
struct Foo {
    refs: RefCell<Vec<Gc<Foo>>>,
}

let foo = Gc::new(Foo {
    refs: RefCell::new(Vec::new()),
});

foo.refs.borrow_mut().push(foo.clone());

drop(foo);

// even though foo had a self reference, it still got collected!

Installation

To use dumpster, add the following lines to your Cargo.toml.

[dependencies]
dumpster = "0.1.2"

Optional features

dumpster has two optional features: derive and coerce-unsized.

derive is enabled by default. It enables the derive macro for Collectable, which makes it easy for users to implement their own collectable types.

use dumpster::{unsync::Gc, Collectable};
use std::cell::RefCell;

#[derive(Collectable)] // no manual implementation required
struct Foo(RefCell<Option<Gc<Foo>>>);

let my_foo = Gc::new(Foo(RefCell::new(None)));
*my_foo.0.borrow_mut() = Some(my_foo.clone());

drop(my_foo); // my_foo will be automatically cleaned up

coerce-unsized is disabled by default. This enables the implementation of std::ops::CoerceUnsized for each garbage collector, making it possible to use Gc with !Sized types conveniently. To use coerce-unsized, edit your installation to Cargo.toml to include the feature.

[dependencies]
dumpster = { version = "0.1.2", features = ["coerce-unsized"]}

License

dumpster is licensed under the GNU GPLv3 any later version of the GPL at your choice. For more details, refer to LICENSE.md.

Modules

  • Thread-safe shared garbage collection.
  • Thread-local garbage collection.

Traits

  • The trait that any garbage-collectable data must implement.
  • A visitor for a garbage collected value.

Derive Macros

  • The derive macro for implementing Collectable.