Adding Reflection to C

telotortium1 pts0 comments

Adding reflection to C

Home

Adding reflection to C

David Priver, May 27th, 2026

Reflection in programming is the ability for a program to introspect its own<br>datastructures and procedures, either at compile time or at run time. This is a<br>key building block of metaprogramming. This is frequently used for<br>automatically generating serialization and deserialization code, in-app<br>debuggers and structure explorers.

C, as a venerable and minimalistic language, offers no such capability. As a<br>result, C programmers are forced to resort to a few options:

Duplicate the metadata in parallel structures that need to be kept up to<br>date with the real data structure definitions.

Stop using the native syntax for declaring structures/enums/etc. and instead<br>do it with the primitive metaprogramming of the C preprocessor. Instead of<br>directly declaring a structure, add a new X-macro that can be used to<br>generate the struct and also its type info.

Stop using the C language at all for declaring your structures and instead<br>use an external tool to generate code, such as ad-hoc scripts or some kind of<br>IDL.

Keep declaring your types in C, write a C parser yourself to get that<br>information into your hands and generate the needed reflection data or as<br>inputs to codegen.

I've tried all of these options. They all have severe downsides.

For now we will focus on runtime type info. The goal is to have a datastructure like:

// typeinfo.h<br>#include<br>#include<br>#include<br>struct TypeInfo {<br>const char *name;<br>size_t size, align;<br>size_t fields;<br>struct FieldInfo {<br>const struct TypeInfo *type;<br>const char* name;<br>size_t offset;<br>_Bool is_bitfield;<br>size_t bf_width;<br>size_t bf_offset;<br>} field[1]; // fake FAM, so it can be a member of a union<br>};<br>#define TYPEINFO_INT32 (struct TypeInfo*)0x1<br>#define TYPEINFO_UINT32 (struct TypeInfo*)0x2<br>void print_as_json(const struct TypeInfo* ti, const void* data){<br>if(ti == TYPEINFO_INT32){<br>printf("%d", *(const int*)data);<br>return;<br>if(ti == TYPEINFO_UINT32){<br>printf("%u", *(const unsigned*)data);<br>return;<br>printf("{");<br>for(size_t i = 0; i fields; i++){<br>if(i != 0) printf(", ");<br>const struct FieldInfo* fi = &ti->field[i];<br>printf("\"%s\": ", fi->name);<br>const void* base = (const char*)data + fi->offset;<br>if(fi->is_bitfield){<br>uint32_t v = *(uint32_t*)base;<br>v >>= fi->bf_offset;<br>uint32_t mask = (1u bf_width) - 1;<br>v &= mask;<br>printf("%u", v);<br>else<br>print_as_json(fi->type, base);<br>printf("}");

This is greatly simplified: in practice you'd want info for arrays, unions,<br>function types, pointers, etc.

Option 1: Hand-maintained Parallel Type Info

// option-1.c<br>#include "typeinfo.h"<br>struct Foo {<br>int32_t x, y;<br>union {<br>uint32_t bf_bits; // fake field as you can't<br>// take the address of a bitfield<br>uint32_t is_baz: 1,<br>is_bar: 1,<br>is_foo: 1,<br>_padding: 29;<br>};<br>};<br>const struct FooInfo {<br>union {<br>struct TypeInfo info;<br>struct {<br>const char *name;<br>size_t size, align;<br>size_t fields;<br>struct FieldInfo field[5];<br>};<br>};<br>} typeinfo_Foo = {<br>.name = "Foo",<br>.size = sizeof(struct Foo),<br>.align = _Alignof(struct Foo),<br>.fields = 5,<br>.field = {<br>.type = TYPEINFO_INT32,<br>.name = "x",<br>.offset = offsetof(struct Foo, x),<br>},<br>.type = TYPEINFO_INT32,<br>.name = "y",<br>.offset = offsetof(struct Foo, y),<br>},<br>.type = TYPEINFO_UINT32,<br>.name = "is_baz",<br>.offset = offsetof(struct Foo, bf_bits),<br>.is_bitfield = 1, // no intrinsic to detect this<br>.bf_width = 1, // no intrinsic to get this<br>.bf_offset = 0, // no intrinsic to get this<br>},<br>.type = TYPEINFO_UINT32,<br>.name = "is_bar",<br>.offset = offsetof(struct Foo, bf_bits),<br>.is_bitfield = 1, // no intrinsic to detect this<br>.bf_width = 1, // no intrinsic to get this<br>.bf_offset = 1, // no intrinsic to get this<br>},<br>.type = TYPEINFO_UINT32,<br>.name = "is_foo",<br>.offset = offsetof(struct Foo, bf_bits),<br>.is_bitfield = 1, // no intrinsic to detect this<br>.bf_width = 1, // no intrinsic to get this<br>.bf_offset = 1, // no intrinsic to get this<br>},<br>},<br>};

Option 1 is error-prone, laborious and hard to keep in sync, although it does<br>give you the most control and the ability to customize things (such as<br>serializing simple structs as a json array instead of json objects for vector<br>types, using exactly the right allocator). This option is actually not as bad<br>as people think, but it does take the joy out of programming.

The real drawback is if you get things wrong, the compiler won't help. For<br>example, if you use bitfields (and contrary to popular belief, you should as<br>they lead to significant size savings, with better syntax than<br>#define flags and it's not actually hard to get portable bitfields<br>between compilers, but that's a different blog post), compilers don't offer<br>bitfield offset or width intrinsics so you have to maintain those by hand,<br>which means you can end up reading or writing the wrong bits. (Did you notice<br>that is_foo has the wrong bf_offset?)

#include "option-1.c"

int main(void){<br>struct Foo f = {<br>1, 2,<br>.is_baz = 1,<br>.is_bar = 0,<br>.is_foo = 1,<br>};<br>print_as_json(&typeinfo_Foo.info, &f);<br>// {"x": 1, "y": 2, "is_baz": 1, "is_bar": 0, "is_foo": 0}<br>// oops:...

struct const type name intrinsic typeinfo

Related Articles