Skip to content

Code-generates ANSI C helpers that give your structs reflection-style access to field names, types, and values.

License

Notifications You must be signed in to change notification settings

lcsmuller/reflect-c

Repository files navigation

Reflect-C

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.

Highlights

  • 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.

How the toolchain works

Reflect-C Toolchain Flow

Reflect-C relies on recipes—header fragments that describe your types using a macro DSL. The build system performs four stages:

  1. Collect recipes - you list your .recipe.h recipe files (defaults live under api/).
  2. Expand directives - reflect-c_EXPAND_COMMENTS converts special /*#! ... */ directives into active code before preprocessing.
  3. Preprocess with roles - reflect-c_RECIPES.recipe.h pulls in every recipe multiple times with different REFLECTC_* flags to emit actual C definitions, lookup tables, and wrapper functions.
  4. Emit amalgamated sources - the helper makefile reflect-c.mk produces reflect-c_GENERATED.h/.c alongside an optional static library libreflectc.a for the runtime helpers in reflect-c.c.

The pipeline is intentionally pure-C, so the same commands work on any system with an ANSI C compiler.

Quick start

  1. 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 under api/.

  2. 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.
  3. Regenerate metadata.

    make gen            # rebuilds reflect-c_GENERATED.c/h from all recipes
  4. 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;
    }

Customizing the symbol prefix

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 to reflectc) controls the lowercase prefix used by all runtime functions and struct names, e.g. reflectc_get or reflectc_from_<type>.
  • REFLECTC_PREFIX_UPPER (defaults to REFLECTC) mirrors the same namespace in uppercase form for generated enums and lookup tables such as REFLECTC_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.

VS Code integration

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:

    // .vscode/c_cpp_properties.json
    {
        "configurations": [
            {
                "name": "Linux",
                "includePath": ["${workspaceFolder}"],
                "forcedInclude": ["${workspaceFolder}/reflect-c_intellisense.h"],
                "compilerPath": "/usr/bin/gcc",
                "cStandard": "c89"
            }
        ]
    }

    With this in place you don’t need to touch the individual recipe files; IntelliSense sees the macro definitions on its own.

Recipe syntax

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.

Runtime API highlights

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.

Examples

JSON serialization/deserialization

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

Iterating members generically

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);

Editing nested structures

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);

Extending scalar tags

/* 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.

Building, testing, and integrating

  • Build metadata and library - make gen (default compiler is cc).
  • 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.

Tuple helper generation (optional)

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.

Integrating into another project

  1. 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/ into third_party/reflect-c (any location works as long as your build can reach the files).

  2. 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.

  3. 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, and app_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.

  4. Compile and link - add the generated .c (or .o) plus reflect-c.c to your project, or simply link against the provided libreflectc.a. Ensure your compiler’s include path covers both reflect-c.h and the generated header.

  5. Use the helpers at runtime - include the headers, call reflectc_from_<type> to wrap your instances, and interact with fields via REFLECTC_LOOKUP, reflectc_get_member, or the other API functions described above. Free wrappers with free when done.

  6. 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.

Project structure

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

Limitations & FAQ

  • Memory ownership - reflectc_from_* allocates wrapper trees with malloc. Free them with free 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 calling reflectc_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.

Contributing

Bug reports, recipe ideas, and documentation PRs are welcome! Please run make gen and make -C test before submitting.

License

Distributed under the MIT License. See LICENSE for details.

About

Code-generates ANSI C helpers that give your structs reflection-style access to field names, types, and values.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •