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

Tweak object safety rules to allow static dispatch #2027

Merged
merged 2 commits into from
Jul 30, 2017

Conversation

withoutboats
Copy link
Contributor

@withoutboats withoutboats commented Jun 10, 2017

@withoutboats withoutboats self-assigned this Jun 10, 2017
@withoutboats withoutboats added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jun 10, 2017
@withoutboats
Copy link
Contributor Author

cc @nikomatsakis @eddyb

# Detailed design
[design]: #detailed-design

Today, the rules for object safey work like this:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/safey/safety

@Manishearth
Copy link
Member

The documentation should mention fixing up https://doc.rust-lang.org/error-index.html#method-has-no-receiver

@withoutboats
Copy link
Contributor Author

What about that error message should change? You still can't have methods from the trait on the trait object type, just static methods from other traits (or inherent impls)

@Manishearth
Copy link
Member

Oh, nvm, I see now.

@withoutboats
Copy link
Contributor Author

This raises the question, if Foo is not object safe, can you impl Foo for Foo?

@ranweiler
Copy link
Contributor

This would allow instantiating object types for non-object safe traits in unsafe code, by transmuting [...]

Is there ever a context in this would be sensible? If not, it would be nice if we could modify the proposed design to continue to make this inexpressible, even in unsafe code.

@Manishearth
Copy link
Member

I'm overall not very fond of "let's make this inexpressible even in unsafe code", it invariably ends up coming back to bite us. It's very hard to know a priori what legitimate use cases there are for an API. I'm all for making things hard to do, but when we make things impossible it's often presumptuous.

In fact, I'd argue for there to be a defined API with a stabilization path for constructing trait objects (fn create_object<A, T: ?Sized>(x: *mut A) -> *mut T) instead of needing to frob std::raw which is probably not going to stabilize (since the repr could change) and doesn't help you fetch the appropriate vtable pointers.

@eddyb
Copy link
Member

eddyb commented Jun 10, 2017

You can write that function today, just add an A: Unsize<T> bound and be warned that it's not limited to traits, but other than that it's just a refular unsizing coercion.

@ranweiler
Copy link
Contributor

ranweiler commented Jun 10, 2017

I'm overall not very fond of "let's make this inexpressible even in unsafe code", it invariably ends up coming back to bite us. It's very hard to know a priori what legitimate use cases there are for an API.

That makes sense to me! In this case, it seems like it might be worth thinking about a bit, since it is a question of preserving the status quo or not (vs. simply introducing new restrictions).

Suppose Tr is a trait that is not object safe. The status quo is: we cannot— even with transmute— obtain a trait object for Tr. With this proposal, such a transmuting becomes newly possible, so we are expanding the API.

This is either possibly sensible or it isn't, wrt what it is supposed to mean to be "object safe". If it is possibly sensible, and the rules for object safety are too strict right now, then this RFC adds a feature beyond just support for static dispatch: now you can (with some contortion) obtain a trait object that was previously unobtainable (at least after RFC 255). That's great! It also makes me ask: should there be a safe API for those cases?

If this is provably never sensible— e.g. it permits a new, previously-inexpressible violation of a contract that is always supposed to hold— then to me, it seems there's a small amount of value in maintaining the status quo. This is definitely open to questioning, of course, since prior to RFC 255, any trait could be used to make a trait object. Maybe we lost something there, that we'd regain with this RFC. If so, it would be cool (but not mandatory) to have examples, if they exist.

Of course, since we only recover the ability to create trait objects from non-object safe traits in unsafe code, maybe this sort of "correctness" concern is just really out of place. Naively, it seems like we should never be able to do this, by definition of "object safety". If there are valid use cases, that might suggest a need to modify the definition of object safety further, to accommodate those cases in safe code.

I definitely don't know, hence my interest in concrete examples.

@withoutboats
Copy link
Contributor Author

@ranweiler I think it depends on what you mean by "valid use cases." There are two different ways to interpret that:

  • Is there a good reason to want to make a trait object from an object unsafe trait? The answer is that I don't know of any, but its impossible to affirmatively say there are none.
  • Can you do this without writing an invalid program? That is, does this always just crash or lead to undefined behavior? Here, I have more certainty. You can do this without crashing; you can instantiate it with a null vtable pointer and just never try to access any items on it.

I still don't know why you would want to do that, but it would be a valid program AFAIK.

However, I tend to want to take a "we are all adults here" attitude toward unsafe code; you can do all sorts of inadvisible things. And I don't think restricting this would be very easy to implement, since we'd have to check object safety every time we learn the type of a value, rather than only when seeing these specific casts.

@withoutboats
Copy link
Contributor Author

Actually, I have a (contrived) example of something you could do.

Implementing methods on object-unsafe traits seems fine to me. You can never actually dereference their vtable or data pointers in those methods, because the object type doesn't implement the trait type, so it would be badly typed to call any methods from the vtable.

But that means you could totally define a method on an object unsafe trait, instantiate the object, and then call it:

trait ObjectSafe {
     fn safe(&self) -> u32;
}

trait NotObjectSafe {
     fn not_safe<T: Default>(&self) -> T;
}

trait FooBar {
     fn foobar(&self) -> u32;
}

impl FooBar for ObjectSafe {
     // performs dynamic dispatch through `self.safe()`
     fn foobar(&self) -> u32 { self.safe() }
}

impl FooBar for NotObjectSafe {
    fn foobar(&self) -> u32 {
        // NotObjectSafe does not implement NotObjectSafe, so `self.not_safe()` cannot be called
        0
    }
}

fn main() {
     unsafe {
         let obj: &'static NotObjectSafe = mem::transmute(TraitObject { data: ptr::null(), vtable: ptr::null() });
         obj.foobar()
    }
}

@withoutboats
Copy link
Contributor Author

This RFC would basically let you use object unsafe traits analogously to opaque types; whatever data they carry you can't get to, but you can still operate on them.

@nikomatsakis
Copy link
Contributor

Some thoughts:

A. I think that the rules proposed in this RFC are sensible and a reasonable "baby step" towards better object safety rules.

B. I would very much like to preserve the invariant that Trait: Trait holds so long as Trait is a valid type. This implies that "virtual calls" are kind of compartmentalized in an "automatic" impl that the compiler generates which handles virtual dispatch, rather than being a first-class thing.

This contributes to keeping MIR very simple. I might be overvaluing this, but I feel like there is value in having a simple "core language", and I'd like to see if we can hold the line there.

C. At present, we have this compromise, where a trait Trait can have non-object-safe methods and yet still be object-afe, but only if those methods require Self: Sized. This does invalidate invariant B because the type Trait does not implement Sized, and hence -- even in a generic context where we just know that we have some T: Trait + ?Sized, those methods cannot be used. Effectively this allows you to designate a "core group" of methods that are virtually dispatched, along with a bunch of others that must be statically dispatched. If you then combine this with an impl of Trait for Box<Trait> (or &mut Trait etc), then effectively get "full-featured" trait objects.

D. I think it's orthogonal from this RFC, to some extent, but I do like the idea of people being able to provide their own impl Trait for Trait impl in place of the compiler's automatic, virttual impl. This might allow you to make arbitrary traits 'object-safe'. You could imagine that you are allowed to skip specifying object-safe methods (which will then use virtual dispatch) but you must give definitions for all non-object-safe methods (which would then be statically run). This would presumably mean that they have some implementation that is defined in terms of the core, object-safe methods.

Because Iterator uses fn(self) methods, this wouldn't quite work in this case, but let me give it as a (broken) example anyhow, just to sketch out (and also because I've got finish this comment!). Imagine an alternate universe where Iterator had all the same methods it has today, but it did not have the where Self: Sized bounds on them. In that case, you might imagine supplying a impl like this:

impl<T> Iterator for Iterator<Item = T> {
    type Item = T;

    // No body means: delegate to a virtual call. Only possible
    // if the method is object-safe.
    fn next(&mut self) -> Option<T>; 

    fn map(self, ...) -> Map<Self, ...>{
        // this would presumably e more-or-less the same as the default impl;
        // but here you see the problem, that we can't do that because we need
        // to introduce some kind of pointer type for the trait object.
        //
        // if this were an `&mut self` method, it could be made to work.
    }
}

@withoutboats
Copy link
Contributor Author

A. I think that the rules proposed in this RFC are sensible and a reasonable "baby step" towards better object safety rules.

B. I would very much like to preserve the invariant that Trait: Trait holds so long as Trait is a valid type.

These statements seem to be at odds, but I'm not certain that we mean the same thing. Under this RFC, Trait: Trait would not hold unless Trait is object safe, but Trait would always be a valid type. Are we using these terms differently from one another?

@nikomatsakis
Copy link
Contributor

@withoutboats

These statements seem to be at odds, but I'm not certain that we mean the same thing. Under this RFC, Trait: Trait would not hold unless Trait is object safe, but Trait would always be a valid type. Are we using these terms differently from one another?

You're correct. What I meant really was that Trait: Trait holds if you can create a trait object (and here I'm not referring to sorcery like transmute). Basically, I don't want you to be able to invoke a virtual method m except by doing Trait::m(object). Right now, that suffices, and it also does under your RFC.

@withoutboats
Copy link
Contributor Author

@nikomatsakis Yes, I agree with that restriction

@withoutboats
Copy link
Contributor Author

@rfcbot fcp merge

As @nikomatsakis elucidated, there's a lot more we could do to make object safety more ergonomic (e.g. where Self: Sized on all items, impl Trait for Trait to fill in unvirtualizable items), but none of that conflicts with this RFC. There's very little to discuss here as far as I can see.

@rfcbot
Copy link
Collaborator

rfcbot commented Jun 23, 2017

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

No concerns currently listed.

Once these reviewers reach consensus, 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 the proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. label Jun 23, 2017
@rfcbot rfcbot added final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels Jul 10, 2017
@rfcbot
Copy link
Collaborator

rfcbot commented Jul 10, 2017

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


Today, the rules for object safey work like this:

* If the trait (e.g. `Foo`) **is** object safe:
Copy link
Contributor

@liigo liigo Jul 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean "If it is safe then...", or "It is safe if ..."?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the first

@rfcbot
Copy link
Collaborator

rfcbot commented Jul 20, 2017

The final comment period is now complete.

@withoutboats
Copy link
Contributor Author

I've merged this PR as RFC 2027, tracking issue is at rust-lang/rust#43561

@Centril Centril added A-traits Trait system related proposals & ideas A-typesystem Type system related proposals & ideas A-trait-object Proposals relating to trait objects. labels Nov 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-trait-object Proposals relating to trait objects. A-traits Trait system related proposals & ideas A-typesystem Type system related proposals & ideas final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants