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
struct that holds configuration settings that the user can edit:
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
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:
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.