Metric Panda Games

One pixel at a time.

Reflection Preprocessor in C/C++

Rival Fortress Update #7

This past week I’ve been working on a useful engine feature for Rival Fortress: the Reflection Preprocessor.

Reflection is usually defined as the ability of a program to examine itself at runtime. Many programming languages, such as C# and Java, offer built in semantics that make reflection easy. C++ offers a few tools, in the form of RTTI and templates, but while working on previous projects I found them to come at a performance cost (in case of RTTI) or a compile time cost (in the case of templates.)

For Rival Fortress I decided to build on the asset preprocessor and opengl generator by creating a preprocessor that parses source files looking for annotated sections and generate relevant code.

The Reflection Preprocessor

The preprocessor is a program written in C++ that tokenizes source files looking for code annotated with MREFLECT(...). Between the parenthesis are directives that tell the preprocessor what to do with the code that follows.

The preprocessor runs as part of the build, before anything else, and generates a Reflection.generated.h that is included by the main headers of each translation unit (I currently only use 2 translation units, one for the game+engine and one for the editor+engine).

It only runs on files that have been changed since the timestamp of the last generated file, to keep things super fast. On a cold build it can parse roughly 120 files in less than 0.1 seconds, so it adds very little overhead to the build process.

Reflection for configuration

An example of how I use the preprocessor is to generate configuration reader and writer automatically. The following is an excerpt of the annotated Config struct that holds configuration settings that the user can edit:

struct MPEConfig
  char* ReadPath;
  char* WritePath;

  MREFLECT(Config:"Engine", Default: DEFAULT_MAX_MEMORY,
           Min: MIN_MAX_MEMORY, Max: MAX_MAX_MEMORY,
           Comment: LOC("Max memory that the engine will use")
  u64 MaxMemory;

  MPEFullscreenMode FullscreenMode;

  MREFLECT(Config:"Engine", Default: DEFAULT_WINDOW_WIDTH,
           Min: MIN_WINDOW_WIDTH)
  i16 WindowWidth;
           Min: MIN_WINDOW_HEIGHT)
  i16 WindowHeight;

  // Other members below

When the reflection preprocessor parses this struct it will automatically generate functions to read/write the config file (in .INI format) with the appropriate parser functions for the types specified and validation for eventual min/max values, as well as default values.

The relevant tokens are:

  • Config: the member should be exposed in the config file under the specified section (in the previous example the section is [Engine])
  • Default: the member has a default value
  • Min/Max: the member has a min/max value
  • Comment: a localized comment that is placed before the member in the config value

Member values that are not annotated with MREFLECT are not written to the configuration file. The preprocessor also keeps track of the struct from witch each member came from and when generating the reader/writer functions adds the appropriate parameters of those types.

This makes it very easy to quickly expose configuration settings from anywhere in the project.

This is the output .ini that gets generated:

; Max memory that the engine will use
; Allowed values:
; - BorderlessFullscreen
; - Fullscreen
; - Windowed

As you can see the enum MPEFullscreenMode is treated as a string by stripping redundant prefixes and all possible enum values are prepended to the configuration setting as comments, so the user can easy make changes.

Reflection for much more

I’m currently adding features to the reflection preprocessor as need arises, and I expect it will become more and more useful as time goes on. For example generating data structures automatically (i.e. Hash tables, resizable arrays) based on types, without having to resort to C++ templates is a plus for me. It keeps compile times super fast, and that’s the way I like it.