Turtles all the way down

Step 6 - Turtles all the way down

This is the sixth step, adding recursive structures - each step adds or improves a small feature, this text is just highlighting a few details along the way, the best way to work through is to build and run each one, reading through the code with this description alongside. Similarly reading this alongside the diff for each step will also mean more than either just reading the text or just reading the code alone.
That's what works for me anyway, YMMV.

Previously on the show (aka Step 3) we added struct support, but with a limitation, struct fields had to be scalars, you couldn't have a struct that contained another struct. So now we'll remove that limitation with a minor tweak.

The problem

When registerFieldConverter registers a setter/getter for a struct field it uses a chain of if constexpr branches - one per supported type. Before this step that chain ended with std::chrono::year_month_day, any field with an aggregate type wouldn't hit a matching branch.

To support nested structs we need a catch-all branch for types that are themselves aggregates (structs). We already have a route for that: valueToNative<T> for aggregate types delegates to the StructBuilder path we built back in the struct code. So the setter branch is just:

else if constexpr (std::is_aggregate_v<F> && !std::is_same_v<F, std::string>) {
    s->*member = valueToNative<F>(val); // nested struct via StructBuilder
}

And the getter branch just:

else if constexpr (std::is_aggregate_v<F> && !std::is_same_v<F, std::string>) {
    nativeToValue<F>(s->*member, val); // nested struct via StructBuilder
}

That's the entire C++ change, (std::string is excluded here because that might satisfy is_aggregate_v).

The demo

I'll add a simple nested struct to libdemo; an address and person.

struct Location {
    std::string town   = "London";
    std::string street = "Curlew road";
    int         number = 75;
};

struct PersonInfo {
    std::string name    = "Kate";
    int         age     = 30;
    Location    address = {};  // <-- nested struct field
};

The order matters: Location must be registered before PersonInfo so that when PersonInfo's address field is registered its type is already known. Again *footnote here - TODO, we can add recursive reflect visiting logic and/or add checked assertions, but for now it's kept simple.

XSTRUCT_REFLECT(Location);   // inner first
XSTRUCT_REFLECT(PersonInfo); // outer second

And Demo gets two new methods:

PersonInfo getPersonInfo() { return m_person; }
void       putPersonInfo(PersonInfo p) { m_person = p; }

On the Python side

No changes to the binding machinery - nested structs automatically work because _python_to_c_struct and _c_to_python_struct already call _python_to_c_value / _c_to_python_value recursively for each field, and those now hit the struct branch for Location.

Python sees nested dicts, which is the natural representation:

person = obj.getPersonInfo()
# {'name': 'Kate', 'age': 30, 'address': {'town': 'London', 'street': 'Curlew road', 'number': 75}}

obj.putPersonInfo({
    'name': 'Bob',
    'age': 25,
    'address': {'town': 'Liverpool', 'street': 'Ringo close', 'number': 21}
})
print(obj.getPersonInfo())
# {'name': 'Bob', 'age': 25, 'address': {'town': 'Liverpool', 'street': 'Ringo close', 'number': 21}}

Summary

Just two if constexpr branches were needed to add recursive struct nesting. valueToNative and nativeToValue were already generic over aggregate types - so we just needed to wire the struct field converter into them.

Since this was such a tiny change... while we're at it, before we add all the C++20 templates, we can add on a simplistic "annotation" feature:

    XCLASS_DOC(Demo, "Demo class with various methods to test features");

With this we'll be able to add documentation details to our classes and methods, this is a basic start, although since we're 'reflecting' here we don't have a simple way to add function annotations (yet) so for now that's just bolted on, we can improve that with some macro and template magic later:

    xplat::registerMethodDoc(
        "Demo",   //class
        "setAll", //method
        "Set all basic fields at once", //doc
        "boolVal", "intVal", "floatVal", "textVal", "enumVal" //args
    );

Popular posts from this blog

seven month update

Tracking running Part #2

Capsure RM200 hacking