-
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: Alter mem::forget to be safe #1066
Conversation
Alter the signature of the `std::mem::forget` function to remove `unsafe` Explicitly state that it is not considered unsafe behavior to not run destructors.
This effectively breaks any RAII pattern that depends on a destructor. What's the safe replacement that is guaranteed to run? |
|
||
Primarily, the `unsafe` annotation on the `mem::forget` function will be | ||
removed, allowing it to be called from safe Rust. This transition will be made | ||
possible by stating that destructors **may not run** in all circumstances (from |
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.
nit: “may not” sounds wrong here. “might not” would be better, I think.
@joshtriplett I believe our destructors are still guaranteed to run, unless you:
This is actually how I’d want to see the “destructors might not run” part defined, so it puts people at ease, @alexcrichton. We’re not Java after all, which runs finalizers at its own discretion. Otherwise, I’m undecided about this RFC. While, indeed, |
One thing the RFC doesn't mention is that it's not hard to work around this change in APIs like That said, we will at some point need to spell out some minimal circumstances in which you can rely on a dtor being run, if we want to fully justify a |
Your destructor running is just control-flow integrity. |
As @nagisa mentioned, this RFC doesn't mean that destructors won't be run, it just means that they are not guaranteed to run. All "normal circumstances" will still have destructors run. The current "replacement" for a destructor that's guaranteed to run is to construct a situation which you know avoids the pitfalls where the compiler does not run destructors. For example this means avoiding panicking from a destructor, avoiding
I do agree that the statement "destructors may not be run" may be a little weak, but I'm also somewhat wary of trying to exhaustively list either all location where leaks are possible or all locations where leaks are not possible. Perhaps we could try to list a subset of scenarios where leaks are bugs? For example this code should always run the destructor for fn foo<T>(x: T, f: fn(&X)) {
foo(&x)
}
This is actually true today as well I believe. Due to the fact that a panicking destructor or an |
It's not quite that simple today: rust-lang/rust#24292 (comment) |
Destructors not running if a panic occurs in a destructor is a very annoying bug. On the other hand, destructors of locals always run on a panic (because a double-panic = abort), and you can explicitly drop the local in other cases to handle it assuming control-flow integrity. If you don't put your struct inside of a |
@arielb1 I think we're in total agreement. All I'm saying is that, as with many other things, we will at some point need to write clear guidelines about what you can rely on and need to guarantee when writing unsafe code. The policy can't simply be "you cannot rely on destructors running". |
👍 I think this RFC the correct choice for 1.0. Once we release 1.0 with the ability to write a safe We can still add support for RAII guards post-1.0 by adding the Yes, there'll probably be many places that won't be updated to use |
Well, that depends on what you mean by "depends". :) If the destructor is cleaning up resources, then it continues to work fine. If the destructor is modifying state to move something from inaccessible to accessible (as with mutexes and RefCell, for example), that's fine too. It's just when the value will be accessible anyway but in an inconsistent state that you have a problem and want to use a closure (or perhaps some other mechanism, if we add one in the future). In general, the intention of this RFC is not to say that you can't rely on destructors to run (though there are some important caveats to consider...) but rather that when you relinquish ownership of a value outside of your control, it may get leaked and not run, so you have to consider that. |
Hi! I'm new here, so sorry if this is a rather uniformed point of view. One of the things I really liked about Rust is how it married C++-style RAII, a functional type system, and memory safety. This proposal essentially kills RAII! I think the alternative, guarantee that destructors run, is a much better option. This RFC renders guards of all sorts prone to leaking. I understand that "unsafe" means only "memory unsafe", but when writing real code, you care about other sorts of correctness, too. Guards are a very useful design pattern. I would argue they are more useful than RC! The RFC mentions some outstanding bugs that can lead to unrun destructors. As a user, I expect bugs, even in a release version. It's only 1.0! A vain attempt to rid Rust of all bugs in time for release seems a very poor justification for fundamentally changing (for the worse) the semantics of Drop. Bugs can always be fixed later. You will never get RAII back once you declare that Drop cannot be depended upon. I think that a solution to this problem should be aimed at fixing RC. RC is in many ways antithetical to the philosophy that Rust espouses and drew me to your project. [A]RC is essentially giving up on finding an owner. Most times I've seen reference counting used in C++, it was due to developer laziness. Please don't ditch a very useful design pattern, familiar to every C++ programmer, in the name of RC! |
@terpstra C++ has the same ceveats as we do. If you somehow leak an object (via a loop in refcounted pointer, for example) in C++ it won’t run the destructor either. |
To be clear, this RFC is not proposing any changes to any of the existing
Note that this will require an extensive audit as well to ensure that there are no other forms of leakage. It is also quite difficult (and may have a performance impact) to fix the existing bugs mentioned in this RFC. |
No. C++ will never move an object from the stack to the heap, in the Rust sense. The object on the stack is guaranteed to run its destructor, barring the sorts of things like your program dies. So, guards remain safe to use. A memory leak in the heap is something else entirely. C++ programmers are well aware that heap objects might live forever. |
"To be clear, this RFC is not proposing any changes to any of the existing #[stable] RAII patterns in the standard library. For example the lock() method on a Mutex will still return an RAII guard." That just makes this RFC even worse! Memory safety is not the only safety. Leaking a lock could be even more deadly than reading invalid memory. A straw-man of this proposal is: In other words: this proposal is willing to cause any other form of breakage, as long as it is not memory breakage. |
Rust already allows for many kinds of breakage that aren't memory-unsafety. |
+1 for restricting what we mean by safety, -1 for marking In the future if we come up with a design that fixes the issue with I think it should be fine to be restrictive about what is considered |
@terpstra First, C++ can move objects from the stack to the heap just fine. It does call the destructor on the stack thing, but since it was moved from, it won't close whatever resource we're talking about (assuming the type is properly written). Example: http://ideone.com/gmCR7I Second, deadlocks are possible without leaking guards/not calling destructors (e.g., by acquire the guards in an inconsistent order). Likewise, many other wrong things can be done without it being unsafe, not least because there is no clear, usable set of rules that would disallow all wrong uses while still allowing most of the correct ones. A programming language that catches all bugs is a programming language that can't do anything. |
@rkruppe Typically you make a Guard without a copy constructor. And C++ never "moves" (in the rust sense) anything other than plain-old-data. Classes always copy construct + destruct the old value. Obviously deadlocks and other bugs are outside of Rust's control. That's not the point. The point is that RAII is a useful design pattern and discipline used in avoiding bugs. This RFC proposes to take away its teeth, rending the design pattern useless. |
👎 I'm really not a huge fan of this proposal. In any long-lived application, it's useful to be able to rely on the fact that destructors will run if the program/thread continues to run. If a thread panics, or if the process aborts, not running destructors is fine - otherwise, they should run. The alternative is not being able to rely on side-effects running (e.g. if you're writing a library), something that I think is a mistake. A completely made up example, but: say you're writing a library that communicates over the network. Your Yes, you can just write the API another way, but this is the kind of thing that users will get wrong. Having such an easy way to shoot yourself in the foot seems like a bad idea. |
As for the RFC itself: I am very sad about this whole affair, and I don't think marking Marking Alternative: If we get at least some restricted form of negative bounds, there is a backwards-compatible way forward for guaranteed destruction: An opt-in trait (basically the complement of the |
@terpstra Anyway, I still don't quite like making |
@rkruppe: it wouldn't be backwards compatible to add negative Unless we do some pretty significant breaking changes pre-1.0, the default for unbounded generics will always have to be "safe to leak". |
I wish the RFC was this specific! Proposed doc for mem::forget doesn't have this information either. rust-lang/rust#25187
For the record I'm completely fine with that too. |
Personally, I'm curious what legitimate use mem::forget has that doesn't involve unsafe code. Every use case I know of involves FFI. |
I would like the ability to leak data that I know will live forever so that I can create cycles arbitrarily with |
@pcwalton Can you do that with |
It needs |
I have Generally, I think the fewer methods marked |
Dont forget to mark |
|
@bombless |
|
Sadly, this change makes implementing a safe DMA transfer a lot harder than before. You can no longer use a Especially because the reasoning behind this change doesn't apply for the DMA problem. Before this change, it was guaranteed that if an object contains a lifetime and a destructor, the destructor will get called before the lifetime is released. After this change this is no longer true. |
This isn't really true, or rather, I think it's besides the point. Consider: pub fn forget<T>(val: T) {
use std::{cell::RefCell, rc::Rc};
struct Foo<T>(T, RefCell<Option<Rc<Foo<T>>>>);
let x = Rc::new(Foo(val, RefCell::new(None)));
*x.1.borrow_mut() = Some(x.clone());
} |
@thomcc Point taken. Forget what I said, if it's possible to implement it with purely safe code, then of course it's safe. No questions asked. |
Alter the signature of the
std::mem::forget
function to removeunsafe
Explicitly state that it is not considered unsafe behavior to not run
destructors.
Rendered