From 5dbb197fcdcebe751fd0455ef6a83a30d4bdeeb0 Mon Sep 17 00:00:00 2001 From: Tim McNamara Date: Tue, 16 Dec 2025 00:08:05 +1300 Subject: [PATCH 1/2] Checkpoint from VS Code for coding agent session --- .../_exercises/matmat-multiply/Cargo.lock | 65 ++++++++++++++++++ src/unsafe-deep-dive/pinning/README.md | 19 ++++++ src/unsafe-deep-dive/pinning/phantompinned.md | 29 ++++++++ .../pinning/self-referential-buffer/cpp.md | 53 +++++++++++++++ .../pinning/self-referential-buffer/rust.md | 37 +++++++++++ src/unsafe-deep-dive/pinning/unpin-trait.md | 19 ++++++ src/unsafe-deep-dive/pinning/welcome.md | 30 +++++++++ .../pinning/what-a-move-is.md | 66 +++++++++++++++++++ .../pinning/what-pinning-is.md | 46 +++++++++++++ src/unsafe-deep-dive/pinning/why-difficult.md | 24 +++++++ 10 files changed, 388 insertions(+) create mode 100644 src/unsafe-deep-dive/_exercises/matmat-multiply/Cargo.lock create mode 100644 src/unsafe-deep-dive/pinning/README.md create mode 100644 src/unsafe-deep-dive/pinning/phantompinned.md create mode 100644 src/unsafe-deep-dive/pinning/self-referential-buffer/cpp.md create mode 100644 src/unsafe-deep-dive/pinning/self-referential-buffer/rust.md create mode 100644 src/unsafe-deep-dive/pinning/unpin-trait.md create mode 100644 src/unsafe-deep-dive/pinning/welcome.md create mode 100644 src/unsafe-deep-dive/pinning/what-a-move-is.md create mode 100644 src/unsafe-deep-dive/pinning/what-pinning-is.md create mode 100644 src/unsafe-deep-dive/pinning/why-difficult.md diff --git a/src/unsafe-deep-dive/_exercises/matmat-multiply/Cargo.lock b/src/unsafe-deep-dive/_exercises/matmat-multiply/Cargo.lock new file mode 100644 index 000000000000..8c9fb1a41808 --- /dev/null +++ b/src/unsafe-deep-dive/_exercises/matmat-multiply/Cargo.lock @@ -0,0 +1,65 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "matmat" +version = "0.0.0" +dependencies = [ + "thiserror", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" diff --git a/src/unsafe-deep-dive/pinning/README.md b/src/unsafe-deep-dive/pinning/README.md new file mode 100644 index 000000000000..384124da357b --- /dev/null +++ b/src/unsafe-deep-dive/pinning/README.md @@ -0,0 +1,19 @@ +# pinning + +> **Important Note** +> +> To not add this section to the project's SUMMARY.md yet. Once CLs/PRs to +> accept all the new segments for the Unsafe Deep Dive have been included in the +> repository, an update to SUMMARY.md will be made. + +## About + +This segment explains pinning, Rust's `Pin` type and concepts that relate +to FFI rather than its async use case. Treatment of the `Unpin` trait and the +`PhantomPinned` type is provided. + +## Status + +Provisional/beta. + +## Outline diff --git a/src/unsafe-deep-dive/pinning/phantompinned.md b/src/unsafe-deep-dive/pinning/phantompinned.md new file mode 100644 index 000000000000..b80278c36b1f --- /dev/null +++ b/src/unsafe-deep-dive/pinning/phantompinned.md @@ -0,0 +1,29 @@ +# PhantomPinned + +The idiomatic way to opt-out of Rust's aliasing + +Usage + +```rust,editable +pub struct DynamicBuffer { + data: Vec, + cursor: NonNull, + _pin: std::marker::PhantomPinned, +} + +impl DynamicBuffer { + pub fn push(&mut self, byte: u8) { + self.data.push(byte); + + // TODO: self.data may have reallocated; ensure that th cursorpoints the the correct place + } +} +``` + +
+ +If a type contains a `PhantomPinned`, it will not implement `Unpin` by default. + + + +
diff --git a/src/unsafe-deep-dive/pinning/self-referential-buffer/cpp.md b/src/unsafe-deep-dive/pinning/self-referential-buffer/cpp.md new file mode 100644 index 000000000000..9bf9a639142f --- /dev/null +++ b/src/unsafe-deep-dive/pinning/self-referential-buffer/cpp.md @@ -0,0 +1,53 @@ +# Motivating Example: C++ + +```cpp,editable,ignore +class SelfReferentialBuffer { + char data[1024]; + char* cursor; + +public: + SelfReferentialBuffer() = default; + + SelfReferentialBuffer(SelfReferentialBuffer&& other) + : cursor(data + (other.cursor - other.data)) + { + std::memcpy(data, other.data, 1024); + } +}; +``` + +Investigate on [Compiler Explorer](https://godbolt.org/z/ascME6aje) + +
+ +The `SelfReferentialBuffer` contains two members, `data` is a kilobyte of memory +and `cursor` is a pointer into the former. + +Its move constructor ensures that cursor is updated to the new memory address. + +This type can't be expressed easily in Rust. + +> Note: `char*` is dated, but exists in legacy codebases and is used here for +> simplicity. +> +> If your class includes experienced C++ developers, consider replacing `char*` +> with `std::byte*`. +> +> ```cpp +> #include +> #include +> +> class SelfReferentialBuffer { +> std::byte data[1024]; +> std::byte* cursor = data; +> +> public: +> SelfReferentialBuffer(SelfReferentialBuffer&& other) +> : cursor{data + (other.cursor - other.data)} +> { +> std::memcpy(data, other.data, 1024); +> } +> }; +> ``` + +
diff --git a/src/unsafe-deep-dive/pinning/self-referential-buffer/rust.md b/src/unsafe-deep-dive/pinning/self-referential-buffer/rust.md new file mode 100644 index 000000000000..b5f6a78342c4 --- /dev/null +++ b/src/unsafe-deep-dive/pinning/self-referential-buffer/rust.md @@ -0,0 +1,37 @@ +# Rust + +```rust,editable +// class SelfReferentialBuffer { +// char data[1024]; +// char* cursor; +// +// ... +// +// }; + +// Close to the original, but requires unsafe +struct SelfReferentialBuffer { + data: [u8; 1024], + cursor: *const u8, +} + + +// More idiomatic, with different semantics +struct SelfReferentialBufferSafe { + data: [i8; 1024], + position: usize, +} +``` + +
+ +While Rust would allow us to create a similar struct to the C++ class, it has a +significant cost. + +We would give up references, falling back to raw pointers. This imposes unsafe +code later on. + +A more idiomatic version would be to maintain an offset using a `usize`, then +creating a reference to `self` on demand. + +
diff --git a/src/unsafe-deep-dive/pinning/unpin-trait.md b/src/unsafe-deep-dive/pinning/unpin-trait.md new file mode 100644 index 000000000000..5294b8a15ff1 --- /dev/null +++ b/src/unsafe-deep-dive/pinning/unpin-trait.md @@ -0,0 +1,19 @@ +# Unpin trait + +- `T: Unpin` implies that `T` is not pinned +- Automatically implemented by the compiler for nearly every type +- To opt out of this for your type, add a [`PhantomPinned`] field to your type + (required for FFI) + +
+ +Most types implement `Unpin` automatically `Unpin` types can be moved even when +pinned + +`!Unpin` types cannot be moved once pinned + +Unpin is a promise: "moving me is always safe" + +
+ +[`Pantom`]: https://doc.rust-lang.org/std/marker/struct.PhantomPinned.html diff --git a/src/unsafe-deep-dive/pinning/welcome.md b/src/unsafe-deep-dive/pinning/welcome.md new file mode 100644 index 000000000000..abbd20f8b1e7 --- /dev/null +++ b/src/unsafe-deep-dive/pinning/welcome.md @@ -0,0 +1,30 @@ +# Welcome + +This segment of the course covers: + +- What "pinning" is +- Why it is necessary +- How Rust implements it +- How it interacts with unsafe and FFI + +
+ +"Pinning, or holding a value's memory address in a fixed location,is one of the +more challenging concepts in Rust." + +"Normally only seen within async code, i.e. [`poll(self: Pin<&mut Self>)`], +pinning has wider applicability." + +Some some data structures that are difficult or impossible to write without the +unsafe keyword, including self-referential structs and intrusive data +structures. + +FFI with C++ is a prominent use case that's related to this. Rust must assume +that any C++ with a reference might be a self-referential data structure. + +"To understand this conflict in more detail, we'll first need to make sure that +we have a strong understanding of Rust's move semantics." + +
+ +[poll]: https://doc.rust-lang.org/std/future/trait.Future.html#tymethod.poll diff --git a/src/unsafe-deep-dive/pinning/what-a-move-is.md b/src/unsafe-deep-dive/pinning/what-a-move-is.md new file mode 100644 index 000000000000..58db1360784e --- /dev/null +++ b/src/unsafe-deep-dive/pinning/what-a-move-is.md @@ -0,0 +1,66 @@ +# What a move is in Rust + +Always a bitwise copy, even for types that do not implement `Copy`: + +```rust +#[derive(Debug, Default)] +pub struct DynamicBuffer { + data: Vec, + position: usize, +}; + +pub fn move_and_inspect(x: DynamicBuffer) { println!("{x:?}"); } + +pub fn main() { + let a = DynamicBuffer::default(); + let mut b = a; + b.data.push(b'R'); + b.data.push(b'U'); + b.data.push(b'S'); + b.data.push(b'T'); + move_and_inspect(b); +} +``` + +Generated [LLVM IR] for calling `move_and_expect()`: + +```llvm +call void @llvm.memcpy.p0.p0.i64(ptr align 8 %_12, ptr align 8 %b, i64 32, i1 false) +invoke void @move_and_inspect(ptr align 8 %_12) +``` + +- `memcpy` from variable `%b` to `%_12` +- Call to `move_and_inspect` with `%_12` (the copy) + +
+ +Note that `DynamicBuffer` does not implement `Copy`. + +Implication: a value's memory address is not stable. + +To show movement as a bitwise copy, either [open the code in the playground]() +and look at the or [the Compiler Explorer]. + +Optional for those who prefer assembly output: + +The Compiler Explorer is useful for discussing the generated assembly and focus +the cursor assembly output in the `main` function on lines 128-136 (should be +highlighted in pink). + +Relevant code generated output `move_and_inspect`: + +```assembly +mov rax, qword ptr [rsp + 16] +mov qword ptr [rsp + 48], rax +mov rax, qword ptr [rsp + 24] +mov qword ptr [rsp + 56], rax +movups xmm0, xmmword ptr [rsp] +movaps xmmword ptr [rsp + 32], xmm0 +lea rdi, [rsp + 32] +call qword ptr [rip + move_and_inspect@GOTPCREL] +``` + +
+ +[LLVM IR]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=6f587283e8e0ec02f1ea8e871fc9ac72 +[The Compiler Explorer]: https://rust.godbolt.org/z/6o6nP7do4 diff --git a/src/unsafe-deep-dive/pinning/what-pinning-is.md b/src/unsafe-deep-dive/pinning/what-pinning-is.md new file mode 100644 index 000000000000..6f543af7e310 --- /dev/null +++ b/src/unsafe-deep-dive/pinning/what-pinning-is.md @@ -0,0 +1,46 @@ +# What pinning is + +Abridged `Pin` from the Rust standard library: + +```rust,ignore +#[repr(transparent)] +pub struct Pin { + pointer: Ptr, +} + +impl> Pin { + pub fn new(pointer: Ptr) -> Pin { ... } + + pub fn into_inner(pin: Pin) -> Ptr { ... } + + pub unsafe fn new_unchecked(pointer: P) -> Pin { ... } +} +``` + +
+ +Conceptually, pinning prevents the default movement behavior. + +This appears to be a change in the language itself. + +However, the `Pin` wrapper doesn't actually change anything fundamental about +the language. + +`Pin` doesn't expose safe APIs that would allow a move. Thus, it can prevent +bitwise copy. + +Unsafe APIs allow library authors to wrap types that do not implement `Unpin`, +but they must uphold the same guarantees. + +The documentation of `Pin` uses the term "pointer types". + +The term "pointer type" is much more broad than the pointer primitive type in +the language. + +A "pointer type" wraps every type that implements `Deref` with a target that +implements `Unpin`. + +Rust style note: This trait bound is enforced through trait bounds on the +`::new()` constructor, rather than on the type itself. + +
diff --git a/src/unsafe-deep-dive/pinning/why-difficult.md b/src/unsafe-deep-dive/pinning/why-difficult.md new file mode 100644 index 000000000000..4dd622ff20ce --- /dev/null +++ b/src/unsafe-deep-dive/pinning/why-difficult.md @@ -0,0 +1,24 @@ +# Why Pin is difficult to use + +- `Pin

` is "just" a type defined in the standard library +- This satisfied the needs of its original audience (creators of async + runtimes), without needing to extending the core language +- That audience could accept some of its ergonomic downsides, as users of + `async` would rarely interact with `Pin` directly + +

+ +"You might wonder why Pin is so awkward to use. The answer is largely +historical." + +"`Pin

` offered a simpler implementation for the Rust project than +alternatives". + +"Pin was designed primarily for the ~100 people in the world who write async +runtimes. The Rust team chose a simpler (for the compiler) but less ergonomic +design." + +"More user-friendly proposals existed but were rejected as too complex for the +primary audience, who could handle the complexity." + +

From 2784239b11faf4e5f91ff0696a043f5a05f7a2dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:17:14 +0000 Subject: [PATCH 2/2] Fix cursor reallocation issue in DynamicBuffer - Calculate cursor offset before Vec push operation - Update cursor after potential reallocation to maintain correct pointer - Add explanatory comments about the reallocation handling Co-authored-by: timClicks <27889+timClicks@users.noreply.github.com> --- src/unsafe-deep-dive/pinning/phantompinned.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/unsafe-deep-dive/pinning/phantompinned.md b/src/unsafe-deep-dive/pinning/phantompinned.md index b80278c36b1f..6c66dd649074 100644 --- a/src/unsafe-deep-dive/pinning/phantompinned.md +++ b/src/unsafe-deep-dive/pinning/phantompinned.md @@ -13,9 +13,17 @@ pub struct DynamicBuffer { impl DynamicBuffer { pub fn push(&mut self, byte: u8) { + // Calculate the cursor offset before the push (which may reallocate) + let offset = unsafe { + self.cursor.as_ptr().offset_from(self.data.as_ptr()) + }; + self.data.push(byte); - // TODO: self.data may have reallocated; ensure that th cursorpoints the the correct place + // Update cursor to point to the same offset in the (potentially new) buffer + self.cursor = unsafe { + NonNull::new_unchecked(self.data.as_mut_ptr().offset(offset)) + }; } } ```