Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generic Structs

Generic structs follow the same monomorphization model as generic functions: you write one Rust definition with type parameters, and the macro emits a separate concrete WGSL struct for each (struct, type-args) pair used in the module.

Defining a Generic Struct

#![allow(unused)]
fn main() {
pub struct Pair<T: Copy> {
    pub a: T,
    pub b: T,
}
}

The Copy (or other) bound is Rust-only and stripped from the WGSL.

Usage & Mangling

At every use site, supply the concrete type either as a turbofish on the path or as a type annotation. Each unique instantiation becomes a mangled WGSL struct:

#![allow(unused)]
fn main() {
pub fn use_pair_f32() -> f32 {
    let p = Pair { a: 1.0, b: 2.0 };
    Pair::<f32>::sum(p)
}

pub fn use_pair_i32() -> i32 {
    let p: Pair<i32> = Pair::<i32> { a: 10, b: 20 };
    Pair::<i32>::first(p)
}
}
struct Pair_f32 {
  a: f32,
  b: f32,
}

struct Pair_i32 {
  a: i32,
  b: i32,
}

Generic Impl Blocks

impl<T> Pair<T> blocks are monomorphized alongside the struct. Each method becomes a mangled WGSL function named <Struct>_<type>_<method>:

#![allow(unused)]
fn main() {
impl<T: Copy + std::ops::Add<Output = T>> Pair<T> {
    pub fn first(p: Pair<T>) -> T {
        p.a
    }

    pub fn sum(p: Pair<T>) -> T {
        p.a + p.b
    }
}
}

For Pair::<f32> this yields:

fn Pair_f32_first(p: Pair_f32) -> f32 {
  return p.a;
}

fn Pair_f32_sum(p: Pair_f32) -> f32 {
  return p.a + p.b;
}

Struct Construction

Construct a generic struct by writing the literal form Pair::<f32> { a, b } or by relying on a type annotation. The macro emits a positional WGSL constructor call with the mangled name:

#![allow(unused)]
fn main() {
let p = Pair::<f32> { a: 1.0, b: 2.0 };
}
let p = Pair_f32(1.0, 2.0);

Fields are emitted in declaration order.

Known Limitation: Struct Constructor Mangling

There is a known bug in which the bare struct-constructor form Pair { a, b } (without a turbofish or annotation that the macro can resolve) is not mangled correctly, producing invalid WGSL. Until this is fixed, the recommended workarounds are:

  • Always use the turbofish form Pair::<T> { ... } at construction sites, or
  • Annotate the binding: let p: Pair<T> = Pair { ... }.
  • For modules that exercise the bug and cannot be restructured, suppress auto-validation with #[wgsl(skip_validation)] (see Disabling Validation) so the failing constructor does not break cargo test.

The generic_structs example currently uses #[wgsl(skip_validation)] for this reason:

#![allow(unused)]
fn main() {
#[wgsl(skip_validation)]
pub mod generic_structs {
    pub struct Pair<T: Copy> {
        pub a: T,
        pub b: T,
    }

    impl<T: Copy + std::ops::Add<Output = T>> Pair<T> {
        pub fn first(p: Pair<T>) -> T { p.a }
        pub fn sum(p: Pair<T>) -> T { p.a + p.b }
    }
}
}

Multiple Type Parameters

A struct may take several type parameters; the mangled name joins all concrete types:

#![allow(unused)]
fn main() {
pub struct Cell<K: Copy, V: Copy> {
    pub key: K,
    pub value: V,
}
}

Cell::<u32, f32> produces Cell_u32_f32.

Const Generic Parameters

Structs can also take const N: usize or const N: u32 parameters, which are substituted with concrete integer literals at monomorphization time. This is the natural way to express arrays whose length varies per instantiation:

#![allow(unused)]
fn main() {
pub struct Grid<const N: usize> {
    pub cells: [u32; N],
}

impl<const N: usize> Grid<N> {
    pub fn first(cells: [u32; N]) -> u32 {
        cells[0]
    }
}

pub fn run() -> u32 {
    let g = Grid::<4> { cells: [0, 0, 0, 0] };
    g.cells[0]
}
}

Grid::<4> produces a WGSL struct Grid_4 with cells: array<u32, 4>, and Grid::<4>::first becomes Grid_4_first.

Generic Trait Impls on Array Types

Generic impl blocks on array self types (impl<T: Trait> Trait for [T; N]) are supported. The monomorphizer substitutes the concrete element type and mangles the methods:

#![allow(unused)]
fn main() {
pub trait Zeroable {
    fn zero() -> Self;
}

impl<T: Zeroable> Zeroable for [T; 4] {
    fn zero() -> [T; 4] {
        [T::zero(), T::zero(), T::zero(), T::zero()]
    }
}

pub fn caller_u32_array() -> [u32; 4] {
    Zeroable::zero::<[u32; 4]>()
}
}

The call with [u32; 4] produces a WGSL function _2array_u32_4_zero (the _2 prefix is the bijective mangled encoding of array_u32_4). Similarly, [f32; 4] produces _2array_f32_4_zero.

Limitation: Direct <[u32; 4]>::method() call syntax (QSelf paths) is not yet supported — only T::method() resolved via monomorphization. Tracked in GitHub issue #131.

PhantomData<T> Marker Fields

A generic struct may carry PhantomData<T> fields as type-parameter markers (e.g. for slab-id tags or type-level metadata). PhantomData is re-exported from wgsl_rs::std so the glob import brings it into scope. The proc-macro recognizes PhantomData<_> fields specially: they are retained in the IR (so extensions can observe which type parameter each phantom slot binds) but omitted from the rendered WGSL:

#![allow(unused)]
fn main() {
#[wgsl]
pub mod phantom_example {
    use wgsl_rs::std::*;

    pub struct Id<T> {
        pub index: u32,
        pub phantom: PhantomData<T>,
    }

    pub struct Tagged<T, A> {
        pub x: f32,
        pub t: PhantomData<T>,
        pub a: PhantomData<A>,
    }

    pub fn make_id() -> Id<f32> {
        Id { index: 0u32, phantom: PhantomData }
    }

    pub fn make_tagged() -> Tagged<f32, u32> {
        Tagged { x: 1.0, t: PhantomData, a: PhantomData }
    }
}
}

The rendered WGSL drops phantom fields entirely — Id_f32 has only index: u32, and Tagged_f32_u32 has only x: f32:

struct Id_f32 {
    index: u32
}

struct Tagged_f32_u32 {
    x: f32
}

Construction expressions use the bare PhantomData value (no turbofish). The macro strips PhantomData from the positional constructor call so the rendered arity matches the non-phantom field count: Id { index: 0u32, phantom: PhantomData } becomes Id_f32(0u).

Why retain phantom fields in the IR? Extensions consuming the IR via WgslExtension::modify_ir must be able to see the full type-parameter binding structure of a generic struct. If phantom fields were skipped at parse time, an extension inspecting struct Tagged<T, A> would see type_params: ["T", "A"] but only { x: f32 }, with no way to recover which phantom slot bound which parameter. Keeping Type::Phantom { elem } in the IR preserves the T↔field provenance.