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.

7 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.

2 Likes

Hey, sorry for the late reply!

solve any use-cases not already covered by derive(CoercePointee),

Ah, we minified the above example a bit too far to the point of fitting into the CoercePointee-sized hole :sweat_smile:. The actual usecase we have is closer to this. (Basically - a pointer plus some extra in the same struct).

We were able to somewhat make it work with CoercePointee by allowing user code to split the pointer (as a newtype that implements CoercePointee ) and the remaining data, and then recombine them later (à la from_raw_parts/into_raw_parts).

Our from_raw_parts impl has to be unsafe because only the same parts may be recombined again, though. We resolved that part by providing a macro that does the split/coerce/recombine for the user, which also works at const time.

The macro and unsafe function are not ideal, but usable enough as-is.

Given that I've published the unsize crate which does what's described in the helper function, yes, that would be awesome, exactly what I would have longed for back then. That missing trait bound is why it could only provide a pre-determined set of functions that are safe to construct. (My approach was a macro hack that forces you to demonstrate that unsize coercion occurs in |x: &A| -> &B x without an as _ cast, which is hopefully equivalent to the bound, i.e. only very particular functions are accepted. The trait bound would be much more ergonomic and reassuring).

Then the crate could demonstrate its intended value, which is in moving the trait bound from the coercion site to the constructor of a witness and thus letting you apply coercion to smart pointers even when you locally do not have an impl to demonstrate the bound holding (e.g. you have a data structure that is generic with instantiations expecting dyn Trait instantiations). For that use there's absolutely no problem with the availability of [const] CoerceUnsized being stabilized at a later point.

And feel free to take that as a use-case outside the CoercePointee hole.