Turtles all the way down
Step 6 - Turtles all the way down
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
);