Trait module

Cnx provides a simple way to create and utilize polymorphic interfaces via Trait objects. Trait objects are fat pointers, containing a pointer-to-self and a vtable pointer, that allow use of polymorphic interfaces explicitly (you operate via the Trait object), without the need for a type to carry its vtable in its definition, and without the need for heap allocation (in most cases). For specific examples of Trait objects in action, look into CnxFormat and its use through the Cnx IO, and Cnx's iterator categories.

A general example:

// declare a Trait `NumberGenerator` that requires the functions `generate` and `seed`
Trait(NumberGenerator,
      u32 (*const generate)(NumberGenerator* restrict self);
      void (*const seed)(NumberGenerator* restrict self, u32 seed););

typedef struct ExampleGenerator {
    u32 m_seed;
} ExampleGenerator;

u32 example_generate(NumberGenerator* restrict self) {
    let_mut _self = static_cast(ExampleGenerator*)(self->m_self);
    return _self->m_seed++;
}

void example_seed(NumberGenerator* restrict self, u32 seed) {
    let_mut _self = static_cast(ExampleGenerator*)(self->m_self);
    _self->m_seed = seed;
}

// Implement `NumberGenerator` for our `ExampleGenerator` type with the `example_generate`
// and `example_seed` functions.
// The names of the implementation functions do not have to stem from/build on/etc the names
// in the Trait definition, but it is good practice/convention to do so.
static maybe_unused ImplTraitFor(NumberGenerator,
                                 ExampleGenerator,
                                 example_generate,
                                 example_seed);
// a function that uses the `NumberGenerator` Trait as a parameter
u32 generate(NumberGenerator generator) {
    return trait_call(generate, generator);
}

void do_thing(void) {
    let_mut generator = (ExampleGenerator){.m_seed = 10;}
    // cast `generator` to its `NumberGenerator` implementation and pass that to `generate`
    let num = generate(as_trait(NumberGenerator, ExampleGenerator, generator));
}

Defines

#define Trait(TraitName, functions)
Declares a Trait.
#define SizedTrait(TraitName, concrete_type_size, functions)
Declares a SizedTrait.
#define ImplTraitFor(TraitName, TypeName, ...)
Implements the given trait for the given type.
#define as_trait(TraitName, TypeName, self)
Casts the given object to its implementation for the given Trait.
#define as_sized_trait(TraitName, TypeName, self)
Casts the given object to its implementation for the given SizedTrait.
#define trait_call(FunctionName, self, ...)
Calls the trait function FunctionName on self

Define documentation

#define Trait(TraitName, functions)

Declares a Trait.

Parameters
TraitName - The name to refer to the Trait
functions - The semicolon separated list of functions, in function-pointer syntax, that the Trait requires to be implemented

Declares a Trait with the given name which requires the semicolon separated list of functions to be implemented.

#define SizedTrait(TraitName, concrete_type_size, functions)

Declares a SizedTrait.

Parameters
TraitName - The name to refer to the Trait
concrete_type_size - The maximum possible size of a concrete type implementing this Trait
functions - The semicolon separated list of functions, in function-pointer syntax, that the Trait requires to be implemented

Declares a SizedTrait with the given name which requires the semicolon separated list of functions to be implemented. A SizedTrait is a trait that copies its implementing type into a fixed-size, type-erased buffer instead of referring to it via pointer. This allows Trait objects to be constructed from short-lived values and used past the lifetime of said value without needing heap allocation, but requires that all types implementing the Trait fit within this fixed size buffer. All Cnx iterators use this type of trait for their interface for these reasons.

#define ImplTraitFor(TraitName, TypeName, ...)

Implements the given trait for the given type.

Parameters
TraitName - The Trait to implement
TypeName - The type implementing the trait
... - The comma separated list of function names with which to implement the trait. They are required to be in Trait-definition order, and giving them in another order will result in either a compiler error or undefined behavior at run time (function signature and order dependent as to which would occur).

Implements the given trait for the given type.

#define as_trait(TraitName, TypeName, self)

Casts the given object to its implementation for the given Trait.

Parameters
TraitName - The Trait to cast to
TypeName - The type to cast from (the type of self)
self - The object to cast to its Trait implementation

Casts self, of type TypeName to its implementation of the Trait TraitName

#define as_sized_trait(TraitName, TypeName, self)

Casts the given object to its implementation for the given SizedTrait.

Parameters
TraitName - The SizedTrait to cast to
TypeName - The type to cast from (the type of self)
self - The object to cast to its SizedTrait implementation

Casts self, of type TypeName to its implementation of the SizedTrait TraitName

#define trait_call(FunctionName, self, ...)

Calls the trait function FunctionName on self

Parameters
FunctionName - The Trait function to call on self
self - The Trait object to call FunctionName on
... - The parameter pack of (possible, can contain no parameters) additional parameters to pass to FunctionName

Calls the function FunctionName on self, with the parameters contained in the __VA_ARGS__ parameter pack