|
Toast Engine
|
Authors: Xein (& Dante)
Created on: 11 Jun 2026
The reflection system allows you to get real-time information about any member, function, and other properties of any Node class. It also allows you to find and set any of their values by string, which is useful when you don't know what members are on a class or if one of them exists.
Any Node class comes with its own toast::NodeInfo* info() const; where you can access all the information about its reflected data:
It also allows as, for example, to create a newNode() function that can return different node types without needing to be templated or by checking the type on runtime:
Invalid Button annotations are retained as reflection metadata but ignored by the Inspector and runtime button-event handler.
Scalar C++ enum fields use the Enum attribute to define their Inspector labels. The labels can use either implicit values, assigned in declaration order starting at zero:
or explicit integer values:
All entries in one attribute must use the same form. Empty labels, malformed integers, mixed implicit/explicit entries, or duplicate labels/values make the dropdown metadata invalid; the Inspector then falls back to numeric integer editing. Unknown numeric values remain visible as numbers and are not silently changed to a listed option.
At the reflection boundary the generated accessor converts the enum to its underlying integral value and converts numeric updates back to the declared enum. Prefab files, binary node data, and Inspector events continue to use the existing numeric int_t encoding. Explicit values must fit that encoding. Enum arrays are not currently supported.
Member functions can also be reflected. Mark any member function with [[Reflect]] (the same attribute used for fields) and the generator records its name, return type, and parameters (name, type, and default value if any). Functions can be public or private; the generated code uses the same member access pattern as fields.
Reflected functions are called by name through NodeInfo::call(), mirroring how field accessors hang off the reflection data rather than the Node itself. The return type is supplied as a template parameter and the arguments are forwarded as variadics, so custom structs can be passed directly:
The argument and return types must match the reflected signature after decay (top-level const, references, and so on are stripped for matching). A mismatch in arity or type logs a warning before invoking (and asserts in debug builds), then proceeds, which is undefined behavior. Calling a name that is not reflected logs a warning, asserts in debug, and returns a default-constructed R. Because arguments are passed by value through a generated trampoline, mutable reference parameters do not write back to the caller.
Function metadata is also available through info():
We needed a reflection system for our game engine, which will be used for a lot of core components like serialization and deserialization, editor interoperability, ticking, etc.
Initially, we presented the idea of using C++26 reflection for a deeply integrated static reflection system, but the lack of support for cross-compilation made us having to create our own reflection implementation.
The core of this feature was to implement some sort of static-based member reflection with a code generator but without interfering the client with odd syntax or macros when working on a game. That set us with the rules of not using macros and not modifying the class header file which were some very difficult ground rules.
We implemented the system by parsing the header files with an AST generator and using native [[attributes]] to add aditional information to the fields on our classes.
A completely constexpr templated struct called Reflect<T> would hold all reflected fields, functions and attributes on a given class, with getters and setters and a bunch of other functions like RTTI, Factory functions, destructors, etc. to avoid the use of vtables on the Node classes while maintaining inheritance and polymorphism. A generator coded mainly in rust (tools/reflection_generator) would then parse all header files that contain a [[ToastNode]] attribute and generate an Abstract Syntax Tree with Tree-sitter. Then, it would generate C++ files that would get compiled with the project and would fill out all the information required by the Reflect<T> class.
A simple header file
gets a generated file that populates a Reflect<Demo> struct in the following way:
This is the interface for a NodeInfo struct: