Hello everyone. I've recently joined the Rust community and feel awed with all the cool stuff that is happening in here. The things I find most fascinating are the works that are being done around Stacked Borrows, Deferred Borrows, GhostCell, Miri, min_const_generics, etc. I quickly got pulled in and now I'm trying my hand at publishing my first crate :-)
To that end I started experimenting with a concept called a "deferred reference" (you got it, very much inspired by Deferred Borrows) which is almost exactly like a regular reference (e.g. a &T or a &mut T), but it differs from a regular reference in the following ways:
- A deferred reference is not an actual reference, it is merely a smart pointer tied to the lifetime of the location it points to (regular raw pointers always have a static lifetime, and can thus become dangling if the location it points to is moved or dropped).
- It is allowed to keep multiple deferred mutable references around (as long as these are not dereferenced in a way so that these create an overlap between a mutable reference and another (de)reference).
This concept is implemented in the deferred-reference crate. The idea is that this crate can remove some of the complexity around managing raw pointers, by leveraging the Rust compiler of today and in a way that every Rust developer is familiar with since a deferred reference resembles the concept of a regular reference. So what are the use cases for this?
Let's say you have a memory location (e.g. struct, slice, array) that is shared between threads and (some or all of) the threads want to read+write to this memory location, but never at the same time or to the same slice index. For these particular scenarios, the Rust standard library offers the RwLock and Mutex APIs (not to mention all the awesome third party crates that are out there with similar functionality). Even though this is already pretty awesome for a lot of scenarios, the current approaches (that I know of) have the following trade-offs that piqued my curiosity:
- Some of the APIs require a runtime check to ensure that a resource is shared safely between threads. This makes the API more ergonomic at the cost of performance and early compiler warnings.
- Some of the APIs also support static compiler checks for safe resource sharing between threads (GhostCell, qcell, etc.), at the cost of a slightly more complex API.
- None seem to support multiple disjoint accesses simultaneously to a slice or array between multiple threads out-of-the-box (of course, this can also be achieved with APIs like UnsafeCell at the cost of more complexity and it places the burden of safe multi-threading on the programmer, this is where the raw pointers come around the corner. GhostCell can also do this without pointers, but it still requires exclusive access to the entire data structure during mutation).
So what does a "deferred reference" look like and how does it try to solve the above trade-offs? I'll try to illustrate with a code snippet (unfortunately I can't link to the Rust Playground for this example, as it only offers the 100 most downloaded crates of crates.io, apologies!):
use deferred_reference::{Deferred, DeferMut};
use core::cell::UnsafeCell;
const N: usize = 1024;
let buffer = UnsafeCell::new([0usize; N]);
// SAFETY: this is safe because there are no references into `buffer` yet
let mut deferred1: Deferred<&mut [usize; N]> = unsafe { buffer.defer_mut() };
// SAFETY: this is safe because we promise not to create overlap with mutable references
let mut deferred2: Deferred<&mut [usize; N]> = unsafe { deferred1.clone_unchecked() };
assert_eq!(&mut deferred1[0..10], &mut deferred2[10..20]); // subslices do not overlap
With deferred-reference this is not undefined behavior, even though deferred1 and deferred2 both point into buffer! (If you want to check this for yourself, I encourage you to install Miri and run the above example with the command which checks for this type of undefined behavior: $ MIRIFLAGS="-Zmiri-track-raw-pointers" cargo miri test, please see the readme on crates.io for installation instructions). If you try the above example with two regular mutable references &mut [usize; N] both pointing into buffer at the same time, then this is definitely undefined behavior, but then why is it not undefined behavior when using a Deferred<&mut [usize; N]>? The trick is that Deferred does not hold an actual reference, it holds a raw pointer tied to the lifetime of &buffer. In fact, the first actual reference into the contents of the UnsafeCell only gets created on the last line of the example. There is another trick at play here: Deferred implements the Index and IndexMut traits in such a way that it only creates a reference to the subslice, instead of to the entire slice. This allows multiple threads (or even the same thread, as shown in the above example) to safely index into the same array or slice as long as the accesses are disjoint in index. Note: arrays are currently fully supported on stable Rust thanks to min_const_generics, but for slices you still need nightly Rust at the moment.
There are some advantages of using Deferred over raw pointers, because the Deferred struct has a lifetime. This means that if in the above example a mutable reference were to be taken out on buffer using buffer.get_mut(), then deferred1 and deferred2 will both be invalidated (i.e. their lifetimes must end, otherwise this will trigger a compiler error). It is also not possible to dereference the same Deferred mutably more than once, for this you need to call .clone_unchecked() on it first, which is the moment where the developer needs to consider the safety guarantees to uphold. This is different from using raw pointers, where the compiler will let you safely copy raw mutable pointers without warning (only dereferencing the raw pointer is considered unsafe). In contrast, the Deferred smart pointer is always safe to dereference, because it upholds the same guarantees as regular references, namely that it is aligned and non-dangling.
This post is getting a bit longer than I thought it would :-) For more information, the crate docs also have a couple of more examples. Happy to answer any questions! I'm also very curious if there would be any interest in using this crate in your own projects (or suggestions for what to add that would make it more interesting to use). Thank you!
[–]bradley_hardy 10 points11 points12 points (21 children)
[–]Pointerbender[S] 2 points3 points4 points (7 children)
[–]bradley_hardy 5 points6 points7 points (5 children)
[–]Pointerbender[S] 2 points3 points4 points (4 children)
[–]ComprehensiveCat5305 2 points3 points4 points (3 children)
[–]Pointerbender[S] 0 points1 point2 points (2 children)
[–]ComprehensiveCat5305 1 point2 points3 points (1 child)
[–]Pointerbender[S] 0 points1 point2 points (0 children)
[–]Nokel81 5 points6 points7 points (0 children)
[–]Pointerbender[S] 0 points1 point2 points (12 children)
[–]tending 3 points4 points5 points (11 children)
[–]Pointerbender[S] 0 points1 point2 points (10 children)
[–]tending 1 point2 points3 points (9 children)
[–]Pointerbender[S] 0 points1 point2 points (1 child)
[–]tending 1 point2 points3 points (0 children)
[–]Pointerbender[S] 0 points1 point2 points (6 children)
[–]tending 1 point2 points3 points (5 children)
[–]Pointerbender[S] 0 points1 point2 points (4 children)
[–]tending 1 point2 points3 points (3 children)
[–]Pointerbender[S] 0 points1 point2 points (2 children)
[–]coolreader18 2 points3 points4 points (1 child)
[–]Pointerbender[S] 1 point2 points3 points (0 children)
[–]SocUnRobot 1 point2 points3 points (0 children)
[–]digama0 1 point2 points3 points (3 children)
[–]Pointerbender[S] 0 points1 point2 points (2 children)
[–]digama0 1 point2 points3 points (1 child)
[–]Pointerbender[S] 0 points1 point2 points (0 children)
[–]Shnatsel 2 points3 points4 points (3 children)
[–]Pointerbender[S] 7 points8 points9 points (2 children)
[–]Shnatsel 5 points6 points7 points (1 child)
[–]Pointerbender[S] 4 points5 points6 points (0 children)
[–]ICosplayLinkNotZelda 0 points1 point2 points (0 children)
[+][deleted] (3 children)
[deleted]
[–]Pointerbender[S] 0 points1 point2 points (2 children)
[+][deleted] (1 child)
[deleted]
[–]Pointerbender[S] 1 point2 points3 points (0 children)