Why can't Weak::new() be used with a trait object?

I want to have a weak pointer that can point to various values that implement a trait. A variable declared as let mut weak_ptr: Weak<dyn MyTrait> works for this. Let's say I want to first initialize it to be an empty weak pointer. The following do not work:

//let mut weak_ptr: Weak<dyn MyTrait> = Weak::new(); // error: cannot infer type for `T`
//let mut weak_ptr: Weak<dyn MyTrait> = Weak::<dyn MyTrait>::new(); // error: function or associated item not found in `std::rc::Weak<dyn main::MyTrait>`
//let mut weak_ptr: Weak<dyn MyTrait> = Default::default(); // error: doesn't have a size known at compile-time

Explicitly specifying the generic type argument as some struct that implements the trait works:

struct ArbitraryStructThatImplTrait;
impl MyTrait for ArbitraryStructThatImplTrait { }
let mut weak_ptr: Weak<dyn MyTrait> = Weak::<ArbitraryStructThatImplTrait>::new()

I believe this is because both Weak::new() and Weak::default() are declared with impl<T> without where T: ?Sized, so the type argument cannot be a dynamically sized type like a trait object.

However, I see no reason why Weak::new() and Weak::default() can't work for dynamically-sized types too. Empty weak pointers of any type work the same way, and the implementation of Weak::new() simply casts the max integer to a pointer -- it doesn't store anything that depends on the type T. I can already create an empty weak pointer of a struct that implements the trait, and assign that to a weak pointer of the trait, so I don't see why I shouldn't be able to create the empty weak pointer of the trait directly.

1 Like

Fat pointer metadata must be always valid even if the pointer is invalid, which means you can’t generate the invalid vtables needed for Weak::new.

Hmm… that seems kinda silly as an empty weak pointer will never use a vtable of T. Could this be made to work if Weak were internally implemented with an Option, with empty pointer being None?

Yes, but then you would lose the non-null optimization and pay that performance penalty on every use even if that cost is unnecessary. You can wrap it in Option instead, and use None to represent not initialized

The think reason for this has to do with CustomDsts. @RalfJung should know more about why pointer metadata should always be valid even if the pointer is invalid.

1 Like

This is an interesting question! I think this is the first use-case I see for a fat reference (not a raw pointer) to carry “bogus” metadata. The reason we require it to be valid is mostly that when i doubt, it seems produent to have a stronger invariant.

I opened a UCG issue for this.

Turns out I was too hasty in my previous comment -- Weak is internally NonNull which is a raw pointer. So this is "just" a raw pointer with invalid metadata, which I would be rather surprised if it caused UB. But we do not have a definite statement allowing this either.

Hmm. I could also use a dangling Weak with ?Sized types here: https://github.com/rust-lang/rust/issues/60728.

IIRC, originally Weak::new() actually allocate a memory as Arc::new() does but never initialize it. I remember that this behavior was stated at the stdlib document, but it's changed anyway.

It was changed because it caused segfaults:

Oh and I also just found this, which is basically the same discussion as this thread:

Turns out I was wrong, and currently zero-initializing a *mut dyn Trait actually SIGILLs:

If the vtable cannot be accessed (the pointer is invalid, dereferencing is UB), for what reason would just using a dangling vptr or even just &[0; N] be wrong?

Even if we require the vtable ptr in *const dyn _ to be nonnull, I see no reason to require it to point to an actual vtable (yet). And even if e.g. size_of_val would like to work with raw pointers, just have a dummy vtable just enough initialized to say size zero. (In effect, have a dummy ZST that implements all traits by causing UB if used, then point to its vtable.)

1 Like

I would expect that a ptr::size_of_val(*const dyn Trait) -> usize function would be unsafe, rather than requiring the vtable pointer always be a valid reference (instead of a raw pointer).
This looks like the same mistake that was done with function pointers (being, in practice, &'static references), but until a non-unsafe raw size_of_val is published, there is hope to backtrack.

Let's see if eddyb writes a blog post justifying the current status quo (which feels like a bug to me).

@eddyb ^^

You're hoping for too much, there's what I wrote in my first comment and my second comment on the linked issue.

What I'll say is that I think both possibilities are theoretically reasonable, although maybe one is potentially more sound:

  1. dynamic types are always valid (even if behind raw pointers), i.e. size_of_val is safe even if it takes a raw pointer
  2. raw pointers are always valid no matter their bitpattern, i.e. ptr::null can be safe even for unsized pointees
1 Like

I was hinting at:

I think such write up would be quite interesting :slight_smile:

1 Like

The reason is not having to figure out an intermediate form of "validity" between "proper vtable" and "don't care".

Yes, this could be refined further, but it needs careful design considering custom DST. Until then, the rule is that wide pointer metadata must always be valid, no matter the pointer type. It is easier to relax UB later than to strengthen it.

Personally I'd be in favor of saying that metadata may be invalid for raw pointers, that also avoids having to specify an intermediate form of validity. But clearly that does not reflect the current reality.

Note that this is troublesome with "thin trait objects" where there is no metadata -- a *const Thin most certainly will not have a safe size_of_val.

5 Likes