Skip to content

A cell library that decouples data from read-only and exclusive permissions to access the data

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

khonsulabs/modalcell

ModalCell

crate version Live Build Status Documentation for main branch

This crate is considered experimental. It contains unsafe code that the author is fairly confident in, but the crate author is seeking feedback on its approach to safety.

ModalCell provides an approach for using Rust's borrow checker to enforce access to a value via an associated mode. This crate decouples permissions from data, but unlike many existing GhostCell-like crates, this crate splits permissions into two modes: exclusive access and shared.

Consider this example:

use modalcell::{ExclusiveCell, ExclusiveMode, RefMut, SharedCell, SharedMode};

// Create our shared mode.
let mut shared = SharedMode::new();
// Obtain exclusvie access to the mode.
let exclusive: ExclusiveMode<'_, _> = shared.as_exclusive();
// Create a new cell with the initial value of `1`. This cell can be
// accessed `mut`-ably using an `ExclusiveMode`.
let mut exclusive_cell: ExclusiveCell<usize, _> = exclusive.new_cell(1);
// Obtain a SharedCell, which can be converted to a `&T` using a
// `SharedMode`.
let shared_cell: SharedCell<usize, _> = exclusive_cell.as_shared();

// The Rust compiler now guarantees safe access to the data.
// shared_cell.get(&shared); // Because `exclusive` borrowed from
//                           // `shared`, this is a compilation error.

let cell_contents: &usize = shared_cell.get(&shared);
assert_eq!(*cell_contents, 1);

// To change the value, we must obtain exclusive access again.
let exclusive: ExclusiveMode<'_, _> = shared.as_exclusive();

// `&T` references prevent entering exclusive mode. This can be tested by
// uncommenting the following line:
// assert_eq!(*cell_contents, 1);

// Each RefMut tracks the lifetime of the &mut ExclusiveCell<T> as well as
// the lifetime of the `ExclusiveMode`. This ensures that no `&mut T` can be
// created without exclusive access to `shared`.
let mut cell_contents: RefMut<'_, usize, _> = exclusive_cell.get_mut(exclusive);
*cell_contents = 2;

assert_eq!(*shared_cell.get(&shared), 2);

// Accessing `shared_cell` can only be done safely if `cell_contents` isn't
// used again. Uncommenting this line will produce a compiler error.
// *cell_contents = 3;

The approach taken by this crate gives similar properties to an Rc<RefCell<T>>, except that with an Rc<RefCell<T>> the contents must be accessed through std::cell::Ref/std::cell::RefMut. This crate allows direct access to shared references without an intermediate type. With this type, the Rust compiler enforces correctness, while RefCell requires runtime checks.

This crate still performs one check at runtime: is the mode being passed the same one that was used to create the cell. Because this is a logic bug, passing an incorrect mode will result in a panic.

This crate also provides an implementation that allows using this mode of data access in multi-threaded code:

use std::sync::mpsc::{Receiver, SyncSender};

use modalcell::threadsafe::{ExclusiveCell, SharedMode};

fn main() {
    let mut shared = SharedMode::new_threadsafe();
    let cell = shared.new_cell(0);
    let shared_cell = cell.as_shared();

    let (counting_sender, counting_receiver) = std::sync::mpsc::sync_channel(1);
    let (printing_sender, printing_receiver) = std::sync::mpsc::sync_channel(1);

    // Spawn a thread that updates the value.
    std::thread::spawn(|| counting_thread(cell, counting_receiver, printing_sender));

    loop {
        // Send the mode to the thread, allowing it to gain mutable access.
        counting_sender.send(shared).unwrap();
        // Wait for mode to be returned to us.
        shared = printing_receiver.recv().unwrap();
        // Use it to gain access a reference of the value.
        let value: &usize = shared_cell.get(&shared);
        println!("New Count: {value}");
        // Stop after 10.
        if *value == 10 {
            break;
        }
    }
}

fn counting_thread(
    mut cell: ExclusiveCell<usize>,
    receiver: Receiver<SharedMode>,
    sender: SyncSender<SharedMode>,
) {
    while let Ok(mut shared) = receiver.recv() {
        // Enter exclusive mode. This is a borrow-checker only operation and has
        // no runtime overhead.
        let exclusive = shared.as_exclusive();
        // Gain access to the cell's contents using our exclusive marker.
        let mut contents = cell.get_mut(exclusive);
        // Update the value through DerefMut.
        *contents += 1;
        // Return the `SharedMode` back to the other thread.
        sender.send(shared).unwrap();

        // Attempting to use `exclusive` now will result in a compiler error.
        // let _error = cell.get_mut(exclusive);
        // let _error = *contents;
    }
}

What is the use case for this?

Consider an application where data access is split into two phases: creation/update and read-only. During the creation/update phase, read-only references (SharedCell<T>) to the underlying data (ExclusiveCell<T>) can be placed in a structure that is passed onto the read-only phase of the application. When the underlying data needs to be updated, control can be passed back to the creation/update phase.

This approach would normally require Rc<RefCell<T>> or Arc<Mutex<T>> and incur runtime overhead to ensure that no safety constraints are violated. This crate offers an approach that removes nearly all runtime checks by proving the code is safe with the Rust compiler.

About

A cell library that decouples data from read-only and exclusive permissions to access the data

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Languages