`Rc/Arc::Borrowed`: An object-safe version of `&Rc<T>`/`&Arc<T>`

There has been some interest in using self: &Rc<Self> and self: &Arc<Self> as method receivers. The idea is that the method doesn’t always need to take the Rc/Arc by value, so instead you take a reference to it, and then the method can clone the Arc/Rc if it needs to extend its lifetime.

The problem is that you can’t perform dynamic dispatch with an &Rc<Self>, and so such methods cannot be called on a &Rc/Arc<dyn Trait>.

Here is a potential solution: introduce a new type that can be created from an &Rc<T> or &Arc<T>. It’s just a copy of the pointer, but has the lifetime of the reference it was created from, so the Rc/Arc cannot be modified during its lifetime. For example:

struct Borrowed<'a, T: 'a + ?Sized> {
    ptr: NonNull<RcBox<T>>,
    _marker: PhantomData<&'a T>,
}

impl<'a, T: ?Sized> Clone for Borrowed<'a, T> {
    fn clone(&self) -> Self {
        Self {
            ptr: self.ptr,
            _marker: PhantomData,
        }
    }
}

impl<'a, T: ?Sized> Copy for Borrowed<'a, T> {}

impl<T: ?Sized> Rc<T> {
    fn borrow<'a>(&'a self) -> Borrowed<'a, T> {
        Borrowed {
            ptr: self.ptr,
            _marker: PhantomData,
        }
    }
}

impl<'a, T: ?Sized> Borrowed<'a, T> {
    // poor method name, I know
    fn clone(self) -> Rc<T> {
        // TODO
        // increment the counter, return an Rc just like `<Rc as Clone>::clone` does
    }
}

impl<'a, T: ?Sized, U: ?Sized> CoerceUnsized<Borrowed<'a, U>> for Borrowed<'a, T> 
where
     T: Unsize<U>,
{}

impl<'a, T: ?Sized, U: ?Sized> DispatchFromDyn<Borrowed<'a, U>> for Borrowed<'a, T> 
where
     T: Unsize<U>,
{}

impl<'a, T: ?Sized> Deref for Borrowed<'a, T> {
    type Target = T;

    fn deref(&self) -> &T {
        unsafe { &(*self.ptr.as_ptr()).value }
    }
}
1 Like

I don't think this is the problem. Dynamic dispatch has to do with traits, and object safety is a property of the trait, so it has nothing to do with Rc/Arc.

You can look into this, it seems to be what you are talking about

This has nothing to do with arbitrary_self_type, but DispatchFromDyn.

The problem is &Rc<dyn Trait> currently is a thin pointer like &(Rc<()>, &VTable), not the regular DST representation like (&Rc<()>, &VTable). (There is no unsize-coercion relationship to &Rc<dyn Trait> from &Rc<T>.) So the compiler cannot generate the code for dynamic dispatch of &Rc<dyn Trait> like the single-pointer counterpart:

fn dyn_dispatch_rc(self: Rc<dyn Trait>) {
    let vtable = metadata(self);
    (vtable.fptr_of_method)(self as Rc<()>)
    // ^ just take the thin pointer part
}

However I don’t think we need to introduce this Borrow type either. We just need to tell the compiler to obtain the vtable not from the pointer itself, but its pointee:

fn dyn_dispatch_rc_ref(self: &Rc<dyn Trait>) {
    let vtable = metadata(*self); // ← take *self not self
    (vtable.fptr_of_method)(self as &Rc<()>)
    // object-slice the metadata part away
}

Meaning we should change the DispatchFromDyn trait to require a method to extract the vtable

trait DispatchFromDyn<T> {
    fn vtable(dst: &T) -> &'static VTable;
}
impl DispatchFromDyn<Rc<U>> for Rc<T> {
    fn vtable(dst: &Rc<U>) -> &'static VTable {
        metadata(*dst)
    }
}
impl DispatchFromDyn<&Rc<U>> for &Rc<T> {
    fn vtable(dst: &&Rc<U>) -> &'static VTable {
        metadata(**dst)
    }
}
2 Likes

Oh, I wasn’t aware of that, thank you.

Getting the vtable isn’t the problem — that can be done by using the Deref impl to get an &dyn Trait and extracting the vtable pointer. The problem is you need to turn an &Rc<dyn Trait> into an &Rc<T>, and I don’t really see a good way to do that, since we’re changing what the reference points to.

The idea here is that we avoid that problem by removing the extra layer of indirection. We can turn a Borrowed<dyn Trait> into a Borrowed<T> just fine.

This concept of a “borrowed copy” can be made more general, actually. For any type T, as long as T has no interior mutability (e.g. via UnsafeCell), we could create a borrowed version of T that is valid for the lifetime 'a from an &'a T, because we know that for the lifetime 'a, the value (of type T) will not be modified. This BorrowedCopy<'a, T> type could implement Copy regardless of whether or not T does.

To implement this type, we just need two things:

  • a BitsOf<T> type that has the size and alignment of a T, and can be created from an &T, but is not guaranteed to be valid, has no Drop implementation, and implements Copy
  • expose the Freeze trait from libcore, which is implemented for types that don’t have internal mutability

That type is &'a T

Which type?

A type that borrows, is valid for a specified lifetime, and is Copy.

Yes, they’re very similar. &'a T is pointer to a T that is guaranteed to be valid for the lifetime 'a; BorrowedCopy<'a, T> is a copy of a T that is guaranteed to be valid for the lifetime 'a.

A bad way to do that is simply transmute &Rc<dyn Trait> to &Rc<T>. This works because &Rc<dyn Trait> should be represented as &(Rc<()>, &VTable), so we could reinterpret-cast the reference to only refer to its header &Rc<()>. This is similar to C-style cast to base-class.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.