-
Notifications
You must be signed in to change notification settings - Fork 48
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
Interoperability With C++ Destruction Order #135
Comments
This issue is not meant to be used for technical discussion. There is a Zulip stream for that. Use this issue to leave procedural comments, such as volunteering to review, indicating that you second the proposal (or third, etc), or raising a concern that you would like to be addressed. |
@rustbot second I'm happy to be the liason for this. |
old but relevant discussion can be found here: An issue regarding the default drop order rust-lang/rfcs#744, which was resolved by this RFC: rust-lang/rfcs#1857 |
It looks like this was seconded, and we just need to create an issue for it -- I"m going to do this! |
I created a tracking issue as described on the initiatives process. Based on the amount of engagement we saw on the zulip topic for the MCP, I am inclined to create a dedicated Zulip stream for this project. |
I'm closing this proposal as accepted. I do want to note that we've adopted a revised process around this kind of work. This particular work seems to qualify as an "experiment" with @cramertj as the liaison. |
Proposal
Summary and problem statement
Correct C++/Rust interop involves extensive use of
ManuallyDrop
to control Rust destruction semantics and match with C++, creating unpleasant API surfaces.Motivation, use-cases, and solution sketches
I'm working on "high-fidelity" C++/Rust interop. In particular, if C++ can work with values, I would like Rust code to be able to do the same, so as to allow seamless migration and interop between the two languages without unexpected design or performance changes. This is possible, even simple, using a library similar to
moveit
.However, the semantics around object destruction complicate this, forcing pervasive use of
ManuallyDrop
.What's so bad about
ManuallyDrop
?It makes the structs difficult to work with. For example, here is normal Rust or C++ code:
But with
ManuallyDrop
, it becomes:This ends up applying to every non-
Copy
field in every C++ struct.We would not accept that kind of user experience for native Rust types, and so, I believe, we should not accept it for FFI C++ types. Ideally, the user experience should be the same, even if there is some increased complexity behind the scenes when automatically generating C++ bindings (e.g. automatically generated
Drop
implementations.) The more difficult it is to use from Rust, the harder it is to port existing C++ code to Rust, or justify writing new Rust code in an existing C++ codebase.Destruction Order
C++, unlike Rust, has a fixed initialization and destruction order: fields are initialized in declaration order, and destroyed in the reverse order. This causes the lifetimes of each field to be properly nested: for a
struct S {T1 x; T2 y;};
, the lifetime ofy
is within the lifetime ofx
. This allows self-referential structs to have fields that contain references to other fields above them, without any risk of undefined behavior.In Rust, initialization order is not fixed (fields are initialized in the order they are specified in the expression that creates the struct), and so the destruction order can be anything. Rust chooses to destroy in declaration order, which means that for self-referential structs, you must actually place self-referential fields above the fields they refer to, and initialize in the reverse order.
For example, consider the following C++ struct:
The following equivalently laid out Rust struct would destroy in the incorrect order:
Correct destruction order, today
Today, the following Rust binding would work, because while
S
is nontrivial, it does not have a user-defined destructor. (That's a common case: for example, 99.8% of structs in my (large) codebase do not have a user-defined destructor, due to the way the Google C++ Style Guide pushes type design.)For as long as this type is present in both C++ and Rust, one of the two languages will need to override field destruction order using the language's equivalent of
Drop
, where every non-trivially-destructible (C++) or non-Copy (Rust) field is wrapped in that language's equivalent ofManuallyDrop
. This presents an ongoing API friction for users.Note: there is no other way to work around this, today. Any C++/Rust binding which works by calling destructors of the C++ subobjects individually must manually drop in order to drop in the correct order.
Invoking foreign destructors
In both C++ and Rust, destruction occurs in two parts:
Drop
in Rust)The only customization hook is #1: you can define
Drop
in Rust or the destructor in C++, and this is run in addition to standard destruction logic of #2.But when destroying an object, you don't get to pick and choose: in both C++ and Rust, you always destroy the entire object, including all subobjects/fields, rather than being allowed to only run part of the destruction logic and leave the rest for later. (This is true both for standard C++, as well as for specific C++ ABIs like the Itanium ABI.)
Therein lies the problem:
If we tried to create an equivalent Rust struct which invokes the destructor via FFI, we'd be stuck:
We can't fill in
Drop::drop
correctly: there is no way to call the destructor that doesn't also result in destroying the string field, which would be a double-destroy, because that field will also be destroyed in Rust by the drop glue.Correct foreign destruction, today
The only way around this is to wrap every non-
Copy
field in aManuallyDrop
, so that it is not destroyed by the implicit drop glue:Note: there is no other way to work around this, today. Any C++/Rust binding which allows calling the C++ destructor must manually (not) drop, to avoid double-destruction.
Alternatives
Suppress the drop glue via an attribute
We could add an attribute, such as
#[manually_drop]
(or#[manually_drop_fields]
), which suppresses the drop glue from dropping the whole struct: the fields will not be automatically dropped, but must be dropped by hand using e.g.std::ptr::drop_in_place
. The field is still dropped when directly assigned to, unlikeManuallyDrop
-- from a user's point of view, it looks like any other field, except that it isn't dropped automatically in the drop glue. (Since there's no sensible way to handle it, we should either forbid partial destructuring, or forbid destructuring entirely.)This is the most general proposal, which solves all the issues above.
In this case,
ManuallyDrop
would just be another type, defined as:To have C++-like field destruction order, one can mark the struct
#[manually_drop]
, and manually drop each field in aDrop
impl:And one can invoke a foreign destructor without double-dropping, by suppressing the drop glue with
#[manually_drop]
:(This could also improve union ergonomics: we could permit
#[manually drop] union U { x: T }
in place ofunion U { x: ManuallyDrop<T> }
. These features would also presumably interact in the obvious way: Unions would allow, in addition toCopy
types, any#[manually_drop]
type which has noDrop
impl. (Including, of course,ManuallyDrop
itself.))Reverse field drop order via an attribute
For codebases following the Google C++ Style Guide, the normal case for a struct (a class with public fields) is that it does not have a user-defined destructor. Therefore, it would suffice for the majority of types whose fields are accessible from Rust to simply adopt C++ destruction order, via an attribute that specifies C++ destruction order:
(I do not propose making this the default, now or ever -- it would only be useful for repr(C) structs, and only those which are representing C++ types.)
The structure of the attribute allows for expansion: presumably
from_end
andfrom_start
are present by default. Other drop orders could be:dont_care
oroptimal
or similar: choose the drop order based on what would be most efficient (e.g. drop in the order of the fields as they are laid out in memory).do_not_drop
ornone
: the same thing as#[manually_drop]
above.explicit
(contentious): one could e.g. write#[drop_order(explicit)] struct S { #[drop_order(1)] a: T1, #[drop_order(0)] b: T2}
.Links and related work
moveit
Initial people involved
Changelog
2022-01-06 (major)
ManuallyDrop
in C++ interop generally.DropGlue
trait to#[manually_drop]
attribute -- much simpler! Also made this the preferred alternativelManuallyDrop
as structurally pinned. (This solves the pinned unions issue only.)2022-01-10 (minor)
#[manually_drop]
: assignment still drops the field, should forbid (partial) destructuring.2022-01-11
ManuallyDrop
assignment example2022-01-20
ManuallyDrop
structural pin projection, unions, etc., as they are solvable without language changes.Summary of discussion in Zulip
<=2022-01-06
#[drop_fields_reversed]
or#[drop_from_end]
?"ManuallyDrop
, which is designed for this, isn't sufficient.ManuallyDrop
docs say not to use it for this, but to use implicit drop order. Also presents an API surface we wouldn't consider acceptable in non-FFI code.manually_drop
attribute which means that allfields are implicitly manually dropped and then it is the responsibility of the author to drop them in the destructor."
ManuallyDrop
to a pure library type, no lang item needed"ManuallyDrop
doesn't guarantee it's structurally pinned -- users could write their ownManuallyDrop
type which is, and it would be usable inunion
.fn drop (bar: Pin<&move Bar>, baz: Pin<&move Baz>)
, to avoid all the unsafe code that comes with#[manually_drop]
<= 2022-01-10
Drop
impl, which suppresses destructuring, so the destructuring question isn't critical.#[manually_drop]
only suppresses drop glue, not assignment etc. (Also, should probably suppress destructuring, because that would be unsafe.)<= 2022-01-11
drop_order(backward)
(ordrop_order(from_end)
) rather thandrop_from_end
. Needs a third order to justify -- perhaps "do_not_drop_fields
", which is what#[manually_drop]
is? (Note: "dont_care
" is suggested later.)#[drop_order(explicit)] struct S { #[drop_order(1)] a: T1, #[drop_order(0)] b: T2}
<= 2022-01-17
drop_order(dont_care)
which drops in the most efficient order.<= 2022-01-20
ManuallyDrop
cannot be (safely) structurally pinned due to the drop guarantee -- you'd need to guarantee the pinned interior is not forgotten.ManuallyDrop
is private, then you can pin-project it manually and uphold drop guarantee and other pin invariants. So e.g. to create a binding for a union, you would instead expose a newtype wrapper around a union, with accessor methods that do not directly exposePin<ManuallyDrop<T>>
for the fields.The text was updated successfully, but these errors were encountered: