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

Support upcasting of dyn Trait values #3324

Merged
merged 12 commits into from
Dec 10, 2022

Conversation

nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Oct 10, 2022

Summary

Enable upcasts from dyn Trait1 to dyn Trait2 if Trait1 is a subtrait of Trait2.

This RFC does not enable dyn (Trait1 + Trait2) for arbitrary traits. If Trait1 has multiple supertraits, you can upcast to any one of them, but not to all of them.

This RFC has already been implemented in the nightly compiled with the feature gate trait_upcasting.

Credit

@crlf0710 has driven the implementation work for this.

Rendered

@nikomatsakis nikomatsakis added T-lang Relevant to the language team, which will review and decide on the RFC. I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. labels Oct 10, 2022
@nikomatsakis
Copy link
Contributor Author

I'm nominating this for the lang-team meeting. This feature has been under discussion for some time.

@afetisov
Copy link

I didn't see any mention of marker traits. I can have many marker traits as supertraits, and the algorithm as described in the RFC would produce a lot of spurious entries and vtables. For example, this

trait A {}
trait B {}
trait C {}
trait D: A + B + C {}

would produce, if I understand the algorithm correctly,

Vtable entries for `<S as D>`: [
    MetadataDropInPlace,
    MetadataSize,
    MetadataAlign,
    TraitVPtr(<S as B>),
    TraitVPtr(<S as C>),
]

Vtable entries for `<S as B>`: [
    MetadataDropInPlace,
    MetadataSize,
    MetadataAlign,
]

even though we could use a single vtable for all marker traits and their marker subtraits.

Also, I suppose the compiler knows that autotraits don't affect the vtables? And how would this feature interact with specialization?

@oxalica
Copy link

oxalica commented Oct 11, 2022

It's worth mentioning in "Drawbacks" that current impl has extra memory cost for traits with multiple supertraits, even though the user does not really use upcasting. In other words, it's not zero-cost. This can potentially increase cache pressure due to spread out of vtable ptrs.

@nikomatsakis
Copy link
Contributor Author

@afetisov

Also, I suppose the compiler knows that autotraits don't affect the vtables?

Correct. I don't know how we treat traits like marker traits that have no methods at present, but certainly it would be possible to optimize those as well.

And how would this feature interact with specialization?

It doesn't in particular. Specialization would affect what happens when methods are called, but that's an orthogonal consideration.

@nikomatsakis
Copy link
Contributor Author

@oxalica I added a mention of that to the downsides, thanks.

@nikomatsakis nikomatsakis removed the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Oct 11, 2022
@nikomatsakis
Copy link
Contributor Author

Discussing in the lang-team meeting. This proposal has been under discussion for some time, and is always something we expected to work, so even though the RFC was only recently opened, I am going to move to merge...

@rfcbot fcp merge

@rfcbot
Copy link
Collaborator

rfcbot commented Oct 11, 2022

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

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), 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 Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Oct 11, 2022

* as a single, combined vtable
* as a "very wide" pointer with one vtable per trait

Choose a reason for hiding this comment

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

A third option: The vtable for Trait1 + Trait2 contains entries for the vtables for Trait1 and Trait2. It's an extra layer of indirection, but much smaller and doesn't explode as quickly, while keeping upcasts trivial.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is what I meant by the first option, I think. You wind up needing each subset of the various traits involved -- well, maybe not EVERY subset, since the vtable for A + B might have A as a prefix.

I think this is what we will wind up doing, regardless.

@Aaron1011
Copy link
Member

One drawback to the vtable layout design: The ordering of super traits (and where clauses) now has performance implications for both the defining crate and downstream crates. While we'll probably never stabilize the vtable layout we may want to tell users to put the "most commonly used" supertrait first when defining a trait.

nikomatsakis and others added 2 commits October 18, 2022 17:11
Co-authored-by: teor <teor@riseup.net>
Co-authored-by: teor <teor@riseup.net>
@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 Oct 26, 2022
@rfcbot
Copy link
Collaborator

rfcbot commented Oct 26, 2022

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

@rfcbot rfcbot added finished-final-comment-period The final comment period is finished for this RFC. and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. labels Nov 5, 2022
@rfcbot
Copy link
Collaborator

rfcbot commented Nov 5, 2022

The final comment period, with a disposition to merge, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

This will be merged soon.

@oxalica
Copy link

oxalica commented Nov 6, 2022

The extra cost that I mentioned in #3324 (comment) seems still not included?

It's worth mentioning in "Drawbacks" that current impl has extra memory cost for traits with multiple supertraits, even though the user does not really use upcasting.

I'm quite concern about this since embedded-system users may find their memory footprint unexpectedly grown without a single code change.
If we cannot solve this. I'd rather suggest disable upcasting for traits have multiple supertraits, and only add TraitVPtr when explicitly noted via something like #[upcastable]

@crlf0710
Copy link
Member

crlf0710 commented Nov 6, 2022

@oxalica The vtable format has long been in its current format since Jun 2021 (1.56), and is considered an implementation detail. So there won't be any memory footprint grown with this RFC accepted. It's only the feature defined in this RFC will make use of this implementation detail to supply new operations.

Although embedded-system users usually won't really write such complex trait code, if they do and they cares, i'd suggest they request a thinner vtable with some attribute with a new RFC, so we can more aggressively tailor the actual layout of the vtable entries(like removing vacant entries), instead of the opposite.

@nbdd0121
Copy link

nbdd0121 commented Nov 6, 2022

@oxalica The vtable format has long been in its current format since Jun 2021 (1.56), and is considered an implementation detail. So there won't be any memory footprint grown with this RFC accepted. It's only the feature defined in this RFC will make use of this implementation detail to supply new operations.

No, this is not just implementation detail. While dyn trait upcasting stabilisation itself does not change the vtable format, it commits us to a specific format where it'll no longer be backward compatible to remove TraitVPtr from the vtable since it'll remove the ability to upcast some traits.

Furthermore, in one sense, the 1.56 vtable format change could be considered a I-heavy regression because it introduces space penalty for everyone even though nobody can use trait upcasting on stable. IMO it's not a valid argument to say "the regression is already there" as a reason to commit to the regression.

Although embedded-system users usually won't really write such complex trait code, if they do and they cares, i'd suggest they request a thinner vtable with some attribute with a new RFC, so we can more aggressively tailor the actual layout of the vtable entries(like removing vacant entries), instead of the opposite.

Rust's zero-cost abstraction principle, if anything, suggests that any feature with cost should be opt-in instead of opt-out. Plus, opting out from upcasting will be much more difficult from opting-in. It's not backward compatible to opt a super trait relation out from upcasting, but it's backward compatible to opt into upcasting.

Removing vacant entries is not relevant; it is an optimisation that all Rust user will benefit.

@nikomatsakis
Copy link
Contributor Author

Hi everyone! Thanks for the feedback. As discussed in recent @rust-lang/lang meetings, I've pushed text that (a) identifies the size impact of supporting upcasting as a downside, (b) discusses possible future ideas for how to improve it and (c) indicates why the RFC does not include "opt-in" at the trait level. However, I also added an unresolved question to re-evaluate that decision prior to stabilization.

I do have a request. It would be really helpful to have some data for this discussion. What would be ideal is some examples of the amount of extra data generated by these vtables for some particular Rust applications, and some other comparison (such as how this compares to the overall size of the binary, or to the impact of panic=abort).

@nikomatsakis
Copy link
Contributor Author

Also, I'd appreciate it if people could review what I wrote for accuracy.

@nikomatsakis
Copy link
Contributor Author

nikomatsakis commented Dec 8, 2022

@afetisov Ah, I just saw your comment, that's helpful context. I'll try to summarize it in the RFC. I believe the compilation time blowup is likely related to trait resolution and independent from vtable layout.

@afetisov
Copy link

afetisov commented Dec 8, 2022

@nikomatsakis From the model described in the RFC, it doesn't make sense to me that vtable size would not grow exponentially. Could it be that LLVM is doing some deduplication?


## Arbitrary combinations of traits

It would be very useful to support `dyn Trait1 + Trait2` for arbitrary sets of traits. Doing so would require us to decide how to describe the vtable for the combination of two traits. There is an intefaction between this feature and upcasting, because if we support upcasting, then we must be able to handle upcasting from some subtrait to some arbitrary combination of supertraits. For example a `&mut dyn Subtrait`...
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
It would be very useful to support `dyn Trait1 + Trait2` for arbitrary sets of traits. Doing so would require us to decide how to describe the vtable for the combination of two traits. There is an intefaction between this feature and upcasting, because if we support upcasting, then we must be able to handle upcasting from some subtrait to some arbitrary combination of supertraits. For example a `&mut dyn Subtrait`...
It would be very useful to support `dyn Trait1 + Trait2` for arbitrary sets of traits. Doing so would require us to decide how to describe the vtable for the combination of two traits. There is an interaction between this feature and upcasting, because if we support upcasting, then we must be able to handle upcasting from some subtrait to some arbitrary combination of supertraits. For example a `&mut dyn Subtrait`...

@burdges
Copy link

burdges commented Dec 8, 2022

I've not grasped why safe code should be able to cast raw pointers, like removing autotraits sure, but surely *const dyn T2 as *const dyn T1 could just be an unsafe operation with no real consequences? I'd expect the current formulation winds up fine, and likely the restrictions upon emitting a raw pointer with invalid vtable would make sense anyways, but why does safe code benefit from this cast?

@nbdd0121
Copy link

nbdd0121 commented Dec 8, 2022

One motivating factor is that we currently have no unsafe coercions, and I don't think forbidding this cast in safe code worth the addition language complexity of introducing an entire new class of coercion.

The current consensus is simply that we should not make upcasting
opt-in.
@nikomatsakis
Copy link
Contributor Author

nikomatsakis commented Dec 10, 2022

@afetisov I'm not sure, I'd defer to @crlf0710 on that point. I've weakened the text in the RFC though accordingly.

nikomatsakis and others added 4 commits December 10, 2022 06:15
@nikomatsakis nikomatsakis merged commit ea58235 into rust-lang:master Dec 10, 2022
@nikomatsakis
Copy link
Contributor Author

Huzzah! The @rust-lang/lang team has decided to accept this RFC. Further comments can be placed on the tracking issue, rust-lang/rust#65991.

@nikomatsakis
Copy link
Contributor Author

I've not grasped why safe code should be able to cast raw pointers, like removing autotraits sure, but surely *const dyn T2 as *const dyn T1 could just be an unsafe operation with no real consequences? I'd expect the current formulation winds up fine, and likely the restrictions upon emitting a raw pointer with invalid vtable would make sense anyways, but why does safe code benefit from this cast?

@burdges, to me the main considerations are:

  • compatible with method dispatch in the future
  • simpler casting model, and consistent with the fact that *dyn Foo + Send is currently upcastable to *dyn Foo

In contrast, the downside seems pretty minor. There's not a very strong reason to create *const dyn Foo with invalid metadata, and the restriction that such a pointer should not be released to "the wild" seems quite reasonable to me. If there are public-facing APIs that may be returning garbage *const dyn values, I think it would be good to return an Option.

There are a number of write-ups on the design repository as well. This one seems to cover a lot of the tradeoffs.

@burdges
Copy link

burdges commented Dec 12, 2022

We've TraitObject for anytime we'd actually fiddle with the metadata pointer I guess.

bors added a commit to rust-lang-ci/rust that referenced this pull request Nov 22, 2023
…affleLapkin

Stabilize RFC3324 dyn upcasting coercion

This PR stabilize the `trait_upcasting` feature, aka rust-lang/rfcs#3324.

The FCP was completed here: rust-lang#65991 (comment).

~~And also remove the `deref_into_dyn_supertrait` lint which is now handled by dyn upcasting coercion.~~

Heavily inspired by rust-lang#101718
Fixes rust-lang#65991
bors added a commit to rust-lang-ci/rust that referenced this pull request Nov 22, 2023
…affleLapkin

Stabilize RFC3324 dyn upcasting coercion

This PR stabilize the `trait_upcasting` feature, aka rust-lang/rfcs#3324.

The FCP was completed here: rust-lang#65991 (comment).

~~And also remove the `deref_into_dyn_supertrait` lint which is now handled by dyn upcasting coercion.~~

Heavily inspired by rust-lang#101718
Fixes rust-lang#65991
github-actions bot pushed a commit to rust-lang/miri that referenced this pull request Nov 23, 2023
Stabilize RFC3324 dyn upcasting coercion

This PR stabilize the `trait_upcasting` feature, aka rust-lang/rfcs#3324.

The FCP was completed here: rust-lang/rust#65991 (comment).

~~And also remove the `deref_into_dyn_supertrait` lint which is now handled by dyn upcasting coercion.~~

Heavily inspired by rust-lang/rust#101718
Fixes rust-lang/rust#65991
Copy link

@joshka joshka left a comment

Choose a reason for hiding this comment

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

Typos Food trait should be Eat

Comment on lines +65 to +67
trait Eat { fn eat(&mut self); }
trait Grab { fn grab(&mut self); }
trait Sandwich: Food + Grab { }
Copy link

Choose a reason for hiding this comment

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

Suggested change
trait Eat { fn eat(&mut self); }
trait Grab { fn grab(&mut self); }
trait Sandwich: Food + Grab { }
trait Eat { fn eat(&mut self); }
trait Grab { fn grab(&mut self); }
trait Sandwich: Eat + Grab { }


```rust
let s: &mut dyn Sandwich = ...;
let f: &mut dyn Food = s; // coercion
Copy link

Choose a reason for hiding this comment

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

Suggested change
let f: &mut dyn Food = s; // coercion
let f: &mut dyn Eat = s; // coercion


These coercions work for any kind of "pointer-to-dyn", such as `&dyn Sandwich`, `&mut dyn Sandwich`, `Box<dyn Sandwich>`, or `Rc<dyn Sandwich>`.

Note that you cannot, currently, upcast to *multiple* supertraits. That is, an `&mut dyn Sandwich` can be coerced to a `&mut dyn Food` or a `&mut dyn Grab`, but `&mut (dyn Food + Grab)` is not yet a legal type (you cannot combine two arbitrary traits) and this coercion is not possible.
Copy link

Choose a reason for hiding this comment

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

Suggested change
Note that you cannot, currently, upcast to *multiple* supertraits. That is, an `&mut dyn Sandwich` can be coerced to a `&mut dyn Food` or a `&mut dyn Grab`, but `&mut (dyn Food + Grab)` is not yet a legal type (you cannot combine two arbitrary traits) and this coercion is not possible.
Note that you cannot, currently, upcast to *multiple* supertraits. That is, an `&mut dyn Sandwich` can be coerced to a `&mut dyn Eat` or a `&mut dyn Grab`, but `&mut (dyn Eat + Grab)` is not yet a legal type (you cannot combine two arbitrary traits) and this coercion is not possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this RFC. T-lang Relevant to the language team, which will review and decide on the RFC. to-announce
Projects
None yet
Development

Successfully merging this pull request may close these issues.