-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Tracking Issue for feature(pin_static_ref): Pin::{static_ref,static_mut} #78186
Comments
…-tracking-issue, r=withoutboats Add tracking issue number for pin_static_ref Forgot to add a tracking issue in rust-lang#77726. Opened rust-lang#78186 as tracking issue.
Sorry for my ignorance, but how is this sound? It seems to trade on the idea that EDIT: Thanks for your help everyone, it turned out I had a flawed understanding of how reborrowing works. Cheers |
This has been resolved elsewhere, but effectively you cannot both call It's a bit subtle, but once you create a (I would love to have a compiler with one specific borrow exception, to demonstrate what exactly would be broken) Also, IIRC there was a soundness issue with |
I had this question as well.. the answer is that you can. But once you put your |
|
Hm, interesting. Use case (e.g. in the standard library): static MUTEX: MutexThatGetsVeryAngryIfItIsMoved = ...;
impl MutexThatGetsVeryAngryIfItIsMoved {
pub fn lock(self: Pin<&Self>) -> ... { ... }
}
fn something() {
let guard = Pin::static_ref(&MUTEX).lock();
...
} Instead of having all its methods unsafe with the requirement that it hasn't been moved, the methods require |
I think this is a (contrived) counterexample, demonstrating that // Invariant: the integer is 0, except behind shared *unpinned* references it can be whatever.
// There is no `&self` operation so we do not care about those values.
struct Nasty(i32);
impl Nasty {
pub fn new() -> Self {
Nasty(0)
}
pub fn new_shared() -> &'static Self {
// We seem to violate the invariant here, but there is no operation
// on `&self` so this is not a problem.
&Nasty(1)
}
pub fn check(self: Pin<&Self>) {
// It would be sound to cause UB for non-0 values here.
// IOW, this assertion can never fail (for safe callers).
assert!(self.0 == 0);
}
} With The example is somewhat similar in spirit to those examples which show that Of course this is a contrived example, but it does demonstrate that adding |
Is that really a promise that Pin makes though? https://doc.rust-lang.org/stable/std/pin/struct.Pin.html#safety does not really explicitly state that. It only says:
where pinned is defined a bit above as
By that definition, the |
The current docs don't say either way. Being English-language as opposed to formal docs, they don't make sufficiently explicit what exactly the requirement is. Adding This would also be the first proof obligation that special-cases the (FWIW, I agree that the current docs also don't rule out |
With your example, someone can currently already do let r = Nasty::new_shared();
// SAFETY: We know for sure *r is never invalidated or moved (or even modified at all).
let p = unsafe { Pin::new_unchecked(r) };
p.check(); Which should be perfectly fine according to the current documentation, right? |
The docs for
The thing is, what you rely on with |
'Pinned' is defined both here and here as a property of a pointee (not of a pointer), specifically that it won't be moved/invalidated/deallocated. So the object referred to by the reference returned by |
This is sufficient but not necessary for something to be pinned. This describes consequences of the entire pinning setup. But the underlying concept of the "invariant of the pinned typestate" is what makes this actually precise. And the notion of "moving data" does not come up there. Since data consists just of a-priori meaningless bytes, it is entirely unclear what "moving data" should even mean. The pointee returned by By the arguments you are using (in very broad analogy), interior mutability is unsound because "data behind a shared reference is read-only" so I can always |
FWIW, the principle behind this your example let r = Nasty::new_shared();
// SAFETY: We know for sure *r is never invalidated or moved (or even modified at all).
let p = unsafe { Pin::new_unchecked(r) };
p.check(); is even stronger -- I think you are basically saying that for shared data (types behind a shared reference), "pinned" or "not-pinned" must be equivalent. At this point we might as well add a safe method to turn I'm not on the top of my game today, but on first sight it seems to me this is what happens... also see chapter 3 of this blog post where I mused about the "shared pinned" typestate. Back then my model did not have it, but that post contains an example that needs it. |
No, we don't document shared references like that. Where is that quote from? On your blog post you say:
which matches with the definition of 'pinned' in the std documentation.
Huh, that makes no sense to me.
Shared data with a static lifetime. And as far as I know, we don't define 'not-pinned' or 'unpinned' anywhere, other than just the possible absence the guarantee that something is pinned. |
"Pin guarantees X" does not mean "Pin is equivalent to X". It means "Pin implies X". So this does not define Pin to be X, not at all. For example: |
This discussion is going nowhere. I give up for today. |
Many Rust tutorials explain shared references as read-only. For example, from the Rust book.
Note that the reference docs link there. It was one of the key insights of RustBelt that sharing really is its own invariant. The very same happens with pinning.
I might have spoken nonsense here, I'll re-think this once I am of a fresh mind. |
Ah, you wrote this while I was typing. ;) I agree we need to change our approach. Taking a step back here, we are using entirely different vocabulary to talk about the same thing. So each of us repeating ourselves over and over is not going to lead anywhere. I am not sure how to make progress, but I will think about finding more examples -- I've found those to be a great vehicle for conveying all the subtleties that surround a formal model of a type system like Rust's. You have articulated why you think What would help me is if you could try to explain what you think is wrong with this type. Which part of the pinning docs is violated by defining a type like this, and performing this kind of reasoning? This is the same kind of reasoning that underpins all formal soudness arguments for Rust types with internal unsafety. What I claim is that both |
(EDIT: I'm adding this note before sending the comment, but do note that I started writing it a few comments earlier in the thread) @RalfJung I'm trying to understand this thread, so correct me if I'm wrong, but it sounds like:
Now, there are three things I think we can all agree are important:
And given the above discussion I have these questions:
|
No, I'm sorry, but it's not. Formally, references are either shared or unique; mutability is orthogonal to this. There are several types that are safe to mutate through shared references (the cells, atomics, mutexes), and in general shared mutability is fine so long as the other rust soundness guarantees are upheld. Shared references being immutable is basically only a convention, albeit one that's assumed by the compiler and optimizer and requires |
@Lucretiel I am well aware. And yet, the borrow checker is entirely based on the idea that shared data is immutable; that is the justification for allowing shared references to be accessible from many functions and even threads. It just happens to be the case that we can slightly break that rule, and mutate some shared state under careful restrictions. The justification for this is extremely tricky -- the only proposal so far that properly justifies this is based on a form of "typestate" where shared data is subject to a separate invariant that needs to be suitably related to the "fully owned" "default" typestate. Almost all formal models of Rust ignore this, and just declare shared data to be read-only -- which is entirely correct if you only consider the borrow checker itself; it only breaks down once you also consider the standard library! (I am ignoring So, what essentially happens here is that types like
If it's assumed by the compiler then it's not a convention, it's part of the fudamentals of the language. :) So really, it's the other way around: the fact that we can mutate shared data is just a convention, and it works as long as we are sneaky enough about it such that the type checker doesn't have to notice.
Indeed, we follow the same approach that already worked so well for shared references to explain interior mutability. I don't know any other way to model pinning (or interior mutability), so I think it is fair to say that both of them are typestate in disguise -- even if their designers might not have realized this.
I assume "let people learn Iris and show them the formal definition" is not an acceptable answer? ;) I have sketched what I think the model should be in a blog post. That model is enough to give rise to guarantees like "pinned data cannot be moved" (for some reasonable interpretation(s) of what that statement actually means). It is also enough to justify safe API encapsulation for some simple self-referential data structures. I think it appropriately captures the intuition behind
It basically requires adding another "axiom" to the set of rules that the typestate needs to satisfy. This is allowing
I mentioned this is similar to examples showing that The rules that these typestates need to satisfy are necessarily a two-sided contract -- whoever defines a type with a custom invariant needs to ensure their typestate is sufficiently well-behaved; whoever is working with other people's types needs to not make any assumptions about their typestate beyond that basic notion of "well-behavedness". I think it is natural to make the list of axioms the typestate needs to satisfy as small as possible -- interior mutability shows that this provides a fertile breeding ground for creative (ab)use of existing types; and we could spend a lot of time thinking of more or less "reasonable" axioms we could add without any idea if those could ever be useful. (Also more axioms means more proof obligations for unsafe code authors.) So I think I want to push back on this 'assumed to be fixed based on what is possible in the language at the time' -- we have to fix the axioms somehow, so we make a kind of survey to see what axioms are needed and then cut that down to the barest minimum possible. I think this leads to much more informative and useful models than the alternatives (I am not even really sure what those alternatives would be). |
All that said, I don't feel like I have understood
Concretely, my question is: would anything go wrong if we added an operation that converts This may sound like a strange question when the intuition is entirely around whether "data is moved", but from a typestate perspective this is actually a rather obvious question: are there two separate "shared unpinned" and "shared pinned" typestates, or are those the same? This is not a question that the formal model can answer, it is a question that Rust has to decide about and then the formal model can reflect that answer. If the two typestates are identical -- if converting In the past, I felt like that conversion should probably be disallowed, but now that there is some desire to allow the conversion specifically for the |
I believe the usecase is having resources with "stable addresses" (such as C libraries that may keep their own pointers to the Rust-managed data), without having to always (@m-ou-se can explain better, but AFAIK the standard library already uses this feature in that way, for OS-provided synchronization primitives that require a stable address even when not in a "locked" state) For this usecase, Or in other words, Whereas |
A better example is
Yes, And more generally: A function taking a |
Yeah I am aware of the general point, but without a concrete example it is hard to tell how relevant that is.
|
RwLock is indeed an interesting example, thanks! I think I can make sense of how Details of my formal reasoning (probably not very comprehensible)
The key is that the shared unpinned invariant is a disjunction supporting both the case where the lock is already initialized and the lock where we are just a regular shared borrow of a fully owned uninitialized lock. Having a syntactic equality test in Basically, pinning actually starts immediately when sharing starts for the static lifetime; calling So, I don't think I have any fundamental objections left here. I am still not particularly happy about having to add another axiom though, and looking at those PRs made me wonder if it would not be a reasonable alternative to have a macro that creates a pinned |
I'm not sure how comfortable I feel with macros that provide safety guarantees based on shadowing etc. I guess it's commonly used by now so we'll just have to accept it. As for pinned statics specifically, I don't think it makes sense to add a macro. Avoiding having to explicitly call |
@rfcbot merge |
Team member @m-ou-se has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
I have no particular objection to these APIs, but I have to say, after reading the discussion above, I don't actually have a firm conceptual grasp of either why |
Also, these routines do not appear to be documented. Examples would be helpful for why someone might want to use them. |
Basically, this whole things comes down to the question of whether a Stabilizing this means accepting this and making those practical expectations match the theoretical promises about Pin. It means we accept that it's possible to make Ralf's example fail the assertion with only safe code. (Imagine there's a Edit: Note that the current standard library documentation (both here and here) already says that something that doesn't 'move'* is considered 'pinned', (vaguely?) implying that |
More specifically we are saying that anything that has a
I would say the docs say that for something to be pinned, it must not move -- which is an implication the other way around than what you claim. "Something doesn't move" is a rather vague term to pin down (heh), so we cannot reasonably use it as the definition of pinning, just in informal documentation.
If they do it for local statics that they control all access to, that's easy. Adding |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
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. |
…=scottmcm Stabilize pin_static_ref. FCP finished here: rust-lang#78186 (comment) Closes rust-lang#78186
…=scottmcm Stabilize pin_static_ref. FCP finished here: rust-lang#78186 (comment) Closes rust-lang#78186
…=scottmcm Stabilize pin_static_ref. FCP finished here: rust-lang#78186 (comment) Closes rust-lang#78186
…=scottmcm Stabilize pin_static_ref. FCP finished here: rust-lang#78186 (comment) Closes rust-lang#78186
…=scottmcm Stabilize pin_static_ref. FCP finished here: rust-lang#78186 (comment) Closes rust-lang#78186
…=scottmcm Stabilize pin_static_ref. FCP finished here: rust-lang#78186 (comment) Closes rust-lang#78186
…=scottmcm Stabilize pin_static_ref. FCP finished here: rust-lang#78186 (comment) Closes rust-lang#78186
Tracking issue for
#![feature(pin_static_ref)]
, as added by #77726.Interface:
@rustbot modify labels: +B-unstable +C-tracking-issue +T-libs
The text was updated successfully, but these errors were encountered: