A new type

Step 4 - A new type. std::chrono::y_m_d

This is the fourth step, adding a new type, date - 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.

We've got the framework working nicely, we can markup classes and basic types, passing in and returning pod types or dictionaries as structs. Now let's add another native type to show the steps needed. Dates makes sense, we have native versions in Python (and Java) and we can now map to a C++20 std::chrono::year_month_day.

Encoding

How to wire this new type into the variant we have? I don't want to add new types into the union for every new type, but all I really need here is a new id in our variant enum and then I can just hold an integer for our date. Let's just use a unix epoch date int and store in our intValue with a couple of helpers to convert std::chrono:

inline int64_t yearMonthDayToDays(const std::chrono::year_month_day &ymd)
{
    using namespace std::chrono;
    return sys_days{ymd}.time_since_epoch().count();
}

inline std::chrono::year_month_day daysToYearMonthDay(int64_t daysSinceEpoch)
{
    using namespace std::chrono;
    return year_month_day{sys_days{days{daysSinceEpoch}}};
}

The type system additions

In the metadata header we add Type::Date to our enum and a TypeMap specialisation:

enum class Type { Int, Double, String, Bool, Enum, Struct, Date, Unsupported };

template <> struct TypeMap<std::chrono::year_month_day> {
    static constexpr Type value = Type::Date;
};

and XPLAT_TYPE_DATE = 6 in the C API enum so the number can cross the language boundary. (with hindsight I should reorder these integers!)

Conversion specialisations

The invoker layer needs two template specialisations - one to go from XPlatValueyear_month_day and one for the reverse:

template <>
inline std::chrono::year_month_day valueToNative<std::chrono::year_month_day>(const XPlatValue &val)
{
    return daysToYearMonthDay(val.data.intValue);
}

template <>
inline void nativeToValue<std::chrono::year_month_day>(
    const std::chrono::year_month_day &native, XPlatValue &val)
{
    val.type          = XPLAT_TYPE_DATE;
    val.data.intValue = yearMonthDayToDays(native);
}

That's the pattern for any new basic scalar type - two helper functions and two specialisations. If we need to add additional integer types, we just need the helpers and converters.

Struct field support

The struct field converter in xplat_reflect.hpp already handles int, double, string etc. with a chain of if constexpr branches. We just add a date branch in both the setter and getter paths:

// setter
else if constexpr (std::is_same_v<F, std::chrono::year_month_day>) {
    using namespace std::chrono;
    s->*member = year_month_day{sys_days{days{val.data.intValue}}};
}

// getter
else if constexpr (std::is_same_v<F, std::chrono::year_month_day>) {
    val.type          = XPLAT_TYPE_DATE;
    val.data.intValue = yearMonthDayToDays(s->*member);
}

The demo

The demo class gains a date member and accessors:

date m_date = {year{1971} / month{11} / day{26}};

date getDate() { return m_date; }
void putDate(date d) { m_date = d; }

On the Python side

The Python date helpers are trivial, we just shift to our epoch:

META_TYPE_DATE = 6
_EPOCH_DATE = datetime.date(1970, 1, 1)

def _date_to_epoch_days(value: datetime.date) -> int:
    return (value - _EPOCH_DATE).days

def _epoch_days_to_date(days: int) -> datetime.date:
    return _EPOCH_DATE + datetime.timedelta(days=int(days))

The binding code already calls _python_to_c_value and _c_to_python_value for every argument and return value, so we only need to add META_TYPE_DATE branches there. Python datetime.date objects now round-trip transparently:

obj = demo.Demo()
d = obj.getDate()          # datetime.date(1971, 11, 26)
obj.putDate(date(2024, 6, 1))

Struct fields of type year_month_day work automatically - the struct field converter just updated handles it:

ts = obj.getStruct()
print(ts['m_date'])        # datetime.date(2025, 12, 25)

The pattern

Steps to add an additional type:

  1. Add to the Type enum
  2. Add a TypeMap<T> specialisation
  3. Add valueToNative<T> / nativeToValue<T> specialisations
  4. Add branches in the struct field converter
  5. Add a constant and helpers in the Python binding

Popular posts from this blog

seven month update

Tracking running Part #2

Capsure RM200 hacking