Home

Awesome

C++ Reflection and Serialization

A simple header based library for reflection and serialization in C++.

Goals

Case

This library was created primarily for use with an ECS architecture, where it can provide serialization of components, scripting language bindings to get and set components and call methods on systems, as well as a unified way to access data, e.g. for undo/redo in an editor or manual manipulation in a development console. Since reflection of every component and system class is required to fully enable this, there must be very little effort required per class. While performance is a minor concern for most of these cases, it is important that performance and memory use/alignment remain unaffected in "ordinary" use. When working with components, a name or id is normally available which can be mapped to a store or management class, while it may be acceptable to rely on virtual methods for reflection of systems.

Solution

Use visitors and template specialization for double dispatch on action (read, write, call) and member type. An optional macro removes the need to repeat the name of each field, eliminating a potential source of bugs. Reading, writing and passing arguments is facilitated by an intermediate JSON element on which inspection, manipulation and serialization can be performed.

JSON Element and Serialization / Deserialization

The provided json::Element class is utilized by the reflection system, however it should be fairly straight-forward to replace it with your own intermediate data model. It can of course also be used on its own, without the reflection system.

Examples

Reading and writing data

struct Component {
    std::string field1;
    std::vector<int> field2;

    // Make reflectable:
    void reflect(xyz::core::Reflection &r) {
        XYZ_REFLECT(r, field1);
        XYZ_REFLECT(r, field2);
    }
};
// Read:
xyz::core::ReflectionSink sink;
component.reflect(sink);
std::cout << sink.sink.object()["field1"].str();

// Write:
xyz::core::ReflectionSource source;
source.source.object()["field1"] = "new value";
component.reflect(source);

Type id

The type_id method template will produce an integer identifying a type for the duration of the program's execution. Note that it is NOT guaranteed to be stable across runs.

xyz::core::type_id<Component>() == xyz::core::type_id<Component>();
xyz::core::type_id<Component>() != xyz::core::type_id<int>();

This is useful for safe downcasts without RTTI and dynamic_cast, as well as for looking up manager classes, etc.

class StoreBase {
public:
    virtual ~StoreBase() {}
    virtual int create() = 0;
    virtual xyz::json::Element get(int) = 0;
    virtual void set(int, xyz::json::Element) = 0;
};

template<typename C>
class Store: public StoreBase {
    std::vector<C> components;

    virtual int create();
    virtual xyz::json::Element get(int);
    virtual void set(int, xyz::json::Element);
};

std::map<xyz::core::TypeId, StoreBase*> stores;

auto COMPONENT_ID = xyz::core::type_id<Component>();
stores[COMPONENT_ID] = new Store<Component>();
int myComponent = stores[COMPONENT_ID]->create();
stores[COMPONENT_ID]->set(myComponent, xyz::json::Element());

Calling methods

class System {
public:
    std::vector<Component> foo(int x, int y) { ... }
    void bar() { ... }

    // Make reflectable:
    virtual void reflect(xyz::core::Reflection &r) {
        XYZ_REFLECT_METHOD(r, System, foo);
        XYZ_REFLECT_METHOD(r, System, bar);
    }
};
// Call method:
xyz::json::Array args(2);
args[0] = xyz::json::Number(42);
args[1] = xyz::json::Number(123);

xyz::core::ReflectionCaller caller("foo", args);
system->reflect(caller);

if(caller.found)
    std::cout << caller.result.array().size();

Reflectors

The Reflector class template can be specialized to enable reflection of types which can't be modified to add a reflect method.

template<>
class xyz::core::Reflector<Vector3, void>: public xyz::core::AbstractReflector {
public:
    typedef Vector3 field_type;

    Reflector(field_type &field)
        :field(field) {}

    json::Element read() {
        json::Array array(3);
        array[0] = field.x;
        array[1] = field.y;
        array[2] = field.z;
        return json::Element(array);
    }

    void write(const json::Element &data) {
        json::Array array = data.array();
        if(array.size() != 3) {
            throw json::TypeError("Vector3 requires array with three Number elements.");
        }
        field.x = array[0].number();
        field.y = array[1].number();
        field.z = array[2].number();
    }

protected:
    field_type &field;
};

Or simply:

template<>
xyz::json::Element xyz::core::Reflector<Vector3>::read() {
    json::Array array(3);
    array[0] = field.x;
    array[1] = field.y;
    array[2] = field.z;
    return json::Element(array);
}
template<>
void xyz::core::Reflector<Vector3>::write(const json::Element &data) {
    json::Array array = data.array();
    if(array.size() != 3) {
        throw json::TypeError("Vector3 requires array with three Number elements.");
    }
    field.x = array[0].number();
    field.y = array[1].number();
    field.z = array[2].number();
}

TODO

License

Distributed under the MIT License.