Reflection-friendly data describes complex C types without hand-written metadata. Reflect-C generates that metadata for you at compile time, so you can explore, serialize, or mutate structs, unions, and enums from generic code while staying within portable ANSI C.
- Zero-cost metadata - generation happens at build time, no runtime parsing.
- Struct/union/enum coverage - handles nesting, pointers, arrays, and qualifiers.
- Runtime helpers - walk members, compute pointer depth, copy values, or allocate arrays through a uniform API.
- Battle-tested - JSON round-trip examples and unit tests validate the generated metadata.
- Self-contained - depends only on the standard library and your recipes.
Reflect-C relies on recipes—header fragments that describe your types using a macro DSL. The build system performs four stages:
- Collect recipes - you list your
.recipe.h
recipe files (defaults live underapi/
). - Expand directives -
reflect-c_EXPAND_COMMENTS
converts special/*#! ... */
directives into active code before preprocessing. - Preprocess with roles -
reflect-c_RECIPES.recipe.h
pulls in every recipe multiple times with differentREFLECTC_*
flags to emit actual C definitions, lookup tables, and wrapper functions. - Emit amalgamated sources - the helper makefile
reflect-c.mk
producesreflect-c_GENERATED.h/.c
alongside an optional static librarylibreflectc.a
for the runtime helpers inreflect-c.c
.
The pipeline is intentionally pure-C, so the same commands work on any system with an ANSI C compiler.
-
Clone and build the generator tools.
git clone https://github.com/lcsmuller/reflect-c.git cd reflect-c make gen
This compiles
reflect-c_EXPAND_COMMENTS
, builds the runtime library, and generates amalgamated sources from the sample recipes underapi/
. -
Describe your types in a recipe. Recipes are thin macro invocations that stay valid headers. Place a file like
api/person.recipe.h
in the repository:/* person.recipe.h */ #ifdef REFLECTC_DEFINITIONS /*#! #include <stdbool.h> */ #endif PUBLIC(struct, person, 4, ( (_, _, char, *, name, _, 0ul), (_, _, int, _, age, _, 0ul), (_, _, bool, _, active, _, 0ul), (_, _, char, *, email, _, 0ul) ))
A few macros worth knowing (see Recipe syntax for details):
PUBLIC
/PRIVATE
chooses whether the generated symbols are exported.- Tuple columns encode qualifier, container (
struct
,union
,enum
), raw type, pointer decoration, member name, array dimensions, and a user-provided attribute mask (unsigned long
). /*#! ... */
directives - Required for any#include
or#define
statements that the generator needs to see. These directives become active during generation but remain comments in normal compilation units. Without them, the preprocessor never sees your helper includes and generation fails silently.
-
Regenerate metadata.
make gen # rebuilds reflect-c_GENERATED.c/h from all recipes
-
Use the runtime API.
#include "reflect-c.h" #include "reflect-c_GENERATED.h" int main(void) { struct person alice = {"Alice", 30, true, "alice@example.com"}; struct reflectc *r = reflectc_from_person(&alice, NULL); /* Fast indexed access via generated enums */ size_t name_pos = REFLECTC_LOOKUP(struct, person, name, r); const char *name = reflectc_get_member(r, name_pos); printf("%s is %d years old\n", name, *(int *)reflectc_get_member(r, REFLECTC_LOOKUP(struct, person, age, r))); free(r); /* You own the wrapper memory */ return 0; }
The public runtime API is namespaced through two macros so you can avoid collisions when embedding Reflect-C into an existing project:
REFLECTC_PREFIX
(defaults toreflectc
) controls the lowercase prefix used by all runtime functions and struct names, e.g.reflectc_get
orreflectc_from_<type>
.REFLECTC_PREFIX_UPPER
(defaults toREFLECTC
) mirrors the same namespace in uppercase form for generated enums and lookup tables such asREFLECTC_LOOKUP__struct__member
.
Override them before including reflect-c.h
, AND supply -D
flags when compiling:
#define REFLECTC_PREFIX rc_compile
#define REFLECTC_PREFIX_UPPER RC_COMPILE
#include "reflect-c.h"
cc -DREFLECTC_PREFIX=rc_compile -DREFLECTC_PREFIX_UPPER=RC_COMPILE ...
Generated recipe output inherits these settings as long as the same macros are provided during the metadata build (see test/compile.c
for a concrete example), keeping runtime and generated symbols in sync.
Reflect-C recipes rely on custom macros such as PUBLIC
/PRIVATE
. If you use VS Code and want IntelliSense to understand those macros, choose one of the following approaches:
-
One-off include while editing Add the stub header at the top of a recipe while you work on it:
#include "reflect-c_intellisense.h" /* IDE-only */
Remove the include before committing, or wrap it in
#ifdef __INTELLISENSE__
so builds ignore it. -
Project-wide configuration Force-include the stub header so every recipe file picks it up automatically:
With this in place you don’t need to touch the individual recipe files; IntelliSense sees the macro definitions on its own.
Recipes live in .recipe.h
files that can be included safely in regular code. Each entry is a macro of the shape
PUBLIC(container_kind, type_name, member_count, (members...))
where container_kind
is struct
, union
, or enum
. Member tuples differ slightly depending on the container:
Container | Tuple signature | Example |
---|---|---|
struct / union |
(qualifier, container, type, decorator, name, dimensions) |
(_, _, char, *, name, [32]) |
enum |
(enumerator, assignment_op, value) |
(LEVEL_ONE, =, 1) |
Special columns:
- Qualifier -
const
,volatile
, or_
for none. - Container -
struct
,union
, or_
for plain arithmetic types. - Decorator - pointer depth (
*
,**
) or_
for scalars. - Dimensions - array declarators (e.g.,
[4]
).
During generation the recipes are replayed under different macros: once to emit actual C definitions, once to create lookup enums, once to build metadata tables, and finally to emit constructor functions (reflectc_from_<type>
).
For an exhaustive walkthrough see docs/recipe-format.md.
The runtime layer (reflect-c.h
/ reflect-c.c
) manages metadata trees produced by the generator.
Function | Purpose |
---|---|
struct reflectc *reflectc_from_<type>(<type> *self, struct reflectc *reuse) |
Materialize metadata for an instance. Pass NULL to allocate a fresh tree or reuse an existing buffer for arrays. |
size_t reflectc_length(const struct reflectc *member) |
Compute the effective length, expanding array dimensions on demand. |
size_t reflectc_get_pos(const struct reflectc *root, const char *name, size_t len) |
Lookup a member by string at runtime. |
REFLECTC_LOOKUP(struct, type, field, root) |
Generated macro returning a compile-time index for a field. |
const void *reflectc_get_member(...) |
Dereference the pointer to a member inside the wrapped object with bounds checking. |
const void *reflectc_deref(const struct reflectc *field) |
Resolve multi-level pointers and array declarators to the "natural" pointed-to object. |
const void *reflectc_set(...) / reflectc_memcpy |
Copy data into a member, with size guards. |
const char *reflectc_string(...) |
Copy string data into pointer fields, allocating storage when needed. |
void reflectc_array(const struct reflectc *root, size_t length) |
Resize metadata for treated-as-array members (e.g., JSON arrays). |
All wrappers share the same layout defined in struct reflectc
so tooling can walk types generically.
test/test.c
demonstrates a full round-trip using JSON-Build and jsmn-find
. The reflection metadata drives both the serializer and parser, including pointer depth detection for NULL
handling. Run it with:
make -C test
./test/test
struct reflectc *wrapper = reflectc_from_baz(&baz, NULL);
for (size_t i = 0; i < wrapper->members.length; ++i) {
const struct reflectc *field = &wrapper->members.array[i];
printf("%.*s -> size %zu\n", (int)field->name.length, field->name.buf, field->size);
}
free(wrapper);
size_t number_pos = REFLECTC_LOOKUP(struct, bar, number, bar_ref);
int new_value = 1337;
reflectc_set_member(bar_ref, number_pos, &new_value, sizeof new_value);
/* hooks.recipe.h */
#ifdef REFLECTC_DEFINITIONS
/*#!
#include <stdbool.h>
#include <stddef.h>
typedef size_t reflectc_words_t;
typedef unsigned long reflectc_numbers_t;
enum reflectc_custom_types {
REFLECTC_TYPES__reflectc_words_t = REFLECTC_TYPES__EXTEND,
REFLECTC_TYPES__reflectc_numbers_t,
};
*/
#endif
PUBLIC(struct, hooks, 4, (
(_, _, int, _, value, _),
(_, _, bool, _, flag, _),
(_, _, reflectc_words_t, _, words, _),
(_, _, reflectc_numbers_t, _, numbers, _)
))
At runtime the custom members resolve to the new enum values:
struct hooks sample = {21, true, 512u, 7ul};
struct reflectc *wrapper = reflectc_from_hooks(&sample, NULL);
size_t words_pos = REFLECTC_LOOKUP(struct, hooks, words, wrapper);
const struct reflectc *words = &wrapper->members.array[words_pos];
if (words->type == (enum reflectc_types)REFLECTC_TYPES__reflectc_words_t) {
printf("words: %zu\n", *(reflectc_words_t *)reflectc_get_member(wrapper, words_pos));
}
free(wrapper);
These snippets, plus additional walkthroughs, are collected in docs/examples.md.
- Build metadata and library -
make gen
(default compiler iscc
). - Debug builds -
make debug-gen
keeps debug symbols in the generated library and runtime. - Clean artifacts -
make clean
removes generated files,make purge
also removes the static library. - Run unit tests -
make -C test
builds the test harness, then run./test/test
.
The header reflect-c_TUPLE.h
contains preprocessor helpers used by the recipe DSL. It is checked in with a default arity, but you can regenerate it to support a larger maximum tuple size when needed.
-
Regenerate with a custom maximum (e.g., 64):
make tuples REFLECTC_TUPLE_MAX=64
-
The generator is a small ANSI C program at
tools/gen_tuples.c
Commit the regenerated header if you want downstream builds to avoid running the generator.
-
Vendor the sources (recommended: Git subtree)
Use Git subtree to vendor this repository into your project without submodule friction.
-
Add the upstream remote and create the subtree under
third_party/reflect-c
:git remote add reflect-c https://github.com/lcsmuller/reflect-c.git git subtree add --prefix=third_party/reflect-c reflect-c master --squash
-
Update later to pull new changes from upstream:
git fetch reflect-c git subtree pull --prefix=third_party/reflect-c reflect-c master --squash
Alternatively, you can copy
reflect-c/
intothird_party/reflect-c
(any location works as long as your build can reach the files). -
-
Author your recipes - place your own
.recipe.h
files (e.g.,recipes/player.recipe.h
) in a directory you control. Use the macro DSL to describe each struct/union/enum, and include supporting headers inside the/*#! ... */
blocks. -
Generate metadata during your build - invoke the helper makefile with your recipe directory and desired output stem. For example:
make -C third_party/reflect-c API_DIR=../app/recipes OUT=app_reflect gen
This produces
app_reflect.c
,app_reflect.h
, andapp_reflect.o
alongside the runtime library. For CMake, Meson, or others, wrap that command in a custom build step so it re-runs when recipes change. -
Compile and link - add the generated
.c
(or.o
) plusreflect-c.c
to your project, or simply link against the providedlibreflectc.a
. Ensure your compiler’s include path covers bothreflect-c.h
and the generated header. -
Use the helpers at runtime - include the headers, call
reflectc_from_<type>
to wrap your instances, and interact with fields viaREFLECTC_LOOKUP
,reflectc_get_member
, or the other API functions described above. Free wrappers withfree
when done. -
Optional polish - commit generated files if you need deterministic builds without the generator present, or extend the emitted enum range by adding typedefs and enums mapped to
REFLECTC_TYPES__EXTEND
within your recipes.
reflect-c/
├── recipes/ # Core macro recipes that drive generation
├── api/ # (create this) your application-specific recipes
├── reflect-c.c/.h # Runtime helpers shared by all generated wrappers
├── reflect-c.mk # Build helper invoked by `make gen`
├── docs/ # Supplemental documentation (plan, recipe syntax, examples)
└── test/ # JSON demo and integration tests using Greatest
- Memory ownership -
reflectc_from_*
allocates wrapper trees withmalloc
. Free them withfree
when done. - Bitfields/variadic arrays - not supported today; stick to plain declarators.
- Pointer depth heuristics -
reflectc_deref
dereferences multi-level pointers once for convenience. Override by callingreflectc_get_member
directly. - Custom allocators - the runtime uses
malloc
/calloc
/realloc
. Swap in your own versions before linking if you need arena integration. - Compiler support - tested with ANSI C (C89) compliant compilers. Newer language features (designated initializers,
_Static_assert
) are not assumed.
Bug reports, recipe ideas, and documentation PRs are welcome! Please run make gen
and make -C test
before submitting.
Distributed under the MIT License. See LICENSE
for details.