<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/pining-for-arc-downcasting-in-rust-oeaabfbjs" -->

---
title: Pining for Arc Downcasting in Rust | daily.dev
description: Explores a niche Rust problem encountered while building a build driver: how to cheaply downcast an Arc&lt;JSON&gt; into a narrower type like Arc&lt;String&gt; without...
canonical: https://daily.dev/posts/pining-for-arc-downcasting-in-rust-oeaabfbjs
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: Pining for Arc Downcasting in Rust | daily.dev
og:description: Explores a niche Rust problem encountered while building a build driver: how to cheaply downcast an Arc&lt;JSON&gt; into a narrower type like Arc&lt;String&gt; without...
og:url: https://daily.dev/posts/pining-for-arc-downcasting-in-rust-oeaabfbjs
og:image: https://api.daily.dev/og/posts/oeaabfBJs.png
og:image:alt: Pining for Arc Downcasting in Rust
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Pining for Arc Downcasting in Rust

**[Lobsters](https://daily.dev/sources/lobsters)** · 13 min read · 2 upvotes · 0 comments

## Summary

Explores a niche Rust problem encountered while building a build driver: how to cheaply downcast an Arc<JSON> into a narrower type like Arc<String> without cloning expensive data. Walks through why plain Arc downcasting fails, an unsafe lifetime-extension attempt that breaks due to move semantics, and finally a working solution using Pin, Arc, and a self-referential struct (PinRef) with project/filter_project/try_project methods to safely downcast while preserving pointer validity. Includes full code examples and links to a real implementation in the author's repository.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://wolfgirl.dev/blog/2026-09-29-pining-for-arc-downcasting-in-rust>

## Questions this post answers

### Why can't I downcast an Arc<JSON> to an Arc<String> in Rust by pattern matching on value.deref()?

Calling deref() on the Arc produces a reference tied to the temporary borrow inside the function, not to the original Arc pointer, so it cannot be returned as part of a new Arc. Arc's reference-counting machinery only works with the original pointer, meaning any derived pointer from deref() dies when the function returns, forcing a full clone instead.

_Developers wrestling with smart pointer downcasting patterns can find deeper Rust internals discussions on daily.dev._

### How can I create a self-referential struct in Rust that holds both a value and a pointer into that value without unsafe lifetime extension bugs?

Wrap the value in Pin<Arc<MustPin<V>>> with a PhantomPinned marker to guarantee address stability, then store a raw pointer (*const T) into that pinned data computed once via addr-of, rather than transmuting a reference to 'static. Accessing the value only through Deref, using unsafe dereferencing of the raw pointer, keeps the pointer and its backing allocation tied together safely.

_Anyone designing self-referential Rust structs can track patterns like Pin-based address stability on daily.dev._

### How do I safely project or downcast a field out of a Pin<Arc<T>> wrapper in Rust while preserving pointer validity?

Define a project method taking a closure with an arbitrary lifetime bound, for<'a> FnOnce(&'a T) -> &'a U, apply it to the raw pointer's dereferenced value, and store the resulting pointer as the new *const U inside a PinRef. The arbitrary lifetime bound is essential because it prevents choosing a smaller lifetime that could allow projecting fields with interior mutability, which would be unsound.

_Engineers building custom pointer projection APIs in Rust can follow similar unsafe-code deep dives on daily.dev._

## Community take

How the wider developer community reacted, aggregated from 2 discussions and 10 comments across hackernews, lobsters (as of 2026-09-29).

**TL;DR:** The discussion is a technical deep-dive praising the approach while pointing out simpler alternatives (like the yoke crate or relying on Arc's pointer stability) and identifying a genuine soundness bug in the PinRef::project implementation.

**Sentiment:** 50% positive · 40% mixed · 10% skeptical

**The case for**

- The technique is seen as neat and a useful exploration of a real Rust pain point.
- Some argue Arc's pointee is already address-stable, so Pin may be unnecessary, simplifying the approach.
- The yoke crate was cited as prior art solving a similar zero-copy problem, which some found reassuring the concept is valid.

**The pushback**

- A commenter found a minimal repro showing PinRef::project is actually unsound as written due to lifetime variance issues.
- Some question whether Arc::unwrap_or_clone() could already solve the underlying use case, suggesting the custom solution may be overengineered.
- Relying on raw Arc pointer stability instead of Pin might introduce unsoundness that wasn't fully considered.

**By community**

- hackernews (mixed): No comment content was provided, so no discussion signal is available.
- lobsters (mixed): Commenters appreciated the technique and pointed to prior art like yoke and simpler Arc-based alternatives, but a detailed soundness bug in the projection method was also uncovered and discussed constructively.

**Hottest debate:** Whether Pin is actually necessary given Arc's pointer stability, versus the discovery that the PinRef::project method is unsound as written.

**Open questions**

- Can the PinRef API be made fully sound without requiring T: 'static?
- Does using raw Arc pointer stability instead of Pin introduce other unsoundness not yet identified?

**Highlights**

> As written, `PinRef::project` is unsound: [minimal repro](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=207648d7b1d7397b90d1e2009c7e1205). The issue here is that `impl for<'a> FnOnce(&'a T) -> &'a U` does not actually mean "for all lifetimes `'a`" -- this would be invalid, because `T` might not be valid for all lifetimes `'a`. Instead, it means "for all lifetimes `'a` that don't outlive `T`". Thus if you have two lifetimes `'x` and `'y` such that `'y` outlives `'x`, and you have a `PinRef<_, &'x _>`, you can project with a function that returns a reference with lifetime `'y`, since `'y` outlives all lifetimes `'a` that don't outlive `'x`. I believe this particular exploit can be patched by requiring that `T: 'static`, though it's not immediately clear to me that that makes the API sound. I think it's worth particularly questioning this logic: > How we should interpret this is: If we can go from a `&T` to a `&U` for an arbitrary lifetime `'a`, that means `*U` is a fixed offset from `*T`. Since it's valid for the function to return some `'static` reference not derived from the `&T`, this is not true as written. Of course, a `'static` reference does not cause an issue for the desired use-case, but I think it highlights that this isn't the correct line of reasoning. Instead, I might consider the following reasoning: > Because the function must work for an arbitrary lifetime `'a`, we can decide arbitrarily how `project` determines the lifetime it supplies to the function. In particular, we can choose "whatever the lifetime of `val` _ends up being_" (looking into the future). Combined with ensuring that the bound actually is "for any lifetime `'a`", this reasoning seems sound to me?
> — [T6 on lobsters · 1 points, 1 comments](https://lobste.rs/s/dayzwt/pining_for_arc_downcasting_rust#c_tbg0kj)

> Super neat! Playing around with this a bit, I'm not sure you actually need the Pin. The pointee of an Arc should already be addr stable. If we take a look at the [Arc::as_ptr](https://docs.rs/async-std/latest/async_std/sync/struct.Arc.html#method.as_ptr) doc it says: > The counts are not affected in any way and the Arc is not consumed. The pointer is valid for as long as there are strong counts in the Arc. It is completely possible I am missing some other unsoundness this introduces but here is a version with just Arc https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=6f430a92bbb2409d096c8c7d96532514
> — [dov on lobsters · 3 points, 1 comments](https://lobste.rs/s/dayzwt/pining_for_arc_downcasting_rust#c_avrpci)

> This is neat! I think [yoke](https://docs.rs/yoke/latest/yoke/) does something similar. It was [designed](https://github.com/unicode-org/icu4x/blob/main/utils/yoke/design_doc.md) for zero-copy deserialization but I think it could be used for the same thing. It seems like it doesn't use `Pin` because instead of `Deref` it has a `get()` method which returns a local lifetime, but I'm not too sure of the specifics.
> — [fractalbeauty on lobsters · 8 points, 2 comments](https://lobste.rs/s/dayzwt/pining_for_arc_downcasting_rust#c_vwga17)

> Thanks for the repro!! I knew I was missing something... Intuitively, it feels like going from small lifetime `'x` to larger lifetime `'y` isn't a problem on its own. Like, it's not _unsound_ (tho it is weird) to return a `'static` reference in the projection. Instead, I think the problem is that the `Deref` implementation doesn't obey the lifetime bound of `T`. I'm not actually sure it's possible to write the trait to make it obey that as-is (might need a to add a phantom field), but `T: 'static` should close that gap the same. I like your reasoning too, better phrased, mind if I credit you in an update?
> — [polywolf on lobsters · 1 points](https://lobste.rs/s/dayzwt/pining_for_arc_downcasting_rust#c_jn9d1n)

> Does `Arc::unwrap_or_clone()` not work here? I'm a bit confused as to the need here. You're already taking the arc as value. Maybe I'm not understanding the use case.
> — [junon on lobsters · 1 points](https://lobste.rs/s/dayzwt/pining_for_arc_downcasting_rust#c_ult8tg)

**Source threads**

- [hackernews](https://news.ycombinator.com/item?id=49898049) · 1 points · 0 comments
- [lobsters](https://lobste.rs/s/dayzwt/pining_for_arc_downcasting_rust) · 23 points · 10 comments

## Similar posts on daily.dev

- [Async Rust: Pinning demystified](https://daily.dev/posts/async-rust-pinning-demystified-rfgkmdeya) · The New Stack · 2 upvotes · 0 comments
- [What is std::pin::Pin in Rust?](https://daily.dev/posts/what-is-std-pin-pin-in-rust--sewemh0rx) · Lobsters · 3 upvotes · 0 comments
- [A hard rain's a-gonna fall: decoding JSON in Rust — Bitfield Consulting](https://daily.dev/posts/a-hard-rain-s-a-gonna-fall-decoding-json-in-rust-bitfield-consulting-rhfrqlybm) · Bitfield Consulting · 0 upvotes · 0 comments
- [Parse, don't Validate and Type-Driven Design in Rust — ramblings of @harudagondi](https://daily.dev/posts/parse-don-t-validate-and-type-driven-design-in-rust-ramblings-of-harudagondi-je8owvusb) · Hacker News · 0 upvotes · 0 comments

---

Tags: [#rust](https://daily.dev/tags/rust)

[View this post on daily.dev](https://daily.dev/posts/pining-for-arc-downcasting-in-rust-oeaabfbjs)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"Pining for Arc Downcasting in Rust","url":"https://daily.dev/posts/pining-for-arc-downcasting-in-rust-oeaabfbjs","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/pining-for-arc-downcasting-in-rust-oeaabfbjs"},"datePublished":"2026-09-29T20:15:15.988Z","dateModified":"2026-09-29T23:16:28.557Z","description":"Explores a niche Rust problem encountered while building a build driver: how to cheaply downcast an Arc<JSON> into a narrower type like Arc<String> without...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/8c722625d224bc2ba1d0a0a347c5077a?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/8c722625d224bc2ba1d0a0a347c5077a?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"Lobsters","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"Lobsters","logo":"https://media.daily.dev/image/upload/s--tl8v_Fku--/f_auto,t_logo/v1698841318/logos/lobste.jpg","url":"https://daily.dev/sources/lobsters"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/pining-for-arc-downcasting-in-rust-oeaabfbjs","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":2},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"rust","timeRequired":"PT13M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Lobsters","item":"https://daily.dev/sources/lobsters"},{"@type":"ListItem","position":3,"name":"Pining for Arc Downcasting in Rust"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/pining-for-arc-downcasting-in-rust-oeaabfbjs#faq","mainEntity":[{"@type":"Question","name":"Why can't I downcast an Arc<JSON> to an Arc<String> in Rust by pattern matching on value.deref()?","acceptedAnswer":{"@type":"Answer","text":"Calling deref() on the Arc produces a reference tied to the temporary borrow inside the function, not to the original Arc pointer, so it cannot be returned as part of a new Arc. Arc's reference-counting machinery only works with the original pointer, meaning any derived pointer from deref() dies when the function returns, forcing a full clone instead. Developers wrestling with smart pointer downcasting patterns can find deeper Rust internals discussions on daily.dev."}},{"@type":"Question","name":"How can I create a self-referential struct in Rust that holds both a value and a pointer into that value without unsafe lifetime extension bugs?","acceptedAnswer":{"@type":"Answer","text":"Wrap the value in Pin<Arc<MustPin<V>>> with a PhantomPinned marker to guarantee address stability, then store a raw pointer (*const T) into that pinned data computed once via addr-of, rather than transmuting a reference to 'static. Accessing the value only through Deref, using unsafe dereferencing of the raw pointer, keeps the pointer and its backing allocation tied together safely. Anyone designing self-referential Rust structs can track patterns like Pin-based address stability on daily.dev."}},{"@type":"Question","name":"How do I safely project or downcast a field out of a Pin<Arc<T>> wrapper in Rust while preserving pointer validity?","acceptedAnswer":{"@type":"Answer","text":"Define a project method taking a closure with an arbitrary lifetime bound, for<'a> FnOnce(&'a T) -> &'a U, apply it to the raw pointer's dereferenced value, and store the resulting pointer as the new *const U inside a PinRef. The arbitrary lifetime bound is essential because it prevents choosing a smaller lifetime that could allow projecting fields with interior mutability, which would be unsound. Engineers building custom pointer projection APIs in Rust can follow similar unsafe-code deep dives on daily.dev."}}]}
```

