Why do we need object safety rules?

https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html#object-safety

Okay, so it is not possible to call trait methods that take or return Self by-value, or don't have the &self parameter, etc, but other methods should still be usable, right?
Why does Rust need to prevent creation of such trait objects altogether?

It's because trait objects are unsized, meaning that their size is unknown to whomever calls these methods. For example, if I say:

struct Foo32(u32);
struct Foo64(u64);
trait Bar {
    fn clone(&self) -> Self;
}
impl Bar for Foo32 { /**/ }
impl Bar for Foo64 { /**/ }
let x: &dyn Bar = /**/;
let y = x.clone();

How would the compiler know how large to make the stack frame (Whose size must be known statically currently)? Would it be 4 bytes (For Foo32) or 8 bytes (For Foo64)?
Another example is passing by value, which is currently possible using the unsized_locals feature:

trait Bar2 {
    fn drop_self(self);
}
impl Bar2 for Foo32 { /**/ }
impl Bar2 for Foo64 { /**/ }

let x: dyn Bar2 = *(Box::new(2u32) as Box<dyn Bar2>);
x.drop_self();

But in standard, stable code, it is impossible to own an unsized type which is not under some indirection.

Any trait with object-unsafe methods can be made object safe by adding where Self: Sized to all of the object-unsafe methods.

// this trait is not object-safe
trait Clone {
    fn clone(&self) -> Self;
}

// this one is
trait Clone {
    fn clone(&self) -> Self where Self: Sized;
}

I don't know why it's like this, though, rather than considering all traits to be object safe.

3 Likes

As I said, I understand, why certain methods cannot be called on trait objects, but why shouldn't I be able to to call baz below?

trait MyTrait {
    fn foo() -> Self;
    fn bar(&self) -> Self;
    fn baz(&self) -> &str; // Object-safe!
}

In other words, why restrict trait object creation, rather than not allowing to call object-unsafe methods?

Because the signature of MyTrait::bar says all MyTraits have one but dyn MyTrait doesn't.
And Rust is very careful with hidden things on signatures (AFAIK only lifetime elision).

Actually that means that the type dyn MyTrait has an item bar. There is no syntax to refer to bar without also specifying the type. So MyType::bar or <MyType as MyTrait>::bar. But with trait objects, we don't have this type info, so we can't call these functions even if we wanted to.

Because it wants to have the rule that dyn Foo: Foo -- the opposite, where a trait object doesn't always implement its trait, is just weird.

8 Likes

Related discussion here: Does object safety prevent any legitimate use cases?

This may be more pleasing from a language-theoretical point of view, but on the other hand it's pretty annoying that I can't have a Vec<Box<dyn Foo>> just because Foo has some object-unsafe method (which I had no intention of using anyway). I might even call this weird.

I'd prefer that the compiler refused to coerce dyn Foo as Foo if and when I actually asked for that, rather than stopping me earlier just because this situation might arise.

1 Like

This seems to be counter to Rust's philosophy. Rust wants to prevent problems as early as possible, so

  • we add bounds to generics instead of duck-typing in order to guarantee that we get the desired behavior
  • we have a conservative lifetime checker to guarantee validity of references
  • finally, we don't allow you to create trait objects that are not object safe to guarantee that you don't lose any behavior in the process of converting T to dyn Foo.

The common theme is that Rust tries to be conservative when there are a number of options, and making dyn Foo: Foo seems to be in line with that.

2 Likes

I wonder if the best way about this is to add an additional bound to all the trait objects + asFoo, where there's a trait asFoo which has a method you can call that returns the Foo itself.

So something like the following:

trait Foo {
    fn clone_self(&self /*Ok*/) -> Self; /*Not ok*/
}
impl Foo for u32 { /**/ }
let x: Box<dyn Foo> = Box::new(31415u32); //Say this was okay.
fn bar<T: Foo + ?Sized>(value: &T) {
    value.clone_self();
}
foo(&*x);

Which makes sense from a language theoretical standpoint, if I have a T: Foo, surely I should be able to use a function Foo::fn(&self) -> _, but then we impede with T: Foo + ?Sized which would mean that <Foo + ?Sized>::fn(&self) -> Self makes no sense, so I guess this would contradict the case of there being a dyn Foo created or used.

Unless we use generic constraints as previously mentioned:

trait Foo {
    fn clone_self(&self) -> Self where Self: Sized;
}
foo(&*x);

This would fail to compile because dyn Foo: !Foo. It is weird, but should be doable. (I am against this because it seems like it would be a source of confusion if added)

I'm sorry, I don't quite understand, do you mean dyn AnyKindOfTrait: !AnyKindOfTrait? Or dyn NotObjectSafeTrait: !NotObjectSafeTrait? Because in the case we try the following:

trait Foo {}
impl Foo for dyn Foo {}

The compiler complains about dyn Trait inherently having Trait implemented for it.

This one, this is hypothetical right now

1 Like

I'm curious, is it possible to make (probably through the procedural macro) a "wrapper-trait", which implements only object-safe methods of some other trait by delegating to their original implementation and which is implemented always whether the original trait is? It seems that this will remove the confusion: if you don't need the non-object-safe methods - use the wrapper, and all is OK. The only issue I can see now is the name conflicts when both traits are in scope (and, of course, the very possibility of the auto-generation - without it this is not very feasible).

1 Like

Yeah, I think that would be a good way of solving this problem! To solve the naming issue, we could just supply a different name from the trait, probably let the user of this proc-macro select the name.

I was thinking not about the trait name, but about the method names. Check this (playground):

trait NonObjectSafe {
    fn non_safe() -> Self;
    fn safe(&self);
}

trait ObjectSafe {
    fn safe(&self);
}

impl<T> ObjectSafe for T where T: NonObjectSafe {
    fn safe(&self) {
        <Self as NonObjectSafe>::safe(self)
    }
}

impl NonObjectSafe for u32 {
    fn non_safe() -> Self {
        1
    }
    fn safe(&self) {
        println!("I'm {}. Object safe methods FTW!", self);
    }
}

fn main() {
    0u32.safe(); // error[E0034]: multiple applicable items in scope
}
2 Likes

Oh, I see what you mean now. The only solution seems to be documenting that both the NonObjectSafe trait and the ObjectSafe trait shouldn't be brought into scope at the same time for ergonomic reasons. Althoough, this doesn't seem to be a problem for generic functions so this may not be as bad as it seems. Just import the NonObjectSafe where you need it, and you can use the fully qualified name for the ObjectSafe version (which should be more rare, so this verbosity should be manageable)

1 Like

Well, sometimes it does and other times it doesn't. Look at Send and Sync for example: the compiler could require you to derive these traits explicitly, but that'd be too annoying, so it doesn't. I might also point to stuff like trailing return values and even type inference in general. The choice between implicit vs explicit is made on a case-by-case basis. I don't think there exists some widely agreed "Rust philosophy" on these matters.