Pin/Unpin sometimes feels slapped on top of an existing language

(Note: Not sure if this is the right forum to bring up this topic, but since I'm a newcomer to Rust and I see this from a user perspective, I thought it's best if I bring it up here.)

Rust is often criticized for being hard to learn (whether that's true or not). My personal experience is that asynchronous Rust was particularly confusing to me at times. I think that was due to two things:

  • Complex type signatures and issues with lifetimes and bounds regarding function arguments and returned futures;
  • Having to understand pinning and unpinning, in particular in combination with boxing, dyn objects, mutable references, access to fields of pinned structures (projections and structural pinning), etc., including figuring out where and when to pin values passed to functions.

I feel torn whether I should find Pin and Unpin

  • beautiful constructs to elegantly extend Rust with a needed feature without having to touch the core of the language (i.e. without having to make it more difficult/complex), or
  • an ugly add-on to circumvent shortcomings of the language.

Digging into the old Rust RFC 2349, I noticed that it has been discussed about making Pin and Unpin integral parts of the language.

The Pin type could instead be a new kind of first-class reference – &'a pin T. This would have some advantages – it would be trivial to project through fields, for example, and "stack pinning" would not require an API, it would be natural. However, it has the downside of adding a new reference type, a very big language change.

Is that still an issue or is the current approach seen as the best thing to do?

P.S.: An argument in favor of keeping Pin and Unpin as is, would be that Sized is implemented as a marker trait too.

1 Like

I remember watching a Tom Scott rant discussion on text encodings and he referred to UTF-8 as an "elegant hack".

With Pin I know enough to be dangerous but not enough to be useful, so I can't really respond to the rest of your question, but after watching async/await and Pin evolve from threads on i.rl.o to withoutboats' excellent blog posts to being finally merged into stable, I think "elegant hack" resonates feels right. It's a genius solution to a hard problem, but that problem was also self-inflicted and came about because we didn't know this would be a problem 10 years ago when the language was being developed.

6 Likes

I think ideally Rust should have got a first-class support for either self-referential types or non-movable types, but these features are very difficult to support in Rust.

I don't like Pin due to its complexity, and I don't think it is a solution for self-referential types, but it was a good solution for getting async fn shipped.

7 Likes

I think it isn't a solution because self-referential types still require unsafe, right? If I understand it right, then Pin just allows one to create a safe interface for self-referential types, but under the hood it won't work without unsafe.

A pro-argument for Pin/Unpin, however, would be that the core language can be kept easier/simpler. But with async becoming more used in practice, that may be only a theoretic advantage.

Indeed. Sometimes users drop in this forum with a need for a self-referential type, try Pin, and end up disappointed that it didn't remove unsafe, boxing, and the need to understand Pin itself didn't make the code easier to write.

Keeping the core language "simpler" is not always a good thing. If there's an inherent complexity in a problem, then you can't remove it, you can only move it around.

For example, std::move in C++ has been added outside of the core language, and it's a sub-optimal solution with usability issues. Rust made it simpler to use by adopting it natively.

Conversely, something like copy/move constructors can make self-referential types possible without the concept of Pin. Adding them to Rust outside of the core language in Rust requires a lot of clever code (although I'm not suggesting to add move ctors to Rust specifically, since that clashes with Rust's existing design).

5 Likes

That avoids the extra Boxing (e.g. in a Vec), but doesn't it come with the overhead of having to execute type-specific code for each move of value having a cyclic type? Depending on the architecture, it may be advantageous, but I think that simply creating a pointer to pinned memory on the heap (aka Box::pin) could be more efficient in many cases.

Thanks a lot for the link to that video. It was enlightening (and the first half of the video was also helpful to recapture what Pin/Unpin does). For those who do not like to watch the whole video, a look at the moveit crate may be of interest.

I.e. as long as Rust aims to create highly efficient code (which is a good thing!), then there will be inherent complexity Rust programmers have to deal with. So maybe it's just a matter where a programmer will be confronted with the issues of pinning / byte-wise moving.

Perhaps it is possible to make things easier when introducing new language constructs that could replace Pin and Unpin, but I'm not sure. I guess there would be both advantages and disadvantages. I'd be happy if I'm proven wrong though (i.e. if there is a way to make things easier without having other disadvantages). :grinning_face_with_smiling_eyes:

1 Like

But how would that work? How would the &pin know which fields are structural and which are not?

Or would all &pin projections be unsafe?

How would Pin<Box<T>>, Pin<Arc<T>> and Pin<MyCustomPtr<T>> work?

I am somewhat new to Rust (never used the language professionally) and I still struggle a lot with Pin/Unpin too. I'm coming from a C background fwiw.

I don't know. I actually just cited the RFC here :innocent:. I guess there could be a default, which can be overriden with a special keyword? Similar to what the pin-project crate does. (Luckily I never had to use projections yet.)

Again, I think it could be done by using a special keyword, e.g. pin, similar to mut. I don't want to say it would be easier in the end, and not sure how exactly it would work. I just wanted to bring up that it has been discussed (or at least thought of), that pinning a value to a fixed address in memory could be implemented in different ways than using a zero-cost smart-pointer (Pin) and a marker trait (Unpin).

Not sure if that was better. Like I said, I feel torn how I feel about Pin and Unpin.

It just confused me a lot, and I guess I'm not the only one. But over time, I feel more confident with it. Many people in this forum helped me to get a better understanding! So thanks to you all, and good luck to @robber: Don't give up, and feel free to ask any question if you're stuck somewhere with Pin/Unpin. :slightly_smiling_face:

1 Like