Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracking issue for std::ptr::NonNull::cast #47653

Closed
SimonSapin opened this issue Jan 22, 2018 · 22 comments
Closed

Tracking issue for std::ptr::NonNull::cast #47653

SimonSapin opened this issue Jan 22, 2018 · 22 comments
Labels
B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@SimonSapin
Copy link
Contributor

SimonSapin commented Jan 22, 2018

ptr::NonNull<T> is similar to *mut T. Its cast method added in #47631 is similar to raw pointer casting with the as operator.

impl<T: ?Sized> NonNull<T> {
    pub fn cast<U>(self) -> NonNull<U> {
        unsafe {
            NonNull::new_unchecked(self.as_ptr() as *mut U)
        }
    }
}

Note that T is ?Sized but U is not (or we’d get a E0606 "vtable kinds may not match" error on as)

@joshlf
Copy link
Contributor

joshlf commented Jan 22, 2018

Would it be possible to add a cast_unsized or similar method that converts into a fat pointer? This is probably less urgent - the necessity of cast comes from the fact that NonNull should have feature parity with *mut, and *mut has as - but it's probably worth at least discussing.

@SimonSapin
Copy link
Contributor Author

SimonSapin commented Jan 22, 2018

How would that work? How does this method you pick a vtable or a slice length? The only way I know in safe Rust (other than ad-hoc APIs like String::into_boxed_str) of creating fat pointers is from thin ones through CoerceUnsized, which NonNull already supports:

#![feature(box_into_raw_non_null)]
use std::ptr::NonNull;

fn main() {
    // Creating thin pointers
    let a: NonNull<[u8; 10]> = Box::into_raw_non_null(Box::new([42; 10]));
    let b: NonNull<String> = Box::into_raw_non_null(Box::new(String::new()));

    // Coercing to fat pointers to dynamically-sized types
    let _a: NonNull<[u8]> = a;
    let _b: NonNull<std::fmt::Display> = b;
}

@joshlf
Copy link
Contributor

joshlf commented Jan 22, 2018

I suppose I was thinking particularly about statically typed but dynamically sized fat pointers (e.g., to slices). I'm not sure how you'd answer the question about vtables. So, concretely, converting between, e.g., NonNull<[u32]> and NonNull<[u8]>, where the length field can just be maintained because they're both the same number of bytes (or multiplied by four? are fat pointer length fields byte size or element count?).

@SimonSapin
Copy link
Contributor Author

Turns out you can cast a raw slice to another. This leaves the length unchanged regardless of the item type, which is often wrong. For example, this compiles and prints random (stack?) memory.

fn main() {
    let b = [1, 2, 3, 4, 5];
    let ptr = cast_slice::<u8, u64>(&b);
    for x in unsafe { &*ptr } {
        println!("{:016x}", x)
    }
}

fn cast_slice<T, U>(s: *const [T]) -> *const [U] {
    s as _
}

However that’s the case where the types on both sides of as are known to be raw slices. In the fully-generic ?Sized case the compiler doesn’t know what code to emit.

@joshlf
Copy link
Contributor

joshlf commented Jan 22, 2018 via email

@SimonSapin
Copy link
Contributor Author

This compiles, however wrong it may be:

fn main() {
    struct Foo(u8, [u8]);
    struct Bar(String, [u64]);
    let ptr: *const Foo = unimplemented!();
    ptr as *const Bar;
}

But this is still slice to slice. If trait objects are involved, they need to be the same trait.

At least that’s the current implementation of as. I don’t have a strong opinion on how it should be.

@alexcrichton alexcrichton added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. B-unstable Blocker: Implemented in the nightly compiler and unstable. labels Jan 22, 2018
@joshlf
Copy link
Contributor

joshlf commented Jan 22, 2018

OK, probably worth it to just stick with cast for the time being and only address this if it becomes an issue for someone.

@TimNN TimNN added the C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC label Jan 23, 2018
@SimonSapin
Copy link
Contributor Author

SimonSapin commented Mar 4, 2018

This one-line method landed in Nightly on 2018-02-07. If stabilized in the current 1.26 cycle, it will reach Stable on 2018-05-11 after four months. @aturon, is this enough baking time to start FCP?

@SimonSapin
Copy link
Contributor Author

#49669 adds a Void extern type (probably to be renamed) and uses it with NonNull. I’ve found myself trying to use this method to try to cast to NonNull<Void>, which doesn’t work because cast requires the destination type to be Sized. We can’t fully generalize to ?Sized because the method wouldn’t know how to make up a vtable pointer or slice length. It’s unfortunate that today’s type system can’t precisely express the actual requirement of a pointer being thin, but I think we can live with it. #49669 ends up adding a specific NonNull::as_void method.

TR;DR: Despite limitations I think we should stabilize this as-is.

@rfcbot fcp merge

@rfcbot
Copy link

rfcbot commented Apr 8, 2018

Team member @SimonSapin has proposed to merge this. The next step is review by the rest of the tagged teams:

No concerns currently listed.

Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. and removed proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. labels Apr 8, 2018
@rfcbot
Copy link

rfcbot commented Apr 11, 2018

🔔 This is now entering its final comment period, as per the review above. 🔔

bors added a commit that referenced this issue Apr 18, 2018
stabilize a bunch of minor api additions

besides `ptr::NonNull::cast` (which is 4 days away from end of FCP) all of these have been finished with FCP for a few weeks now with minimal issues raised

* Closes #41020
* Closes #42818
* Closes #44030
* Closes #44400
* Closes #46507
* Closes #47653
* Closes #46344

the following functions will be stabilized in 1.27:
* `[T]::rsplit`
* `[T]::rsplit_mut`
* `[T]::swap_with_slice`
* `ptr::swap_nonoverlapping`
* `NonNull::cast`
* `Duration::from_micros`
* `Duration::from_nanos`
* `Duration::subsec_millis`
* `Duration::subsec_micros`
* `HashMap::remove_entry`
@jethrogb
Copy link
Contributor

Ran into the issue where I can't cast between NonNull slice types.

@SimonSapin
Copy link
Contributor Author

fn main() {
    let a: *const [u8] = b"foo";
    println!("{:04x?}", unsafe { &*(a as *const [u16]) })
} 

Playground, output:

[6f66, 0a6f, 0000]

The as operator will happily cast a raw slice to another raw slice type by copying the length without consideration of the sizes of the item types, in this case creating a buffer overflow. This is such a footgun that I’m not sure NonNull not supporting it is a bad thing.

You can use NonNull::from(slice::from_raw_parts(ptr, len)) after considering whether len needs some computation. If alignment is also an issue, [T]::align_to might help as well.

@jethrogb
Copy link
Contributor

I understand that you can do unsafe (and unsound) things when casting pointers. NonNull is advertised as “*mut T but non-zero and covariant.” It should be able to do everything a regular pointer can do. Of course there are workarounds, but they're just that. I ended up not using NonNull in my code because it was too cumbersome compared to raw pointers.

@SimonSapin
Copy link
Contributor Author

It’s already discussed above why we can’t just add U: ?Sized to NonNull<T>::cast::<U>(). Do you think it would be useful to add NonNull<[T]>::cast_slice::<U>(self) -> NonNull<[U]> that “dumbly” copies the length like as does?

@joshlf
Copy link
Contributor

joshlf commented Aug 28, 2018

It’s already discussed above why we can’t just add U: ?Sized to NonNull<T>::cast::<U>(). Do you think it would be useful to add NonNull<[T]>::cast_slice::<U>(self) -> NonNull<[U]> that “dumbly” copies the length like as does?

That actually might be useful if only to give us a place to document the footgun that is casting slice pointers. Since it's a keyword, as doesn't have documentation like methods do.

@jethrogb
Copy link
Contributor

jethrogb commented Aug 28, 2018

It’s already discussed above why we can’t just add U: ?Sized to NonNull::cast::(). Do you think it would be useful to add NonNull<[T]>::cast_slice::(self) -> NonNull<[U]> that “dumbly” copies the length like as does?

Yes, except I think it should just be called cast

@SimonSapin
Copy link
Contributor Author

@joshlf rustdoc does support documenting keywords, these days:
https://doc.rust-lang.org/std/#keywords
https://github.com/rust-lang/rust/blob/1.28.0/src/libstd/keyword_docs.rs#L11

@jethrogb Re calling it cast I see what you mean, but in generics today we don’t have a way to accept Sized types and slices but not trait objects, or to encode that the return type can only be a slice if the input type is as well. Or maybe we could with a dedicated unsafe trait?

@SimonSapin
Copy link
Contributor Author

SimonSapin commented Aug 28, 2018

Hmm I half-expected an impl coherence error, but this compiles. I’m not sure how I feel about potentially adding it to the standard library though.

unsafe trait Cast<T: ?Sized> {
    fn cast(ptr: *mut Self) -> *mut T;
}

// implied: T: Sized
unsafe impl<T, U: ?Sized> Cast<T> for U {
    fn cast(ptr: *mut Self) -> *mut T {
        ptr as _
    }
}

unsafe impl<T, U> Cast<[T]> for [U] {
    fn cast(slice: *mut Self) -> *mut [T] {
        unsafe {
            let ptr = (*slice).as_mut_ptr();
            let len = (*slice).len();
            std::slice::from_raw_parts_mut(ptr as _, len)
        }
    }
}

@jethrogb
Copy link
Contributor

No need for a trait:

struct Ptr<T: ?Sized>(*const T);

impl<T> Ptr<T> {
    fn cast<U>(self) -> Ptr<U> {
        Ptr(self.0 as *const _)
    }
}

impl<T> Ptr<[T]> {
    fn cast<U>(self) -> Ptr<[U]> {
        Ptr(self.0 as *const _)
    }
}

@SimonSapin
Copy link
Contributor Author

@jethrogb That code doesn’t have T: ?Sized on the first impl though (to allow e.g. extracting the data pointer of a raw trait object, which NonNull::cast supports today), and adding it makes the two impls conflict.

@jethrogb
Copy link
Contributor

jethrogb commented Aug 28, 2018

Yes, you're right, I didn't look at the docs in detail enough to see that existing bound

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

6 participants