-
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
RFC: #[derive(SmartPointer)] #3621
RFC: #[derive(SmartPointer)] #3621
Conversation
Co-authored-by: Lukas Wirth <lukastw97@gmail.com>
My main thought is that maybe this should just require repr(transparent), which simplifies a lot of the explanation of the requirements part. |
I don't think there should be a hard requirement that smart pointers be transparent. But since the requirements are very similar, it might make sense to incorporate the requirements by reference, for simplicity of documentation. |
After doing a bit more research to get myself acquainted with the What these traits really are about is ensuring proper variance, which is especially weird since variance is usually only exposed to programmers in Rust through weird mechanisms like
Smart pointers require both of these just by the nature of how references in Rust work in general: because autoderef lets us borrow things mostly automatically, we expect this automatic borrowing to happen even if a smart pointer is between us and our data. However, it's important to realise that this is just about variance and we only tie it specifically to smart pointers because unsized types in Rust today have to be used behind pointers. If we allow unsized locals, whose RFC was accepted, now these types become even more just about variance. You should always be able to pass arrays into functions which accept unsized slices, return unsized trait objects by returning a concrete object, etc., etc. Honestly, thinking of this more specifically as a way of managing variance, I do question whether the current method of manually implementing these traits is the right approach, and whether we should rely more on something like Of course, we can't just use
Actually, the allocator API runs amok with this current proposal, too, since it would require that an allocator be zero-sized. If we reframe the proposal from the perspective of unsized types (treating smart pointers as implicitly always sized), this "single field" instead turns into "last field," like the current unsized requirements. I believe that zero-sized types are also not allowed to be after the unsized field of a struct, but we could probably amend this pretty easily. Maybe we could have some kind of
Basically ensuring that everything works as you'd expect. A few footnotes:
|
@clarfonthey: A few things regarding your last comment-- It's not correct to call the relationship between Their relationship is called unsizing in Rust, which is an explicit coercion operation inserted into the MIR and involves actually changing the type layout of pointer. Also,
Also the last field requirement you mention is a limitation of data being
(disclaimer that i've been too busy to read the actual rfc yet, just wanted to clarify some stuff before people started also responding and a whole thread gets generated based off of false premises)
Footnotes
|
Oh, huh, I was under the impression that I just assumed that allocators were kind of in the same boat, where they could be present like the counts for references but would be peeled away somehow, also like those. In terms of my mention of contravariance: I definitely am jumbling up terms here since generally what people mean by variance is about subtyping and supertyping, whereas I'm using it to mean literally varying the type via the unsize and dynamic dispatch operations you describe. It's a sloppy analogy for sure, but the main reason I used it is to point out that it shouldn't be explicitly derived and instead inferred somehow like variance currently is. I guess in that sense, it's closer to the mathematical definitions of stuff like homomorphisms, where instead of stating that two things have identical properties, you have the existence of operations which can be applied while retaining certain properties. Subtle differences but similar analogies. |
The last-field limitation actually means nothing ABI-wise since the Rust layout allows reordering the fields use std::{ptr::NonNull, mem::offset_of};
struct MySmartPointer<T: ?Sized> {
extra: u16,
interior: NonNull<T>,
}
fn main() {
type P = MySmartPointer<usize>;
assert_eq!(offset_of!(P, interior), 0);
assert_eq!(offset_of!(P, extra), 8);
} So, to call
However, since |
the Rust compiler is what chooses what order the fields are in, which also means the Rust compiler can always leave the unsizable field at the end, as I'm guessing it currently does. This means conversion from/to |
I can see how But I disagree with calling |
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.
Is there a reason to use the fully qualified path of Sized
for U
? For T
the prelude is used.
My example code in #3621 (comment) already showed it does not. We are not talking about maybe-unsized struct tail here, but a pointer to a maybe-unsized type which can appear anywhere. struct X<T: ?Sized> {
extra: u16,
tail: T, // yes, this field is guaranteed to be at the end of X<T>
}
/* layout of X<T>:
+---------------+
0 | extra |
+---------------+
2 |x(padding)x x x|
| x x x x x x x |
4 |x x x x x x x x|
| x x x x x x x |
6 |x x x x x x x x|
+---------------+
8 | tail |
| (*actual |
A | offset |
| depends on |
C | align_of T) |
| |
: :
*/
struct P<T: ?Sized> {
extra: u16,
ptr: *const T, // note that it is a pointer here, there is no guarantee about offset of `ptr` in P<T>
}
/* layout of P<T>:
+---------------+
0 | ptr |
| ~~~~~~~~~~~> +---------------+
2 | | | *ptr |
| | | |
4 | | : :
| |
6 | |
+---------------+
8 | extra |
+---------------+
A |x(padding)x x x|
| x x x x x x x |
C |x x x x x x x x|
| x x x x x x x |
E |x x x x x x x x|
+---------------+
*/
Coercion from |
|
||
Whenever a `self: MySmartPointer<Self>` method is called on a trait object, the | ||
compiler will convert from `MySmartPointer<dyn MyTrait>` to | ||
`MySmartPointer<MyStruct>` using something similar to a transmute. Because of |
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.
It's actually worse than a transmute of these values, we're effectively transmuting the function pointer. This means the two types do not just have to be layout compatible, they have to be function call ABI compatible. That's also why allowing extra fields in these types is pretty difficult.
We actually currently rely on them being the equivalent of transparent. The only reason DispatchFromDyn does not just check the The way we call arbitrary-self receiver functions via vtables relies on repr(transparent) -- specifically, it relies on ABI compatibility. There's a subtle invariant here that all instances of repr(Rust) types that follow the repr(transparent) rules are indeed ABI-compatible to their one non-1-ZST field even if the repr(transparent) attribute is not present. I hope all our ABI adjustment logic ensures this invariant (and we have tests covering the basics), but this is easy to get wrong. This would be much easier if we could rely on repr(transparent)... |
Is there any plan to make this work with allocators at all, or are we basically stuck with this design for the foreseeable future? Since as I mentioned, the current RFC proposal would also be unable to do anything with non-zero-sized allocators for reasons mentioned. I'd guess that kernel development would at best be dealing with ZSTs anyway since storing an extra pointer or something like it would be undesirable for performance reasons, but it's still a question that feels worth asking. What I'd imagine would happen here is the fields for the allocator would always be after the pointee to ensure that you can still cast the pointer and have it Just Work, but that requires a bit of compiler help. |
Extending DispatchFromDyn to be able to cover more cases (i.e., to handle types that are not just newtyped pointers) is a non-trivial topic on its own, I'd rather not derail this RFC by going there. |
One question I have is about FFI smart pointers, which were one of the key use cases for arbitrary self types. If I understand correctly, these smart pointers cannot easily be e.g. It seems a little awkward to call this derive Is there perhaps a way where the FFI smart pointers could be made compatible with this? By having dyn metadata (manually?) placed in a second field, perhaps? |
@davidhewitt I think you underestimate the extent to which something like pub struct ARef<T: AlwaysRefCounted> {
ptr: NonNull<T>,
_p: PhantomData<T>,
}
pub unsafe trait AlwaysRefCounted {
fn inc_ref(&self);
unsafe fn dec_ref(obj: NonNull<Self>);
} Making this work with All that said, mixing FFI and |
This RFC is clearly the result of a lot of thorough discussion and iteration. It anticipated many of my questions with better answers than I expected. Thank you for pushing it forward, @Darksonn, and to the many others who contributed. After reading it I agree that On the @rfcbot reviewed |
I do not mind making it require repr(transparent) but the actual implementation of that is probably somewhat cumbersome. The existing requirements about repr aren't verified syntactically, but by the compiler in it's built in checks for implementations of DispatchFromDyn. However, we can't add the repr(transparent) check there because that wouldn't work for some standard library types, so we would probably need to add a new unstable AssertTransparent trait with a similar verification and emit an implementation of that. |
Until now |
I agree that the RFC should mention the point about the derive not matching any trait name (either as a drawback, or in the unresolved questions by expanding the name bikeshed to include the possibility of a regular attribute). While this would be a first for the standard library, it matches the way user-defined derives work (matching the derive with a trait name is by convention only) and it's only meant to be used by advanced users. So I don't think we have to be too careful about violating their expectations. |
That's already mentioned in the drawbacks section of the rfc. |
… r=davidtwco SmartPointer derive-macro <!-- If this PR is related to an unstable feature or an otherwise tracked effort, please link to the relevant tracking issue here. If you don't know of a related tracking issue or there are none, feel free to ignore this. This PR will get automatically assigned to a reviewer. In case you would like a specific user to review your work, you can assign it to them by using r? <reviewer name> --> Possibly replacing rust-lang#123472 for continued upkeep of the proposal rust-lang/rfcs#3621 and implementation of the tracking issue rust-lang#123430. cc `@Darksonn` `@wedsonaf`
Rollup merge of rust-lang#125575 - dingxiangfei2009:derive-smart-ptr, r=davidtwco SmartPointer derive-macro <!-- If this PR is related to an unstable feature or an otherwise tracked effort, please link to the relevant tracking issue here. If you don't know of a related tracking issue or there are none, feel free to ignore this. This PR will get automatically assigned to a reviewer. In case you would like a specific user to review your work, you can assign it to them by using r? <reviewer name> --> Possibly replacing rust-lang#123472 for continued upkeep of the proposal rust-lang/rfcs#3621 and implementation of the tracking issue rust-lang#123430. cc `@Darksonn` `@wedsonaf`
@rfcbot reviewed |
@rfcbot resolve require-repr-transparent I don't have strong feelings regarding whether the macro should check for it syntactically or use an internal marker trait. I'm happy to leave how the requirement is checked up to compiler folks in the implementation. |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
The current state of this RFC is a little confusing on the requirement of The "Input Requirements" section lists that it must be transparent as a requirement, but non of the examples include a repr attribute on the structs. And that requirement isn't mentioned in the "Requirements for using the macro" section in the guide. It also isn't very clear why the AssertReprTransparent trait is needed. |
CustomPointerI would recommend naming the derive CustomPointer(T)Mayhaps the syntax to specify the pointee type should be It would avoid collision with any other derive/macro. Truly Custom PointersI apologize if I missed it in the RFC, is an inner pointer (somewhere) necessary for this trait to work? It sometimes is useful to create "pointer-like" objects which internally use an offset, either for compression ( It's not essentially -- especially if we consider the trait a stop gap measure -- but may be worth mentioning. |
Yes, both CoerceUnsized and DispatchFromDyn directly modify the inner pointer without going through |
Jumping onto the bikeshed train, I'd like to note that the spelling "ptr" is far more common in std than "pointer". In particular, all items of the form "foo pointer" are named |
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. |
And add #[repr(transparent)] to the examples.
@tmccombs Thanks. I removed the unnecessary details about how |
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.
Huzzah! The @rust-lang/lang team has decided to accept this RFC. Please follow along on rust-lang/rust#123430.
Use custom smart pointers with trait objects.
Rendered
Tracking issue: rust-lang/rust#123430
Co-authored by @Darksonn and @Veykril
Thank you to @compiler-errors for the original idea