Stabilization proposal: Unsize trait

We (@ekuber and I) would like to propose partial stabilization of "DST Custom Coercions" rust-lang/rfcs#982, specifically the stabilization of the Unsize trait (#![feature(unsize)]). Critically, this would not include the stabilization of CoerceUnsized.

Unsize is marked as not implementable by user code, which would allow for its stabilization without constraining the trait's shape: no breakage would be expected even if the underlying design changes (for example, by adding functions to the trait), as only std/rustc can implement it. Only compiler-guaranteed unsize coercions are allowed by this trait—the trait just encodes what the compiler already guarantees and does. Look at the docs for all details on the compiler guarantees.

Having Unsize as stable would allow for custom type coercions without conversion by providing an API with a helper function, while the CoerceUnsized trait design is still in progress, which would eventually make the helper function unnecessary and its body can become a direct as cast, kept only for potential backwards-compatibility.

impl<T: ?Sized> CustomSmartPointer<T> {
    const fn unsize<K: ?Sized>(self) -> CustomSmartPointer<K>
    where
        T: Unsize<K>,
    {
        CustomSmartPointer { inner: self.inner }
    }
}

fn main() {
    let mut x = 255;
    let p = CustomSmartPointer { inner: &mut x };
    let y = p.unsize::<dyn Foo>();
    println!("{}", y);
}

(full example)

Notably, this approach also allows for this helper to be a const fn. Additionally, you can also support coercions of multiple separate pointers if necessary, which CoerceUnsized doesn't (currently?) support.

On stable Rust there's currently no adequate full alternative. Any currently-stable way of providing this coercion necessarily requires providing access to the raw pointer to the API's consumer/caller, by requiring them to pass in a closure performing the cast. Allowing for this helper function to be const would also require a nightly feature for impl [const] Fn. With Unsize it is possible to do this without closures, and only allow sound operations, all from within the smart pointer's code.

impl<T: ?Sized> CustomSmartPointer<T> {
    const unsafe fn unsize<K: ?Sized>(
        self,
        coerce: impl [const] FnOnce(*mut T) -> *mut K,
    ) -> CustomSmartPointer<K> {
        CustomSmartPointer {
            inner: coerce(self.inner),
        }
    }
}

const fn coerce(x: *mut u8) -> *mut dyn Display {
    x as _
}

fn main() {
    let mut x = 255;
    let p = CustomSmartPointer { inner: &mut x };
    let y = unsafe { p.unsize(coerce) };
    println!("{}", y);
}

(full example)

Note that by taking an fn pointer, the user can perform arbitrary operations that might not conform to the safety invariants needed by the pointer, so the API should be marked as unsafe, making for a poorer API:

// ⚠️ can do any casting operation, not just coercion ⚠️
const fn shouldnt_be_allowed(x: *mut dyn Display) -> *mut i8 {
    x as _
}

Ferrous Systems had an experiment for CoerceUnsized 2 years ago for encoding more information of the pointer in the traits themselves, but given that Unsize is useful independently of CoerceUnsized, and that the trait is not user-implementable, if those experiments (or any other extensions) are eventually adopted, it wouldn't affect the usage of the Unsize trait on stable being proposed.

We'd like to see if there is appetite of stabilizing just Unsize.

6 Likes

The two things I would like to hear are

  • is there pent-up demand for this pattern to allow for a way of doing P<dyn T> to P<dyn K> beyond us?
  • are there things that we missed? Is there a reason that stabilizing Unsize alone would cause other problems, with either other features either current of planned?
1 Like

I don't know that I fully understand what exactly is being stabilized under this proposal, but I'd love something that lets me write

<&Foo>::unsize::<dyn Bar>(&foo)

to be explicit about the exact unsizing coercion that's happening (converting &Foo into &dyn Bar in this case). So if this proposal lets me write that, then I'd love to see it go forward!

1 Like

We're proposing stabilizing #![feature(unsize)] and not #![feature(coerce_unsized)].

I'm not sure if this is the same thing you meant, but this is possible: <CustomSmartPointer<dyn Foo>>::unsize::<dyn Display>(y). If you meant <&dyn Foo>::unsize::<dyn Bar>(&foo) where foo is already &dyn Foo and you want &dyn Bar, then you could just cast: &foo as &dyn Bar.

Is this what you mean? You would be able to implement that (as you can see right there), but it would not be part of the stdlib -- only the marker trait itself would be.

I meant where Foo is a concrete type and Bar is a trait which Foo implements, as I find I'm more often unsizing a concrete type into a trait than a trait into a super-trait.

And yes, I'm aware that as casts exist, but I'm against as casts because they're so overloaded and it's not always apparent what's going on, and having an explicit unsize() method on the various pointer-likes in the stdlib would be much better for clarity imo.

3 Likes

An RFC for this should address:

https://rust-lang.zulipchat.com/#narrow/channel/144729-t-types/topic/Why.20isn't.20the.20Unsize.20trait.20reflexive

An RFC should address whether this helps solve any use-cases not already covered by derive(CoercePointee), which is much closer to stabilization than CoerceUnsized.

1 Like