Skip to content

Commit

Permalink
Add missing docs and config for 0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
radekvit committed Jul 16, 2023
1 parent cd52599 commit de53907
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 3 deletions.
28 changes: 25 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
[package]
name = "pared"
version = "0.1.0"
authors = ["Radek Vít <radekvitr@gmail.com>"]
edition = "2021"
rust-version = "1.56"
description = "Projected reference counted pointers"
repository = "https://github.com/radekvit/pared"
license = "MIT OR Apache-2.0"
keywords = [
"arc",
"rc",
"projected",
"aliasing",
"shared_ptr",
]
categories = [
"data-structures",
"memory-management",
"no-std",
"rust-patterns",
]

[features]
# default_featurs = ["std"]
default = ["std"]
std = []

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

[dev-dependencies]
doc-comment = "0.3.3"
doc-comment = "0.3.3"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg=docsrs"]
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ in a reference-counted pointer while still retaining the same shared ownership o
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.

## Usage
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
Expand Down Expand Up @@ -69,6 +71,64 @@ if use_from_tuple {
}
```

## Background
C++'s [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) has an
[aliasing constructor (8)](https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr)
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`](https://doc.rust-lang.org/std/sync/struct.Arc.html) 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`](https://doc.rust-lang.org/std/sync/struct.Arc.html#method.into_raw)
or
[`Rc::into_raw`](https://doc.rust-lang.org/std/rc/struct.Rc.html#method.into_raw)
in a structure that stores this (potentially `?Sized`) pointer in a
`MaybeUninit<[*const ();2]>` (credit to [Alice Ryhl](https://users.rust-lang.org/u/alice)
from the [forum](https://users.rust-lang.org)).
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`.

## Alternatives
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](https://lib.rs/yoke) should do the trick for `T: Sized` types.

## Acknowledgments
- [Christopher Durham](https://users.rust-lang.org/u/cad97/summary) for
[giving information](https://users.rust-lang.org/t/could-arc-have-arc-aliased-that-would-behave-like-c-shared-ptrs-aliasing-constructor/96924/5)
about prior art and outlining issues with first drafts
- [Alice Ryhl](https://users.rust-lang.org/u/alice) for
[solving](https://users.rust-lang.org/t/type-erasing-pointers-to-t-sized/96984/3)
how to transparently store pointers to `?Sized` types
- [Frank Stefahn](https://users.rust-lang.org/u/steffahn) for reviewing the original API ideas
and spotting a difficult-to-spot soundness hole with `Debug`

## License

&copy; 2023 Radek Vít [radekvitr@gmail.com].
Expand Down
2 changes: 2 additions & 0 deletions src/erased_ptr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ impl core::fmt::Debug for TypeErasedPtr {
impl TypeErasedPtr {
/// Type-erase a possibly-unsized pointer,
/// only preserving the bit-representation of its pointer.
#[inline]
pub(crate) fn new<T: ?Sized>(ptr: *const T) -> Self {
let mut res = Self(MaybeUninit::zeroed());

Expand All @@ -47,6 +48,7 @@ impl TypeErasedPtr {
///
/// # Safety
/// This can only be called with `Self` that has been created from the exact same `T`.
#[inline]
pub(crate) unsafe fn as_ptr<T: ?Sized>(self) -> *const T {
core::mem::transmute_copy(&self.0)
}
Expand Down
50 changes: 50 additions & 0 deletions src/prc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,27 @@ impl<T: ?Sized> Prc<T> {
}
}

/// Provides a raw pointer to the data.
///
/// The counts are not affected in any way and the `Prc` is not consumed. The pointer is valid for
/// as long as there are strong counts in the `Prc` or in the underlying `Rc`.
///
/// # Examples
///
/// ```
/// use pared::prc::Prc;
///
/// let x = Prc::new("hello".to_owned());
/// let y = Prc::clone(&x);
/// let x_ptr = Prc::as_ptr(&x);
/// assert_eq!(x_ptr, Prc::as_ptr(&y));
/// assert_eq!(unsafe { &*x_ptr }, "hello");
/// ```
#[must_use]
pub fn as_ptr(this: &Self) -> *const T {
NonNull::as_ptr(this.projected)
}

/// Creates a new `Weak` pointer to this allocation.
///
/// This `Weak` pointer is tied to strong references to the original `Rc`, meaning it's not
Expand Down Expand Up @@ -397,6 +418,35 @@ pub struct Weak<T: ?Sized> {
}

impl<T: ?Sized> Weak<T> {
/// Returns a raw pointer to the object `T` pointed to by this `Weak<T>`.
///
/// The pointer is valid only if there are some strong references.
///
/// # Examples
///
/// ```
/// use pared::prc::Prc;
/// use std::ptr;
///
/// let strong = Prc::new("hello".to_owned());
/// let weak = Prc::downgrade(&strong);
/// // Both point to the same object
/// assert!(ptr::eq(&*strong, weak.as_ptr()));
/// // The strong here keeps it alive, so we can still access the object.
/// assert_eq!("hello", unsafe { &*weak.as_ptr() });
///
/// drop(strong);
/// // But not any more. We can do weak.as_ptr(), but accessing the pointer would lead to
/// // undefined behaviour.
/// // assert_eq!("hello", unsafe { &*weak.as_ptr() });
/// ```
///
/// [`null`]: core::ptr::null "ptr::null"
#[must_use]
pub fn as_ptr(&self) -> *const T {
NonNull::as_ptr(self.projected)
}

/// Attempts to upgrade the `Weak` pointer to a [`Prc`], delaying dropping of the inner value
/// if successful.
///
Expand Down
11 changes: 11 additions & 0 deletions src/prc/erased_rc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub struct TypeErasedRc {
}

impl TypeErasedRc {
#[inline]
pub(crate) fn new<T: ?Sized>(arc: Rc<T>) -> Self {
Self {
ptr: TypeErasedPtr::new(Rc::into_raw(arc)),
Expand All @@ -23,6 +24,7 @@ impl TypeErasedRc {
}
}

#[inline]
pub(crate) fn downgrade(&self) -> TypeErasedWeak {
TypeErasedWeak {
// SAFETY: downgrade is guaranteed to return an erased pointer to Weak<T>
Expand All @@ -32,12 +34,14 @@ impl TypeErasedRc {
}
}

#[inline]
pub(crate) fn strong_count(&self) -> usize {
// SAFETY: once set in TypeErasedRc::new, self.lifecycle is never modified,
// which guarantees that self.lifecycle and self.ptr match
unsafe { (self.lifecycle.strong_count)(self.ptr) }
}

#[inline]
pub(crate) fn weak_count(&self) -> usize {
// SAFETY: once set in TypeErasedRc::new, self.lifecycle is never modified,
// which guarantees that self.lifecycle and self.ptr match
Expand All @@ -46,6 +50,7 @@ impl TypeErasedRc {
}

impl Clone for TypeErasedRc {
#[inline]
fn clone(&self) -> Self {
// SAFETY: once set in TypeErasedRc::new, self.lifecycle is never modified,
// which guarantees that self.lifecycle and self.ptr match
Expand All @@ -55,6 +60,7 @@ impl Clone for TypeErasedRc {
}

impl Drop for TypeErasedRc {
#[inline]
fn drop(&mut self) {
// SAFETY: once set in TypeErasedRc::new, self.lifecycle is never modified,
// which guarantees that self.lifecycle and self.ptr match
Expand All @@ -69,6 +75,7 @@ pub(crate) struct TypeErasedWeak {
}

impl TypeErasedWeak {
#[inline]
pub(crate) fn upgrade(&self) -> Option<TypeErasedRc> {
Some(TypeErasedRc {
// SAFETY: upgrade_weak is guaranteed to return an erased pointer to Rc<T>
Expand All @@ -78,12 +85,14 @@ impl TypeErasedWeak {
})
}

#[inline]
pub(crate) fn strong_count(&self) -> usize {
// SAFETY: once set in TypeErasedWeak::new, self.lifecycle is never modified,
// which guarantees that self.lifecycle and self.ptr match
unsafe { (self.lifecycle.strong_count_weak)(self.ptr) }
}

#[inline]
pub(crate) fn weak_count(&self) -> usize {
// SAFETY: once set in TypeErasedWeak::new, self.lifecycle is never modified,
// which guarantees that self.lifecycle and self.ptr match
Expand All @@ -92,6 +101,7 @@ impl TypeErasedWeak {
}

impl Clone for TypeErasedWeak {
#[inline]
fn clone(&self) -> Self {
// SAFETY: once set in TypeErasedWeak::new, self.lifecycle is never modified,
// which guarantees that self.lifecycle and self.ptr match
Expand All @@ -101,6 +111,7 @@ impl Clone for TypeErasedWeak {
}

impl Drop for TypeErasedWeak {
#[inline]
fn drop(&mut self) {
// SAFETY: once set in TypeErasedWeak::new, self.lifecycle is never modified,
// which guarantees that self.lifecycle and self.ptr match
Expand Down

0 comments on commit de53907

Please sign in to comment.