tagged_dispatch_macros 0.1.0

Procedural macros for memory-efficient trait dispatch using tagged pointers
Documentation

tagged_dispatch

Crates.io Documentation License

Memory-efficient trait dispatch using tagged pointers. Like enum_dispatch, but your enums are only 8 bytes on 64-bit systems, regardless of the variant size!

Features

  • 🎯 8-byte enums - Constant size regardless of variant types
  • ⚡ Zero-cost dispatch - Inlined, no vtable overhead
  • 📦 Familiar API - Works like enum_dispatch
  • 🔧 No allocator required - Works with no_std (bring your own allocator)
  • 🚀 Cache-friendly - Better locality than fat enums
  • 🏗️ Arena allocation support - Optional arena allocation for even better performance

Installation

Add this to your Cargo.toml:

[dependencies]
tagged_dispatch = "0.1"

# Optional: Enable arena allocation support
tagged_dispatch = { version = "0.1", features = ["allocator-bumpalo"] }

Quick Example

use tagged_dispatch::tagged_dispatch;

// Define your trait
#[tagged_dispatch]
trait Draw {
    fn draw(&self);
    fn area(&self) -> f32;
}

// Create an enum with variants that implement the trait
#[tagged_dispatch(Draw)]
enum Shape {
    Circle(Circle),
    Rectangle(Rectangle),
    Triangle(Triangle),
}

// Implement the trait for each variant
struct Circle { radius: f32 }

impl Draw for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}", self.radius);
    }

    fn area(&self) -> f32 {
        std::f32::consts::PI * self.radius * self.radius
    }
}

struct Rectangle { width: f32, height: f32 }

impl Draw for Rectangle {
    fn draw(&self) {
        println!("Drawing a {}x{} rectangle", self.width, self.height);
    }

    fn area(&self) -> f32 {
        self.width * self.height
    }
}

struct Triangle { base: f32, height: f32 }

impl Draw for Triangle {
    fn draw(&self) {
        println!("Drawing a triangle with base {} and height {}", self.base, self.height);
    }

    fn area(&self) -> f32 {
        0.5 * self.base * self.height
    }
}

fn main() {
    // Create shapes using generated constructors
    let shapes = vec![
        Shape::circle(Circle { radius: 5.0 }),
        Shape::rectangle(Rectangle { width: 10.0, height: 5.0 }),
        Shape::triangle(Triangle { base: 8.0, height: 6.0 }),
    ];

    // Dispatch trait methods
    for shape in &shapes {
        shape.draw();
        println!("Area: {}", shape.area());
    }

    // Only 8 bytes per enum, not size_of::<largest variant>()!
    assert_eq!(std::mem::size_of::<Shape>(), 8);
}

When to Use

Use tagged_dispatch when:

  • ✅ You have many instances and memory usage is critical (8 bytes vs potentially hundreds)
  • ✅ Your variants are large or vary significantly in size
  • ✅ You can accept the heap allocation overhead
  • ✅ You want better cache locality for collections

Use enum_dispatch when:

  • ✅ You want stack allocation and no heap overhead
  • ✅ Your variants are similarly sized or small
  • ✅ You have fewer instances
  • ✅ You need the absolute fastest dispatch (no pointer indirection)

Use trait objects when:

  • ✅ You need open sets of types (not known at compile time)
  • ✅ You're okay with 16-byte fat pointers
  • ✅ You need to work with external types you don't control

Advanced Features

Arena Allocation

For high-performance scenarios, use arena allocation to get Copy types and eliminate individual allocations:

#[cfg(feature = "allocator-bumpalo")]
{
    use tagged_dispatch::tagged_dispatch;

    #[tagged_dispatch]
    trait Process {
        fn process(&self, value: i32) -> i32;
    }

    #[tagged_dispatch(Process)]
    enum Processor<'a> {  // Note the lifetime parameter
        Doubler(Doubler),
        Squarer(Squarer),
    }

    struct Doubler;
    impl Process for Doubler {
        fn process(&self, value: i32) -> i32 { value * 2 }
    }

    struct Squarer;
    impl Process for Squarer {
        fn process(&self, value: i32) -> i32 { value * value }
    }

    // Create an arena builder
    let builder = Processor::arena_builder();

    // Allocate variants in the arena - returns Copy types!
    let proc1 = builder.doubler(Doubler);
    let proc2 = builder.squarer(Squarer);

    // These are Copy - just 8 bytes each!
    let proc3 = proc1;  // Copied, not moved!

    assert_eq!(proc1.process(5), 10);
    assert_eq!(proc2.process(5), 25);
    assert_eq!(proc3.process(5), 10);
}

Multiple Trait Dispatch

Dispatch multiple traits through the same enum:

#[tagged_dispatch]
trait Draw {
    fn draw(&self);
}

#[tagged_dispatch]
trait Serialize {
    fn serialize(&self) -> String;
}

#[tagged_dispatch(Draw, Serialize)]
enum Shape {
    Circle(Circle),
    Rectangle(Rectangle),
}

Default Implementations

Traits with default implementations work as expected:

#[tagged_dispatch]
trait Animal {
    fn make_sound(&self) -> &str;

    fn legs(&self) -> u32 {
        4  // Default implementation
    }
}

Non-Dispatched Methods

Mark trait methods that shouldn't be dispatched with #[no_dispatch]:

#[tagged_dispatch]
trait MyTrait {
    fn dispatched(&self) -> i32;

    #[no_dispatch]
    fn not_dispatched() -> &'static str {
        "This won't be dispatched"
    }
}

Architecture Requirements

This crate requires x86-64 or AArch64 architectures where the top 7 bits of 64-bit pointers are unused (standard on modern Linux, macOS, and Windows systems).

Limitations

  • ⚠️ Supports up to 128 variant types (7-bit tag)
  • ⚠️ Generic traits are not yet supported
  • ⚠️ Requires heap allocation for variants (or arena allocation)
  • ⚠️ Only works on x86-64 and AArch64 architectures

Safety

This crate uses unsafe code for tagged pointer manipulation. All unsafe operations are carefully documented and tested. The safety invariants are:

  1. Pointers are always valid and properly aligned
  2. Tags are always within the valid range (0-127)
  3. Proper cleanup via Drop implementation
  4. Type safety enforced at compile time

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.