Conversation
Extract the per-method reflexive forwarding bodies into reflexive_entries and emit a #[macro_export] macro_rules! named after each shim that stamps impl SourceTrait for <object>. The reflexive option now invokes that macro (@bare/@boxed) instead of emitting the impls inline. This is an internal refactor with no behavior change: the macro is the shared linking mechanism that a downstream #[trait_object(Shim)] will also invoke (@mount) to mount the source trait onto its own principal.
trait_object now names the carrier trait to mount (DynClone, DynHash, or any #[dyn_shim] shim) rather than the Clone/Hash capability, and emits no impls itself: it invokes each carrier's generated @mount macro, the same linking mechanism reflexive uses. This unifies Clone/Hash with arbitrary shims: - dyn_shim_recognized now generates a @mount macro for DynClone/DynHash carrying the bridge impls (replacing the inline trait_object_bridge). - #[trait_object(DynFoo)] mounts a shim's source trait onto a different principal that inherits the shim, e.g. impl Foo for dyn Bar where Bar: DynFoo. This is the general case the old Clone/Hash-only form could not express. - trait_object is no longer feature-gated, since a shim carrier needs no feature; the DynClone/DynHash carriers it can name still require theirs. Surface change: #[trait_object(Clone)] becomes #[trait_object(DynClone)]. Tests, examples, and UI snapshots updated.
Rewrite the trait_object docs (macro, README, crate root) around mounting a carrier onto a trait object: the shipped DynClone/DynHash carriers and, generally, any #[dyn_shim] shim mounting its source trait. Add the mount_trait example showing #[trait_object(DynPriced)] giving dyn Product a non-dyn-compatible Priced, and cross-link it from the trait_object example.
Restructure the README as a few short, compelling use cases (mixed Box<dyn> collections, Clone/Hash on trait objects, reflexive/trait_object mounting, foreign traits) that direct to the rustdoc for the full rules. Reword carrier-rejection comments to describe behavior rather than past spellings.
The Rule example had only a dispatchable method, so dyn Rule already worked and the shim was pointless. Give Rule a generic threshold method that rules out dyn Rule, so the shim and reflexive impl are what enable the erased value at all.
A method that is non-dyn-compatible only because of a generic type parameter (or argument-position impl Trait) bounded by a single trait and used behind a reference can now be kept in the shim instead of skipped, by annotating it #[dyn_shim(erase)]. The shim lowers the parameter to a trait object (&mut impl Write -> &mut dyn Write) and the forwarding call reborrows the argument so the source method's parameter re-infers to the sized reference type, sound whenever the bound provides impl Bound for &mut (dyn Bound) (as std does for Hasher, Write, Read). The transform is extracted into erase_generic and ErasedFn, a shared substrate: the recognized Hash bound's hidden __dyn_shim_hash method, which previously hand-rolled exactly this erasure of Hash::hash's generic hasher, is now generated by running a synthetic fn(&self, &mut impl Hasher) through erase_generic. forward is split so an erased method routes through forward_erased, and skip suppresses only the generic / impl-Trait disqualifiers under erase (never Self, which cannot be erased). erase_generic rejects what it cannot lower with a method-site error: a const generic, a multi-bounded parameter, or a parameter used by value, in the return type, or in more than one argument. Tests: erased dispatch through Box<dyn Shim> for both the named-generic and impl-Trait forms, a UI snapshot for the return-type rejection, and the existing Hash suite continues to pass through the refactored carrier.
A `-> Self` builder is non-dyn-compatible (Self is unsized as the trait object) and was either skipped or, in a reflexive impl, given a panicking stub. #[dyn_shim(boxed)] instead makes the shim method return Box<dyn Shim>: the blanket impl boxes the concrete result, which unsizes to the shim object because the implementor is Shim and the method requires Self: 'static (a bound that, unlike Self: Sized, keeps the method in the vtable). A reflexive = boxed impl then satisfies the source -> Self, since there Self is exactly Box<dyn Shim>, so a consuming or chaining builder keeps working on an erased value instead of panicking. This is the general form of the boxing the recognized Clone bound applies to clone. forward gains the shim name and routes a boxed method through forward_boxed; skip suppresses only the Self disqualifier under boxed. forward_boxed rejects shapes it cannot box (a non-Self return, a generic method, Self in an argument), and the macro rejects combining boxed with auto-trait markers on a reflexive shim, since Box<dyn Shim> cannot carry + Send. Tests: a consuming and a by-ref builder dispatched through Box<dyn Shim> and through an impl-Step-by-value boundary (the boxed reflexive impl), plus a UI snapshot for the marker rejection.
Extract build_boxed_method: given a method signature and a Self-typed expression, it fills in the -> Box<dyn shim markers> return and the where Self: 'static markers bound, and boxes the expression. This is the boxing half of #[dyn_shim(boxed)], factored out the way erase_generic is the erasure half shared with the Hash carrier. expand_clone now generates each per-combo __dyn_shim_clone_box method through build_boxed_method (its boxed expression is Clone::clone(self)), and forward_boxed builds a -> Self builder's shim method through the same primitive (boxed expression: the forwarded source call). Clone is thus the marker-combo consumer of the boxing substrate, mirroring how __dyn_shim_hash consumes erase_generic. No behavior change: the full Clone suite, including every auto-trait marker combination and the ToOwned / DynClone-upcast paths, passes unchanged. The source-builder marker gate stays: a boxed builder's reflexive impl forwards through the shared, combo-independent mount entries, which can only satisfy the marker-free Box<dyn shim>, whereas Clone re-attaches the marker in its own per-combo bridge.
Record what lifting #[dyn_shim(boxed)]'s auto-trait-marker gate would take (suffixed shim methods plus per-combo mount routing) at the gate itself, so the boundary is documented where it is enforced.
A #[dyn_shim_foreign] method with a default body is now provided by the shim rather than forwarded: it is emitted verbatim on the shim trait and left out of the blanket impl, the reflexive impl, and the mount macro, so its body computes from the shim's forwarded methods. This lets a foreign shim add a convenience method the foreign trait does not declare; previously the macro forwarded it to a <T as Source>::method call that did not exist and failed to compile. The local #[dyn_shim] form is unchanged: a default body lives on the re-emitted source trait and is forwarded like any other method, so an implementor's override is still honored (is_provided returns false there). Also adds a test that a default body on an UNFORWARDABLE method already works through a reflexive impl: a generic method is skipped from the shim and omitted from impl SourceTrait for Box<dyn Shim>, which inherits the source default and dispatches its forwarded calls back through the shim. Tests: foreign provided default callable on Box<dyn Shim> and excluded from the reflexive impl (so the box still satisfies the foreign trait), plus the local reflexive default-inheritance case. Docs: a Provided methods section on dyn_shim_foreign and a README note.
Two more remediations for a method that cannot forward through the shim in a reflexive impl, beyond the panicking stub: #3 `where Self: Sized` on a `reflexive = bare` impl: such a method is not part of the unsized object's surface, so it is omitted from `impl Foo for dyn DynFoo` entirely. Calling it on a `&dyn DynFoo` is then a compile error (Sized not satisfied) rather than a runtime panic. Previously the macro rejected the shim, since the method could neither forward nor be stubbed in the bare form. The boxed object is Sized, so a `boxed` impl still needs a stub or default. #5 `#[dyn_shim(stub = <expr>)]`: a custom fallback body for the erased impl, so the method can degrade to a value (None, Default::default(), ...) instead of aborting. `#[dyn_shim(panic)]` is now the sugar for it: the Helper::Panic variant is replaced by Stub(Option<Expr>), where None is the panic-with-generated-message case and Some(expr) is the custom body. Helper loses Copy/PartialEq (Expr is neither); the equality checks become matches!. Tests: bare omits a required Self:Sized method (compiles, and a UI snapshot pins the compile error on &dyn); a stub = None method degrades to None through the reflexive impl. Docs: the reflexive remediations are now a most-to-least-preferable list in dyn_shim, the foreign helper note lists stub/erase/boxed, and the README reflexive note is rewritten. UI snapshots for the widened helper-argument and unstubbed-method messages regenerated.
Record at the forward_boxed generic-method rejection what supporting a generic -> Self builder would take: the two transforms touch disjoint parts of the signature (erase the arguments, boxing the return), so composing them is mechanical but needs a way to request both helpers on one method and a forwarder that runs erase_generic then boxes.
Rewrite the reflexive example around the remediation ladder for methods that cannot forward through the shim: explain<W: Write> via erase (real forwarding), parsed<T> via stub = None (graceful degradation), and threshold<T> via panic (last resort), alongside the existing bare+boxed bridge. Add a builder example for #[dyn_shim(boxed)], which needs reflexive = boxed (a bare dyn cannot be a returned Self) so it cannot share the bare+boxed reflexive example. Also silence unused-variable warnings on a stub: the restated signature keeps the argument names but a panic/stub body ignores them, so emit #[allow(unused_variables)] on stub methods. Previously latent because the only stubbed methods in tests were argless (fresh/make); surfaced by a stubbed parse(&self, text).
The examples are named after the dyn_shim concept they demonstrate (clone, hash, reflexive, trait_object, foreign); the new one shows #[dyn_shim(boxed)], so name it boxed.rs to match, not after the builder pattern it happens to use.
README: drop the mechanics paragraphs added this session (erase, the remediation ladder, foreign provided-defaults) so it is again a few short use cases that defer to the docs. The reflexive section keeps a one-line mention of the remediations and trait_object mounting; the Documentation section now enumerates what rustdoc covers (the per-method helpers, reflexive, recognized bounds, trait_object, foreign, features). Rustdoc review: - dyn_shim Method Selection: note that a generic/impl-Trait argument can be erased and a -> Self builder boxed, cross-linked to those sections, so the skip list points at its remediations. - Reflexive Impl: the boxed paragraph re-introduced boxed right after the ladder already listed it; reframe it as the detailed elaboration. - crate root: fix the unindented list continuations (rustdoc/clippy warnings) and reorder so the colon introduces the DynClone/DynHash list directly rather than a paragraph. cargo doc --all-features passes under -D warnings (no broken links).
Drop the use-case sections and all mechanics prose; the README is now just one dyn_shim example (a mixed Box<dyn DynParser> collection from a non-dyn-compatible Parser) and the dyn_hash drop-in, pointing to the rustdoc for everything else.
The attribute that mounts a carrier onto an already-dyn-compatible trait is renamed from trait_object to dyn_shim_bind, putting it under the same prefix as dyn_shim and dyn_shim_foreign. The new name reflects what it does: it binds a carrier's capability (a dyn_shim shim's source trait, or the shipped DynClone/DynHash) onto dyn YourTrait, so the object satisfies a trait it cannot list as a supertrait. The internal mechanism follows the same rename: the generated per-carrier mount macro becomes a bind macro, with @Bind arms, build_bind_macro, and bind_arm. Where "mounted impl" would collide with trait-bound wording it is reworded to "generated impl". The external dyn-clone/dyn-hash macros (clone_trait_object!, hash_trait_object!) and a struct field named trait_object keep their names; only the attribute and its machinery are renamed.
The @Bind macro arm re-spelled the Clone/ToOwned/Hash impls that clone_bridge and hash_bridge already emit for the recognized-bound path. Generalize those emitters to a ToTokens principal so the arm can hand in the $principal/$marker metavariables as literal tokens and delegate, leaving one source of truth for the bridge impls. The two paths now differ only in the carrier: a generated shim method versus __clone_box. Generated tokens are unchanged.
erase_by_value.rs pins the macro's direct rejection of a by-value generic parameter (no reference to lower). erase_bound_not_forwarding.rs pins the natural rustc error when a sized parameter's bound has no reference-forwarding impl, so the reborrowed `&mut dyn Bound` does not satisfy it. The first snapshot is macro-emitted and toolchain-stable; the second is a rustc trait error, blessed under 1.96.0.
#[dyn_shim(erase)] reborrows an erased argument so the source method's type parameter re-infers to the sized reference type `&mut dyn Bound`, which needs an `impl Bound for &mut dyn Bound`. A `?Sized` parameter has no such constraint: the lowered object can be forwarded by name, so the parameter infers to `dyn Bound` itself, satisfied by any object-safe bound. erase_generic records a per-parameter ?Sized flag and picks the new Erased::Direct forwarding for it; sized parameters and impl Trait arguments keep reborrowing.
Run rustfmt across the workspace, drop needless borrows on the hash-comparison baselines in the integration tests and the dyn_hash example, and allow the intentional `fn by_self(self: Self)` arbitrary self type that exercises by-value receiver rewriting.
Reduce the overlapping example set from ten programs to five, each covering one capability: shim (core), clone_and_hash (recognized bounds), bind (dyn_shim_bind with both carrier kinds), reflexive (reflexive impls and remediations), and foreign. Point to them from the README.
A `#[dyn_shim_foreign]` method with a default body is shim-local: the shim trait is its only home, since there is no source trait to fall back to. When such a method is not dyn-compatible it cannot be a method of the shim, so it was dropped silently along with its body. Error instead, naming the reason and the ways to resolve it. The local form is unaffected: it re-emits the source trait with the default intact, where it is still reached on concrete types and inherited through a reflexive impl, so skipping there loses nothing. Detection is signature-based, so a method made non-dyn-compatible only by a `where Self:` bound other than literal `Sized` still slips through; the dyn_shim_foreign docs now name that limitation.
| // `Product` is dyn-compatible and is the type held behind `dyn`. The three | ||
| // carriers bind `Priced`, `Clone`, and `Hash` onto its objects. |
There was a problem hiding this comment.
You mention "carriers" in the top-level documentation. Now that we are seeing it in action, reuse that term in this example.
Also, show this partially expanded. Folks new to this idea aren't thinking in terms of "shims", and, if they are reading examples, they are new to this idea. Rather, they are thinking in impl blocks, so show them a couple impl blocks.
| } | ||
|
|
||
| // Generic over `Priced` by reference: a `&dyn Product` satisfies it. | ||
| fn shelf_price(p: &(impl Priced + ?Sized)) -> u32 { |
There was a problem hiding this comment.
Needing to specify ?Sized feels intrusive. After all, this is how users will be interacting with the crate. But, I'm not sure if there is a way to work around this generally. For this specific case, it can be omitted if there is a blanket impl for Priced:
impl<T> Priced for &T
where T: Priced {
/* ... */
}There was a problem hiding this comment.
What about &mut? self seems impossible... and then there's all the other pointer types if we want to cover this.
There was a problem hiding this comment.
And as for AsRef and AsMut, it could help somewhat, but I'm not sure how to handle traits where only AsRef is available.
There was a problem hiding this comment.
Another issue here, even if we try to do something smart like detect when a trait needs AsMut by method receivers, I think these blanket impls will collide with reflexive shims, which allows more forwarding than the blanket, but doesn't have these nice ergonomics for calling without ?Sized.
I'm really not sure what's best to do here.
This branch is a large feature drop.
Review the CHANGELOG.md first, its
[Unreleased]section is the better list of what changed.Highlights:
#[dyn_shim_foreign]for shimming a trait from another crate.reflexive = bare | boxed) so an erased value satisfies the source trait, witherase/boxed/stub/panicremediations for methods that can't forward.#[dyn_shim_bind]to mount a carrier (DynClone,DynHash, or any shim) onto a trait object that can't list it as a supertrait.DynClone/DynHashshims behind features, as near drop-in replacements for thedyn-clone/dyn-hashcrates.