Crates.io | grit-data-prison |
lib.rs | grit-data-prison |
version | 0.4.0 |
source | src |
created_at | 2023-04-08 02:01:14.238196 |
updated_at | 2023-05-05 21:50:34.571303 |
description | A crate providing the struct Prison |
homepage | |
repository | https://github.com/gabe-lee/grit-data-prison.git |
max_upload_size | |
id | 833389 |
size | 297,002 |
This crate provides the struct Prison.visit()
methods
that take closures that are passed mutable references to the values, or by using the .guard()
methods to
obtain a guarded mutable reference to the value.
This documentation describes the usage of Prison
grit-data-prison on Crates.io
grit-data-prison on Lib.rs
grit-data-prison on Github
grit-data-prison on Docs.rs
This package is still UNSTABLE and may go through several iterations before I consider it good enough to set in stone, see changelog
insert_at()
and overwrite()
, see changelog for detailsI wanted a data structure that met these criteria:
This crate is on crates.io
First, add this crate as a dependency to your project:
[dependencies]
grit-data-prison = "0.3"
Then import [AccessError] and [CellKey] from the crate root, along with the relevant version you wish to use in the file where it is needed (right now only one flavor is available, [single_threaded]):
use grit_data_prison::{AccessError, CellKey, single_threaded::Prison};
Create a Prisoninsert()
type methods
Note the following quirks:
mut
to mutate itinsert()
and its variants return a [Result]<[CellKey], [AccessError]> that you need to handlelet prison: Prison<String> = Prison::new();
let key_hello = prison.insert(String::from("Hello, "))?;
prison.insert(String::from("World!"))?;
From here there are 2 main ways to access the values contained in the Prison
You can use one of the .visit()
methods to access a mutable reference
to your data from within a closure, either mutably or immutably
prison.visit_mut_idx(1, |val_at_idx_1| {
*val_at_idx_1 = String::from("Rust!!");
Ok(())
});
The rules for mutable or immutable references are the same as Rust's rules for normal variable referencing:
Visiting multiple values at the same time can be done by nesting .visit()
calls,
or by using one of the batch .visit()
methods
prison.visit_ref(key_hello, |val_0| {
prison.visit_ref_idx(1, |val_1| {
println!("{}{}", *val_0, *val_1); // Prints "Hello, Rust!!"
Ok(())
});
Ok(())
});
prison.visit_many_ref_idx(&[0, 1], |vals| {
println!("{}{}", vals[0], vals[1]); // Also prints "Hello, Rust!!"
Ok(())
});
use grit_data_prison::{AccessError, CellKey, single_threaded::Prison};
fn main() -> Result<(), AccessError> {
let prison: Prison<String> = Prison::new();
let key_hello = prison.insert(String::from("Hello, "))?;
prison.insert(String::from("World!"))?;
prison.visit_mut_idx(1, |val_at_idx_1| {
*val_at_idx_1 = String::from("Rust!!");
Ok(())
});
prison.visit_ref(key_hello, |val_0| {
prison.visit_ref_idx(1, |val_1| {
println!("{}{}", *val_0, *val_1); // Prints "Hello, Rust!!"
Ok(())
});
Ok(())
});
prison.visit_many_ref_idx(&[0, 1], |vals| {
println!("{}{}", vals[0], vals[1]); // Also prints "Hello, Rust!!"
Ok(())
});
Ok(())
}
You can also use one of the .guard()
methods to obtain a guarded wrapper around your data,
keeping the value marked as referenced as long as the wrapper remains in scope.
First you need to import one of PrisonValueMut, PrisonValueRef, PrisonSliceMut, or PrisonSliceRef from the same module as Prison
use grit_data_prison::{AccessError, CellKey, single_threaded::{Prison, PrisonValueRef}};
Then obtain a guarded wrapper by using the corresponding .guard()
method
let prison: Prison<String> = Prison::new();
let key_hello = prison.insert(String::from("Hello, "))?;
prison.insert(String::from("World!"))?;
let grd_hello = prison.guard_ref(key_hello)?;
As long as the referencing rules aren't violated, you can guard (or visit) that value, even when other values from the same
prison are being visited or guarded. The guarded wrappers (for example PrisonValueRef)
keep the element(s) marked with the appropriate form of referencing until they go out of scope.
This can be done by wrapping the area it is used in a code block, or by manually passing it to the
associated ::unguard()
function on the wrapper type to immediately drop it out of scope and update the
reference count.
The guarded wrapper types all implement [Deref], [AsRef], and [Borrow], while the mutable versions also implement [DerefMut], [AsMut], and [BorrowMut] to provide transparent access to their inner values
{
let grd_hello = prison.guard_ref(key_hello)?;
let grd_world = prison.guard_ref_idx(1)?;
println!("{}{}", *grd_hello, *grd_world); // Prints "Hello, World!"
}
// block ends, both guards go out of scope and their reference counts return to what they were before
let mut grd_world_to_rust = prison.guard_mut_idx(1)?;
*grd_world_to_rust = String::from("Rust!!");
PrisonValueMut::unguard(grd_world_to_rust); // index one is no longer marked mutably referenced
let grd_both = prison.guard_many_ref_idx(&[0, 1])?;
println!("{}{}", grd_both[0], grd_both[1]); // Prints "Hello, Rust!!"
use grit_data_prison::{AccessError, CellKey, single_threaded::{Prison, PrisonValueRef, PrisonValueMut, PrisonSliceRef}};
fn main() -> Result<(), AccessError> {
let prison: Prison<String> = Prison::new();
let key_hello = prison.insert(String::from("Hello, "))?;
prison.insert(String::from("World!"))?;
{
let grd_hello = prison.guard_ref(key_hello)?;
let grd_world = prison.guard_ref_idx(1)?;
println!("{}{}", *grd_hello, *grd_world); // Prints "Hello, World!"
}
// block ends, both guards go out of scope and their reference counts return to what they were before
let mut grd_world_to_rust = prison.guard_mut_idx(1)?;
*grd_world_to_rust = String::from("Rust!!");
PrisonValueMut::unguard(grd_world_to_rust); // index one is no longer marked mutably referenced
let grd_both = prison.guard_many_ref_idx(&[0, 1])?;
println!("{}{}", grd_both[0], grd_both[1]); // Prints "Hello, Rust!!"
Ok(())
}
Operations that affect the underlying [Vec] can also be done
from within .visit()
closures or while values are guard()
-ed as long as none of the following rules are violated:
let prison: Prison<u64> = Prison::with_capacity(5);
prison.insert(0)?;
prison.insert(10)?;
prison.insert(20)?;
prison.insert(30)?;
prison.insert(42)?;
let mut accidental_val: u64 = 0;
let mut grd_0 = prison.guard_mut_idx(0)?;
prison.visit_ref_idx(3, |val| {
accidental_val = prison.remove_idx(4)?;
prison.insert(40)?;
Ok(())
});
*grd_0 = 80;
PrisonValueMut::unguard(grd_0);
// No values are actively referenced here so we can perform
// an action that would cause re-allocation safely
for i in 0..100u64 {
prison.insert(i + 100)?;
}
Also provided is a quick shortcut to clone values out of the Prison
let prison: Prison<String> = Prison::new();
let key_0 = prison.insert(String::from("Foo"))?;
prison.insert(String::from("Bar"))?;
let cloned_foo = prison.clone_val(key_0)?;
let cloned_bar = prison.clone_val_idx(1)?;
For more examples, see the specific documentation for the relevant types/methods
Also included is the struct JailCell
JailCell includes the same basic interface as Prison and also employs reference counting, but with a much simpler set of safety checks
use grit_data_prison::{AccessError, CellKey, single_threaded::{JailCell, JailValueRef}};
fn main() -> Result<(), AccessError> {
let string_jail: JailCell<String> = JailCell::new(String::from("'Bad-Guy' Bert"));
string_jail.visit_mut(|criminal| {
let bigger_bad = String::from("Dr. Lego-Step");
println!("Breaking News: {} to be set free to make room for {}", *criminal, bigger_bad);
*criminal = bigger_bad;
Ok(())
})?;
let guarded_criminal = string_jail.guard_ref()?;
println!("{} will now be paraded around town for public shaming", *guarded_criminal);
assert_eq!(*guarded_criminal, String::from("Dr. Lego-Step"));
JailValueRef::unguard(guarded_criminal);
Ok(())
}
See the documentation on JailCell for more info
For the visit()
methodology, closures provide a safe sandbox to access mutable references, as they cant be moved out of the closure,
and because the visit()
functions that take the closures handle all of the
safety and housekeeping needed before and after.
Since closures use generics the rust compiler can inline them in many/most/all? cases.
The guard()
methodology requires the values not be able to leak, alias, or never reset their reference counts,
so they are wrapped in structs that provide limited access to the references and know how to
automatically reset the reference counter for the value when they go out of scope
The short answer is: it should be mostly safe. I welcome any feedback and analysis showing otherwise so I can fix it or revise my methodology.
Prison follows a few simple rules:
In addition, it provides the functionality of a Generational Arena with these additional rules:
insert()
operations return a [CellKey] that pairs the element index with the current largest generation valueIt achieves all of the above with a few lightweight sentinel values:
access_count
[usize] on Prison itself to track whether any reference is in activeCell
or Free
variant:
Free
elements act as nodes in a doubly linked list that tracks free indexes
Cell
ref_count
[usize] that tracks both mutable and immutable referencesgeneration
[usize] to use when matching to the [CellKey] used to access the indexT
(see performance for more info on the actual specifics)
Attempting to perform an action that would violate any of these rules will either be prevented from compiling or return an [AccessError] that describes why it was an error, and should never panic.
let prison: Prison<String> = Prison::new();
prison.insert(String::from("cannot be stolen"));
let mut steal_mut_ref: &mut String;
let mut steal_prison: Prison<String>;
prison.visit_mut_idx(0, |mut_ref| {
// will not compile: (error[E0521]: borrowed data escapes outside of closure)
steal_mut_ref = mut_ref;
// will not compile: (error[E0505]: cannot move out of `prison` because it is borrowed)
steal_prison = prison;
Ok(())
});
struct MyStruct(u32);
fn main() -> Result<(), AccessError> {
let prison: Prison<MyStruct> = Prison::with_capacity(2); // Note this prison can only hold 2 elements
let key_0 = prison.insert(MyStruct(1))?;
prison.insert(MyStruct(2))?;
let grd_0 = prison.guard_mut(key_0)?;
assert!(prison.guard_mut(key_0).is_err());
assert!(prison.guard_ref_idx(0).is_err());
PrisonValueMut::unguard(grd_0);
prison.visit_mut(key_0, |val_0| {
assert!(prison.visit_mut(key_0, |val_0_again| Ok(())).is_err());
assert!(prison.visit_ref(key_0, |val_0_again| Ok(())).is_err());
assert!(prison.visit_mut_idx(0, |val_0_again| Ok(())).is_err());
assert!(prison.visit_ref_idx(3, |val_3_out_of_bounds| Ok(())).is_err());
assert!(prison.guard_mut(key_0).is_err());
assert!(prison.guard_ref(key_0).is_err());
assert!(prison.guard_ref_idx(3).is_err());
prison.visit_ref_idx(1, |val_1| {
assert!(prison.remove_idx(1).is_err()); // would delete memory referenced by val_1
assert!(prison.remove(key_0).is_err()); // would delete memory referenced by val_0
assert!(prison.insert(MyStruct(3)).is_err()); // would cause reallocation and invalidate any current references
Ok(())
});
Ok(())
});
Ok(())
}
no_std
: This crate can be used with the no_std
feature to use only imports from the core
library instead of the std
library
Major Malfunctions:
this crate can be passed one of three (optional) features that define how the library handles behavior that is DEFINITELY un-intended and should be considered a bug in the library itself. It defaults to major_malf_is_err
if none are specified:
major_malf_is_err
: major malfunctions will be returned as an [AccessError::MAJOR_MALFUNCTION(msg)], this is the default even if not specifiedmajor_malf_is_panic
: major malfunctions will result in a call to panic(msg)
describing the unexpected behaviormajor_malf_is_undefined
: branches where a major malfunction would nomally be are replaced with [unreachable_unchecked()], possibly allowing them to be removed from compilation entirely(Benchmarks are Coming Soon™)
Prison
Although the abstract of each PrisonCell<T>
is as described as found in How is This Safe?!,
the truth of the matter it that Rust was not optimising the memory footprint where it could have done so using Enums, so I had to roll my own
type of enum:
Cell
or Free
variant, with the variant tracked in the top bit of one of its fields:
refs_or_next
holds a [usize] that holds either the reference count in Cell
variant or the next free in Free
variantd_gen_or_prev
holds a [usize] that holds either the generation count in Cell
variant or the prev free in Free
variant
d_gen_or_prev
is reserved for marking the variant of the PrisonCell
(the d
is for discriminant
). This means the ACTUAL maximum generation count is isize::MAX, but the prev index is unafected because a [Vec] cannot have more than isize::MAX elements anyway...val
is a [MaybeUninit<T>
] that is always assumed uninitialized when the element is in Free
state, and always assumed initialized when it is in Cell
state.Therefore the total additional size compared to a [VecT
This crate is very much UNSTABLE, meaning that not every error condition may be tested, methods may return different errors/values as my understanding of how they should be properly implemented evolves, I may add/remove methods altogether, etc.
Possible future additions may include:
Guard
api for a more Rust-idiomatic way to access valuesAtomicPrison<T>
AtomicJailCell<T>
UnPrison<T>
AtomicUnPrison<T>
This crate is on crates.io The repo is on github
Feel free to leave feedback, or fork/branch the project and submit fixes/optimisations!
If you can give me concrete examples that definitely violate memory-safety, meaning that the provided references can be made to point to invalid/illegal memory or violate aliasing rules (without the use of additional unsafe :P), leak memory, or otherwise cause unsafe conditions (for example changing an expected enum variant to another where the compiler doesnt expect it to be possible), I'd love to fix, further restrict, or rethink the crate entirely.
The best way to do this would be to follow these steps:
bug/something
or issue/something
branch off of the dev
branchdev
branch with a message explaining everythingpeek_ref()
and peek_ref_idx()
to return [Result<T, AccessError>] instead of [Optionpeek_ref()
to JailCell
Result
from the beginning to match the existing API and allow easy error propogation inside functions that expect AccessError
s without a bunch of boilerplate testing for Some
/None
just to return a AccessError::ValueDeleted
anywaypeek_ref()
and peek_ref_idx()
, UNSAFE methods that allow the caller to get a reference to a value while bypassing reference counting and other safety checksescort()
methods renamed to guard()
methodsvisit()
and guard()
methods split into _ref()
and _mut()
variantsmajor_malf_is_err
, major_malf_is_panic
, major_malf_is_undefined
that allow conditional compilation choices for behavior that is certainly a bug in the libraryinsert_at()
and overwrite()
on indexes that werent the 'top' free in the stack, all other free indexes above them in the stack would be forgotten and never re-used. However, they should be freed when the entire Prison is freed. Sorry!clone_val()
methods to shortcut cloning a value when T implements [Clone]PrisonValue
and PrisonSlice
to reduce their memory footprintescort()
api function (why didnt I think of this earlier?)