An alternative to `qcell` and `ghost-cell` that instead uses const generics

Overview

Purpose

This crate is another attempt at the ghost-cell / qcell saga of cell crates. This provides an alternative to std::cell::RefCell that can allow interior mutability checked at compile time, rather than runtime. Because Rust doesn't allow for unlimited creation of invariant generics, this always comes with a rather large complexity cost. Whereas ghost-cell uses invariant lifetimes and qcell can use either invariant lifetimes or newtypes, this crate instead uses const generic usizes.

Pros

As with other *cell crates, this model provides interior mutability checked at compile time. Unlike ghost-cell's model, this crate doesn't require all of your borrows to exist in a closure and, unlike qcell::TCell, this crate allows for more than three simultaneous borrows.

Cons

First, any item that contains a Cell or Token must be generic over const ID: usize. You may choose to get rid of this if you are sure that, for example, a certain instance of a struct will always have ID = 3

Second, in order to provide a safe API, Tokens must be built using a TokenBuilder struct that ensures IDs are unique. There may in the future be a way around this, but don't hold your breath!

Third, the use of explicit usize discriminants makes passing a Cell or Token to outside crates inherently unsafe. It is recommended to instead send raw values, e.g., with Cell::into_inner().

Example

use frankencell::*;
let (token1, next) = first().unwrap().token();
let (token2, _) = next.token();

let a = Cell::new('a');
let b = Cell::new('b');

println!("{}", a.borrow(&token1));
println!("{}", b.borrow(&token2));

// The following fails to compile:
println!("{}", a.borrow(&token2));
println!("{}", b.borrow(&token1));

Future improvements

Currently because of how const works, it is impossible for a const fn to return different values on different calls. In order to generate unique IDs however, the following would have to be possible:

const fn inc() -> usize {
    // Insert magic here
}

#[test]
fn test_inc() {
    assert_eq!(inc(), 0);
    assert_eq!(inc(), 1);
    assert_eq!(inc(), 2);

    // user-facing API is now significantly better
    let token_3: Token<3> = Token::next();
    let token_4: Token<4> = Token::next();
}

This may become possible when/if heap allocations are allowed in const contexts, but even then this pattern will likely never be officially endorsed by the Rust compiler.

It may also be possible with macros when/if macros are allowed to keep a local state (rust-lang/rust issue 44034).

Should I use this?

Probably not. At the moment this is really more of a proof-of-concept. There's still a lot of work that needs to go into the compiler and, even then, this may not be a viable solution.

If you're simply looking for something that's more ergonomic than ghost-cell and qcell, the cell-family crate seems to have a good approach. cell-family` crate seems to have a good approach.

Comments
  • Bounds on `Sync for Cell<T, ID>` are insufficient

    Bounds on `Sync for Cell` are insufficient

    Since Cell::borrow_mut() allows a &Cell<T, ID> to be converted into a &mut T (i.e., Cell<T, ID> is an interior mutability type), Cell<T, ID> should be Sync only if T: Send + Sync. However, the current implementation only requires T: Sync. To illustrate the issue:

    // frankencell = "=0.1.0"
    // cargo +nightly miri run
    
    #![feature(exclusive_wrapper)]
    
    use frankencell::Cell;
    use std::{cell::Cell as StdCell, sync::Exclusive, thread};
    
    fn main() {
        let mut token = frankencell::first().unwrap().token().0;
        let s = StdCell::new("Hello, world!");
        let cell = Cell::new(Exclusive::new(&s));
        thread::scope(|scope| {
            scope.spawn(|| {
                cell.borrow_mut(&mut token).get_mut().set("Hello, Rust!");
            });
            println!("{}", s.get());
        });
    }
    
    error: Undefined Behavior: Data race detected between (1) Read on thread `main` and (2) Write on thread `<unnamed>` at alloc1637. (2) just happened here
       --> /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cell.rs:413:31
        |
    413 |         mem::replace(unsafe { &mut *self.value.get() }, val)
        |                               ^^^^^^^^^^^^^^^^^^^^^^ Data race detected between (1) Read on thread `main` and (2) Write on thread `<unnamed>` at alloc1637. (2) just happened here
        |
    help: and (1) occurred earlier here
       --> src/main.rs:17:24
        |
    17  |         println!("{}", s.get());
        |                        ^^^^^^^
        = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
        = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
        = note: BACKTRACE (of the first span):
        = note: inside `std::cell::Cell::<&str>::replace` at /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cell.rs:413:31: 413:53
        = note: inside `std::cell::Cell::<&str>::set` at /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cell.rs:363:19: 363:36
    note: inside closure
       --> src/main.rs:15:13
        |
    15  |             cell.borrow_mut(&mut token).get_mut().set("Hello, Rust!");
        |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
    note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
    
    error: aborting due to previous error
    
    opened by LegionMammal978 1
  • v0.2.0

    v0.2.0

    • Fixed Cargo.toml to publish
    • Fixed warnings
    • Fixed repository thing
    • Add non-pub PhantomData to TokenBuilder and TokenWith
    • Add Send requirement on impl Sync for Cell
    • Fixed Debug impl, removed Display impl
    • Make the first() function thread-safe
    • Added init_tokens, fixed tests
    • Updated README
    • Update to v0.2.0
    opened by spencerwhite 0
  • TokenBuilder can be constructed

    TokenBuilder can be constructed

    builder::TokenBuilder has no private fields and can be constructed by anyone outside the API. Will add a PhantomData marker to prevent this. Also do this with TokenWith to prevent having to use getters and setters.

    opened by spencerwhite 0
  • `Debug` and `Display` implementations unsoundly call `Cell::get()`

    `Debug` and `Display` implementations unsoundly call `Cell::get()`

    This allows an immutable reference to alias with a mutable reference returned from Cell::borrow_mut().

    // frankencell = "=0.1.0"
    // cargo +nightly miri run
    
    use frankencell::Cell;
    
    fn main() {
        let mut token = frankencell::first().unwrap().token().0;
        let cell = Cell::new("Hello, world!");
        let s = cell.borrow_mut(&mut token);
        println!("{cell}");
        println!("{s}");
    }
    
    error: Undefined Behavior: trying to retag from <3165> for SharedReadOnly permission at alloc1645[0x0], but that tag does not exist in the borrow stack for this location
        --> /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:2406:1
         |
    2406 | fmt_refs! { Debug, Display, Octal, Binary, LowerHex, UpperHex, LowerExp, UpperExp }
         | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
         | |
         | trying to retag from <3165> for SharedReadOnly permission at alloc1645[0x0], but that tag does not exist in the borrow stack for this location
         | this error occurs as part of retag at alloc1645[0x0..0x10]
         |
         = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
         = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
    help: <3165> was created by a Unique retag at offsets [0x0..0x10]
        --> src/main.rs:9:13
         |
    9    |     let s = cell.borrow_mut(&mut token);
         |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    help: <3165> was later invalidated at offsets [0x0..0x10] by a SharedReadOnly retag
        --> src/main.rs:10:5
         |
    10   |     println!("{cell}");
         |     ^^^^^^^^^^^^^^^^^^
         = note: BACKTRACE (of the first span):
         = note: inside `<&mut &str as std::fmt::Display>::fmt` at /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:2400:71: 2400:78
         = note: inside `std::fmt::write` at /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:1232:17: 1232:59
         = note: inside `<std::io::StdoutLock<'_> as std::io::Write>::write_fmt` at /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/mod.rs:1682:15: 1682:43
         = note: inside `<&std::io::Stdout as std::io::Write>::write_fmt` at /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:715:9: 715:36
         = note: inside `<std::io::Stdout as std::io::Write>::write_fmt` at /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:689:9: 689:33
         = note: inside `std::io::stdio::print_to::<std::io::Stdout>` at /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:1007:21: 1007:47
         = note: inside `std::io::_print` at /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/stdio.rs:1074:5: 1074:37
    note: inside `main`
        --> src/main.rs:11:5
         |
    11   |     println!("{s}");
         |     ^^^^^^^^^^^^^^^
         = note: this error originates in the macro `fmt_refs` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
    
    note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
    
    error: aborting due to previous error
    
    opened by LegionMammal978 0
  • `frankencell::first()` results in a data race when called from multiple threads

    `frankencell::first()` results in a data race when called from multiple threads

    To illustrate, this program raises an error under Miri:

    // frankencell = "=0.1.0"
    // cargo +nightly miri run
    
    use std::thread;
    
    fn main() {
        thread::spawn(|| {
            frankencell::first();
        });
        frankencell::first();
    }
    
    error: Undefined Behavior: Data race detected between (1) Write on thread `main` and (2) Write on thread `<unnamed>` at alloc2958. (2) just happened here
        --> /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs:1621:23
         |
    1621 |     pub const fn take(&mut self) -> Option<T> {
         |                       ^^^^^^^^^ Data race detected between (1) Write on thread `main` and (2) Write on thread `<unnamed>` at alloc2958. (2) just happened here
         |
    help: and (1) occurred earlier here
        --> src/main.rs:10:5
         |
    10   |     frankencell::first();
         |     ^^^^^^^^^^^^^^^^^^^^
         = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
         = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
         = note: BACKTRACE (of the first span):
         = note: inside `std::option::Option::<frankencell::TokenBuilder<0>>::take` at /home/lm978/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs:1621:23: 1621:32
         = note: inside `frankencell::first` at /home/lm978/.cargo/registry/src/github.com-1ecc6299db9ec823/frankencell-0.1.0/src/lib.rs:93:14: 93:26
    note: inside closure
        --> src/main.rs:8:9
         |
    8    |         frankencell::first();
         |         ^^^^^^^^^^^^^^^^^^^^
    
    note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
    
    error: aborting due to previous error
    

    For this pattern, I'd suggest using a Once:

    use std::sync::Once;
    
    static FIRST: Once = Once::new();
    
    pub fn first() -> Option<TokenBuilder<0>> {
        let mut builder = None;
        FIRST.call_once(|| {
            // SAFETY: `Once` guarantees that this will only be called at most once.
            builder = Some(unsafe { TokenBuilder::new() });
        });
        builder
    }
    
    opened by LegionMammal978 0
Owner
SpencerBeige
SpencerBeige
Like a cell, but make lifetimes dynamic instead of ownership

LendingCell is a mutable container that allows you to get an owned reference to the same object. When the owned reference is dropped, ownership return

Kalle Samuels 19 Dec 15, 2022
A discord bot for detecting ghost pings

Anti Ghost Ping Status This is not the production bot code nor a working bot, just a rewrite in rust. How to Run Requirements: Postgres db Fill out .e

Anti Ghost Ping 3 Aug 9, 2022
Statically allocated, runtime initialized cell.

static-cell Statically allocated, initialized at runtime cell. StaticCell provides a no-std-compatible, no-alloc way to reserve memory at compile time

null 4 Oct 2, 2022
A general-purpose, transactional, relational database that uses Datalog and focuses on graph data and algorithms

cozo A general-purpose, transactional, relational database that uses Datalog for query and focuses on graph data and algorithms. Features Relational d

null 1.9k Jan 9, 2023
Uses the cardano mini-protocols to receive every block and transaction, and save them to a configurable destination

cardano-slurp Connects to one or more cardano-node's, streams all available transactions, and saves them to disk (or to S3) in raw cbor format. Usage

Pi Lanningham 16 Jan 31, 2023
This is choose, a human-friendly and fast alternative to cut and (sometimes) awk

Choose This is choose, a human-friendly and fast alternative to cut and (sometimes) awk Features terse field selection syntax similar to Python's list

Ryan Geary 1.4k Jan 7, 2023
Conference Monitoring Project based on Image Recognition that uses Rust Language and AWS Rekognition service to get the level of image similarity.

Conference Monitoring System based on Image Recognition in Rust This is a Conference Monitoring Project based on Image Recognition that uses Rust Lang

Pankaj Chaudhary 6 Dec 18, 2022
`ls` alternative with useful info and a splash of color 🎨

?? Natls ?? Why Natls? Showing file permissions Showing file size Showing the date that the file was modified last Showing the user that the file belo

Will 1.2k Dec 19, 2022
fd is a program to find entries in your filesystem. It is a simple, fast and user-friendly alternative to find

fd is a program to find entries in your filesystem. It is a simple, fast and user-friendly alternative to find. While it does not aim to support all of find's powerful functionality, it provides sensible (opinionated) defaults for a majority of use cases.

David Peter 25.9k Jan 9, 2023
Count your code by tokens, types of syntax tree nodes, and patterns in the syntax tree. A tokei/scc/cloc alternative.

tcount (pronounced "tee-count") Count your code by tokens, types of syntax tree nodes, and patterns in the syntax tree. Quick Start Simply run tcount

Adam P. Regasz-Rethy 48 Dec 7, 2022
Faster and better alternative to Vtop written in Rust.

Rtop Faster and better alternative to Vtop written in Rust. Work in Progress Features Lightweight < 1MB Responsive UI Sort Process by Memory, CPU Usag

Squitch 7 Nov 18, 2022
Lightweight alternative Discord client with a smaller footprint and some fancy extensible features.

Dorion Dorion is an alternative Discord client aimed and lower-spec or storage-sensitive PCs that supports themes, plugins, and more! Table of Content

SpikeHD 20 Jan 2, 2023
Like grep, but uses tree-sitter grammars to search

tree-grepper Works like grep, but uses tree-sitter to search for structure instead of strings. Installing This isn't available packaged anywhere. That

Brian Hicks 219 Dec 25, 2022
📺(tv) Tidy Viewer is a cross-platform CLI csv pretty printer that uses column styling to maximize viewer enjoyment.

??(tv) Tidy Viewer is a cross-platform CLI csv pretty printer that uses column styling to maximize viewer enjoyment.

Alex Hallam 1.8k Jan 2, 2023
hexyl is a simple hex viewer for the terminal. It uses a colored output to distinguish different categories of bytes

hexyl is a simple hex viewer for the terminal. It uses a colored output to distinguish different categories of bytes (NULL bytes, printable ASCII characters, ASCII whitespace characters, other ASCII characters and non-ASCII).

David Peter 7.3k Dec 29, 2022
A terminal clock that uses 7-segment display characters

Seven-segment clock (7clock) 7clock.3.mp4 This is a clock for terminals that uses the Unicode seven-segment display characters added in Unicode 13.0.

Wesley Moore 4 Nov 11, 2022
Intuitive find & replace CLI (sed alternative)

sd - s[earch] & d[isplace] sd is an intuitive find & replace CLI. The Pitch Why use it over any existing tools? Painless regular expressions sd uses r

Gregory 4k Jan 4, 2023
Alternative to *fetch, uwuifies all stats.

owofetch-rs Alternative to *fetch, uwuifies all stats. Installation: Arch: AUR Other Linux distros: Either compile the source with cargo build --relea

nett_hier 6 Dec 26, 2022
fcp is a significantly faster alternative to the classic Unix cp(1) command

A significantly faster alternative to the classic Unix cp(1) command, copying large files and directories in a fraction of the time.

Kevin Svetlitski 532 Jan 3, 2023