macro_rules! obfuscate {
(
type: $inner:ty,
module: $mod_name:ident,
derives: [ $($trait:ident),* $(,)? ],
align: $align:literal $(,)?
) => { ... };
(
type: $inner:ty,
module: $mod_name:ident,
derives: [ $($trait:ident),* $(,)? ] $(,)?
) => { ... };
(
type: $inner:ty,
module: $mod_name:ident,
align: $align:literal $(,)?
) => { ... };
(
type: $inner:ty,
module: $mod_name:ident $(,)?
) => { ... };
(@impl
type: $inner:ty,
module: $mod_name:ident,
derives: [ $($trait:ident),* ],
align: $align:literal,
check: $check:ident $(,)?
) => { ... };
}Expand description
Generate a type-erased, inline wrapper for a single value.
Expands into a private module containing an Opaque struct that
stores one value of type: behind a fixed-shape representation that
never names the inner type. Downstream crates that handle the wrapper
never have to resolve the inner type, which breaks the transitive
monomorphization chain and can visibly cut compile times for deeply
generic types.
§Example
use cubecl_common::obfuscate;
#[derive(Clone, Debug, PartialEq)]
struct Tensor { shape: Vec<usize>, data: Vec<f32> }
obfuscate!(
type: Tensor,
module: tensor,
derives: [Send, Sync, Debug, PartialEq, Clone],
align: 8,
);
let t = tensor::Opaque::new(Tensor { shape: vec![2, 2], data: vec![1.0; 4] });
let copy = t.clone();
assert_eq!(t, copy);
// Recover the concrete type when you actually need its fields.
let inner: Tensor = t.into_inner();
assert_eq!(inner.shape, vec![2, 2]);The macro call must sit at module scope (or crate root); it cannot be
nested inside a function body, since the generated module’s super
would not see locals.
§When to reach for this
- A type appears in many generic instantiations and shows up as a compile-time bottleneck.
- You can’t use
Box<dyn Trait>: you need the concrete type back, or you want to avoid the heap. - The wrapper’s API surface (
new/as_ref/as_mut/into_inner) is enough for the boundary you’re crossing.
§Syntax
obfuscate!(
type: <inner-type>,
module: <module-name>,
derives: [<trait>, ...], // optional
align: <integer-literal>, // optional; default is 64
);type: and module: are mandatory and must appear in that order.
derives: and align: are both optional; if present, they must
appear in the order shown above.
derives: controls which opt-in trait impls are emitted on
Opaque. Omit the key entirely (or pass []) for none.
align: has two modes:
- Omitted (default). The wrapper is fixed at 64-byte alignment.
The macro only verifies
align_of::<inner>() <= 64; over-aligned small types are wasted bytes but it always works. - Specified. The wrapper has exactly that alignment, and the
macro asserts the literal equals
max(core::mem::align_of::<inner>(), core::mem::align_of::<*mut ()>()). Wrong values are aconstassertion failure at the call site.
A literal is required because #[repr(align(N))] does not accept
const expressions — the macro can’t substitute align_of::<inner>()
for you.
§Generated API
The macro emits a single struct, Opaque, with inherent methods
scoped pub(super) — visible to the parent module of the generated
wrapper and nowhere else:
impl Opaque {
fn new(inner: Inner) -> Self;
fn as_ref(&self) -> &Inner;
fn as_mut(&mut self) -> &mut Inner;
fn into_inner(self) -> Inner;
}
// Plus a `Drop` impl that runs the inner's destructor exactly once.
// `into_inner` suppresses this drop and returns the value instead.Any extra trait impls listed in derives: are emitted alongside.
§Derives
The wrapper is !Send + !Sync and has no extra trait impls by
default. Opt in via the derives: list:
| Token | Kind | Behaviour |
|---|---|---|
Send, Sync | unsafe impl | Caller asserts the inner type satisfies the marker. No bound check is emitted. |
Debug, Display | safe | Forwards the Formatter to the inner value — width/precision/alternate flags survive. |
PartialEq | safe | self.as_ref() == other.as_ref() |
Eq | safe marker | Pairs with PartialEq. |
Hash | safe | Hashes the inner value. |
Clone | safe | Deep clone via the inner’s Clone. Storage is independent. |
Default | safe | Constructs from <Inner as Default>::default(). |
Any other identifier in derives: [...] is a compile error at the
call site. All non-marker impls require the inner type to implement
the trait — the impl is concrete (not generic), so the bound is
checked at expansion.
§Storage and safety
The wrapper holds the inner value inline — no allocation. Storage
uses pointer-sized MaybeUninit<*mut ()>
slots rather than bytes, so any pointers owned by the inner value
retain their provenance through the round trip. A naive
[u8; size_of::<T>()] buffer would strip pointer provenance on
read, breaking under Miri / Tree Borrows for any inner type that
owns an Arc, Box, or similar.
The wrapper carries #[repr(C, align(N))]. N is either the
caller-declared align: literal (asserted strictly against the
required value) or 64 by default (asserted permissively against
align_of::<inner>() <= 64). Wrong values are a compile error
rather than runtime UB.
§Why a macro?
size_of::<T>() and align_of::<T>() are usable in const position
only for concrete types. Without the unstable
feature(generic_const_exprs), you cannot write
struct Wrapper<T> { bytes: [u8; size_of::<T>()] } as a generic. The
macro side-steps this by expanding a fresh, concrete wrapper per use
site.