Data Types (dtypes)

Overview

The dtypes.h header is the foundation of the library’s type system. It provides a runtime registry of data type descriptors that is shared across every data structure in the library — arrays, matrices, and any future structures you add. Rather than hardcoding type information into each data structure, the library treats type identity as a first-class, centrally managed concern.

This design serves two goals. First, it allows the library to perform type-safe operations on type-erased byte buffers at runtime, catching mismatches between a data structure and the type of data being written into or read from it. Second, it allows users to define and register their own custom types — structs, composite types, or any fixed-size data — and use them with any data structure in the library without modifying library source code.

Any project that uses this library should include dtypes.h directly when working with the type registry. All other library headers (c_tensor.h, etc.) include dtypes.h transitively, so explicit inclusion is only necessary when interacting with the registry outside the context of a specific data structure.

#include "dtypes.h"

How It Works

Every data type in the library is identified by a dtype_id_t, which is a 32-bit unsigned integer. Each ID maps to a dtype_t descriptor stored in an internal registry. The descriptor records the ID, the size in bytes of one element of that type, and a human-readable name used for debugging and logging.

When a data structure such as an array is initialized for a given type, it stores that type’s ID. Every subsequent operation — push, get, insert — validates the ID against the type the structure was initialized with. This ensures that data written as floats cannot be silently read back as integers or any other type.

The registry is populated at startup with a set of built-in C primitive types. Users extend the registry by registering their own descriptors before using them with any data structure.

Initialization

The registry must be initialized before use. All data structure init functions in the library call init_dtype_registry() internally, so in most cases no explicit call is required. However, if the registry is needed before any data structure is initialized — for example, to register custom types at program startup — it should be called explicitly.

if (init_dtype_registry() == false) {
    // handle error
}

It is safe to call init_dtype_registry() multiple times. After the first successful call, subsequent calls return true immediately without re-registering built-in types.

Built-in Types

The following type IDs are registered automatically by init_dtype_registry(). They cover all common C primitive types and should not be reused for user-defined types.

Constant

ID

C Type

UNKNOWN_TYPE

0

Uninitialized or invalid

FLOAT_TYPE

1

float

DOUBLE_TYPE

2

double

LDOUBLE_TYPE

3

long double

CHAR_TYPE

4

char

UCHAR_TYPE

5

unsigned char

INT8_TYPE

6

int8_t

UINT8_TYPE

7

uint8_t

INT16_TYPE

8

int16_t

UINT16_TYPE

9

uint16_t

INT32_TYPE

10

int32_t

UINT32_TYPE

11

uint32_t

INT64_TYPE

12

int64_t

UINT64_TYPE

13

uint64_t

BOOL_TYPE

14

bool

SIZE_T_TYPE

15

size_t

STRING_TYPE

16

string_t

IDs 17 through 50 are reserved for future built-in type expansion. Do not assign user-defined types to values in this range.

Registering User-Defined Types

User-defined type IDs must be greater than or equal to USER_BASE_TYPE (50). Each ID must be unique within the registry. The recommended convention is to define named constants relative to USER_BASE_TYPE so that any future change to its value does not require updates throughout user code. In total the library can handle 255 data types, leaving 150 data types that can be defined by the user.

#define MY_VEC3_TYPE    (USER_BASE_TYPE + 1u)
#define MY_COMPLEX_TYPE (USER_BASE_TYPE + 2u)

To register a type, define a dtype_t descriptor and pass it to ensure_dtype_registered(). This function is idempotent — calling it multiple times with the same descriptor is safe. It is the preferred registration function for typed wrapper init functions because it combines library initialization, duplicate detection, and registration into a single call.

#include "dtypes.h"

typedef struct { float x; float y; float z; } vec3_t;

static const dtype_t vec3_desc = {
    MY_VEC3_TYPE, sizeof(vec3_t), "vec3"
};

vec3_array_t* init_vec3_array(size_t indices, bool growth) {
    if (ensure_dtype_registered(&vec3_desc) == false) return NULL;
    return init_array(indices, MY_VEC3_TYPE, growth);
}

If you need finer control, register_dtype() can be called directly. Unlike ensure_dtype_registered, it returns false if the type is already registered, so it should only be used when duplicate registration is an error condition rather than a no-op.

Checking Registry Capacity

The registry holds a maximum of MAX_DTYPES (255) entries in total, including built-in types. The 16 built-in types registered at startup consume the first 16 slots, leaving 150 slots for user-defined types under default configuration. Use available_dtype_slots() to check remaining capacity before registering types in applications that define many custom types.

size_t slots = available_dtype_slots();
if (slots == 0u) {
    // registry is full
}

Looking Up a Type

lookup_dtype() searches the registry by ID and returns a pointer to the internal descriptor if found. This is useful when you need to retrieve the size or name of a type from its ID alone, for example in generic data structure operations or for diagnostic output.

const dtype_t* desc = lookup_dtype(FLOAT_TYPE);
if (desc != NULL) {
    printf("type: %s, size: %zu bytes\n", desc->name, desc->data_size);
}

The returned pointer is owned by the registry and remains valid for the lifetime of the program. It must not be modified or freed by the caller.

API Reference

Defines

UNKNOWN_TYPE

Uninitialized or invalid type identifier.

FLOAT_TYPE

IEEE 754 single-precision floating point (float)

DOUBLE_TYPE

IEEE 754 double-precision floating point (double)

LDOUBLE_TYPE

Extended precision floating point (long double)

CHAR_TYPE

Signed character (char)

UCHAR_TYPE

Unsigned character (unsigned char)

INT8_TYPE

Signed 8-bit integer (int8_t)

UINT8_TYPE

Unsigned 8-bit integer (uint8_t)

INT16_TYPE

Signed 16-bit integer (int16_t)

UINT16_TYPE

Unsigned 16-bit integer (uint16_t)

INT32_TYPE

Signed 32-bit integer (int32_t)

UINT32_TYPE

Unsigned 32-bit integer (uint32_t)

INT64_TYPE

Signed 64-bit integer (int64_t)

UINT64_TYPE

Unsigned 64-bit integer (uint64_t)

BOOL_TYPE

Boolean type (bool)

SIZE_T_TYPE

Unsigned size type (size_t)

STRING_TYPE

String type (string_t)

USER_BASE_TYPE

IDs 16u - 999u are reserved for future built-in type expansion.

Do not use these values for user-defined types.

First valid ID for user-defined types.

User-defined type IDs must be >= USER_BASE_TYPE to avoid conflicts with built-in and reserved IDs. Each user-defined ID must be unique within the registry. Example:

#define MY_VEC3_TYPE  (USER_BASE_TYPE + 1u)
#define MY_QUAT_TYPE  (USER_BASE_TYPE + 2u)

MAX_DTYPES

Maximum total number of types that can be registered.

Applies to built-in and user-defined types combined. Built-in types consume the first 16 slots on initialization, leaving (MAX_DTYPES - 16) slots available for user-defined types. Registration will fail if this limit is exceeded. Check available_dtype_slots() before registering if capacity is a concern.

Typedefs

typedef uint32_t dtype_id_t

Unique identifier for a registered data type.

A 32-bit unsigned integer used to identify data types throughout the library. Built-in type IDs are defined as constants below. User-defined types must use IDs >= USER_BASE_TYPE to avoid conflicts with built-in and reserved IDs.

Functions

bool init_dtype_registry(void)

Initialize the dtype registry and register all built-in types.

Must be called before any other registry function. Safe to call multiple times — subsequent calls return true immediately without re-registering built-in types. All data structure init functions in this library call this function internally, so explicit calls are only necessary if the registry is needed before any data structure is initialized.

Returns:

true Registry initialized successfully and all built-in types registered.

Returns:

false Initialization failed; registry should be considered unusable.

bool register_dtype(const dtype_t *desc)

Register a new data type in the dtype registry.

Adds a dtype_t descriptor to the registry so it can be used with any data structure in the library. Registration fails if the ID is already taken, the registry is full, or the descriptor is invalid. Prefer ensure_dtype_registered() over this function when the type may have already been registered by a prior call.

Parameters:
  • desc – Pointer to a dtype_t descriptor. Must not be NULL. desc->id must be != UNKNOWN_TYPE and not already registered. desc->data_size must be > 0. desc->name should be a valid non-NULL string for debugging.

Returns:

true Type registered successfully.

Returns:

false Registration failed. Possible reasons:

const dtype_t *lookup_dtype(dtype_id_t id)

Look up a registered type descriptor by ID.

Searches the registry for a dtype_t with the given ID. Returns a pointer to the internal descriptor if found. The returned pointer is valid for the lifetime of the registry and must not be modified or freed by the caller.

Parameters:
  • id – The dtype_id_t value to search for.

Returns:

Pointer to the matching dtype_t descriptor if found.

Returns:

NULL if no type with the given ID has been registered.

bool ensure_dtype_registered(const dtype_t *desc)

Ensure a type is registered, initializing the registry if needed.

Combines init_dtype_registry(), duplicate detection, and register_dtype() into a single safe call. This is the preferred registration function for typed wrapper init functions, as it is idempotent — calling it multiple times with the same descriptor is safe and has no side effects after the first successful registration.

Typical usage in a typed init function:

static const dtype_t float_desc = { FLOAT_TYPE, sizeof(float), "float" };

float_array_t* init_float_array(size_t indices, bool growth) {
    if (ensure_dtype_registered(&float_desc) == false) return NULL;
    return init_array(indices, FLOAT_TYPE, growth);
}

Parameters:
  • desc – Pointer to a dtype_t descriptor. Must not be NULL.

Returns:

true Type is registered and ready for use (either was already registered or was just registered successfully).

Returns:

false Registration failed or registry could not be initialized.

size_t available_dtype_slots(void)

Return the number of remaining slots available in the dtype registry.

The total registry capacity is MAX_DTYPES. Built-in types registered by init_dtype_registry() consume slots from this total. This function can be used to check available capacity before attempting to register user-defined types, particularly in applications that register many custom types.

Returns:

Number of unused registration slots remaining. Returns 0 if the registry is full.

struct dtype_t
#include <c_dtypes.h>

Descriptor for a registered data type.

Holds the identity, size, and name of a data type. Each data structure in the library uses this descriptor to perform type-safe operations on type-erased byte buffers. Built-in descriptors are registered automatically by init_dtype_registry(). User-defined descriptors must be registered explicitly via ensure_dtype_registered() or register_dtype() before use.

Example of a user-defined descriptor:

static const dtype_t vec3_desc = {
    USER_BASE_TYPE + 1u, sizeof(vec3_t), "vec3"
};

Public Members

dtype_id_t id
size_t data_size
const char *name

Summary of ID Ranges

Range

Purpose

0

Reserved — UNKNOWN_TYPE, indicates uninitialized state

115

Built-in C primitive types, registered by init_dtype_registry()

16999

Reserved for future built-in type expansion

1000 and above

User-defined types; must be >= USER_BASE_TYPE