Pre-RFC: Generic Pointer Casts, aka ptr.cast() for fat pointers

Currently, ptr.cast() can't cast to fat pointers, because the trait bounds that it would need can't be expressed in the type system.

In this pre-RFC I propose we add a new built-in trait to express the required bounds and allow casting to fat pointers with .cast(). This means that all possible as casts involving pointers can now be expressed by standard library functions.

rendered

1 Like

Since your explanation correctly points out that .cast exists only because itā€™s less error-prone than as casts, and you also note that this adds footguns to .cast, I think keeping the .cast method unmodified should be listed as an alternative.

I feel like conversion thatā€™s re-interpreting some as-cast-compatible metadata for a different target type might be its own operation.

Similarly, the conversion thatā€™s removing all metadata could be its own operations.

Both of these may already be expressible using existing Pointee trait infrastructure (Iā€™ll have to double-check this, didnā€™t write the code yet), and if thatā€™s true, all youā€™re suggesting is might be to make .cast into a (perhaps overly powerful) single tool that can combine both (or all three, depending on how you cound) of these flavors of pointer-casting under the same name.

Indeed:

#![feature(ptr_metadata)]

use std::ptr::Pointee;

fn cast_same_meta<T: ?Sized, U: ?Sized>(x: *const T) -> *const U
where
    U: Pointee<Metadata = <T as Pointee>::Metadata>,
{
    let (addr, meta) = x.to_raw_parts();
    std::ptr::from_raw_parts(addr, meta)
}

fn cast_remove_meta<T: ?Sized, U>(x: *const T) -> *const U {
    x.to_raw_parts().0.cast()
}

Edit: Nevermind, that signature isnā€™t quite usableā€¦

#[repr(transparent)]
struct Wrapper<T: ?Sized>(T);

fn from_ref<T: ?Sized>(value: &T) -> &Wrapper<T> {
    unsafe {
        &*cast_same_meta(core::ptr::from_ref(value))
    }
}
error[E0271]: type mismatch resolving `<Wrapper<T> as Pointee>::Metadata == <T as Pointee>::Metadata`
  --> src/lib.rs:22:11
   |
20 | fn from_ref<T: ?Sized>(value: &T) -> &Wrapper<T> {
   |             - expected this type parameter
21 |     unsafe {
22 |         &*cast_same_meta(core::ptr::from_ref(value))
   |           ^^^^^^^^^^^^^^ expected type parameter `T`, found `Wrapper<T>`
   |
   = note: expected associated type `<T as Pointee>::Metadata`
              found associated type `<Wrapper<T> as Pointee>::Metadata`
   = note: an associated type was expected, but a different one was found
note: required by a bound in `cast_same_meta`
  --> src/lib.rs:7:16
   |
5  | fn cast_same_meta<T: ?Sized, U: ?Sized>(x: *const T) -> *const U
   |    -------------- required by a bound in this function
6  | where
7  |     U: Pointee<Metadata = <T as Pointee>::Metadata>,
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `cast_same_meta`

For more information about this error, try `rustc --explain E0271`.
error: could not compile `playground` (lib) due to 1 previous error

Given your RFC text also features such a signature (except the cast_same_meta method you provide also doesnā€™t compile), and only lists ā€œwe cannot express ā€˜source and target have the same metadata or target is thinā€™ā€ as a downside there, perhaps mentioning this issue in your text and/or suggesting that it should somehow be fixed could be relevant, too.

I listed this in the unresolved questions:

  • Should we relax the bounds of .cast() directly or add a new function .cast_unsized() with the relaxed bounds? A new function could avoid potential footguns (and not be insta-stable).

On the one hand cast_unsized feels more unsafe than cast, but on the other hand the safety requirements are the mostly same: The caller must promise that the new type is compatible with the old type. IMO casting from *mut u8 to *mut u32 is just as unsafe as casting from from *mut [u8] to *mut [u32]. And if you use *mut u8 to mean "void pointer" instead of "pointer to u8", you should probably be using *mut () instead.

I don't understand how that would be useful, isn't that just regular cast? That works for casting fat->thin already.

I'd argue that that's a bug unrelated to this RFC and with the fix applied your example already compiles.

That would still not work for casting *mut dyn Trait -> *mut (dyn Trait + Send), but maybe the correct solution to this is to just redefine the metadata of dyn Trait + Send to be DynMetadata<dyn Trait> instead of DynMetadata<dyn Trait + Send>. Except we'd also need something for dyn Send, like making it DynMetadata<NoPrincipal> with NoPrincipal being some opaque type defined in core. Then we can just define cast_same_metadata on pointers (with a better name) and it would probably work for all real use cases.

The downside of this approach is that the following still wouldn't work, even though the compiler arguably has enough information to make it work:

fn cast_same_meta<T: ?Sized, U: ?Sized>(x: *const T) -> *const U
where
    U: Pointee<Metadata = <T as Pointee>::Metadata>,
{
    x as _
}
1 Like

My bad, I missed that ordinary cast supports this already.


The other alternative still is just to offer not the union of cast and cast_same_meta as new functionality, but only cast and cast_same_meta separately.

Unless there are important use-cases that would need the union of both to work.

This might be a rather uncommon use-case. (And the other way is already an implicit conversion.)

So perhaps overall some additional motivating examples could be useful.

(On that note, even the case of repr(transparent) wrapper isnā€™t ideal, because ultimately that conversion (between references) should be made possible without unsafe, eventually, hopefully. And examples with casting between a repr(C) type and unsized fields and multiple fields[1] would typically involve some offset [since the unsized field comes last] which an extended cast might not support nicely, either.)


  1. otherwise it could just be repr(transparent) ā†©ļøŽ

One use case in my own code is a "cross-thread function call". Lots of functions in my code want to use a particular Context object, which is only accessible from a single thread. Thus, I have a

static CHANNEL: Mutex<Option<Sender<Box<dyn FnOnce(&mut Context) + Send>>>>;

where the calling thread sends a closure, and the thread with access to the Context repeatedly receives closures and executes them, until the sending side is closed. But what if I want to send a closure that doesn't live for 'static? My trick is to use a runtime borrowing mechanism, much like thread::scope(), where the calling thread waits until it receives notification of an output.

type Command = Box<dyn FnOnce(&mut Context) + Send>;

static CHANNEL: Mutex<Option<Sender<Command>>> = Mutex::new(None);

fn with_context<F, T>(f: F) -> T
where
    F: FnOnce(&mut Context) -> T + Send,
    T: Send,
{
    let (output_send, output_recv) = mpsc::sync_channel(0);
    struct CommandInput<F, T> {
        f: F,
        output_send: SyncSender<T>,
    }
    let input = CommandInput { f, output_send };
    let command: Box<dyn FnOnce(&mut Context) + Send> = Box::new(|cx| {
        let CommandInput { f, output_send } = input;
        let output = panic::catch_unwind(AssertUnwindSafe(|| f(cx)));
        output_send
            .send(output)
            .expect("error: could not send command output");
    });
    let mut send_guard = CHANNEL.lock().expect("error: channel is poisoned");
    let send = if let Some(send) = &*send_guard {
        send
    } else {
        let (send, recv) = mpsc::channel();
        // spawn a new receiver thread, and send `recv` to it...
        send_guard.insert(send)
    };
    // SAFETY:
    // If `send.send()` errors, `command` is dropped when this function unwinds.
    // If `output_recv.recv()` errors, `f` has already been dropped.
    // Otherwise, `f` has been called and the output has been sent.
    let result = send.send(unsafe { mem::transmute(command) });
    drop(send_guard);
    result.expect("error: thread panicked");
    let output = output_recv
        .recv()
        .expect("error: could not receive command output");
    output.unwrap_or_else(|payload| panic::resume_unwind(payload))
}

Notice the one line of unsafe code, which transmutes a Box<dyn FnOnce(&mut Context) + Send + 'short> into a Box<dyn FnOnce(&mut Context) + Send + 'static>, so that it can be sent through the static CHANNEL. Right now, it depends on fat-pointer layout not being affected by lifetimes, but a cast that could perform the lifetime transmute more explicitly would be nice.

2 Likes

I was trying to make up an argument against casting lifetimes of dyn Trait pointersā€¦ (though so far Iā€™ve only managed to apply this to lifetime parameters, not the + 'a lifetime from the trait bound). The argument involves soundness of arbitrary_self_types. Now Iā€™m noticing, casts that change lifetimes are already allowed through the as operator o.O

Guess we have a soundness issue, then :sweat_smile:

#![feature(arbitrary_self_types)]

trait Static<'a> {
    fn proof(self: *const Self, s: &'a str) -> &'static str;
}

fn bad_cast<'a>(x: *const dyn Static<'static>) -> *const dyn Static<'a> {
    x as _
}

impl Static<'static> for () {
    fn proof(self: *const Self, s: &'static str) -> &'static str {
        s
    }
}

fn extend_lifetime(s: &str) -> &'static str {
    bad_cast(&()).proof(s)
}

fn main() {
    let s = String::from("Hello World");
    let slice = extend_lifetime(&s);
    println!("Now it exists: {slice}");
    drop(s);
    println!("Now itā€™s gone: {slice}");
}

Rust Playground


Edit: Looks like thatā€™s recent change, in 1.75.

FWIW, (I'm fairly sure) we've guaranteed that for any type, if you solely change lifetimes in ā€œvariant positions,ā€[1] the transmute is always valid (layout and function call abi are equivalent).

...except when I checked where I thought this was documented, I didn't see it. It might still be a PR. (It's certainly already an implicit requirement by the nature of for<'a> fn and variant lifetime coercion.) There might've been a caveat about Trait + 'a as well.


  1. The qualification here is due to the use of universal lifetime quantification creating distinct types, e.g. for<'a> fn(&'a &'a ()) and for<'a, 'b> fn(&'a &'b ()). ā†©ļøŽ

I'm guessing this is caused by Never consider raw pointer casts to be trival by Nilstrieb Ā· Pull Request #113262 Ā· rust-lang/rust Ā· GitHub

1 Like

Youā€™re about 3 minutes too late (good guess though!), I have finished bisecting it by now. Writing an issue, currently.


Edit:


Edit2: Turns out, the issue is larger and older as casts like *const dyn Trait<u8> to *const dyn Trait<u16> are supported since forever (Rust 1.2).

Edit3: Apparently, this is even relevant in the context of the (recently stabilized, about to be released in 1.76) trait upcasting:

3 Likes

Shifted to top as important: T: PointerCast<U> must be the builtin trait if it's going to capture all allowed as pointer casts. The deciding trait cannot be in terms of pointee metadata, because as pointer casts can apply unsizing coercions, e.g. *mut T as *mut dyn Trait.


Back to the OP, I think the following API could work and not feel too out of place along pointer.cast::<U>() as "cast-to-Sized":

impl<T: ?Sized> *mut T {
    pub fn cast_with<U: ?Sized, V: ?Sized>(self, meta: *const V) -> *mut U
    where V: Pointee<Metadata = <U as Pointee>::Metadata>;
}

This takes its shape from pointer::with_metadata_of. To cast to a different type with a shared metadata kind is p.cast_with(p) instead of p.cast(). This doesn't have the relaxation to allow the single method to also do cast-unsized-to-sized, but does it need to? Other than wanting to expose a singular pointer::cast function, I don't really see any use case for writing code generic over casting to same-meta-or-sized pointees. The only real benefit I see is doing all reinterprets through cast such that each "cast family" method is for changing "one thing" about the pointer, with_metadata_of and map_addr being the ones for changing the meta or addr parts.

(I'm also of the stated opinion that the Pointee trait should switch to a "kind" model like discriminant/Discriminant/DiscriminantKind. This doesn't have a particular impact other than it's maybe easier to justify "PointeeKind normalization" such that the as-compatible type families share a kind, i.e. have <dyn Any as PointeeKind>::Metadata == <dyn Any as PointeeKind>::Metadata.)

Making PointerCast the builtin instead of MetadataCast would complicate the implementation

Citation needed. Pointer cast validity is checked between T and U, so a builtin PointerCast trait would be that exact same check that already exists. In fact, unsizing coercions applied with as, so MetadataCast isn't sufficient. (shifted to top)

I didn't actually intend PointerCast to capture all possible casts between pointers that can be expressed with as casts, but only those that must be expressed with as casts (ignoring std functions). Specifically not counting that coercions can also be expressed with as, because they are very fundamentally different from "normal" pointer-to-pointer casts, which are a no-op at machine code level.

Whether an as cast is a coercion cast will be checked via the Unsize/CoerceUnsized traits before checking PointerCast/MetadataCast. I will amend the RFC to clarify this.

I don't really see the advantage of this function over cast_same_meta. It looks like ptr1.cast_with(ptr2) would be the same as ptr1.with_metadata_of(ptr2).cast_same_meta(). In what cases would I use this function with ptr1 != ptr2?

Yes, having that and cast_same_meta is the main alternative to this RFC. The metadata of dyn Trait + 'a and dyn Trait + 'static being different is the main reason I came up with the current MetadataCast design in the first place.

Citation: I implemented this. With my approach MetadataCast was required, but PointerCast was not. ( Generic Pointer Casts by lukas-code Ā· Pull Request #6 Ā· lukas-code/rust Ā· GitHub )

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