Rust project goals: Immobile types and guaranteed destructors
Posted by paavohtl 6 hours ago
Comments
Comment by germandiago 15 minutes ago
I saw in D years ago how they also checked into this flexibility after getting some use cases for it (in this case, copying): https://github.com/dlang/DIPs/blob/master/DIPs/accepted/DIP1...
Comment by panstromek 2 hours ago
Comment by stymaar 5 hours ago
I'm very glad they found a way to add it eventually, as it's really filling a glaring hole in the language.
Comment by q3k 3 hours ago
Will this integrate with existing code that uses Pin<T>? If not this will split the ecosystem even further...
Comment by ordu 2 hours ago
Comment by Georgelemental 1 hour ago
Comment by ordu 52 minutes ago
Comment by mcherm 34 minutes ago
Comment by simonask 24 minutes ago
But I also suspect there are important differences between `!Move` and `Unpin` that I'm not sure about.
Comment by safercplusplus 40 minutes ago
Specifically, Rust's "necessarily-trivial-destructive" moves make it possible for a memory location previously holding a valid object to become invalid without a destructor (or any other handler) being called. Accommodating this possibility resulted in unforeseen (by many) limitations, particularly in the safe subset. (See "the leakpocalypse".) This was partially addressed by the introduction of "pinning" into the Rust language. The posted github page suggests that this sort of pinning is not the ideal approach, and that it is more effective to make the "unmovability" of an object a property of the object's type, rather than a property of the reference to the object, as is the case with the pinning approach.
To be clear, we're talking about Rust-style "necessarily-trivial-destructive" movability here. Traditionally, C++ doesn't really support this sort of movability. That is, even if an object's contents are ("conceptually") moved to a different location, the original source object remains (at its original location) until it is otherwise destroyed (and its destructor called). So in C++, all types are "immovable" in the sense of the posted github page.
The github page notes how these "immovable" types can support self-references completely in the safe subset in a way that pinning can't.
> This unblocks patterns that are currently impossible in safe Rust.
For an idea of some other unblocked patterns, you can consider so-called "norad" pointers [1] (and proxy pointers [2]) in the SaferCPlusPlus library. Analogous to how `RefCell` references can be used to express references that cannot be statically verified to conform to Rust's "aliasing-xor-mutability" restrictions, "norad" pointers can be used to express references that cannot be statically verified to be lifetime safe. This would include, for example, all manner of cyclic references beyond just "self-references".
I think you could implement a version of these norad pointers in Rust that can safely target these immovable types (whose destructor is guaranteed to be called while the object is still in its original location). But note that the C++ implementation uses static inheritance (which Rust does not support) to avoid the noise having to access the target object as "interior" content (like with `RefCell`s).
With the availability of these flexible references, one could imagine immovable types becoming popular in things like games / entity component systems, GUI frameworks, browser engines, and any place where "back pointers" would be convenient. One might even imagine that at some point, types being "immovable" could become the popular default for object types in Rust (among biological and/or non-biological Rust programmers). At which point, people may decide that actually they do want (the contents of) some of their immovable types to be "movable", but they don't necessarily need the object to be destructively movable. So you could imagine the introduction of standard `nondestructive_move()` (and `nondestructive_move_from()`) methods that would be companions of the existing `clone()` (and `clone_from()`) methods. At which point Rust would have counterparts for C++ copy and move constructors (and assignment operators).
In my view, this adoption of the C++ model (potentially) addresses Rust's main limitation. With one consequence being to potentially make automated translation of C and C++ code to (reasonable code in) the safe subset of Rust much more feasible than seems to be currently.
[1] https://github.com/duneroadrunner/SaferCPlusPlus/blob/master...
[2] https://github.com/duneroadrunner/SaferCPlusPlus/blob/master...
Comment by simonask 14 minutes ago
For example, if you have a `Pin<Box<Vec<u8>>>`, it's safe to turn that into a `Pin<&mut [u8]>`.
Any system that replaces `Pin` will probably have to maintain that same property, which wouldn't naïvely happen using C++-like move semantics, right? Or maybe I'm overassuming?
Comment by yccs27 5 hours ago
https://without.boats/blog/pinned-places/
Does this project goal mean that the rust maintainers have decided to implement @yoshuawuyts' immovable types proposal in favor of pinned places?
Comment by rienbdj 4 hours ago
Comment by Ygg2 4 hours ago
> # How does this relate to the "pin ergonomics" initiative?
> This work is an alternative to Project Goal 2025H2: Continue Experimentation with Pin Ergonomics, which includes the following extensions:
> A new item family pin in lvalues, e.g. &pin x, &pin mut x, &pin const x.
> A one-off overload of Rust's Drop trait, e.g. fn drop(&pin mut self).
> A new item kind pin in patterns, e.g. &pin <pat>.
> Notably, this work does not solve pin's duplicate definition problem, meaning that even with these extentions we still end up with Trait and PinnedTrait variants of existing traits. The Drop trait being the exception to this, since the initiative is proposing to special-case it using a one-off overload.
https://github.com/rust-lang/rust-project-goals/blob/main/sr...Comment by yccs27 1 hour ago
Comment by Tazerenix 5 hours ago
Comment by dubi_steinkek 1 hour ago
Comment by Tazerenix 8 minutes ago
Inferring the capabilities of the function from the traits of the types of the arguments is similar to tracking effects. The function charges `drop<T>` when `x: T` goes out of scope, which is handled by the trait implementation. If Rust had a proper algebraic effects type system, you would be able to see this directly in the signature of the function (and even more, if the trait impls themselves had their effects tracked, you'd be able to see from the signature of the function the side effects of deallocation of its owned variables, like if `drop<File>` performs `io`).
Comment by skitter 5 hours ago
Comment by simonask 4 hours ago
let txn = create_transaction();
// do something with the transaction
txn.commit(); // consume the txn
Right now, you can't implement this API without choosing between either silently rolling back unless the user calls `commit()`, or panicking in the Drop impl for the transaction if the user didn't explicitly call either `commit()` or `rollback()`.Your only current choice is to use closures, which are much less composable, because you need a variant for each flavor: infallible, fallible, async fallibe, etc.
start_transaction_async(async || { /* ... */ TransactionResult::Commit });
start_transaction_async_try(async || { /* ... */ Ok(TransactionResult::Commit });
Ick.If instead the transaction is a must-move type, you would get a compiler error if you fail to call exactly one of either commit or rollback, and particularly you would be forced to consider what happens at every exit point (early-out via `?` no longer just forgets the transaction). Very nice.
Comment by ordu 2 hours ago
Can you elaborate how it may work? I mean if I create a function:
fn fail_silently(txn: Transaction) {}
then the calling code would pass the compiler, but this function presumably isn't, ok. But what can make these functions to pass:
impl Transaction { pub fn commit(self) { ... } pub fn rollback(self) { ... } }
Would you need to destructure self or what?
Comment by yccs27 1 hour ago
Comment by vlovich123 1 hour ago
> How would you handle destructors with arguments?
https://smallcultfollowing.com/babysteps/blog/2025/10/21/mov...
Comment by melodyogonna 4 hours ago
Comment by virtualritz 3 hours ago
That is a big lever language designers can use if they painted themselves into a corner.
Comment by yccs27 1 hour ago
Comment by simonask 43 minutes ago
In fact, that's exactly how I would expect it to work, but there may be non-obvious drawbacks.
Comment by dubi_steinkek 1 hour ago
Comment by simonask 45 minutes ago
Should it be possible to construct a `Vec<T>` whose size can never change? Is there a subset of Vec's API that can be annotated with `where T: ?Move`? These are all important design questions, with the potential to break 99% of existing Rust code.
Comment by OskarS 5 hours ago
Isn’t that why mem::forget is safe, because you can always implement it yourself safely? How do you get around that?
Comment by stymaar 5 hours ago
But there's an easy solution for that: you make the reference-counted smart pointers require their pointee type to be Forget. It will be like how Arc<T> doesn't implement Send unless <T: Sync>.
Comment by klauserc 2 minutes ago
Could of course be plugged by saying `!Forget : !Send`, but wouldn't that preclude legitimate useful scenarios for `!Forget`?
Comment by skitter 5 hours ago
Comment by hnc99rxjlw 1 hour ago
Comment by suddenlybananas 6 hours ago
Comment by simonask 5 hours ago
Currently, Rust has scoped threads: Threads that are guaranteed to terminate before the function that spawned them returns. This is powerful because it allows you to pass references to data that lives on your own stack to threads that you spawn, without any bookkeeping or synchronization mechanism - just the normal borrow checker rules.
For example, you can allocate a large array, then split it into multiple non-overlapping slices, and then have a group of threads populate each slice, all in safe Rust code.
But the same isn't true for async tasks in Rust, because futures are just objects representing a state machine, and they don't get any special treatment. In particular, they carry no guarantee that the state machine will actually run to completion, which is fundamentally different from how functions run (stack frames are guaranteed to unwind in some way, either by returning or panicking, unless the entire program has terminated).
To make the situation worse, there are many cases where Rust futures are much more prone to cancellation than synchronous code, because that is also one of the big benefits of using async in the first place - for example, you may be running multiple futures in parallel, pick the result from the one that finishes first, and then cancel the rest.
Getting this stuff under control is why people say that "async cancellation" is a difficult problem to solve, and that is true in all languages that have async. These traits will hopefully make it much easier to work with in Rust.
(There are also many other interesting things you could do with this, unrelated to async. Immovable and unforgettable are both interesting properties of an object that could be used to design many cool APIs in general.)
Comment by pornel 1 hour ago
It doesn't really add anything new and flashy, but removes some annoying warts.
Sync code has scoped threads that enable multi-threaded execution within a function, without having to ensure the data outlives the function call. Async can't do that while guaranteeing safety. This makes tokio::spawn awkward and annoying, and is a major source why people dislike Rust's async.
Low-level async code that polls Futures requires using the Pin wrapper type, which is unergonomic, and doesn't really guarantee safety, but it's more like a "be careful here" sign. Proposed changes would make that code look more like normal Rust and work without unsafe escape hatches.
Comment by simonask 30 minutes ago
It's worth mentioning that there is, in fact, no language out there other than Rust that can even do this in the first place.
Some languages give the illusion that they support it by boxing the stack frame of async functions and letting a garbage collector deal with the consequences, but that comes with significant drawbacks too (additional GC pressure, heap allocation overhead, requiring a GC in the first place).
You can do it with C++ coroutines, but it's much harder to do correctly than in Rust if you want to maintain any sense of conviction that the system is correct.
The main reason that structured async concurrency would be so awesome to have is that it feels like Rust has the right set of features that could enable it with a set of constraints that are so much more attractive than any other language out there can provide - no overhead, "just works" with no drawbacks.
(For the record, you can actually get pretty far today using primitives like `FuturesUnordered` instead of `tokio::spawn` and similar, but this sidesteps the runtime's scheduler, so YMMV. This basically creates a task-local mini-scheduler for your futures, which may or may not be sufficient.)
Comment by aabhay 5 hours ago
All really awesome, non controversial and ergonomic things.
Comment by simonask 27 minutes ago
The problem today is that the compiler-synthesized struct implementing `Future` for each async function cannot contain an instance of itself without boxing, because it would create a type of infinite size. That's a separate problem that's also hard to solve nicely, because the call tree might be deep, and deciding where to cut (using Box::pin) is non-trivial.