Marking Arc/Rc/Weak as #[repr(transparent)]

Currently, the Arc, Rc, and Weak structs do not have a #[repr(…)]; they don't have a guaranteed type layout. However, all of their constituent types already do have guaranteed layouts. In particular, in rust-lang#68099 their inner ArcInner and RcBox types were marked as #[repr(C)] to ensure certain transmutations were/remained sound.

Type Details

Each of these structs contains a single NonNull pointer to their backing allocation, with the strong references also containing a PhantomData (but that is zero-sized so it's not layout-relevant).

pub struct Arc<T: ?Sized> {
    ptr: NonNull<ArcInner<T>>,
    phantom: PhantomData<ArcInner<T>>,
}

pub struct Weak<T: ?Sized> {
    ptr: NonNull<ArcInner<T>>,
}

#[rustc_insignificant_dtor]
pub struct Rc<T: ?Sized> {
    ptr: NonNull<RcBox<T>>,
    phantom: PhantomData<RcBox<T>>,
}

pub struct Weak<T: ?Sized> {
    ptr: NonNull<RcBox<T>>,
}

The Rust reference tells us that pointers to a given type have a consistent layout, and NonNull is #[repr(transparent)] so it will as well. It has a zero niche, but that doesn't affect its own layout, it just allows that single unused bit pattern in its representation to be used by a containing enum. Adding #[repr(transparent)] to the outer structs would guarantee that they also share this representation.

#[repr(transparent)]
#[rustc_layout_scalar_valid_range_start(1)]
#[rustc_nonnull_optimization_guaranteed]
pub struct NonNull<T: ?Sized> {
    pointer: *const T,
}
#[repr(C)]
struct ArcInner<T: ?Sized> {
    strong: atomic::AtomicUsize,
    weak: atomic::AtomicUsize,
    data: T,
}

#[repr(C)]
struct RcBox<T: ?Sized> {
    strong: Cell<usize>,
    weak: Cell<usize>,
    value: T,
}

Would it be reasonable to add #[repr(transparent)] to those structs so that they could also have guaranteed memory layouts? From my (shallow) understanding of rustc, I expect the current generated layouts are already like that in practice, so it shouldn't affect performance or code generation at all, but the layout could hypothetically change in future compiler versions, so it's not safe to rely on.


This would allow &Arc<T>/&Rc<T> references to be safely transmuted to corresponding &Weak<T> references. I've wanted to do a couple times when I've had an Arc<T> but I'm calling a function that expects a &Weak<T>. The intermediate Arc::downgrade() and associated weak reference count changes feel wasteful when I know the types are the same internally, and the static guarantees carried by Weak<T> are strictly weaker than those carried by Arc<T>. (This was originally asked about on Stack Overflow).

Perhaps this could be exposed as a safe Arc/Rc::as_weak associated function, something like this…

impl<T: ?Sized> Rc<T> {
    /// Convert a reference to an [`Rc`] into a reference to a [`Weak`] of
    /// of the same type.
    ///
    /// This is a type-only operation; it doesn't modify the inner reference
    /// counts.
    ///
    /// # Examples
    ///
    /// ```
    /// use std::rc::{Rc, Weak};
    ///
    /// let five: &Rc<i32> = &Rc::new(5);
    ///
    /// let weak_five: &Weak<i32> = Rc::as_weak(five);
    ///
    /// ```
    pub const fn as_weak<'a>(this: &'a Self) -> &'a Weak<T> {
        unsafe { mem::transmute::<&'a Rc<T>, &'a Weak<T>>(this) }
    }
}

…or maybe that could be left as an exercise for library authors.


Is this a reasonable idea? Am I missing an obvious flaw? Thanks for reading.

The only thing I see is from the Nomicon, which says:

This can only be used on structs with a single non-zero-sized field (there may be additional zero-sized fields). The effect is that the layout and ABI of the whole struct is guaranteed to be the same as that one field.

The snag then is that Rc<T> and Arc<T> (I'm not sure about the Weak types) have 2 fields, neither of them ZST:

  • a refcount
  • the value of type T that shared ownership is being imposed on

They each have only one sized field, a pointer to an RcBox or ArcInner (accompanied by a PhantomData). The refcount and value both live behind the pointer so that they can be shared by multiple Rc or Arc instances.

1 Like

I don't think we should guanratee layouts of structs when not necessary (not that I'm on libs-api-team or whatever). An associated function without an ABI guarantee would be better if this is indeed a common case, because it leaves more room for std while still allowing that safely (do note, however, that in fact it does restrict it to be #[repr(transparent)] internally, but perhaps it won't be the case in the future). Do you have a case where you need #[repr(transparent)], and a conversion function is not enough?

5 Likes

The stated use case, converting &Arc<T> into &Weak<T> without modifying the counts, should also work with #[repr(C)] as long as the internal structure of Arc and Weak remains identical.

1 Like

Good point. I do not have a motivating case for #[repr(transparent)] other than the &Arc to &Weak conversion. I'd be happy to just have the associated function, but I have assumed the public #[repr(transparent)] would be necessary to implement the function without breaking the NonZero/enum/niche optimization.

It's kind of a gray area whether #[repr(transparent)] makes a public API commitment, but I would say it does not when the internal type is still private.

2 Likes

I would suggest proposing the function in a PR or ACP, then. A layout guarantee is much stronger; a "well we commit to being able to offer this reference conversion" is certainly constraining, but still gives more freedom -- it could offset the pointer if needed, for example.

1 Like

See this issue. There seems to be general agreement that it does not create any commitment if the field isn't public.

Edit: This naturally assumes the lack of a commitment elsewhere (such as documentation).

4 Likes

Thanks for all of the informative feedback! I've proposed Rust PR #100472: Add function Arc/Rc::as_weak(…) to convert &Arc/Rc to &Weak based on this discussion.