Pin
without.boats
without.boats
It’s only true if the object is not Unpin, but most normal objects are Unpin, so Pin usually does nothing. It took me a very long time to finally understand this. The set of types T for which Pin<T> does anything at all is very niche and weird and IMO this isn’t sufficiently highlighted by the documentation.
I also do think the documentation has improved a lot over the years. I was surprised when I checked it while drafting this that it seemed to focus on the right things pretty well; circa 2019 I remember it being a lot more focused on specifying the contract in a way that really belongs in something like the Rust reference and not the std API docs.
Instead, it operates as more of a "bring your own meaning", where the provider of the InnerType further creates any number of (internally unsafe) methods and APIs to manipulate a pinned object soundly. The only purpose of Pin<P> itself is to provide a pointer with fewer "intrinsic capabilities" (e.g., swapping &muts, moving out of Boxes, etc.), so that the inner type can allow further capabilities on top of that.
I suspect it's this nebulousness of meaning that confuses people the most. It certainly took me a fair while to figure it out. All the ideas about structural vs. non-structural fields are just to facilitate popular access patterns like "this one field is just plain old data, but this other field contains an object that itself wants to be pinned".
This giving up of certain rights gives other rights (such as to store self-referential values), which are the reason you give it up. This is how contracts between components just work: similarly, giving up the right to mutate through a reference allows you to alias the reference at the same time. I'm always reminded of this line from Lincoln, about a very different and much graver subject: "If we submit to law, Alex, even submit to losing freedoms - the freedom to oppress for instance - we may discover other freedoms previously unknown to us."
I do agree that the fact that you can't use those rights in safe code is an educational problem, because one can't easily demonstrate what you can do with a pinned reference except "call a poll method which the compiler has generated for you."
Sure you can: it's just that the target has to provide its own methods for it. It's perfectly valid (if pointless) to write
struct PracticallyUnpin(..., PhantomPinned);
impl PracticallyUnpin {
fn unpin_mut(self: Pin<&mut Self>) -> &mut Self {
// SAFETY: We're the ones writing the rules here
unsafe { Pin::into_inner_unchecked(self) }
}
...
// (no other unsafe methods or impls)
}
and then the caller can do whatever they want with that reference, e.g., moving the value. Unpin isn't a magic word: it's just a generic way for the target to indicate that pinned pointers can safely regain full capabilities.Of course, putting a Pin around an object does further restrict what you can do with it generically in unsafe code. But I'd further count these under the umbrella of "intrinsic unsafe capabilities", which Pin removes, but which the target can later restore ad libitum. Compare the question of whether a fn replace_with(&mut T, impl FnOnce(T) -> T) (aborting on panic) is sound, which really comes down to the "unsafe capabilities" of a &mut reference.
This is not to say, of course, that the target need not be very circumspect about which capabilities it restores! It has a responsibility not to create an unsound interface that might result in UB under permitted usage. E.g., if we have a type that owns a generic non-Unpin future, then that type must follow the strictest Pin invariants w.r.t. that future. But otherwise, it's entirely up to the target type which capabilities it wants to restore under which circumstances.
1. If this were public, or if you ever move out of the reference you get from that function, you would be violating the pin contract so this code would then be invalid.
2. Since its impossible to depend on the pin contract generically and this type doesn't actually depend on it, this is really just an indirect equivalent of implementing Unpin for the type, which is safe, so this code is valid.
I do think the second interpretation is correct (and I think its what the UCG group has decided), but this is a really nuanced conversation about the interpretation of unsafe code and validity. You started this thread by saying this is the reason pin is difficult for users: I am completely certain the median user is not in the weeds about what is and isn't valid unsafe code in pointless hypotheticals; the totally different set of idioms to get the same behavior for pinned references as for ordinary references is a much more pressing issue.
I agree that the latter is indeed a pressing issue. But my point is that the former isn't about unsafe code so much as safe code: safe users of a concrete pinned target type may observe any number of different API surfaces, some of which allow modifying or swapping out various parts of the pinned object.
For a practical example of this, when the target type uses one of the pin-projection crates, it has a choice in which fields to denote as structural or non-structural, which affects how much the user can modify each field down the line. Indeed, a type could usefully choose make all of its fields non-structural, if only its address is externally referenced.
The pinning invariants simply don't fully constrain what the user of the target type can and cannot do, except for those constraints needed for the target type's own soundness.
There is a more elegant design which is basically reversed from pinning, but that's backward compatible with Rust. I also describe that in the next post. This pinned fields vs unpinned fields aspect of the behavior is the "necessary accidental complexity" of existing in the context of all in which we live and what came before us.
My usage of pin essentially comes down to "I try something, and the compiler complains at me, so I pin stuff and then it compiles."
It's never been such a hurdle in the day to day coding I need to do that I've been forced to sit down and truly grok it.
Whereas in C++ I was regularly treading in the water of "stuff I don't know, but must use" and getting eaten by crocodiles
1. Velcro hooks do not stick to smooth surfaces: Pin -> Velcro, Unpin -> Smooth
2. Magnets do not affect non-magnetic materials: Pin -> Magnet, Unpin -> NonMagnetic/Glass/Brass
3. Glue does not adhere to non-stick surfaces: Pin -> Glue, Unpin -> NonStick
This way, it becomes clear that a "Velcro" fixes an item in place, and if an item is "Smooth," it is unaffected by the "Velcro" mechanism.
Given Rust’s ecosystem naming themes it would have been beautiful to rename the trait to something something magnet and non-magnetic :’)
Isn't it that `Unpin` means the object is always ready to be pinned? (I read the article last night and already forgot whether pinning requires a fixup step)
So a `T: Pin + !Unpin` is like a sheet of paper that can only be fixed by stapling it, but a `T: Pin + Unpin` is like a painting with a hook which can be mounted on a nail and unmounted without damaging the hook
- the double negative of „!Unpin”
- Rust trait names are not adjectives
Pin + Detach(-able) would be a less confusing name.
That being said, you can velcro smooth objects all you want, but you'll still separate (Move) them easily :) or put a magnet to glass/brass objects.
PS mind you that Pin is not a trait, only Unpin is, so Pin<T: Unpin> or Pin<T: !Unpin> is a more accurate way or writing what you described :)
I don't claim to know the answer either, but it reminds me of a great talk from Dave Abrahams, who worked on the value semantics for Swift together with Chris Lattner (who started Mojo). The talk is "Value Semantics: Safety, Independence, Projection, & Future of Programming" [0]
Not being able to store references in objects does solve the problem of "self-referential structs" in that you just can't implement code like the code Rust compiles to, but that isn't at all what the quoted paragraph says about Mojo so I am quite lost as to what they mean.
> So far, we've talked about values that live in memory, which means they have an identity (an address) that can be passed around among functions (passed "by reference").
Source: https://docs.modular.com/mojo/manual/lifecycle/life#trivial-...
---
If their self referential structs require indirection, I agree that they're weaker then what's available in Rust. Hopefully they provide more details at some point. The "No pin requirement" section in particular focused on Mojo's async ergonomics, not Mojo's async perfs.
Maybe there could have been a way to opt in to moving-via-reference for those rare cases when you need it. Or maybe this whole problem could have been avoided by making swap and replace unsafe? I'd love to see someone explore that design space.
Rust has to be backward compatible and already decided you can move out of an &mut, but there are definitely cleaner designs that can be, unburdened by what has been.
It feels like there's very few communities that people going so in depth publically about the nitty gritty of their language, and it's so cool to see.
I did get annoyed recently by the trait async stabilization that promised us a good trait-variant [1] which has been abandoned. Makes it so much harder to build a library without it.
Though I would add that Rust's async is not just about webdev; it has had success in embedded contexts e.g. the popular https://github.com/embassy-rs/embassy?tab=readme-ov-file#emb...
I think the need for Pin could then disappear, because the user would have no way to destroy it, since it's opaque and elsewhere on the heap, and therefore no way to move it (because having move-constructors implies that moving is conceptually destroying then recreating things).
This has a very nice effect of allowing merging and inlining of Futures before they're executed.
It's similar to how Rust does immutability — there's no immutable memory, only immutable references.
Almost all types would #[derive(Move)], which implements a trivial move function that copies the bytes. But this opens the door to self-referential types, futures, and lots of other things that need more complex move behaviour. (Actually, it might make more sense to define two traits, mirroring the difference between Copy and Clone. One is a marker trait which tells the compiler that the bytes can just be moved. The other allows custom "move constructor" implementation.)
I want move because pin is so hard to understand. Its a complex idea wrapped in double- or sometimes triple negatives. fn<X: !Unpin>(...). Wat? I drop off at unsafe pin-projecting. When is that safe? When is it not? Blah I'm out.
Moving from rust-without-move to rust-with-move would be inconvenient, because basically every struct anyone has written to date with rust needs #[derive(Move)] to be added. Including in std. And all types in existing editions that aren't pinned would need the compiler to infer a Move trait implementation. This should be possible to do mechanically. It would just be a lot of work.
Async rust is horrible. Especially compared to futures / promises in almost any other language. At some point, someone will make a new rust-like systems language which has an improved version of rust's memory safety model, a Move trait, and better futures. I'd personally also love comptime instead of rust's macro system.
I love rust. I love all the work the team has put into it over the years. But the language I’m really looking forward to is the language that comes after rust. Same idea, but something that has learned from rust’s mistakes. And it’s increasingly becoming clear what that better rust-like language might potentially look like. I can't wait.
I think a lot of people think pin is confusing but don't actually try to learn it. When I've sat with people and helped them they understand pretty quickly what pin solves and how it works.
I very strongly think move constructors would be even more complex than pin.
> Implementing the Unpin trait for T expresses the fact that T is pinning-agnostic: it shall not expose nor rely on any pinning guarantees.
So, ??. Then there's macros for pin-project, which most projects in the wild use, but some are unsafe. Why? Which of my fields can I safely expose using pin-project?
I tried to implement a custom SSE-style streaming protocol over HTTP, to make a rust server implementation of Braid. I spent about a week trying, including pouring over the implementations of server-sent events and websockets in one of the HTTP libraries. Ultimately I failed to get it to work. (This is before TAIT and some other recent features, so things are probably be better now.)
I picked up javascript to write my server instead, and I had the protocol implemented about 20 minutes, in just 20 or so lines of code.
I adore rust, and I'd much rather a rust implementation than one based on nodejs. But I ran into skill issues here. Pin and futures in rust are hard. Or at least, I found them hard. I'm sure if I took another crack at it I'd be able to figure it out. But I don't want to spend so many of my brain cells on the language. I want to spend my attention thinking about my problem domain. Like I can in javascript.
Rust is an amazing language. But yeah, I really think that pin doesn't meet the standard that the rest of the language sets. I think it could use a rethink.
For what it's worth, all of the practical problems you encountered with using Pin are exactly what my next post is to show how to solve.
> I'm curious why you needed to deal with `Pin` instead of using async functions.
The protocol I was trying to implement streams messages over time over a single HTTP request thats kept alive for a long time. This is how Server-Sent Events (SSE) works, and its how Google Chat in gmail was first implemented in a way that supported IE 5.5 (!!!).
This was a couple years ago now, so the details are a bit fuzzy. And I was relatively new to rust at the time. I was, at the time, still sometimes surprised by the borrow checker.
My goal was to make a writable async stream that I could push messages into from other parts of my program. And it also needed backpressure. When you sent messages into the stream, the protocol implementation it encoded them and streamed them into the body of my HTTP response object. I was (I think) using hyper.
This is before TAIT was in rust, and for one reason or another I needed to store / reference the future object I was making. (If you use an async fn(), you don't get a name for the Future type the function returns. So I couldn't put the return type in my struct, because I couldn't name it.)
So I ended up writing a custom struct that implemented Future, so I could reference the future elsewhere in my code. Hence, implementing Poll myself. I can't honestly remember how Pin came into it all. I think hyper's API for doing this sort of thing stored a Pin<T> or something.
I remember at some point trying to write a where clause using higher ranked type bounds to describe the lifetime of a future object that was- or wasn't- associated with the lifetime of the corresponding HTTP request. And that may or may not have been Pinned, and I gave up.
It might be fun to revisit this at some point now rust's async support has matured a little. And now that I've matured a lot in how I understand rust. I certainly don't imagine that everyone using async will run into the sort of quagmire that I hit. But this was the first thing I ever really wanted to do with async rust, and it felt horrible to fall on my face trying.
Reflecting on a point from the article, it's possible that the ?Move being required in every declaration might have been better on this aspect. The point here about not being able to remember where the requirement to deal with Pin comes from is an indicator: the virality of the key traits involved, along with implicit implementation is a particularly tricky mix, it leads to action at a distance, which is also why first time engagements are so hard for users. Mix in some misunderstandings and you're in nope territory.
Having written a bunch of async code over 20 years (first exposure was twisted in the early 2000s and all sorts of stuff since - including manual function/stack wrangling on my own reactor), async in rust is like many other things in rust: It forces you to deal with the problems up-front rather than 6-12 months later once something is in prod. It helps to stop and understand why the compiler is yelling at you - for me anyway once I do grok that I'm glad I didn't have a repeat of $BUG_THAT_TOOK_DOWN_PROD_FOR_A_WEEK - that was a bad week.
That said, I'm curious if/when/why/how anyone uses Pin<T> outside of a custom Future implementation?
The way it's phrased it makes me understand a bit that it could be used within FFI, like if you get a pointer from a extern function that returns a *mut T, I could have better semantics by wrapping it with Pin<&mut T>, but then it says
"Another fact about the pinned typestate is that for most types it is completely irrelevant. If the value of type can never contain any self-references, pinning it is useless"
Mind you I'm very new at FFI but I'd like to understand what are the best ways to wrap them into safe Rust.
Interacting with system types that rely on their address. Eg I think std uses a pin equivalent for mutex/futex on some operating systems because the kernel docs say the userland lock object must not change its address after initialization (even when not locked, which is normally the only condition and doesn’t need Pin because you can’t move something that has a reference to it).
All async code -- all of it -- is a hack to implement lightweight threads through a lot of syntactic sugar for state management. In a language like Rust it adds a ton of complexity that simply doesn't need to exist without it.
Fixing the efficiency and scaling issues of threads would make all of this just go away. Poof. Gone.
It's a bit like the trillion dollar mistake of "null" in languages like Java-- a ton of complexity that results from one design decision (or in this case lack thereof).
Threads make it difficult to fully use CPU and network, without over-subscribing either one. If you start handing off tasks between threadpools, you're on the path to reimplementing futures (or you work on callbacks/events, which make the code fragmented, and which async/await was meant to be a syntax sugar for).
The alternative for cancellation and timeouts requires weaving a Context object like golang does, and then having issues with leaf code naively calling functions that don't obey the Context properly, which is only marginally better than pains with non-async functions in async code.
The latter is what Go did. I'm not a giant Go fan but not having async is far and away the best thing about the language and makes up for almost all its other faults.
Anything the language makes the programmer think about detracts from the programmer's ability to think about the actual problem they are solving.
BTW Rust is still superior to C++ and I use it, so I am not dissing Rust too badly. The async cancer is found across the entire ecosystem, not just Rust, so my comment was more against async programming in general as opposed to getting threading right. If we could get threading right we would massively simplify all programming everywhere.
Ultimately it boils down to the fact that it's 2024 and we still run everything on 1970s operating systems.
Its async is not easily composable. Anything beyond async 101 becomes a tangled web of DIY goroutines, wait groups, channels, callbacks, special empty signal channels for selects, and the Context objects which need careful management to be shared but not shared too much to cancel and time out right parts at the right time.
And then all of that has to be thread-safe, even though golang built-in types aren't. You need to pay sync overhead even for I/O-bound workloads or thread-per-core designs, because you don't control the fat runtime, and some of your green threads may be real.
Tokio's mt spawn adds burdensome Send and 'static requirements, and often gives too fine granularity, especially if the tasks still need to return results to their caller.
spawn() is often replaceable with join_all and streams. These allow same-thread temporary data, and are runtime-agnostic.
The other self-inflicted pain is stubborn avoidance of boxed futures. Every escaping pointer in golang is heap allocated, and nearly everything goes through interfaces. If you allow Box and dyn that often in Rust, you can skip a lot of the type system and lifetime complexity too.
The harmful decisions Rust made highlight its ingrained culture of doubling down on previous mistakes at all costs.
There seems to be no reevaluation of the cost/benefit ratio once the "preferred approach" turns out to be non-viable.
"We want to have feature X, consequences be damned" is rarely a winning move in language design.
Do you think "actually fixing" OS threads was a reasonable alternative? What would you prefer for a high performance abstraction instead of async?
I'd just wager that the effort of getting something like that into shape is smaller than the costs of async/await.
(And no, Rust's playing with green threads once 15 years ago and failing due to quality-of-implementation issues is not an excuse to dismiss everything that came after it off the bat.)
Though it's probably not worth discussing this whole topic with Rust fans currently:
Many made async/await part of their personality and have little experience to offer besides breathlessly pointing to one of the half dozen blog articles trying to defend async/await.
It will take a few years until that language feature runs through all stages of grief and one can have an adult discussion about it.
https://news.ycombinator.com/item?id=41034778
https://news.ycombinator.com/item?id=41004954
My body of work should be enough
This is a separate discussion, maybe you can put some of that body of work here.
I think this attitude is a major problem. You wrote something public and here you have lots of attention and engagement and you think you're being "generous" by replying to people. If you don't want people to read it, take it down. If you don't want to engage with people on hacker news, just ignore the whole thing. Calling comments "despicable" with no explanation is not a great reaction to people giving your article the attention you wanted in the first place.
(His older accounts show the same patterns of throwing childish tantrums and blowing up on random people.)
You weren't kidding. It's the exact same attitude of focusing on authority rather than explaining things technically.
Hacker News comments are divided between useful, inquisitive engagement, to which I react positively and smarmy, self-important bullshit, to which I react with scorn. You have made clear your allegiance to the latter. Sad!
If someone replies with insults and then just links to a google search page you wouldn't think they're some sort of authority because they promised you the evidence to their claim is out there somewhere.
If you want people to actually respect what you're saying you need to do things differently from both angles. Stop the toxic claims of authority and give simple explanations that are direct replies to what people are talking about.
Rather than “pin!(…)”, would “immovable!(…)” be any better? Probably not, and it’s difficult to think of a better one.
So maybe a shorthand “colloquial” name doesn’t make sense, and something descriptive like “prevent_moving!(…)” and a PreventMove trait might be better?
It seems like clickbait to me and the opposite purpose of having a site of headlines if they don't have any information.
A nice surprise coming to this article with no context :)
But because Rust fumbled it so badly, the usual answer is "rewrite your program in a language other than Rust."
(aside: there are about 4 different ways to implement object moving, and an efficient language needs to be aware of several of them)
What are they?
1. trivial copies. This is similar to 2, but means you do not have to do `swap`-like things in cases for non-destructive moves (which are, in fact, also important, they just shouldn't be the only kind of move. Note that supporting destructive moves likely implies supporting destructuring).
2. trivial moves. You can just use `memcpy`, `realloc`, etc. This is the only kind of move supported by Rust. C++ can introspect for it but with severe limitations. Note that "trivial" does NOT mean "cheap"; memory can be large.
3. moves with retroactive fixup (old address considered dead). What you do here is call `realloc` or whatever, then pass the old address (or the delta?) to a fixup function so that it (possibly with offset) can be replaced with the new address. Great caution is required to avoid UB by C's rules (the delta approach may be safer?). The compiler needs to be able to optimize this into the preceding when the fixup turns out to be empty (since generic wrapper classes may not know if their fields are trivially-movable or not).
4. full moves (both old and new address ranges are valid at the same time). C++ is the only language I know that supports this (though it is limited to non-destructive moves). One major use for this is maintaining "peer pointers" without forcing an extra allocation. Note that this can be simulated on top of "no moves" with some extra verbosity (C++98 anyone?).
Related to this, it really is essential for allocators to provide a "reallocate in place if possible, else fail" function, to avoid unnecessary move-constructor calls. Unfortunately, real-world allocators do not actually avoid copies if you use `malloc_usable_size` + `realloc`. If emitting C, note that you must avoid `__attribute__((malloc))` etc. to avoid setting off the bug-laden-piece-of-crap that is `__builtin_dynamic_object_size`.
Random reminder that "conditionally insert and move my object(s) into a container (usually a map), but keep my object alive if an equal one was already there" is important, and most languages do it pretty badly.
Linear types are related but it's all Blub to me.
They struggle a lot more with Unpin, and particularly the dynamics as code changes. This comes up more often than in probably should in an async context. (re. "should", the problem is it shows up because of closure changes most often, and the errors (last I was teaching) take the user to the wrong place, typically the just need to allocate, but there's no practicum on their error understanding path, see below).
Users are integrating a change of some kind, get a compiler error and then struggle even further to really understand when and how the unpin marker exists, but a change, potentially at a fair distance from the code that they're editing, is now making compilation time demands that they both understand and address the issue.
When those users enter the argument with rustc, they come to a page that starts with:
Implementing the Unpin trait for T expresses the fact that T is pinning-agnostic: it shall not expose nor rely on any pinning guarantees. This, in turn, means that a Pin-wrapped pointer to such a type can feature a fully unrestricted API. In other words, if T: Unpin, a value of type T will not be bound by the invariants which pinning otherwise offers, even when “pinned” by a Pin<Ptr> pointing at it. When a value of type T is pointed at by a Pin<Ptr>, Pin will not restrict access to the pointee value like it normally would, thus allowing the user to do anything that they normally could with a non-Pin-wrapped Ptr to that value.
Now this is reasonable documentation if you're in the thick of this universe, but to the average reader this is full of self-referential definition. When you have a weak understanding of Pin, then Unpin being defined in terms of Pin makes no sense, it's the same as Pin being defined in terms of Pin. All of this documentation needs to shift to a "systems engineering" or "in practice" basis first, and follow with theory. Most users give up with the docs before the second sentence they can't understand.All of the cleverness (reliance on a priori theory) in the docs for these core types needs to be moved out of the way for most readers - replaced by much simpler terminology and practicum suitable for the average knuckledragger. Normal people who end up here in practice are on average already at saturation point for working memory, they have "no stack space left" for trying to unpack language theory.
Is it just that they use an API which requires Unpin? If so, what APIs do you encounter causing the issue most often?
If you're one of the "rust/go in title" haters... please go rage somewhere else.
https://hn.algolia.com/?query=by%3Adang%20change%20title&sor...
Was this not the first sentence of the article when you wrote this?
"I build cool thing X in rust applause" sucks and is borderline spammy imo
But when discussing a language feature, a type, including the language name should be a given and not trigger any rust haters
> Otherwise please use the original title, unless it is misleading or linkbait; don't editorialize.
> If you submit a video or pdf, please warn us by appending [video] or [pdf] to the title.
I don’t want a video so this is a good warning. “I wrote hello in rust” is not an article I want to read. “Type erasure in rust” could be an article I might click on.
The title “Pin” is so meaningless it’s like clickbait: someone might waste their time clicking on it and be annoyed, especially if they read synchronously (I just open a bunch of tabs in background in a single pass so it’s not as bad for me).
In this case I clicked the comments first to see what would happen if I were to click on the article. During the week I’d just skip it.
Yesterday there was a link to a tweet, so the title had to be made up. It could have been “Biden drops out” (people have lots of context) but was “Joe Biden stands down as Democratic candidate”. Just “Biden” would have been clickbait.