A new type
Step 4 - A new type. std::chrono::y_m_d
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 XPlatValue → year_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:
- Add to the
Typeenum - Add a
TypeMap<T>specialisation - Add
valueToNative<T>/nativeToValue<T>specialisations - Add branches in the struct field converter
- Add a constant and helpers in the Python binding