irys
If it can be expressed as a trait bound, you can reflect over it.
Compile-time trait reflection for Rust. Automatically discover which traits a type implements — without per-type annotation, without proc macros, on stable Rust.
Quick Start
use *;
use fmt;
// 1. Define a capability
;
// 2. Register it — ONE time, covers ALL types that implement Display
register_capability!
// 3. Reflect any value
let envelope = reflect!;
assert!;
let display = envelope..unwrap;
assert_eq!;
Why irys?
| System | Registration cost | Trait discovery? | Generic capabilities? |
|---|---|---|---|
| irys | 1 per trait | Yes, automatic | Yes (with type params) |
bevy_reflect |
1 per type + 1 per type×trait | Partial | No |
typetag |
1 per type×trait | No (serde only) | No |
inventory/linkme |
1 per type | No | No |
std::any::Any |
None | No (downcast only) | No |
With irys, you register a capability once and it blanket-covers every type satisfying the trait bound. 50 types implementing Display? One registration. 500 types? Still one registration.
Features
- Zero per-type boilerplate — blanket detection via trait bounds
- Generic capabilities —
Iterator<Item=T>,Future<Output=T>,Stream<Item=T>with full type inference - Stable Rust — no nightly, no proc macros, one dependency (
seq-macro) - Registries — namespace isolation, no slot conflicts between libraries
- Pay for what you use — probe only the slots you care about
- Vec/slice ownership model —
Envelope,EnvelopeRef<'a>,EnvelopeMut<'a> - Override semantics — last-write-wins gives you specialization without specialization
- Graceful mismatches — mismatched capability maps return
None, never panic - Order-independent fields —
register_capability!fields in any order - Full compiler support — Rust's type inference resolves capabilities, catches ambiguities
Generic Capabilities
The real power of irys: register a capability once with a generic type parameter, and the compiler resolves it for every concrete type:
use PhantomData;
;
// Register ONCE — works for ALL item types
register_capability!
// Query with specific types:
envelope. // does it stream strings?
envelope. // does it stream events?
envelope. // get a &mut dyn Stream<Item = u8>
This works with any trait that has associated types or type parameters:
// Futures
;
register_capability!
// Iterators
;
register_capability!
No other Rust reflection library can do this. bevy_reflect, typetag, inventory — none support generic capability detection with full compiler inference. With irys, the Rust compiler does the heavy lifting: it infers type parameters, catches ambiguities at compile time, and eliminates unmatched probes entirely.
The register_capability! Macro
Fields can be provided in any order. All fields:
| Field | Required | Description |
|---|---|---|
slot |
Yes | Slot number within the registry |
cap |
Yes | The capability marker type |
trait_bound |
Yes | Trait bound(s) that types must satisfy |
registry |
No | Registry (defaults to DefaultRegistry) |
generics |
No | Extra generic params: [I: 'static, U: Clone] |
where |
No | Additional where clause bounds: [<I as Trait>::Assoc: Debug] |
// All of these are equivalent:
register_capability!
register_capability!
register_capability!
Adapter Traits — Compositional Capabilities
The most powerful pattern: define an adapter trait that composes multiple constraints, register it once, and it's automatically detected on any type satisfying the combination.
"I don't care WHAT this iterates — just that each item is serializable":
// Adapter trait — erases the item type
// Blanket impl — any Iterator with Serialize items qualifies
;
register_capability!
Now ANY iterator with serializable items is detected — Vec<LogEntry>, Vec<Metric>, anything. No per-type registration. No enumerating item types. The compiler's trait resolution handles it.
No other Rust reflection library can do this. It requires blanket detection + full compiler inference, which only irys provides.
Ownership Model
Like Vec<T> / &[T] / &mut [T]:
// Owned — consumes the value
let envelope = reflect!;
// Shared borrow — non-consuming, read-only
let envelope_ref = reflect_ref!;
// value is still usable here
// Mutable borrow — non-consuming, mutable trait access
let mut envelope_mut = reflect_mut!;
envelope_mut..unwrap.reset;
// value is mutated in place
Conversions:
envelope.as_ref()→EnvelopeRefenvelope.as_mut()→EnvelopeMutenvelope_mut.as_ref()→EnvelopeRef(downgrade)
Registries
Namespaces that isolate your capability slots from other libraries:
;
register_capability!
// Only probe what you care about
let envelope = reflect!;
Two libraries can both use slot 0 without conflict — they're in different registries. You control compile-time cost by probing only the ranges you need.
The Reflectable Trait
For generic code, implement Reflectable to enable reflection without knowing the concrete type:
// One-liner via helper macro:
impl_reflectable!;
// Or with custom registries:
impl_reflectable!;
// Now generic code works:
Cloning Envelopes
Register Clone as a capability, then use caps() + from_raw():
use Any;
;
register_capability!
// Clone an envelope:
let cloned_data = envelope..unwrap.clone_boxed;
let cloned_envelope = from_raw;
The map clone is cheap (just Arc refcount bumps). Mismatched data/maps safely return None.
Composing with Other Libraries
irys wraps any ecosystem trait as a capability:
// bevy_reflect — structural introspection
register_capability!
// erased_serde — type-erased serialization
register_capability!
// std::error::Error — error handling
register_capability!
// Any dyn-safe trait in the ecosystem — just register it
How It Works
irys uses autoref specialization + const generics on stable Rust:
-
register_capability!generates two impls per capability:- An inherent impl on
Probe<__ProbeTarget, Registry, N>with a trait bound (fires when__ProbeTarget: Trait) - A blanket trait impl on
&Probe<__ProbeTarget, Registry, N>(fallback no-op)
- An inherent impl on
-
reflect!expands into a loop (viaseq!) probing each slot. Rust's method resolution prefers the inherent impl when the bound is satisfied, otherwise the no-op trait fires. -
The compiler resolves this statically — no runtime branching. Undetected capabilities are eliminated entirely by the optimizer.
-
Registries are type parameters on
Probe, giving each namespace independent method resolution. -
Generic capabilities (like
IterCap<I>) work because the compiler monomorphizes at thereflect!()call site — it knows the concrete type, infersI, and resolves the correctHasCapimpl.
Limitations
- Concrete types at
reflect!()call site — the compiler must see the actual type. For generic contexts, useReflectable. This is the same limitation every Rust reflection library has. - Capabilities only propagate downward —
reflect!()can only detect capabilities whoseregister_capability!was visible when the crate was compiled. Workaround: accept a pre-constructedEnvelopeorimpl Reflectableto push thereflect!()call downstream. - Ambiguous generic registrations — if a type implements
Trait<A>ANDTrait<B>, a generic registration forTrait<T>will fail with a compile error (the compiler can't pick whichT). Register concrete instances instead (Cap<A>at slot 0,Cap<B>at slot 1). This is the compiler protecting you from an ambiguous capability map. - Manual slot numbers — you pick them, the compiler catches collisions. Registries prevent cross-library conflicts.
unsafeinternally — fat pointer transport usestransmute_copy. Sound (guaranteed layout), but the internal code hasunsafeblocks. The public API is fully safe.
Installation
[]
= "0.2"
License
MIT