Crate placid

Crate placid 

Source
Expand description

§placid - Separated ownership and in-place construction

placid extends Rust’s ownership model with owned and uninit references with pinning variants (semantically &own T, &uninit T, and &pin own T or Pin<&own T>). It also provides macros, traits, and functions for constructing and manipulating these types, achieving safe in-place construction.

§The problem

In Rust, the ownership of a value is typically bound to the value itself. In other words, there is no way to separate the ownership of a value from it.

This creates challenges in scenarios where:

  • You need to construct large values in-place to build address-aware objects ergonomically;
  • You want to transfer ownership of large objects without expensive memcpys;

Traditional Rust forces you to construct values on the stack, then move them, which can be inefficient for large types (though it is able to be optimized by the compiler, but it is not guaranteed). placid solves this by introducing owned and uninit references that carry full ownership through a reference.

§The owning reference: &own T

An owned reference &own T is a reference that carries exclusive ownership of the value it points to. Unlike &T or &mut T, an &own T reference is responsible for dropping the value when the reference is dropped. Unlike direct objects of type T, owned references can be moved around without invalidating the place it is stored in.

Constructing an owned reference directly on the current call stack:

use placid::prelude::*;

let simple = own!(42);
assert_eq!(*simple, 42);

#[derive(Init)]
struct LargeData {
    buffer: [u8; 1024],
}

fn process_owned(data: Own<LargeData>) {
    // The function owns the data and will drop it when done.
    println!("Processing: {} bytes", std::mem::size_of_val(&*data));
} // data is dropped here.

// In a real scenario, you'd construct this in-place rather than moving.
let owned = own!(init!(LargeData { buffer: init::repeat(42) }));
process_owned(owned);

Moving out an owned reference from a regular smart pointer:

use placid::prelude::*;

let boxed = Box::new(String::from("move me"));

let mut left; // Box<MaybeUninit<String>>
// Move out an owned reference from the Box. The original
// allocation is transferred to `left`, mutably borrowed
// by `own`.
let own = into_own!(left <- boxed);
assert_eq!(*own, "move me");

// Drops the String, but not the Box allocation.
drop(own);

// Now we can reuse the Box allocation.
let right = left.write(String::from("new value"));
assert_eq!(&*right, "new value");

§The pinning variant: &pin own T

A pinned owned reference combines the benefits of owned references with pinning guarantees. This is essential for types that must not be moved across places (such as self-referential structs or types with !Unpin). The pinned owned reference ensures both exclusive ownership and address stability.

use placid::prelude::*;
use std::{pin::Pin, marker::PhantomPinned};

#[derive(InitPin)]
struct SelfRef {
    data: i32,
    data_ptr: *const i32,
    marker: PhantomPinned,
}

fn process_pinned(data: POwn<SelfRef>) {
    // The function owns the data and guarantees it won't move
    // This is safe because the data is pinned in place.
    println!("Processing pinned data");
} // data is dropped here, pinning guarantees are maintained.

let pinned = pown!(
    // Provide initial value for the struct.
    init::value(SelfRef {
        data: 42,
        data_ptr: std::ptr::null(),
        marker: PhantomPinned,
    })
    .and_pin(|this| unsafe {
        // SAFETY: We are initializing the self-referential pointer.
        let this = Pin::into_inner_unchecked(this);
        this.data_ptr = &this.data as *const i32;
    })
);
process_pinned(pinned);

§The drop guarantee

In Rust, a pinned value must remain valid on its place until it is dropped. In other words, it must be properly dropped before its place is deallocated or reused. This was done naturally in its type system by binding the ownership of pinned values with its place, such as the Box::pin() method, or preventing them from getting mem::forgeted, such as the pin! macro.

However, once the ownership is separated from the place, such as with &pin own T, it becomes possible to accidentally forget a pinned value before the place gets deallocated, thus violating the drop guarantee.

unsafe { // Not only unsafe, but actually unsound here.
    let b = Box::pin(/* Some !Unpin data */);
    let value: &pin own T = &pin own *b;
    mem::forget(value);

    // Oops! The pinned value was never
    // dropped even after `b` is deallocated!
}

To prevent this, placid enforces that the pinned value inside POwn<T> is always dropped even if the POwn<T> itself is forgotten. This is done by attaching a drop slot to the reference upon creation to track the drop state of the pinned value. When the POwn<T> is dropped, the drop slot is marked as dropped. If the POwn<T> is forgotten, the drop slot destructor will drop the pinned value safely.

That said, the drop slot itself cannot be forgotten either. Therefore, manual creation of it is unsafe, and only safe method is via the drop_slot! macro and its wrappers.

§In-place construction & &uninit T

Uninit references represent writable references to uninitialized memory. They are primarily used for constructing values in-place, allowing for efficient initialization without unnecessary copies or moves. It combines with initializers to facilitate safe and ergonomic in-place construction.

An initializer resembles with a function typed fn(&uninit T) -> Result<&own T, E> (or fn(&uninit T) -> Result<&pin own T, E> for the pinning variant). It converts an uninit reference into an owned reference by initializing the value in-place.

Initializers can be composed together to build complex initialization logic. See the Init and InitPin traits for more details.

Initializers can also adopt a structural strategy by deriving the macro@Init and macro@InitPin macros, which can then be constructed by init! and init_pin! macros respectively. This allows for declarative and ergonomic construction of complex types in-place.

use placid::prelude::*;
use std::{marker::PhantomPinned, pin::Pin, convert::Infallible};

#[derive(InitPin)]
struct A {
    b: i32,
    c: String,
    marker: PhantomPinned,
}

#[derive(InitPin)]
struct Nested {
    #[pin]
    a: A,
    d: [u8; 1024],
    mid: *mut u8,
}

impl Nested {
    pub fn new(c: &str) -> impl InitPin<Self, Error = Infallible> {
        init_pin!(Nested {
            // Nested initializers are supported.
            #[pin]
            a: A {
                // Values and closures are all initializers.
                b: 100,
                c: || c.to_string(),
                marker: init::value(PhantomPinned),
            },
            // There are also a bunch of direct initializer factories.
            d: init::repeat(42),
            mid: std::ptr::null_mut(),
        })
        // `and{_pin}`, `or`, `or_else` are initializer combinators.
        .and_pin(|this| unsafe {
            // SAFETY: We are initializing the self-referential pointer.
            let this = Pin::into_inner_unchecked(this);
            this.mid = this.d.as_mut_ptr().add(512);
            *this.mid = 1;
        })
    }

    pub fn assert_values(&self, c: &str) {
        assert_eq!(self.a.b, 100);
        assert_eq!(&*self.a.c, c);
        assert_eq!(self.d[149], 42);
        assert_eq!(unsafe { *self.mid }, 1);
    }
}

// Initialize on stack.
let uninit = uninit!(Nested);
let slot = placid::drop_slot!();
let pinned = uninit.write_pin(Nested::new("stack"), slot);
pinned.assert_values("stack");

// Initialize in a Box.
let mut place = Box::<Nested>::new_uninit();
let uninit = Uninit::from_mut(&mut place);
let slot = placid::drop_slot!();
let pinned = uninit.write_pin(Nested::new("boxed"), slot);
pinned.assert_values("boxed");

// Convenience methods for placing initialization
// into containers.
let pinned = Box::new_uninit().init_pin(Nested::new("boxed via method"));
pinned.assert_values("boxed via method");

Modules§

init
Traits and types for initializing places.
owned
Owned references and related utilities.
pin
Types and macros for managing pinned owned references with proper drop semantics.
place
Traits and types for working with places in memory.
prelude
A prelude for commonly used items in placid.
uninit
Types and traits for working with uninitialized memory places.

Macros§

drop_slot
Creates a new drop slot for pinned value initialization.
into_own
Creates an owned reference from a container, extracting the contained value.
into_pown
Creates an pinned owned reference from a container, extracting the contained value.
own
Creates a new place initialized with the given expression.
pown
Creates a new pinned place initialized with the given expression.
uninit
Creates a new uninitialized place on the stack.