Skip to content

← Unsafe and Soundness step 18 of 24

Hard Framework

Implementing a reference-counted pointer

Repair MyRc<T>, a single-threaded reference-counted pointer. Cloning a handle must share the allocation and increment its count without cloning T. Dropping the last counted handle must destroy the value and free the allocation exactly once.

Keep the supplied representation and public methods:

struct Inner<T> { strong: Cell<usize>, value: T }
pub struct MyRc<T> { ptr: NonNull<Inner<T>>, _marker: PhantomData<Inner<T>> }

pub fn rc_script(ops: Vec<String>) -> Vec<i64>

Implement Clone manually for every T, without a T: Clone bound. Preserve new, strong_count, Deref and Drop, correcting them if needed. Keep the type neither Send nor Sync. Use the provided script interpreter to exercise the pointer; do not replace it with simulated counts.

Script behavior

There are sixteen slots, numbered 0 through 15. Each holds either no handle or one MyRc<String>. Return one integer for every input operation.

Operation Result
new I S Store a fresh allocation containing S in slot I; return 1.
clone I J Clone I, record its new count, then replace slot J; return the recorded count.
drop I Remove the handle and return its allocation's count minus one.
count I Return the current count for that slot's allocation.
read_len I Return the stored string's UTF-8 byte length through Deref.
Unknown command Return -2.

A required invalid slot index or empty source slot gives -1. Replacing an occupied slot drops its previous handle. The clone result is recorded before that drop, so it can differ from a subsequent count:

operations: ["new 0 hi", "clone 0 0", "count 0", "read_len 0", "drop 0"]
results:    [1,          2,           1,         2,            0]

Preserve the supplied parser. It splits at the first two literal ASCII spaces, without trimming or collapsing spaces. The remainder is S for new, so spaces inside it are preserved; a missing remainder creates an empty string. Slot fields use Rust's usize parser, including an optional leading +. Missing, malformed and out-of-range indices are invalid. For clone, the entire remainder is the destination index. drop, count and read_len ignore any remainder after their index. Empty input returns an empty result.

Count ownership, not pointer copies

A fresh allocation starts at one. Each successful clone adds one ownership obligation, and each destructor releases one. A forgotten handle does not run its destructor, so its count remains and can keep the allocation alive.

The starter derives Clone. That copies its pointer and marker without updating the shared count, and can impose an unnecessary T: Clone bound. Dropping one of the resulting handles can free storage still referenced by another. Further access is undefined behavior; a particular crash or double free is not a guaranteed symptom.

Write a manual Clone that checks for overflow before incrementing, then returns a handle with the same pointer. It must not clone the stored value.

Prevent overflow without corrupting the count

Repeatedly cloning and forgetting handles can increase the count beyond the number of handles still accessible to the program. A safe abstraction must prevent that counter from wrapping even if reaching the limit is impractical in an ordinary test.

The reference uses assert!(count != usize::MAX) before changing the count. If that assertion unwinds, the original count is intact and no new handle has been returned. Catching this panic therefore does not leave a wrapped count. Aborting is another possible policy. The critical requirement is to avoid continuing with an invalid ownership count.

Free only after the last release

If the old count is one, reconstruct the Box from its original pointer and drop it. Otherwise decrement the count. Decrementing first and then checking for zero is also valid if all accesses happen before deallocation; the starter's decrement-before-check order is not itself the bug.

Box::leak leaves the allocation alive and returns a mutable reference with a suitable chosen lifetime. It does not require every T to be 'static. Box::from_raw takes ownership back; call it once, using the pointer from the original allocation. Never read the counter after freeing it. See the Box ownership APIs.

inner(&self) and Deref::deref(&self) return references tied to the handle's borrow. Do not introduce an unconstrained output lifetime that lets a caller use the value after its owners are gone.

Keep the single-threaded boundary

The shared counter uses Cell<usize>, not an atomic operation. NonNull makes the supplied handle neither Send nor Sync, so no manual implementation is needed. Do not add unsafe Send/Sync implementations. Switching to a concurrent pointer requires a separate synchronization and lifetime argument, not merely replacing the counter's type.

Keep the PhantomData marker as part of the supplied ownership representation. It does not free memory or adjust the count; the implementation must do that. See NonNull and the standard library's Rc overview.

Check more than the reported counts

The script checks observable counts and reads. It does not prove that the allocation is freed, that overflow is handled, or that unsafe code is sound. Review those properties separately. A destructor-counting value is useful for testing that the last drop destroys T once, without requiring T: Clone. Passing this exercise requires compilation, test cases and Clippy with warnings denied.