-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Access to traits' associated functions and constants from trait objects #2886
Conversation
Fixed example 1 and a minor typo. Co-Authored-By: kennytm <kennytm@gmail.com>
My bad. I applied the fixes, is there anything in the text that needs clarification or a correction right now? |
I'll note that Also, if given As an aside, You cannot make a trait object from a true DST in Rust, meaning no
|
@burdges Thanks for telling, I actually didn't know that trait objects store their size in a retrievable way. (You're talking about the "sized trait object" thing I wrote about in the future possibilities section, right?) |
I think |
Is We still choose between either
I suppose the fatter pointer works best for |
No. The
Feels like a bad idea, we don't want to change the layout of the fat pointer because that'd break code depending on makeshift
A good idea is to have two vtables per trait ( |
An unsized type like We've previous discussed types that manage their own size, but this requires another builtin trait like
I initially confused myself by poorly remembering those discussions, but doing self sized or truly unsized types requires far more work, and must avoid breaking Rust should add a
Are small box optimizations so important? Yes:
We could even make We always have alloc available in practice, but actually supporting the niche cases without alloc creates significant overhead in Rust. We'd therefore save considerable work accross the ecosystem by simply making |
What does this code do with your proposal? trait Foo {
const CONST: usize;
fn assoc();
}
fn foo<T: Foo + ?Sized>() {
let _ = T::CONST;
T::assoc();
}
fn main() {
foo::<dyn Foo>();
} In Rust |
Not that I have a particular reason to change the layout, but such code should never have been written as we have never given any guarantees of ABI stability for trait objects (or ABI guarantees in general unless explicitly stated). People who copy-pasted |
It's a moot point. Adding an extra word to existing fat pointers is a non-starter for performance reasons. Adding a new kind of trait object type that would be fatter ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, as I happen to read and try to understand this RFC (but my Rust skills are clearly not enough 😄), I noticed a few typos. I hope it's not too early in the review process to tackle these kind of fix.
Co-Authored-By: Jean SIMARD <woshilapin@tuziwo.info>
I added sections describing this. Thanks for pointing out! |
I think you'd have more luck understanding things one step at a time. There isn't anything that scarily complex in the RFC if you read it at a slow pace. Thanks for the corrections, by the way! |
We should probably stick to your actual RFC here @kotauskas so apologies for the derail, but.. If you've a There are no vtable changes for associated constants. Also Initially I mistakenly imagined a I think As hinted above, there was some limited past interest in "self sized" types with multiple unsized fields within the same allocation:
An I think your RFC here sounds much more interesting than any of this unsized stuff actually. ;) |
This RFC really doesn't seem bulletproof enough. I think the author is seriously underestimating the number of edge cases and semantic questions that need to be addressed here, and is approaching the question of trait objects with a mindset of "Here's the feature I want and here is how we can implement it"; rather than methodically inspecting the solution space. For instance, here is one edge case: trait Foo {
const CONST: usize;
}
fn foo<T: Foo + ?Sized>(t1: &T, t2: &T) -> usize {
T::CONST
}
fn main() {
let tobject1: Box<dyn Foo> = Box::from(...);
let tobject2: Box<dyn Foo> = Box::from(...);
foo(&tobject1, &tobject2);
} Does this code compile? Does it fail? If it fails, what is the error message? If it compiles, which vtable does foo use? (keeping in mind that, if I'm not saying these questions have no possible answers. I'm saying that this RFC should be backed up by a through analysis of Rust's template system; an analysis which as a matter of course would find and address edge cases like the one above. As it is, I think the RFC is simply under-specified for how broad its effects are. |
We cannot compile that code in current Rust:
I've forgotten if previous discussions around that issue ever reached a conclusion of whether addressing E0277 is desirable. |
As it seems now, I truly am. That's what the RFC process is, though: no one can realistically know every single edge case when writing an RFC. That being said, it's my first ever time writing an RFC and if it's needed, I'm okay with discussing every edge case to be found, for as long as needed. It's likely that the more people review the RFC, the less the chance of some edge-cases not being found.
I added a section about this, called "the vtable ambiguity rule". This should fully cover this edge case.
There's nothing bad in having an RFC in development phase for a long time. Doing this analysis collectively via the RFC process is much more realistic than simply relying on a single participant digging through the entire template system and finding all edge cases, something that can be done collectively in a much shorter period of time. Once again, that's why the RFC process exists. |
What's going on here, exactly? How does the program find the implementation if we don't have access to `&self`? **We actually do.** Since dynamic dispatch happens in `main` (the calling function), rather than a stub `do_a_thing` "disambiguator" which reads the trait object and redirects execution according to its vtable. Simply put, the entire technical idea of this RFC is *if we have access to the trait object when we're doing dynamic dispatch, why do we pretend that the dynamically dispatched implementation needs `&self` in order to understand what to do when it actually doesn't?* | ||
|
||
## Associated constants | ||
It's not any harder for associated constants, since these can be simply stored in the vtable along with the methods and associated functions. The only reason why this might become an implementation issue is that the vtable lookup code can no longer assume that all elements have the same size alignment. It's of high doubt that this assumption is ever made anywhere inside the compiler, though. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The compiler may have assumed that the v-table has the same layout as a [usize; N]
where N
is the number of elements in the v-table (drop info, and functions).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubt it ever did. Something like this:
struct Vtable {
size: usize,
methods_and_associated_fns: [*const (), N],
constants: (TypeOfConstant1, TypeOfConstant2, TypeOfConstantN)
}
would still work just fine for that purpose.
# Rationale and alternatives | ||
[rationale-and-alternatives]: #rationale-and-alternatives | ||
|
||
The constantly mentioned alternative is declaring the associated functions in traits which are meant to be trait objects with a `&self` parameter, effectively converting all associated functions into trait methods. This becomes a major source of confusion if the trait object is mutably borrowed elsewhere while this "`&self`ified" method is called. This introduces lots of unnecessary artificial curly-bracket scopes which hurt readability in most cases. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One way around this is through arbitrary self-types. Take a raw pointer as the receiver (although this isn't supported just yet, it could be). This way it is illegal to try and access the raw pointer (because it could be invalid), but even raw pointers have a valid vtable, so you can rely on that to dispatch on the required parts.
This way you must construct at least a raw pointer to access associated functions/constants.
This in combo with rust-lang/rust#64490 would allow you to ignore the borrow checker for these sorts of associated functions/constants.
|
||
That workaround is also wrong from a semantic standpoint, since trait methods by design should use the fields of the trait object in some way, either directly or indirectly. Again, an unused `&self` just to combat language limitations introduces confusion. | ||
|
||
Last but not least, "`&self`ifying" works terribly when the trait and the code creating and using trait objects of it are in different crates made by different people. In this case, the library developer has to add `&self` to the associated trait functions, hurting the code using the trait without trait object, which loses the ability to use its associated functions without an actual object of that trait. As a result, library developers would be forced to have the associated functions in their struct's main `impl` block and "`&self`ified" wrappers around these in trait implementations. This is incomprehensibly hacky and non-idiomatic, seriously hurting library design in the long run but inapparent in the beginning of development. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can always convert a trait into one that can be used as a trait object.
// lib a
trait Foo {
fn foo() {}
// ...
}
// lib b
trait DynFoo {
fn dyn_foo(&self) {}
// ...
}
impl<T: Foo + ?Sized> DynFoo for T {
fn dyn_foo(&self) { T::foo() }
}
Then, whenever you need dynamic dispatch you use DynFoo
, and this can be seemlessly integrated into an existing code base.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That workaround is also wrong from a semantic standpoint, since trait methods by design should use the fields of the trait object in some way, either directly or indirectly. Again, an unused
&self
just to combat language limitations introduces confusion.
As mentioned by the RFC, the "newtrait" solution is not less ugly. Why create a macro-worthy construct if you can implement the same thing idiomatically with a little bit of new syntax?
I should've guessed that, thank you! :) I'd think this RFC dies with the example in #2886 (comment) then. If one really needs this, then they've many available tools, like |
I've removed support for using trait objects with associated members in compile-time generics. The intended use of such trait objects in these generics is wrapping them into non-generic newtypes which expose the trait object dynamic dispatch via their associated functions. This means that the RFC is now much less broad. Hopefully this prevents this sort of unsolvable issues. |
I've two questions: First, could we make associated types and constants object safe?
Second, we cannot declare default implementations object safe, meaning this trait cannot be object safe:
But could we perhaps declare object safe versions of non-object safe items, like
Yes, we could just mark this method
except this cannot from a I think one answer goes
I suspect |
Not really. trait Foo {
fn get_value() -> usize;
}
fn foo<T: Foo + ?Sized>(t1: &T, t2: &T) -> usize {
T::get_value();
}
fn main() {
let tobject1: Box<dyn Foo> = Box::from(...);
let tobject2: Box<dyn Foo> = Box::from(...);
foo(&tobject1, &tobject2);
} The fundamental problem at the heart of this RFC is the contradiction between the following rules:
These four rules, taken together, are fundamentally incompatible. This isn't a small edge case that can be ironed out, this is a fundamental contradiction. Any RFC to expand type-safe traits needs to address it. My personal solution would be to have two types of template parameters: regular types and trait FooBar {
type AssociatedType;
fn get_value(&self) -> AssociatedType;
fn static_do_thing();
}
fn foo<T: Foo + ?dyn>(t1: &T, t2: &T) {
let v1 = t1.get_value(); // ok
let v2 = t2.get_value(); // ok
T::static_get_value(); // error, T might be `dyn Foo`
v1 = v2; // error, `t1::AssociatedType` and `t2::AssociatedType` might be different types
} But that's a pretty huge RFC, and it would probably come with a lot of breaking changes that would require an edition switch. |
You can already get that behaviour by adding a |
Well, yes, but in my imaginary proposal you'd be able to do things you can't do right now (eg have associated types and call static methods in specific circumstances). |
There are several more tangible goals here I think: First, Second, trait aliases should similarly work, ala Third, trait objects could exclude associated types and constants almost save exactly like they handle methods non object safe methods:
We're closer to language design here, but this still sounds pretty uncontentious. Fourth, an explicit |
I think another tangible goal might be
Any There are already several such methods in std including: In practice, rust would never use the syntax |
@burdges witb |
True. We actually could provide
|
The latest commit reworks the concept of object safety entirely, patching the edge cases related to use of generics with trait objects. While this may bring increased complexity to the language, this may be the only way this RFC could ever work. |
Hello! In today's "Backlog Bonanza" meeting, we discussed this RFC. Our consensus was that while we do feel the pain of having to add methods for access to constants and so forth from For those reasons, I am going to move to close. Thanks to the author for the suggestion. @rfcbot fcp close |
Team member @nikomatsakis has proposed to close 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 fcp reviewed -- I checked the boxes for those folks who were in the backlog bonanza meeting. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
Last but not least, "`&self`ifying" works terribly when the trait and the code creating and using trait objects of it are in different crates made by different people. In this case, the library developer has to add `&self` to the associated trait functions, hurting the code using the trait without trait object, which loses the ability to use its associated functions without an actual object of that trait. As a result, library developers would be forced to have the associated functions in their struct's main `impl` block and "`&self`ified" wrappers around these in trait implementations. This is incomprehensibly hacky and non-idiomatic, seriously hurting library design in the long run but inapparent in the beginning of development. | ||
|
||
# Prior art | ||
[prior-art]: #prior-art |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GObject (the runtime type system behind GTK, GStreamer, etc.) also allows for such things, which shouldn't be much of a surprise as the vtable is manually managed there.
You can put fields into the vtable (-> associated constants) or function pointers without an instance parameter (-> associated functions). This is used in quite a few places in GStreamer in practice.
Vala, a C#-inspired language on top of GObject that compiles down to C handles these as with the class
keyword AFAIU. Not to be confused with virtual
, which requires an instance parameter, or static
which can't be overridden/implemented by subclasses / interface implementations but exists only exactly once.
public class MyClass {
public class int some_field;
public class void some_function() { ... }
}
The final comment period, with a disposition to close, 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. The RFC is now closed. |
This RFC proposes a relaxation of the trait object safety rules. More specifically, it specifies a minor syntax addition for accessing associated functions and constants from a trait object, as long as the size of the results is known at compile time (acheived by using
Box<dyn Trait>
instead ofSelf
in associated functions and methods).Rendered.