Complexity

Step 2 - Complexity

This is the second step, adding enums and method wrappers - 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.

Step 1's Python binding worked but was deliberately naive: any method call on a PythonClassInstance was blindly forwarded to XPLAT_invoke via __getattr__. Here we can fix that - at library load time we'll now iterate the full metadata and pre-build typed stub methods, one per real C++ method. Let's also add a new type in addition to the primitives.

Another type - Enums

The demo now gets a MyEnum and we expose it with a matching macro:

enum class MyEnum { Red, Green, Blue };
XENUM_REFLECT(MyEnum);

The macro calls xplat::registerEnumReflect<MyEnum>("MyEnum"), which uses C++26 to iterate the enumerators:

static constexpr auto enumerators = []() consteval {
    return std::define_static_array(std::meta::enumerators_of(^^MyEnum));
}();

template for (constexpr auto Enumerator : enumerators) {
    if constexpr (std::meta::has_identifier(Enumerator)) {
        values.push_back({
            std::string(std::meta::identifier_of(Enumerator)),
            static_cast<int>([:Enumerator:])   // splice the enumerator's value
        });
    }
}
Registry::instance().registerEnum(enumName, std::move(values));

Pretty much the same pattern as the method reflection.

Two things worth noting here, first, the consteval lambda: std::meta::enumerators_of returns a value usable only in a constant-evaluated context, so we wrap the call in []() consteval { ... }() to guarantee that. next, [:Enumerator:] is the value splice - similar to the type splice typename[:...:] from Step 1, but here it splices the enumerator itself (its integer value) rather than a type.

The C API gains five new query functions (we'll end up with rather more :)):

size_t      XPLAT_getNumEnums(const Registry *registry);
const char *XPLAT_getEnumName(const Registry *registry, size_t index);
size_t      XPLAT_getEnumValueCount(const Registry *registry, const char *enumName);
const char *XPLAT_getEnumValueName(const Registry *registry, const char *enumName, size_t index);
int         XPLAT_getEnumValueInt(const Registry *registry, const char *enumName, size_t index);

Again the same pattern as the methods, we can ask "how many" and then "give me 1" etc. Python iterates over these to construct real IntEnum classes dynamically:

enum_dict = {}
for j in range(lib.XPLAT_getEnumValueCount(registry, enum_name.encode('utf-8'))):
    value_name = lib.XPLAT_getEnumValueName(registry, enum_name.encode('utf-8'), j).decode('utf-8')
    value_int  = lib.XPLAT_getEnumValueInt(registry, enum_name.encode('utf-8'), j)
    enum_dict[value_name] = value_int

enums[enum_name] = IntEnum(enum_name, enum_dict)

demo.MyEnum.Blue now exists as a proper Python enum value with a .name and .value.

Type-aware method stubs

The bigger change now is how Python builds its method objects. In the first step, __getattr__ returned a generic Method with no type knowledge - any name, any arguments. However, now at bind_library time, we walk every class and every method, read the parameter and return type metadata, and build a properly typed stub for each one:

for j in range(lib.XPLAT_getClassMethodCount(registry, i)):
    method_ptr       = lib.XPLAT_getMethod(registry, i, j)
    method_name      = lib.XPLAT_getMethodName(method_ptr).decode('utf-8')
    return_type      = lib.XPLAT_getMethodReturnType(method_ptr)
    return_type_info = _read_type_info(lib, lib.XPLAT_getMethodReturnTypeInfo(method_ptr))
    num_params       = lib.XPLAT_getMethodParamCount(method_ptr)

    param_types, param_type_names, param_type_infos = [], [], []
    for k in range(num_params):
        param_types.append(lib.XPLAT_getMethodParamType(method_ptr, k))
        param_type_names.append(lib.XPLAT_getMethodParamTypeName(method_ptr, k).decode('utf-8'))
        param_type_infos.append(_read_type_info(lib, lib.XPLAT_getMethodParamTypeInfo(method_ptr, k)))

    methods[method_name] = (param_types, param_type_names, param_type_infos,
                            return_type, return_type_name, return_type_info, param_names)

The result is stored in a methods dict on PythonClass, when __getattr__ is called on an instance it now looks up the name in that dict - if it's not there, it raises AttributeError rather than leaping into the unknown. The Method object it returns also holds the expected types, so it can validate arguments and convert enum return values to their Python IntEnum equivalent rather than returning a raw integer.

New C API functions for method and type queries (I said there'd be more):

size_t      XPLAT_getClassMethodCount(const Registry *registry, size_t classIndex);
MethodInfo *XPLAT_getMethod(const Registry *registry, size_t classIndex, size_t methodIndex);
const char *XPLAT_getMethodName(const MethodInfo *method);
XPlatType   XPLAT_getMethodReturnType(const MethodInfo *method);
size_t      XPLAT_getMethodParamCount(const MethodInfo *method);
const char *XPLAT_getMethodParamName(const MethodInfo *method, size_t paramIndex);
XPlatType   XPLAT_getMethodParamType(const MethodInfo *method, size_t paramIndex);
const void *XPLAT_getMethodReturnTypeInfo(const MethodInfo *method);
const void *XPLAT_getMethodParamTypeInfo(const MethodInfo *method, size_t paramIndex);
XPlatType   XPLAT_typeInfoGetMetaType(const void *typeInfo);
const char *XPLAT_typeInfoGetTypeName(const void *typeInfo);

Again, the same pattern, we ask "how many" and loop over each, keeping the interface simple but verbose. However, this is just a one-off cost when the binding is loaded.

Usage

from xplat import bind_library
bind_library('demo')
from xplat import demo

obj = demo.Demo()
print(obj.getEnum())               # MyEnum.Red
obj.setAll(True, 7, 1.5, "hi", demo.MyEnum.Blue)
print(obj.getEnum().name)          # Blue

obj.putInt(100)
print(obj.getInt())                # 100

With the type logic now, the enum comes back as a proper MyEnum instance, not a bare integer as passed. The methods can be seen in the repl and code calling non-existant functions or using incorrect types will fail at the Python side.

Popular posts from this blog

seven month update

Tracking running Part #2

Capsure RM200 hacking