1 unstable release
Uses new Rust 2024
new 0.1.0-alpha.1 | Mar 19, 2025 |
---|
#539 in Rust patterns
284 downloads per month
49KB
461 lines
FlexCell
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 calltake
orreplace
which require full ownership of the internal value. - While
lend_ref
is active, you cannot callwith_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.