Projections for Arc and Rc that allow exposing only references that come from T.
Reference-counted pointers that contain projections of data stored in std::sync::Arc
or std::rc::Rc
.
This is a "self-referential" type in the vein of ouroboros
or yoke.
This crate specializes to only supporting Arc
and Rc
and only references to fields
obtainable from them, which allows it to provide a much simpler API compared to general
self-referential crates.
Parc can be useful in situations where we want to expose only a part of data stored
in a reference-counted pointer while still retaining the same shared ownership of that data.
We project a field from our stored data to store in Parc, allowing us to only expose that data
to the receiver.
This crate can be used in no_std
environments, given that alloc
is available.
Pointers from this library can be useful in situations where you're required to share ownership of data (e.g. when sending it between threads), but only want to expose a part of the stored data to a part of the code.
use pared::sync::Parc;
#[derive(Debug)]
struct PublicData;
// No Debug
struct SensitiveData;
struct Data {
public: PublicData,
sensitive: SensitiveData,
}
let data = Parc::new(Data { public: PublicData, sensitive: SensitiveData });
let public_only = data.project(|data| &data.public);
std::thread::spawn(move ||
println!("I can only access public data: {:?}", public_only)
).join().unwrap();
When using Parc<T>
or Prc<T>
, the underlying Arc<U>
or Rc<U>
is type-erased, so you can use instances of these pointers interchangeably:
use pared::sync::Parc;
let use_second = true;
let from_tuple = Parc::new((0u8, 1u8, 2u8)).project(|tuple|
if use_second {
&tuple.1
} else {
&tuple.0
}
);
let from_u8 = Parc::new(4u8);
fn check_equal(number: Parc<u8>, reference: u8) {
assert_eq!(*number, reference);
}
let use_from_tuple = true;
if use_from_tuple {
check_equal(from_tuple, 1);
} else {
check_equal(from_u8, 4);
}
C++'s std::shared_ptr has an aliasing constructor (8) that lets you reuse the existing reference count of that shared pointer, but point to new data. This operation is unsafe, since C++ doesn't have a way to restrict you from using this constructor with a pointer to a local variable.
With Rust, we can expose the same operation with a safe API.
Rust's Arc
doesn't store two discinct
pointers to the reference count and the data, so it doesn't include this functionality
out of the box.
We can implement this "aliasing Arc" as a wrapper type around Arc
(and Rc
).
Since we're not interested in the original data stored in the Arc
, we can type-erase the Arc
into an opaque pointer, and we only need to be able to call member functions that don't depend
on the original T
:
- clone (to increment the counter)
- drop (to decrement the counter)
- downgrade (to get the
Weak
pointer) - strong_count and weak_count
Similarly, for Weak
, we only need
- clone (to increment the weak counter)
- drop (to decrement the weak counter)
- upgrade (to get
Option<Arc>
) - strong_count and weak_count
Our wrapper types sync::Parc
, sync::Weak
, prc::Prc
and prc::Weak
store type-erased versions of their respective underlying shared pointers, which allows us to call these methods
on the underlying Arc
s, Rc
s and Weak
s.
When we construct the type-erased versions of Arc
, Rc
, etc., we store a reference to a const
structure with function pointers to each of those operations. Since we always construct from a concrete Arc<T>
or Rc<T>
, we can store a reference to a generic helper type's const
variable that first converts the type-erased pointer back to Arc<T>
, Rc<T>
, or the correct Weak<T>
, and then calls the appropriate function.
We store the underlying pointer as a pointer we obtain from
Arc::into_raw
or
Rc::into_raw
in a structure that stores this (potentially ?Sized
) pointer in a
MaybeUninit<[*const ();2]>
(credit to Alice Ryhl
from the forum).
This allows us to transparently store this pointer and retreive it back inside the concrete implementation functions.
As an aside, "prc" is the sound of farting in Czech, similar to "toot" in English.
This is around 20% of the motivation behind the naming convention for Parc
and Prc
.
If this kind of projection is too simple (e.g. you'd like to store a subset of a struct
's pointers instead of just one), yoke should do the trick for T: Sized
types.
- Christopher Durham for giving information about prior art and outlining issues with first drafts
- Alice Ryhl for
solving
how to transparently store pointers to
?Sized
types - Frank Stefahn for reviewing the original API ideas
and spotting a difficult-to-spot soundness hole with
Debug
© 2023 Radek Vít [[email protected]].
This project is licensed under either of
at your option.
The SPDX license identifier for this project is MIT OR Apache-2.0
.