Skip to main content

obfuscate

Macro obfuscate 

Source
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 a const assertion 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:

TokenKindBehaviour
Send, Syncunsafe implCaller asserts the inner type satisfies the marker. No bound check is emitted.
Debug, DisplaysafeForwards the Formatter to the inner value — width/precision/alternate flags survive.
PartialEqsafeself.as_ref() == other.as_ref()
Eqsafe markerPairs with PartialEq.
HashsafeHashes the inner value.
ClonesafeDeep clone via the inner’s Clone. Storage is independent.
DefaultsafeConstructs 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.