Should pointer methods/functions accept !Sized?


#1

As discussed in this recent RFC, ptr::null and ptr::null_mut have a T: Sized bound. Is there a good reason we can’t remove this bound, making the bound T: ?Sized? I propose that we update these functions to take a T: ?Sized. All other functions/types in the ptr module that do not require size (eq, NonNull, etc) have a T: ?Sized bound.

If folks agree, I can put up a PR.


#2

Currently it is UB to have a null vtable pointer, it’s as if *mut Trait and &mut Trait / Box<Trait> all have the same safe &'static VtableForTrait pointer as the fat metadata.

In the case of:

trait MyTrait {}

fn main() {
    std::ptr::null::<MyTrait>();
}

there is no legal vtable that ptr::null could place in the pointer.


#3

This concept also came up in pr44932 when I wanted to unsize is_null(), and that was later re-added in pr46094 citing the discussion in rfcs#433. Checking for null is a simpler question than producing it though.


#4

Not having a vtable pointer as @dtolnay explained is why simply adding T: ?Sized is rejected by the compiler:

error[E0606]: casting `usize` as `*const T` is invalid
 --> a.rs:1:36
  |
1 | fn null<T: ?Sized>() -> *const T { 0 as *const T }
  |                                    ^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0606`.

#5

Perhaps null() (and null_mut()) should be (based on) compiler intrinsics/builtins; the compiler can then fill in a valid vtbl ptr for a trait object and, more generally, fabricate a valid (but null) ptr to any T.


#6

@vitalyd I don’t know how a compiler intrinsic/builtin would make a difference. The compiler cannot invent an implementation of the trait to use for null pointers, for example in:

#![feature(arbitrary_self_types)]

trait TypeName {
    fn type_name(self: *const Self) -> &'static str;
}

fn main() {
    println!("{}", std::ptr::null::<dyn TypeName>().type_name());
}

#7

Why not? AFAIK, there’s no guarantee about a vtbl being the same even for the same impl (due to CGU differences) so I can’t immediately see a reason why it couldn’t fabricate something as it sees fit.


#8

What string would you expect the code in my previous comment to print?


#9

That would be UB since the ptr is null? So in some sense, it’s like a form of ! - it can pretend to be anything since all you can really do, safely, is check for nullness.


#10

It would be quite unfortunate for that to be UB because it is 100% safe code. A ptr.type_name() is equivalent to (ptr.vtable.type_name)(ptr.data) and should work fine for a null data pointer. But as I commented previously, the UB comes from constructing a fat pointer containing a null vtable pointer.


#11

Perhaps my understanding of method calls is wrong, but I was under the impression that you essentially dereference the self as part of the call - the this (so to speak) receiver has to be valid, even if you don’t use any state from it in the callee. In that, you’d be deref’ing null data ptr, which would be UB.

But your comment implies that my understanding is wrong.


#12

In that code the pointer is not dereferenced, it is passed to the trait method as a pointer.

Here is a simpler (safe, compilable) example.

#![feature(arbitrary_self_types)]

trait MyTrait {
    fn f(self: *const Self);
}

impl MyTrait for u8 {
    fn f(self: *const Self) {
        println!("ptr={:p}", self);
    }
}

fn main() {
    let ptr = 0x3039 as *const u8 as *const MyTrait;
    ptr.f();
}

#13

I understand the pointer is not actually dereferenced - my impression was that the ptr has to be valid as-if it was dereferenced.

In some OO language, a method call involves passing a this ptr as a hidden parameter; whether the parameter must be non-null or not is a lang design decision. I’m aware of arbitrary_self_types but I’m not sure what guarantees/restrictions it carries, such as in this case: is *const Self allowed to be null? Your responses indicate yes. Is that mentioned/documented somewhere? Or does it merely fall out of raw ptrs being allowed to be null and therefore arbitrary self types just “inherit” that?

Also, suppose arbitrary self types were out of the picture or required that self raw ptrs are not null (for sake of argument). Are there other reasons the compiler couldn’t fabricate a vtbl ptr?

At the end of the day, it seems odd that it’s so difficult to fabricate a null ptr, generically. The compiler, in a lot of ways, is likely best positioned to provide that facility (somehow - how exactly, I guess that’s what this thread is about at this point :slight_smile:).


#14

Is there a reason that calling a method on a raw pointer (even if that method is safe) shouldn’t always be unsafe? If it were, then we could say that it was the caller’s responsibility to ensure that the pointer was valid. That would, in turn, allow a fat pointer with both the data and vtable pointers as NULL to be a valid (if not valid to dereference) pointer.


#15

FWIW, this part seems risky because there may be unsafe code relying on vtbl ptrs not being null when it, e.g., deconstructs a fat ptr.


#16

It’s one of the guarantees of Rust that a vtable will always be valid. Option<*const dyn Trait> is the same size as *const dyn Trait, so if you want something similar to a raw pointer with an invalid vtable, you can use Option::None.

EDIT: that is at least the case today, maybe we need an RFC to make this an official guarantee


#17

I’m not sure what wins this gives us; slice pointers *const [T] can be assembled manually, and being able to produce sketchy null pointers to fancy DSTs (imagine something more delicate than a vtable!) is actually unsafe! Think of the reason why slice::from_raw_parts is unsafe; I think this is a rare situation where the metadata actually has a safe default (len: 0).