-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Feature request: Preventing dropping fields of a dropped struct for better C++ interoperability #3006
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
Comments
@rustbot modify labels: -A-mir |
I don't think move assignment leaking is a good argument because it is incorrect in its own regard. To correctly assign a C++ value you must either invoke a move-assignment or a move-constructor for initialization, either of which might be defaulted or automatically generated (I don't know if there is still a difference in C++20). The Rust assignment allows you to effectively initialize with a memcpy which circumvents this. In C++ such an initialization is only sound for TriviallyCopyable types which requires that the type
In other words, it can't have its destructor do anything. Thus, exposing a value or |
Oh you're right, I forget that C++ can overload the move assignment constructor. But shouldn't
What exactly do you mean, when you say "exposing a value or &mut of a C++ object"? I'm going to try to write some macro magic to solve this problem by making |
For assignment it might result in the somewhat correct sequence, although strictly speaking in the C++ sense you would have to insert a call to Internally of course you can use a macro but then again you wouldn't have to strictly worry about the impact of being able to assign. |
Even if there is no such overload, any assignment goes through the implicitly defined operator of the compiler. That's pretty crucial in the object model of C++ in the presence of templated code. The memory model assumes that 'objects' have locations and gives a very limited way to change those locations, this assignment being one of them. Isn't that the basis for reasoning to allow them to be elided in RVO? |
Ok, that means we have problem, a big problem. The only solution I see for bindgen to not declare a
, but to use heap allocation instead if MyClass has overloaded its assignment operator. If you then use If a class did not overload its assignment operator, we can assume that it can be moved using memmove and generate the normal
instead. So we still need the |
Yes, basically. C++ is not a nicely embeddable language. You could use stack-local allocation as well, as in
If it is struct NoCopyOverload {
std::vector<uint8_t> vector;
NoCopyOverload& operator=(const NoCopyOverload&) = default;
}; |
Are you sure that it's TriviallyCopyable and not trivially relocatable? |
I do not know of the status of P1144 (whose number I only found after searching for your question) or another such attribute being accepted but as said I may not be up-to-date. In any case, there should be an inference where every trivially copyable type is trivially relocatable and all other types by user annotations. Providing safe access to |
Do you know good resources for learning details about what assumptions rust makes about memory? |
o.invar = ManuallyDrop::new(Inner{}); you can instead do *o.invar = Inner{}; which will run Now that doesn't completely solve "can't be automated nicely", but it's better at least. |
Hi! Additions or changes to the language need to go through our RFC process. Before an RFC is written, the feature can also be discussed in our internals forum to find other people interested in it. I've transferred this issue to the RFC repository. |
I think another option would be to create a pseudo-smart-pointer type, If the C++ class is called The Rust type When passing around references, you can just use |
I'm currently doing exactly this (see rust-lang/rust-bindgen#1906). |
C++ values are a very special kind values that are always behind |
I read about that one, but I still choose alloc and dealloc, because I don't know how how to construct the pinned object. We have to pin the data BEFORE calling the constructor, but before calling the constructor we only have uninitialized memory. We could construct a Box::pin::<MaybeUninit> type, but then we cannot convert that to Box::pin:: and we're stuck. The advantage of alloc and dealloc is that I'm fairly confident that I know what it does. |
I'm currently working on bindgen. The goal is to let rust code use C++ libraries as nicely as possible. There is one crucial element missing in Rust to achieve this:
Quote from the docs:
We need a way to prevent this and here is why:
Let's say your library looks something like this:
Using godbolt, we find that gcc compiles this to
The important part here is that calling ~Outer() calls ~Inner() and afaik there is no standard conform way to change this.
If we now want to use this library from Rust the naive way to do it would be
The problem here is that
extern_c_destructor_outer(&mut self)
callsextern_c_destructor_inner
(as we saw in godbolts output), and the recursive field dropping will callextern_c_destructor_inner()
again. Calling a destructor twice is UB.How can we prevent this? One way would be not impl Drop and just call the destructors manually, but I think we all know how incredibly tedious that would be. The second obvious way would be to use Options, similarly to how the nomicon does it, but this does not for us, because we rely on the structure having the same size as the C++ equivalent.
Another way you might suggest is wrapping it in ManuallyDrop:
But if for example, you want to reassign invar, you got a Problem, because this
has not only a terrible syntax, but also leaks memory if Inner allocates memory. Everytime you want to assign you would need to drop it manually and this can't be automated nicely, because you cannot overload the assignment operator in Rust. The last possible way would be to not make member variables public and use getters and setters. This would theoretically, but if the goal is to make the Syntax of using a C++ library as nice as possible and as close to the C++ original as possible this is not a good option.
The only good option I could think of is to add a feature to Rustc that lets you disable the recursive dropping. Then you could write:
And the
#[no_field_dropping]
beforestruct Outer
would tell rustc to not dropinvar
when dropping outer.I think this should be zero-runtime memory and cycles cost and very limited compile time memory and cycles cost. If I'm not mistaken, Rustc currently generates the calls to all destructors when a variable goes out of scope. If
#[no_field_dropping]
is set Rustc needs to only generate the call to the destructor of the struct, but not the calls to the destructors of the fields.The text was updated successfully, but these errors were encountered: