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 |
|---|---|---|
|
0 |
Uninitialized or invalid |
|
1 |
|
|
2 |
|
|
3 |
|
|
4 |
|
|
5 |
|
|
6 |
|
|
7 |
|
|
8 |
|
|
9 |
|
|
10 |
|
|
11 |
|
|
12 |
|
|
13 |
|
|
14 |
|
|
15 |
|
|
16 |
|
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:
desc is NULL
desc->data_size is 0
desc->id is already registered
Registry is full (see available_dtype_slots())
-
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" };
Summary of ID Ranges
Range |
Purpose |
|---|---|
|
Reserved — |
|
Built-in C primitive types, registered by |
|
Reserved for future built-in type expansion |
|
User-defined types; must be |