Compile-time type reflection for C. A metaprogram parses your C headers using libclang, then generates static type information tables that you can query at runtime -- struct members, enum values, union fields, annotations, sizes, offsets, and more.
- Overview
- Usage
- Metaprogram Reference
- Type Info Data Model
- Platform Setup
- Building
- Integrating into Your CMake Project
typeinfo works in two phases:
-
Code generation (build time): A metaprogram (
typeinfo_metaprogram) uses libclang to parse your C headers. It finds types annotated withTI_ROOT, recursively walks their members, and emits a pair of.c/.hfiles containing static type information tables. -
Runtime introspection: Your application includes the generated files and the
typeinfo.hheader. You can then inspect any reflected type at runtime -- iterate struct members, look up enum values by name or number, check sizes and offsets, read custom annotations, and so on.
Only types explicitly marked with TI_ROOT (and their transitive dependencies)
are included in the generated output.
Include typeinfo.h and use the TI_ROOT macro to mark types for reflection.
Use TI_ANN(x) to attach custom string annotations to types, members, or
enum values.
#include "typeinfo.h"
typedef struct TI_ROOT {
const char name[256] TI_ANN(CStr);
int health;
float speed;
} Player;
typedef enum TI_ROOT {
WEAPON_SWORD TI_ANN(Melee),
WEAPON_BOW TI_ANN(Ranged),
WEAPON_STAFF TI_ANN(Magic),
} WeaponType;TI_ROOT marks a type as a root for code generation. The metaprogram will
process it and all types it transitively depends on (e.g., if Player had a
pointer to another struct, that struct would also be included).
TI_ANN(x) attaches a string annotation. You can use multiple annotations on
the same declaration. Annotations are available at runtime as NULL-terminated
string arrays. During normal compilation (without the metaprogram), both macros
expand to nothing.
./typeinfo_metaprogram game_types.h -o game_types_typeinfoThis parses game_types.h and generates two files:
game_types_typeinfo.h-- extern declarations for all reflected typesgame_types_typeinfo.c-- static data tables with type information
Each reflected type gets a global variable named typeinfo_<TypeName>. For
the example above, you would get typeinfo_Player and typeinfo_WeaponType.
NOTE: If you get errors about standard headers not being found, you'll need to
-I/usr/lib/clang/<your-clang-version>or, equivalently, the folder in which your compiler keeps those headers. This has to be done since some c standard headers are compiler-specific. The CMake build for example handles this automatically viaclang -print-resource-dir.
Include the generated header and use the type info structures:
#include "game_types.h"
#include "game_types_typeinfo.h"
#include <stdio.h>
#include <string.h>
int main(void) {
// Access struct metadata
Type_Info_Struct* player_info = &typeinfo_Player;
printf("Type: %s (size: %zu bytes)\n", player_info->name, player_info->base.size);
for (size_t i = 0; i < player_info->members_count; i++) {
Type_Info_Member* m = &player_info->members[i];
printf(" .%s at offset %zu\n", m->name, m->offset);
// Check annotations
for (char** ann = m->annotations; *ann; ann++) {
printf(" annotation: %s\n", *ann);
}
}
// Access enum metadata
Type_Info_Enum* weapon_info = &typeinfo_WeaponType;
for (size_t i = 0; i < weapon_info->values_count; i++) {
Type_Info_Enum_Value* v = &weapon_info->values[i];
printf(" %s = %lld\n", v->name, v->value);
}
// Use type info for generic value printing, serialization, etc.
Player p = { "Alice", 100, 5.5f };
// ... walk members and print/serialize based on type tags
}The generated type info references are all Type_Info* at the base level. Cast
to the appropriate variant based on the tag field:
Type_Info* ti = (Type_Info*)&typeinfo_Player;
switch (ti->tag) {
case TYPE_TAG_STRUCT: {
Type_Info_Struct* s = (Type_Info_Struct*)ti;
// iterate s->members ...
break;
}
case TYPE_TAG_ENUM: {
Type_Info_Enum* e = (Type_Info_Enum*)ti;
// iterate e->values ...
break;
}
// ... handle other tags
}See examples/print_types.c for a complete example that recursively prints
any value given its type info, including support for nested structs, arrays,
pointers, unions, and annotation-based special handling (e.g., printing
char[] fields as C strings when annotated with CStr).
USAGE: typeinfo_metaprogram [OPTIONS] [FILE...]
OPTIONS:
-o <out_name> Base name for generated files (required).
Produces <out_name>.h and <out_name>.c
-I<dir> Add an include search path (forwarded to clang)
-std=<std> Set the C language standard (forwarded to clang)
-no-builtin-types Do not emit declarations/definitions for builtin
C types (void, int, float, char, etc.)
-R Recursively walk directories for input files
-h Print usage and exit
NOTE: Depending on your system, the path to clang's builtin headers may need to be passed via
-I. The CMake build handles this automatically viaclang -print-resource-dir.
All type info structures share a common base:
typedef struct {
Type_Info_Tag tag; // Discriminant (TYPE_TAG_STRUCT, TYPE_TAG_ENUM, etc.)
size_t size; // sizeof() the type
size_t alignment; // alignof() the type
} Type_Info;The tag determines which variant to cast to:
| Tag | Variant Type | Extra Fields |
|---|---|---|
TYPE_TAG_VOID |
Type_Info_Void |
(none) |
TYPE_TAG_INTEGER |
Type_Info_Integer |
bool is_signed |
TYPE_TAG_FLOAT |
Type_Info_Float |
(none) |
TYPE_TAG_POINTER |
Type_Info_Pointer |
Type_Info* pointer_to, uint32_t qualifier_flags |
TYPE_TAG_ARRAY |
Type_Info_Array |
size_t num_elements, Type_Info* element_type |
TYPE_TAG_STRUCT |
Type_Info_Struct |
name, annotations, members, members_count |
TYPE_TAG_UNION |
Type_Info_Union |
name, annotations, members, members_count |
TYPE_TAG_ENUM |
Type_Info_Enum |
name, annotations, values, values_count |
Struct and union members are described by Type_Info_Member:
typedef struct {
char** annotations; // NULL-terminated array of annotation strings
const char* name; // Member name
size_t offset; // Byte offset within the struct/union
Type_Info* type; // Type info for this member
uint32_t qualifier_flags; // Qualifiers applied to this struct or union member; Bitmask of
// `Type_Info_Qualifier`
} Type_Info_Member;Enum values are described by Type_Info_Enum_Value:
typedef struct {
char** annotations; // NULL-terminated array of annotation strings
const char* name; // Enumerator name
long long value; // Numeric value
} Type_Info_Enum_Value;The metaprogram also generates globals for all C builtin types (unless
-no-builtin-types is passed):
typeinfo_void, typeinfo_bool, typeinfo_char, typeinfo_signed_char,
typeinfo_unsigned_char, typeinfo_short, typeinfo_unsigned_short,
typeinfo_int, typeinfo_unsigned_int, typeinfo_long,
typeinfo_unsigned_long, typeinfo_long_long, typeinfo_unsigned_long_long,
typeinfo_float, typeinfo_double, typeinfo_long_double
-no-builtin-types is useful to avoid variable redefinition errors if you generate multiple
typeinfo files for a single project.
Install the libclang development package using your distro package manager. Typical names are:
- clang-devel (Fedora)
- libclang-dev (Ubuntu/Debian)
Since every distro seems to do the fuck it wants with clang libraries and headers, make sure you
have libclang.so and the include/clang-c/ folder somewhere on your system after installation.
On Fedora, simply installing clang-devel does the trick.
On Ubuntu, and maybe other distros, the clang package may install a versioned LLVM (e.g.,
LLVM 18 on Ubuntu 24.04). The corresponding llvm-config binary will also be
versioned. Set CMAKE_PREFIX_PATH using the versioned llvm-config:
# For LLVM 18 (Ubuntu 24.04)
cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=$(llvm-config-18 --prefix)
# For LLVM 16 (Ubuntu 22.04)
cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=$(llvm-config-16 --prefix)If you install a specific version of libclang (e.g., libclang-21-dev), use
the matching llvm-config version:
Common version-specific package names: for Ubuntu:
| Ubuntu Version | Default Package | llvm-config Command |
|---|---|---|
| 22.04 (Jammy) | libclang-14-dev |
llvm-config-14 |
| 24.04 (Noble) | libclang-18-dev |
llvm-config-18 |
| Latest / PPA | libclang-21-dev |
llvm-config-21 |
For other distros, good luck. lol.
Install LLVM via Homebrew:
brew install llvmHomebrew's LLVM is keg-only (not linked into /usr/local), so you need to
tell CMake where to find it:
cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=$(brew --prefix llvm)You may also need LLVM's bin directory on your PATH so that clang resolves
to Homebrew's version (used to locate builtin headers):
export PATH="$(brew --prefix llvm)/bin:$PATH"Install LLVM via Chocolatey:
choco install llvm -yThen point CMake at the installation:
cmake -B build -DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_PREFIX_PATH="C:\Program Files\LLVM"
cmake --build build --config ReleaseAlternatively, download the LLVM installer from the
LLVM releases page and set
CMAKE_PREFIX_PATH to wherever you installed it.
- C99 or later compiler (tested with GCC, Clang, and MSVC)
- libclang development libraries and headers
The following are needed only if you use the provided CMake or Make build:
- CMake 3.16+ (for the CMake build) or GNU Make
- The
clangcommand available in your PATH (used to locate builtin headers)
Copy typeinfo_metaprogram.c, extlib.h and include/typeinfo.h into your project.
You will need to setup the compilation and execution of the metaprogram before the compilation of
your project.
NOTE:
typeinfo.his not #included by the metaprogram, so you don't have to provide the path to it with-I.
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config ReleaseThis builds the metaprogram and, by default, the print_types example. To
disable the example:
cmake -B build -DCMAKE_BUILD_TYPE=Release -DTYPEINFO_BUILD_EXAMPLES=OFFRun the example:
# Linux / macOS
./build/examples/print_types
# Windows
build\examples\Release\print_types.exeIf CMake cannot find libclang, you need to set CMAKE_PREFIX_PATH to the
LLVM installation prefix. See Platform Setup for details.
The Makefile build uses GCC by default and directly links against -lclang (no pkg-config is
available for libclang, at least on my system... smh):
make typeinfo_metaprogram # Build just the metaprogram
make examples/print_types # Build the example (also runs code generation)
make clean # Remove build artifactsTo use typeinfo as part of your own CMake project, you can add it as a subdirectory and set up a custom command to run the metaprogram during the build:
add_subdirectory(path/to/typeinfo)
# Run the metaprogram to generate type info from your header
add_custom_command(
OUTPUT
${CMAKE_CURRENT_SOURCE_DIR}/my_types_typeinfo.c
${CMAKE_CURRENT_SOURCE_DIR}/my_types_typeinfo.h
COMMAND typeinfo_metaprogram
-I${CMAKE_CURRENT_SOURCE_DIR}/path/to/typeinfo/include
-I${TYPEINFO_CLANG_BUILTIN_INCLUDE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/my_types.h
-o ${CMAKE_CURRENT_SOURCE_DIR}/my_types_typeinfo
DEPENDS
typeinfo_metaprogram
${CMAKE_CURRENT_SOURCE_DIR}/my_types.h
COMMENT "Generating typeinfo for my_types"
)
add_executable(my_app
main.c
my_types_typeinfo.c
)
target_link_libraries(my_app PRIVATE typeinfo)The typeinfo interface library provides the include path for typeinfo.h and
sets the C99 standard requirement. The TYPEINFO_CLANG_BUILTIN_INCLUDE_DIR
cache variable is set by the typeinfo CMakeLists.txt and points to clang's
builtin headers directory.