#cell #container #issue #borrow #value #double #safe

no-std flexcell

A flexible cell that allows safe circumvention of double borrow issues

1 unstable release

Uses new Rust 2024

new 0.1.0-alpha.1 Mar 19, 2025

#539 in Rust patterns

Download history 274/week @ 2025-02-19 8/week @ 2025-02-26 2/week @ 2025-03-05

284 downloads per month

MIT/Apache

49KB
461 lines

FlexCell

ci

FlexCell is a data structure designed to replace Rust's RefCell, allowing safe circumvention of double borrow issues. By using FlexCell, you can temporarily relinquish an already borrowed reference, enabling it to be safely reborrowed without causing a double borrow error.

Problem to Solve

RefCell is a powerful tool that provides interior mutability, but it can be inconvenient in certain situations. For example, within a function that takes a mutable reference from a RefMut, you cannot borrow the same RefCell again.

In most cases, this restriction is not an issue because you have already mutably borrowed the RefCell and passed it as an argument, so there is no need to reborrow it. However, if a modification requiring the value of the RefCell occurs at the end of a long chain of nested function calls, you would need to add Option<&mut T> type arguments to the signatures of all functions in the call chain to eventually pass it to the function that requires the RefCell. This interface modification can be quite cumbersome and may require changes to the entire project's code.

FlexCell is designed to solve this problem. With FlexCell, you can temporarily relinquish a borrowed reference and safely reborrow it in a lower call chain. This allows you to flexibly manipulate internal values even in complex cyclic dependency patterns or recursive calls.

Key Features

  • Lending Mechanism: FlexCell provides a mechanism to temporarily relinquish (or lend) an already borrowed value (or an entirely new external value).
  • Closure-Based Interface: Unlike RefCell's guard-based interface, FlexCell uses closures to borrow internal values.
  • Safe Memory Management: FlexCell adheres to Rust's memory safety model, managing the provenance and permissions of pointers internally.

Examples

Borrowing Internal Values (with, with_mut)

Here is an example of putting a simple Vec into a FlexCell and performing read/write operations. Each time the given closure ends, the internal borrow is released, allowing subsequent calls to with or with_mut.

use flexcell::FlexCell;

// Create a FlexCell containing a vector
let cell = FlexCell::new(vec![1, 2, 3]);

// Borrow read-only
cell.with(|value| {
    assert_eq!(*value, vec![1, 2, 3]); // Check initial value
});

// Borrow mutably
cell.with_mut(|value| {
    value.push(4);
});

// Borrow read-only again to check the value
cell.with(|value| {
    assert_eq!(*value, vec![1, 2, 3, 4]); // Check modified value
});

Lending External Values (lend_ref, lend)

You can also temporarily lend an external variable to a FlexCell instead of using the value already inside it. In this case, FlexCell temporarily replaces its own value with a pointer to the external variable.

use flexcell::FlexCell;

let cell = FlexCell::new(String::from("original"));
let mut external = String::from("external");

cell.lend(&mut external, || {
    cell.with_mut(|s| {
        s.push_str(" modified");
    });
});

// After the lend closure ends, the external value is modified
assert_eq!(external, "external modified");

// Meanwhile, the original value is restored
cell.with(|val| {
    assert_eq!(*val, "original");
});

In the code above, the lend operation allows FlexCell to temporarily point to the external variable (external), enabling read/write operations within the closure. After the closure ends, the original value of FlexCell is restored.

Replacing, Taking, and Setting Values (replace, take, set)

When FlexCell has full ownership of the internal value (e.g., created with new or set with set), you can take the value out or replace it.

use flexcell::FlexCell;

let cell = FlexCell::new(vec![10, 20, 30]);

// Take the value out
let fully_owned = cell.take();
assert_eq!(fully_owned, vec![10, 20, 30]); // Check the taken value

// Set a new value
cell.set(vec![99, 100]);
cell.with(|value| {
    assert_eq!(*value, vec![99, 100]); // Check the new value
});

// Replace the value
let old = cell.replace(vec![0]);
assert_eq!(old, vec![99, 100]); // Check the replaced old value
cell.with(|value| {
    assert_eq!(*value, vec![0]); // Check the new value after replacement
});

However, you cannot regain full ownership of the internal value if it is already borrowed with with or with_mut. In such cases, you should use try_take or try_replace methods to handle errors.

Limitations and Precautions

Lending Does Not Always Increase Permissions

Calling lend or lend_ref on FlexCell temporarily replaces the internal reference state, allowing reborrowing with different values (or the same value with different permissions). However, this does not always increase permissions. Requests requiring higher permission levels than the lent reference will be impossible until the closure ends and the state is restored, or until another appropriate lend or set is performed.

For example:

  • While lend is active, you cannot call take or replace which require full ownership of the internal value.
  • While lend_ref is active, you cannot call with_mut.

take and replace Can Fail

FlexCell must fully own the internal value to perform take or replace. If the value is already borrowed (read-only or mutable), attempting take and replace will result in a ConsumeError. You can handle errors using try_take and try_replace methods.

Values set Within Borrowing/Lending Contexts Are Temporary

Values set within closures passed to with, with_mut, lend_ref, or lend are only valid during the execution of the closure. Once the closure ends, the internal state is restored, and the set value is dropped.

Simply put:

  • Values set within a closure are temporarily valid only during the closure's execution.
  • Values set within a closure are dropped and disappear when the closure ends.

Single-Threaded Use

Currently, FlexCell is based on core::cell::Cell, so it does not support Sync across multiple threads.

Design Decisions

Closure-Based Interface

While RefCell provides a guard-based interface, FlexCell offers a closure-based interface. Using guards allows delegating lock release to other procedures, providing more flexibility. However, FlexCell adopts the closure approach to enforce the order of lock and unlock, preventing issues in nested borrow/lend patterns and ensuring a safe API.

Conceptually an Option<Box<T>> with Interior Mutability

The internal value of FlexCell is allocated on the heap using Box<T>, and only the pointer is extracted and used through Box::leak. Considering the potential use cases and the convenience and stability of the design, handling the value as a heap-allocated pointer is deemed more appropriate despite some overhead.

Additionally, the value inside FlexCell can be taken, set, or replaced, similar to Option<T>. This approach aims to provide an API that treats the internal unborrowed Box<T> of FlexCell as a pointer, similar to &T or &mut T.

Lending Instead of Unborrowing

FlexCell does not unborrow values. Instead, it safely lends a new pointer that can be borrowed over the already borrowed reference, which cannot be reborrowed. Therefore, whether the reference provided to the lend_ref or lend method points to the same value originally inside FlexCell or not, it will not cause a runtime error. The value will simply be used in subsequent borrows.

In Rust's memory safety model, the provenance and permissions of pointers must be clearly defined. To prevent undefined behavior, each lend must replace the internal reference with a new pointer with appropriate provenance. This means treating the two pointers as entirely different even if they point to the same memory address.

In most use cases I envision, the pointer lent to FlexCell will point to the same value originally inside it. However, due to the reasons mentioned above, FlexCell does not directly restrict the address of the provided reference: there cannot be any optimization benefit from this information in the design, and comparing the two references would only incur runtime costs.

License

FlexCell is made available under either the MIT or Apache-2.0 license.

No runtime deps