return to table of content

Pin

api
17 replies
4h46m

Such an insane amount of work to avoid fixing the real problem: the inefficiency of threads.

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).

simon_o
10 replies
4h22m

This.

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.

withoutboats3
3 replies
4h5m

Despicable comment and false.

CyberDildonics
2 replies
3h24m

Is it 'despicable' just because you don't like what it says? You didn't do anything to refute it.

withoutboats3
0 replies
4m

I've written thousands of words on my blog about the design of async Rust, in which I carefully explain every decision and discuss the strong and weak points. This person regularly post rude low-effort comments like this one. My body of work should be enough to refute the idea that all I'm doing is doubling down. That a large part of Hacker News is fooled by cranks like simon-o does not surprise, but does speak poorly of this communities discernment.

simon_o
0 replies
3h1m

Some Rust people just have to be that dramatic.

redman25
3 replies
3h36m

I think he's talking about OS threads... it has nothing to do with rust's decisions.

simon_o
2 replies
3h1m

It has everything to do with Rust's decisions.

TwentyPosts
1 replies
28m

Talking about a "decision" only makes sense if there's a reasonable alternative.

Do you think "actually fixing" OS threads was a reasonable alternative? What would you prefer for a high performance abstraction instead of async?

simon_o
0 replies
15m

I think there are some lessons that can be learned from Java's virtual thread approach – note that there is a huge gap in requirements and design trade-offs, especially around embedded, when compared to Rust.

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 defined 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.

api
1 replies
4h19m

True but I didn't just mean Rust... I meant virtually all async coding as a pattern.

simon_o
0 replies
4h15m

Agreed, it's just that in Rust async/await hurts more than in e. g. JavaScript where the browser gives you enough hooks to have a "fresh start with(out) async".

withoutboats3
1 replies
4h6m

Marking the difference between a function that synchronizes with a concurrent process and a function that does not is good, actually.

davery22
0 replies
3h19m

If I could ask independently of the sentiment of this thread - I am genuinely curious: Why is marking the difference good? (sorry this is only tangential to the article)

pornel
1 replies
2h59m

Threads don't support cancellation in any reasonable way. Cancellation is immensely useful for networked applications and for GUIs.

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.

api
0 replies
2h27m

Everything you're describing goes back to problems with thread APIs. I agree that threads as implemented are very coarse grained and limited. What I dislike is the idea of inflicting an explosion of cognitive load onto the programmer to manually manage things instead of thinking about how to implement a better kind of thread.

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.

umanwizard
0 replies
2h6m

I doubt rewriting the Linux kernel to "fix the efficiency and scaling issues of threads" is possible, but even if it is, the Rust experts who figured out how to get Pin to work are presumably not the same as the set of kernel experts who would be capable of doing so. So what do you think they should have done, concretely? Just thrown up their hands and said "well, we won't add async to our language, because in theory someday someone might fix Linux to make threads magically fast" ?

redman25
0 replies
3h34m

Unfortunately crossing the user space barrier would cost something regardless of how lightweight "threads" would be. Also, making the OS the scheduler for all async tasks would preclude different scheduler designs since every runtime would have to use the OS's scheduler.

alain_gilbert
16 replies
15h42m

Should add "rust" in the title, so that we know what the article is talking about.

If you're one of the "rust/go in title" haters... please go rage somewhere else.

qingcharles
5 replies
15h25m

This is literally the worst post title I've ever seen on HN :D

lmm
1 replies
13h54m

Not merely worded but also enforced that way.

latexr
0 replies
2h59m

That’s not true. The moderators accept suggestions and changes titles to something other than the original time and again. It is a guideline after all, it depends on context. I have zero doubts a more descriptive title would be accepted in this instance, as long as it were accurate and not clickbait.

https://hn.algolia.com/?query=by%3Adang%20change%20title&sor...

pavlov
1 replies
7h33m

At least it was a pleasant surprise that it’s not about the Humane AI Pin.

qingcharles
0 replies
2h30m

I'd already forgotten about that thing...!

gumby
5 replies
14h7m

Why would anyone hate that? It’s a good marker, like [video], that tells folks like me not to bother clicking.

bowsamic
2 replies
12h47m

It’s against the rules for one

gumby
1 replies
6h31m

The “rules” are guidelines. Good titles should be useful and encourage those interested to read and do a service to those not interested by preemptively not wasting their time. Consider the following guidelines:

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.

bowsamic
0 replies
3h35m

Even if you claim it's a guidelines, it's enforced like a rule

ramon156
1 replies
11h43m

I've seen a decent amount of people that blacklist keywords like rust, blazingly, etc.

port19
0 replies
11h3m

Which makes some sense if rust is only of tangential concern...

"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

tyrust
1 replies
14h46m

The Pin type (and the concept of pinning in general) is a foundational building block on which the rest of the the Rust async ecosystem stands.

Was this not the first sentence of the article when you wrote this?

throwaway89336
0 replies
10h39m

It's about beeing able to quickly scan titles and decide if it's worth clicking on. If all titles were similar, we would spend a lot of time clicking each link to see if it is relevant.

perilunar
0 replies
14h7m

It does indeed have nothing to do with boats.

verdagon
11 replies
13h32m

I can imagine a Rust-like language where we have move-constructors (in TFA), and every generated Future subtype is opaque and also heap allocated for us.

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).

josephg
7 replies
12h33m

Yeah. I can guess how disruptive it would be, but I really wish rust bit the bullet and added a Move trait to std, baked into the language at a similar level as Copy. Move defines a function which moves a value from one address in memory to another. Structs without impl Move cannot be moved.

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.

conradludgate
5 replies
10h14m

I very much disagree that Pin is as hard as everyone makes it out to be. Using the pin! macros, the pin-project crate, and enough as_mut() to get it to compile and it's not hard at all to get a future impl working. It would be good to get this native (which is what boats wants) so it's easier to discover but it's not at all hard by any means

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.

josephg
4 replies
6h11m

I can only speak from my experience, but I really struggled with it. Ok, I understand moving. Pin is ... not moving. But the struct can still move, just ... not when you have a pointer to the struct. Ok, weird, but ok. Pin has a weird, limited set of functions to access the data fields of the struct. Some are unsafe. And then there's Unpin, which sounds like its not-not-move, so, something can move? No. From std:

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.

withoutboats3
3 replies
6h0m

I'm curious why you needed to deal with `Pin` instead of using async functions. What led you to a path in which you needed to implement poll methods yourself?

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.

josephg
2 replies
4h42m

I look forward to your next post on the topic then!

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.

withoutboats3
0 replies
4h28m

Thanks for your write up. This sounds like a perfect use case for async generators (which yield many times and compile to Stream instead of Future), a feature I hope Rust will gain in the next year or two. To receive messages from other parts of the program, I would have the async generator hold the receiving end of a channel.

raggi
0 replies
4m

When I worked on Fuchsia and we had a heavily async set of system interfaces, users went through this learning path very regularly, and it was very painful for many. Folks who reached out for help early in their first engagement on this kind of path got help and following a "learn by doing" started to understand what was going on after a few iterations of the same challenge. Those who struggled trying to figure it out all on their own had a really awful time and in one example even went back to c++ for a sizable project because of the wall they ran into. There's a big gap here for folks who want to self-help their way through this. TAIT reduces the number of cases that come up, but there are still plenty.

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.

sophacles
0 replies
2h19m

Async rust is horrible. Especially compared to futures / promises in almost any other language.

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.

withoutboats3
1 replies
9h19m

You don't need move constructors if every future is heap allocated. But then every call to an async function is a separate allocation, which is terrible for memory locality. Some kind of virtual stack would be much better than that (but then you need garbage collection if you want the stacks to be memory-optimized to be small by default).

anonymoushn
0 replies
8h6m

You could use an actual stack. As I understand it this was not done for questionable reasons relating to borrows of thread-locals. You could also allocate a top-level async function and all of its transitive async callees all at once, if you force the user to put all this information in 1 translation unit. Or you could use a bump allocator specifically for futures used in a certain part of the program, if you're willing to give up using a global allocator for everything. So it seems like there are a lot of options.

pornel
0 replies
6h11m

Pin is a state rather than a property of the data itself.

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.

jauntywundrkind
10 replies
15h34m

Great seeing this backstory. WithoutBoats has had some super active discussions around very topical async iterators, poll, and pin topics already! https://news.ycombinator.com/from?site=without.boats

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.

Sytten
9 replies
15h7m

Its cool, but it also means the development of the language is very very slow. Async is still half backed and super complex, and I say that as a guy who has written rust code 40h/week for the past 3 years.

tjf801
5 replies
14h55m

I fully agree. Sometimes it feels like so much effort is put into rust's async side that the rest of the language ends up taking a back seat and suffering for it.

forrestthewoods
3 replies
13h32m

Rust dev team cares too much about async and webdev imho. Yes I know the web is where 95% of programming jobs live now. But as a C++ systems programmer I simply could not care less about the web. Rust is a systems language trying to get its foot in the web door. I'd selfishly rather it focus on being a superior C/C++. Alas.

pkolaczk
0 replies
6h5m

Async is not for webdev only.

conradludgate
0 replies
10h11m

The nature of open source is that people work on what they want to work on. People work on async because it interests them. But to the same level many people in the lang and libs team don't know async that well. You may perceive it that everyone works only on async but this is just not true.

ChrisSD
0 replies
13h15m

The usual complaint is that there has been little progress on async and webdev in Rust itself since 2018 so it's odd to hear the reverse complaint.

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...

rjh29
0 replies
10h42m

It feels like async is popular and necessary but just not that good fit for rust. Having it is better than not having it I suppose....

withoutboats3
2 replies
9h27m

This isn't why development is slow. In my opinion, if I were still employed to work on Rust improvements to async would have shipped a lot faster. My blogging about it in my free time is my effort, in light of my circumstances, to get the project back to shipping on async.

hitekker
0 replies
1h36m

For someone not in the loop, why not go back into it and take the lead? At least on a part time, volunteer basis.

Sytten
0 replies
5h53m

It would help for sure to have a lead on this. Still there are a lot of opinions on the way forward for async so it would still be slow I think.

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.

[1] https://github.com/rust-lang/impl-trait-utils

zengid
4 replies
15h8m

The term “value identity” is not defined anywhere in this post, nor can I find it elsewhere in Mojo’s documentation, so I’m not clear on how Modular claims that Mojo solves the problem that Pin is meant to solve

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]

[0] https://www.youtube.com/watch?v=QthAU-t3PQ4

withoutboats3
3 replies
9h2m

It's clear that Mojo is in some sense inheriting Swift's notion of "value semantics," but Rust also has "value semantics" in the same sense. Rust just also has references as first class types, whereas Swift (and as far as I can tell, Mojo) only allows references as a parameter passing mode; Mojo expands on Swift's inout parameters by having an immutable reference passing mode as well.

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.

demurgos
2 replies
6h51m

My understanding of _value identity_ refers to the `StableDeref`/yoke approach to self-regerential structs. The value is constructed at a stable address (usually some heap allocation) and you always access it through some pointer. The address is the value's identity. The pointer can move, but the value doesn't move.

withoutboats3
1 replies
6h36m

Could you link to a source for this in Mojo's documentation? This would be a logical interpretation, but it would mean Mojo is planning to adopt a much worse implementation of async than Rust and the post is claiming that Mojo is both faster and easier than Rust.

demurgos
0 replies
2h40m

I'm not familiar with Mojo, so my understanding above was based on their blog post that you linked, and assumptions based on context. Checking their website, I find a sentence equating "identity" with "having an address" though:

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.

LegionMammal978
4 replies
9h55m

My take on why users find Pin difficult: by itself, it has no meaning! This is different from every other wrapper in the language (except maybe AssertUnwindSafe<T>, which basically no one uses for its original purpose). Given a Pin<&mut InnerType>, there's nothing about Pin in the language or standard library that tells you what you can and can't do with it. (Unless the InnerType declares that it's Unpin, which implies that you can do anything you can do with an ordinary pointer.)

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".

withoutboats3
3 replies
9h8m

Pin does have meaning: it means you cannot move the target of this pointer ever again (or invalidate its without running its destructor, which is what moving does that's the problem), unless the type of the target implements Unpin.

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."

LegionMammal978
2 replies
6h59m

Pin does have meaning: it means you cannot move the target of this pointer ever again (or invalidate its without running its destructor, which is what moving does that's the problem), unless the type of the target implements Unpin.

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.

withoutboats3
1 replies
6h39m

There are a couple of ways to interpret this code:

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.

LegionMammal978
0 replies
1h37m

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.

o11c
2 replies
15h49m

Pinning/!Move is useful for so many things outside of async/await.

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)

n3t
1 replies
15h46m

there are about 4 different ways to implement object moving

What are they?

o11c
0 replies
13h48m

0. no moves. This is very often needed for FFI callbacks, among others.

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.

iknowstuff
2 replies
3h33m

When teaching, to make it clear that an "Unpin" item is unaffected by "Pin," I’d suggest analogies from real life where objects remain unaffected despite the use of something designed to hold them in place:

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 :’)

01HNNWZ0MV43FF
1 replies
3h8m

But a smooth object can't be velcroed, nor can wood hold up a magnet.

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

iknowstuff
0 replies
1h31m

I think two things fuck with our brains here:

- 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 :)

fallingsquirrel
2 replies
15h58m

The problem as I see it is that having a &mut reference to something lets you move it via mem::swap/replace (and maybe a few others?) -- but actually needing to do so is rare. It seems to me that if that weren't allowed, taking a &mut reference to a self-referential value would be perfectly safe.

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.

withoutboats3
0 replies
9h16m

This is true. When were working on the problem, the way Aaron Turon put it was that &mut is "too powerful." If &mut didn't give the power to move out of it, the whole design would be a lot simpler. I'll discuss this in my next post about this.

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.

Sytten
0 replies
15h3m

This just doesn't scale and it would have been a non starter because it would break so much exiting code. Mem swap is just one of the ways to move via mutable references, there are many many others. Option::take is one that I use quite often and it would be super weird if that was unsafe.

weavie
1 replies
4h52m

I've been programming Rust professionally for several years now and being totally honest I don't really understand Pin that well. I get the theory, but don't really have an intuitive understanding of when I should use it.

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.

01HNNWZ0MV43FF
0 replies
3h7m

Same. It's one of my most common cases of "Just avoid unsafe and be glad some smart compiler folks already solved this all for me"

Whereas in C++ I was regularly treading in the water of "stuff I don't know, but must use" and getting eaten by crocodiles

umanwizard
1 replies
8h53m

I’ve always thought Pin was difficult to understand because it’s not explained in a clear way in the official docs. In particular, lots of documentation claims things like “Pin ensures that an object is never moved”, which isn’t true!

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.

withoutboats3
0 replies
8h43m

I think this is good feedback and it would be good for the docs to be clearer about this. Of course for the types that you're going to deal with pinned (futures and streams), they're a lot more likely to be those niche objects.

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.

orf
1 replies
8h34m

I think “Pin” is a good example of a technically correct but hard to understand name. “Drop” has a much more familiar meaning because it’s a common action, whereas “pinning” isn’t so common and can mean different things in different contexts.

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?

jollyllama
0 replies
5h48m

It's a bad name for any application handling a Personal Identification Number of any kind.

raggi
0 replies
19m

I think most people get a partial idea of what Pin is fairly quickly when they try, but that's part of the problem.

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.

jackcviers3
0 replies
4h58m

I take it Pin is a fixpoint-type that allows for recursive reference?

CyberDildonics
0 replies
3h54m

Do people like titles like this where there is no information about what the link is (and what little is there has basically nothing to do with the link) ?

It seems like clickbait to me and the opposite purpose of having a site of headlines if they don't have any information.