pin-project and the safe way to reach a pinned field in Rust
A crate for safe and ergonomic pin-projection.
At a glance
- What is it?
- pin-project is a Rust crate that generates projection types for self-referential structs, so a pinned field can be reached through Pin and an unpinned field through a plain mutable reference, without hand-written unsafe code. The cost is a proc macro, a minimum supported Rust version, and a public API that has to be trusted.
- Who is it for?
- pin-project suits a library author who needs to expose a self-referential type to callers without asking them to write unsafe code, since the generated projection is the only place the invariant is enforced, and it suits a no-std project because the crate declares the no-std and no-alloc categories.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 13 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem: a struct that points at itself
Rust's pin-projection model exists for self-referential structs, a type whose field holds a reference into itself. Once such a type is pinned in memory, it can never be moved again, because moving it would invalidate the self-reference. The standard library documentation calls this structural pinning, and it is the reason Pin exists.
The ergonomics problem is what you do next. Your type is pinned, so you hold a Pin<&mut Self>, and you cannot get a &mut Self out of it, because that would allow a move. But you often need to reach a field that is not itself pinned, a plain counter, a name, a configuration value, and mutate it.
pin-project is the crate that solves that. It describes itself as a crate for safe and ergonomic pin-projection, and its job is to generate the projection types for you so that each field is handed back with exactly the right reference type.
The pin-projection types are documented on the standard library page, and the crate exists because the compiler cannot check pinning invariants on its own. A hand-written projection is possible but requires unsafe, and the whole point of the crate is that the unsafe lives in one place that the author has reviewed.
The attribute is the whole interface
Usage starts with a dependency, and the guide is explicit that this is all you need in your manifest:
[dependencies]
pin-project = "1"From there the entry point is the pin_project attribute macro, which creates projection types covering all the fields of a struct or an enum. The examples directory in the repository holds the expanded output for each case, so you can see what the macro generates rather than guessing.
A struct case looks like this:
use std::pin::Pin;
use pin_project::pin_project;
#[pin_project]
struct Struct<T, U> {
#[pin]
pinned: T,
unpinned: U,
}
impl<T, U> Struct<T, U> {
fn method(self: Pin<&mut Self>) {
let this = self.project();
let _: Pin<&mut T> = this.pinned; // Pinned reference to the field
let _: &mut U = this.unpinned; // Normal reference to the field
}
}The mechanics are short. The pin attribute on the field marks it as structurally pinned, the unannotated field is not. Calling project on the Pin produces a projection value, and each field of that value carries the reference type the invariant allows: Pin<&mut T> for the pinned field, &mut U for the other. The code cannot move the pinned field out, because the type system does not offer a way to, and it can freely mutate the other, because the type system does not stop it.
Enums need you to name the projection type
The enum case has one extra step, and the guide is clear about it: to use pin_project on enums, you need to name the projection type returned from the method.
use std::pin::Pin;
use pin_project::pin_project;
#[pin_project(project = EnumProj)]
enum Enum<T, U> {
Pinned(#[pin] T),
Unpinned(U),
}
impl<T, U> Enum<T, U> {
fn method(self: Pin<&mut Self>) {
match self.project() {
EnumProj::Pinned(x) => {
let _: Pin<&mut T> = x;
}
EnumProj::Unpinned(y) => {
let _: &mut U = y;
}
}
}
}The reason is visible in the shape of the code. A struct projection is a struct with the same fields, so the generated name can be inferred. An enum projection is an enum with one variant per original variant, and each variant may hold either a pinned or an unpinned field, so the compiler has to be told what to call it. The project attribute names it, and the match in the example has to handle each variant explicitly.
That explicitness is a feature rather than a quirk. The point of the pin marker is that a reader can see which fields carry a pinning invariant, and in the enum case each variant says so at the variant level rather than at the field level.
The examples directory carries both the original and the expanded source for each case, which is the fastest way to understand what the macro produces for a given shape.
The examples directory doubles as the specification
The repository ships a set of paired files, and reading them tells you what the macro supports.
The pairs are examples/struct-default.rs and examples/struct-default-expanded.rs, examples/enum-default.rs and examples/enum-default-expanded.rs, and then the interesting cases: examples/not_unpin.rs with its expanded counterpart, examples/pinned_drop.rs, examples/project_replace.rs, and examples/unsafe_unpin.rs.
The names hint at the design questions. not_unpin is about a field that is not structurally pinned but is still pinned by some other mechanism, so it must be marked so the macro does not generate a projection for it. pinned_drop is about a type that implements Drop while containing a pinned field, which is a case where naive projection is unsound. project_replace is about providing your own projection method rather than the generated one. unsafe_unpin is about opting out of the safe behaviour deliberately.
The example files are organised as tests as well as documentation, and the expanded versions are the expected output. The dev-dependencies in Cargo.toml show how that is verified: macrotest is pulled from a fork, trybuild from another fork, and both are pinned to a branch named dev-old-msrv-syn, with comments explaining that this adjusts the overwrite behavior and avoids relying on cargo-expand.
That last detail is the interesting one. The expansion is checked in rather than generated at test time, and the fork exists so the test can run on the crate's minimum supported Rust version without needing the cargo-expand tool installed.
A minimum supported Rust version, and a workspace built to test it
The crate pins a specific floor, and the pinning is deliberate rather than incidental.
The manifest sets rust-version to 1.71 with a comment that it should be synchronised with the rust-version field in the other Cargo.toml files, the MSRV badge shown on the project page, and the test job in the CI workflow. The badge on the project page shows MSRV 1.71, and the internal crate is aligned to the same version.
A minimum supported Rust version that a crate keeps working on is a maintenance promise, and this project backs it with structure. The workspace members are pin-project-internal, plus five test crates: tests/auxiliary/macro, tests/no-core, tests/no-std, tests/lint and tests/rust-2015.
Each of those is a compatibility target, and the names describe them precisely. tests/no-core and tests/no-std verify the crate compiles without the standard library and without the allocator, which is why the manifest lists the categories no-std and no-std::no-alloc. tests/rust-2015 checks that the crate still compiles under the 2015 edition. tests/lint checks the crate passes the lints, and the workspace lints section turns on rules such as deprecated_safe and improper_ctypes as warnings.
The categories field also lists rust-patterns, which is the crates.io category for code that works around language limitations, and that is a fair description of what a pin projection is.
The dependency structure is worth one more note. pin-project depends on pin-project-internal with an exact version, pinned to =1.1.13, and a path dependency on the local directory. The internal crate holds the unsafe implementation, and the public crate is a thin proc-macro layer over it. The manifest also configures cargo_check_external_types to allow only pin_project_internal::* to appear in the public API, which is how the boundary between the safe surface and the unsafe core is enforced mechanically.
Licence, tooling and what this crate is not for
The licensing is the standard dual option, and it comes with a condition worth reading.
The project is licensed under either the Apache License, Version 2.0 or the MIT license at your option, and the manifest records it as Apache-2.0 OR MIT. Unless a contribution explicitly states otherwise, any contribution intentionally submitted for inclusion is dual licensed on the same terms, with no additional terms or conditions.
The repository is unusually well equipped with the usual hygiene files: a .clippy.toml, a .cspell.toml for spell checking, a .deny.toml for cargo-deny, a .rustfmt.toml, a .taplo.toml for formatting TOML, a .markdownlint-cli2.yaml, a .shellcheckrc, a .editorconfig, and a .git-blame-ignore-revs file for keeping formatting-only commits out of blame. There is a DEVELOPMENT.md, a CHANGELOG.md, and a tools/ directory that the manifest excludes from the published package along with the dotfiles and DEVELOPMENT.md.
What this crate is not for is worth stating plainly. It is not a way to avoid Pin. If your type is self-referential, you are using Pin whether or not this crate exists. It is also not a general projection utility, and it is not a runtime library: it generates code at compile time and adds nothing to the binary beyond the expansion.
And it is not always the shortest answer. The library pin-project-lite is described as a lightweight version of pin-project written with declarative macros, and it exists because a proc macro is a heavier dependency than a declarative one. For a small project that only needs a struct projection, writing the projection type by hand can be less machinery than adding a proc-macro dependency, at the cost of the unsafe that the macro would have written for you.
Editorial conclusion
pin-project suits a library author who needs to expose a self-referential type to callers without asking them to write unsafe code, since the generated projection is the only place the invariant is enforced, and it suits a no-std project because the crate declares the no-std and no-alloc categories. It does not suit an application that just needs a pinned field once, because writing the projection by hand is shorter than the macro, and it does not suit a project that wants the projection type hidden, since the generated type is part of the public API. Before adopting it, check the 1.71 minimum supported Rust version against your toolchain, read the examples directory for the generated code, and note that the last push was on 2026-10-10 with version 1.1.13 published on 2026-05-13.
Frequently asked questions
What does the pin-project crate do?
It is a crate for safe and ergonomic pin-projection. The pin_project attribute macro creates projection types covering all the fields of a struct or enum, so a pinned field is reached through Pin and an unpinned field through a normal mutable reference.
How do I add pin-project to a Cargo project?
Add the dependency to Cargo.toml as pin-project = "1", then apply the pin_project attribute to your type. The crate is published on crates.io with API documentation on docs.rs.
Why do enums need a named projection type in pin-project?
A struct projection can be inferred, but an enum projection has one variant per original variant and the compiler has to be told what to call it. You name it with the project attribute, for example #[pin_project(project = EnumProj)], and then match on the generated type.
What is the minimum supported Rust version for pin-project?
The manifest sets rust-version to 1.71, and the project badge shows the same floor. The workspace includes test crates for no-std, no-core and the 2015 edition to keep that promise honest, and the internal crate is aligned to the same version.
What licence is pin-project released under?
It is licensed under either the Apache License, Version 2.0 or the MIT license at your option, and the manifest records the expression as Apache-2.0 OR MIT. Contributions are dual licensed the same way unless they state otherwise.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/taiki-e-pin-project)Was this useful?