-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
dyn Trait comparison should not include the vtable pointer #106447
Comments
This was already discussed in lang triage meeting on 2022-11-08 (notes: https://hackmd.io/@rust-lang-team/SyDavQuri) when |
I agree with this in the narrow, since for equality it's not useful. But for inequality, one can rely on it today, no? For example, #[derive(Debug)]
#[repr(transparent)]
struct Foo(u8);
let f = Foo(1);
let p1: *const dyn Debug = &f;
let p2: *const dyn Debug = &f.0;
assert_ne!(p1, p2); // always passes today So is it possible that people could be relying on this side of it already? |
That's only true because trait T {}
impl T for u8 {}
impl T for Foo {}
#[repr(transparent)]
struct Foo(u8);
let f = Foo(1);
let p1: *const dyn T = &f;
let p2: *const dyn T = &f.0;
assert_ne!(p1, p2); // FAILS IN RELEASE MODE, passes in debug mode |
We could change that by making vtable not |
True, but that doesn't solve the issue where multiple instances of the same vtable are emitted in different codegen units. I feel that overall this is a footgun in the language which people are already having to work around by explicitly casting to |
Discussed in @rust-lang/lang today. Conclusions:
|
So today |
Well, x == y does mean that they will behave equivalently. The type might be different, but the behaviour is since the code is the same. |
I'm not sure that's true at the semantic level. The behavior is the same given the same bit patterns, but (if I understand correctly) hypothetically you could have two types with the same bit patterns that have differing meanings, and the same machine code would therefore have differing semantic meanings. Plus, we are now exposing implementation details of an impl at the level of language semantics. For trait objects I can't come up with a realistic example other than what @Amanieu showed above, though. |
I would add that merging of functions is not a purely hypothetical concern. For instance, I've seen it break backtraces where a function that calls
This can be super confusing, especially when combined with inlining, when lacking line numbers (or they're unreliable) in a backtrace, and when (3) is a large function! And while it doesn't threaten language semantics to break backtraces (they're always best effort), it's an example of a situation where reasoning only about a function's behavior is not sufficient to recover the semantic meaning. |
Fair, but I think it is close to the sense that I specified. It seems like we could prevent this merging by including e.g. type-id in the vtable. |
I think that Practically speaking, this leaves 2 options:
Either option is fine and would make dyn trait comparison have actually sane results. |
Crazy idea: deprecate pointer comparison in Rust 2024 in favor of some methods (cc @scottmcm) |
I'm trying to understand the scope of the problem that we face here.
|
@Amanieu wrote (and @tmandry commented on):
How is it ever observably a problem to treat two such cases as equal pointers? Given that the two vtables themselves are equal-enough to be merged, I would think that those two pointers must be contextually-equivalent to each other (in the sense that there is no way to construct a context that distinguishes them), and therefore reporting that they are equivalent is semantically valid. In other words, can you supply me an example of a context that can have differing behavior for |
The difference can be observed in that release and debug builds provide different results, which is usually a sign of a compiler bug and definitely very confusing for users who typically run their tests in debug mode. Here's an example with actual methods in the trait: https://play.rust-lang.org/?version=stable&mode=release&edition=2021&gist=892da2cc585f50f1f33dffe3488dd842 The vtable merging occurs if LLVM decides that all vtable methods have equivalent LLVM IR. This only works in recent LLVM versions with opaque pointer types. I don't think the Rust language should let the definition of what is "equivalent" be decided by a LLVM IR optimization. |
Do we have a survey of when people use I don't think I have a good sense of when people do that, and what they're attempting to accomplish when they do so. |
@rfcbot fcp close We discussed this issue in the lang team meeting again. Meeting consensus was that while we don't know the best way to resolve the behavior of Presuming this FCP completes, we could open an issue describing the problem today (but not proposing this solution) and linking to it. |
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! cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
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. |
As a small incomplete step, I've proposed EDIT: This was added to nightly in #116325 |
Add `ptr::addr_eq` Seconded ACP: rust-lang/libs-team#274 (comment) Tracking issue: rust-lang#116324 cc `@dtolnay` rust-lang#106447
Use `addr_eq` in `{Arc,Rc}::ptr_eq` Since it's made for stuff like this (see rust-lang#106447)
I would like to propose the following plan:
|
We discussed this in the rust-lang/lang meeting. Meeting consensus was that we are
Overall we want We do agree that it would be better to avoid Also, the bar is high for this change because it would be a subtle breaking change to existing stabilized behavior and could cause breakage (if someone was correctly relying on the invariant above). If the new rules were strictly better, that might be fine, but the new rules don't seem strictly better. |
Per the earlier FCP, I am going to close this issue. I appreciate @Amanieu your doggedness in trying to resolve this behavior. =) |
@scottmcm is opening a new issue to track the lint. |
On a personal note, I would much prefer to fix the existing behavior by adding more info into the vtable. |
Issue for adding a lint: #117717 |
…, r=davidtwco Add lint against ambiguous wide pointer comparisons This PR is the resolution of rust-lang#106447 decided in rust-lang#117717 by T-lang. ## `ambiguous_wide_pointer_comparisons` *warn-by-default* The `ambiguous_wide_pointer_comparisons` lint checks comparison of `*const/*mut ?Sized` as the operands. ### Example ```rust let ab = (A, B); let a = &ab.0 as *const dyn T; let b = &ab.1 as *const dyn T; let _ = a == b; ``` ### Explanation The comparison includes metadata which may not be expected. ------- This PR also drops `clippy::vtable_address_comparisons` which is superseded by this one. ~~One thing: is the current naming right? `invalid` seems a bit too much.~~ Fixes rust-lang#117717
…twco Add lint against ambiguous wide pointer comparisons This PR is the resolution of rust-lang/rust#106447 decided in rust-lang/rust#117717 by T-lang. ## `ambiguous_wide_pointer_comparisons` *warn-by-default* The `ambiguous_wide_pointer_comparisons` lint checks comparison of `*const/*mut ?Sized` as the operands. ### Example ```rust let ab = (A, B); let a = &ab.0 as *const dyn T; let b = &ab.1 as *const dyn T; let _ = a == b; ``` ### Explanation The comparison includes metadata which may not be expected. ------- This PR also drops `clippy::vtable_address_comparisons` which is superseded by this one. ~~One thing: is the current naming right? `invalid` seems a bit too much.~~ Fixes rust-lang/rust#117717
…twco Add lint against ambiguous wide pointer comparisons This PR is the resolution of rust-lang/rust#106447 decided in rust-lang/rust#117717 by T-lang. ## `ambiguous_wide_pointer_comparisons` *warn-by-default* The `ambiguous_wide_pointer_comparisons` lint checks comparison of `*const/*mut ?Sized` as the operands. ### Example ```rust let ab = (A, B); let a = &ab.0 as *const dyn T; let b = &ab.1 as *const dyn T; let _ = a == b; ``` ### Explanation The comparison includes metadata which may not be expected. ------- This PR also drops `clippy::vtable_address_comparisons` which is superseded by this one. ~~One thing: is the current naming right? `invalid` seems a bit too much.~~ Fixes rust-lang/rust#117717
…twco Add lint against ambiguous wide pointer comparisons This PR is the resolution of rust-lang/rust#106447 decided in rust-lang/rust#117717 by T-lang. ## `ambiguous_wide_pointer_comparisons` *warn-by-default* The `ambiguous_wide_pointer_comparisons` lint checks comparison of `*const/*mut ?Sized` as the operands. ### Example ```rust let ab = (A, B); let a = &ab.0 as *const dyn T; let b = &ab.1 as *const dyn T; let _ = a == b; ``` ### Explanation The comparison includes metadata which may not be expected. ------- This PR also drops `clippy::vtable_address_comparisons` which is superseded by this one. ~~One thing: is the current naming right? `invalid` seems a bit too much.~~ Fixes rust-lang/rust#117717
We store class instances as Rc<dyn ClassInstanc> values. The pointers to the inner values consist of 1. a pointer to the vtable of the trait 2. a pointer to the data itself Due to rust internals, the vtable pointer can be different for Rc<dyn ClassInstance> values that point to the same *data*, effectively making any comparison involing the vtable pointer moot. This uses a function rust just introduced to compare such pointers. (cf. rust-lang/rust#106447, rust-lang/rust#117717, rust-lang/rust#116325)
…twco Add lint against ambiguous wide pointer comparisons This PR is the resolution of rust-lang/rust#106447 decided in rust-lang/rust#117717 by T-lang. ## `ambiguous_wide_pointer_comparisons` *warn-by-default* The `ambiguous_wide_pointer_comparisons` lint checks comparison of `*const/*mut ?Sized` as the operands. ### Example ```rust let ab = (A, B); let a = &ab.0 as *const dyn T; let b = &ab.1 as *const dyn T; let _ = a == b; ``` ### Explanation The comparison includes metadata which may not be expected. ------- This PR also drops `clippy::vtable_address_comparisons` which is superseded by this one. ~~One thing: is the current naming right? `invalid` seems a bit too much.~~ Fixes rust-lang/rust#117717
…twco Add lint against ambiguous wide pointer comparisons This PR is the resolution of rust-lang/rust#106447 decided in rust-lang/rust#117717 by T-lang. ## `ambiguous_wide_pointer_comparisons` *warn-by-default* The `ambiguous_wide_pointer_comparisons` lint checks comparison of `*const/*mut ?Sized` as the operands. ### Example ```rust let ab = (A, B); let a = &ab.0 as *const dyn T; let b = &ab.1 as *const dyn T; let _ = a == b; ``` ### Explanation The comparison includes metadata which may not be expected. ------- This PR also drops `clippy::vtable_address_comparisons` which is superseded by this one. ~~One thing: is the current naming right? `invalid` seems a bit too much.~~ Fixes rust-lang/rust#117717
This was previously raised in #103763 (comment). Currently, comparing a pair of
*const dyn Trait
raw pointers for equality will check that both the address and vtable are equal.This is problematic because the vtable of a type is not guaranteed to be unique for each type: different types can share the same vtable and the same type can have its vtable instantiated multiple times.
To illustrate this issue, consider the following program which prints
true
in release mode andfalse
in debug mode because LLVM is merging the two identical vtables (playground).It is also possible to achieve the reverse effect where two
dyn Trait
pointers to the same value (with the same base type) compare unequal because a separate vtable was instantiated with each pointer (e.g. due to codegen units or separate crates).In conclusion, vtables are completely useless as a source of truth for pointer equality, so we should only consider the pointer address when comparing
dyn Trait
raw pointers. We currently document this as a footgun (#103567), but it would be better to eliminate this footgun entirely from the language.The text was updated successfully, but these errors were encountered: