Skip to content

Instantly share code, notes, and snippets.

@dwilliamson
Last active May 10, 2021 09:09
Show Gist options
  • Save dwilliamson/58a173a0f41060fcac394aef37f9fb7e to your computer and use it in GitHub Desktop.
Save dwilliamson/58a173a0f41060fcac394aef37f9fb7e to your computer and use it in GitHub Desktop.
Static/compile-time reflection with JSON serialisation. Uses https://github.com/dropbox/json11
#include "Reflection.h"
#include <string>
#include <vector>
namespace mks
{
struct Rect
{
int top;
int left;
int bottom;
int right;
};
enum class EdgeType
{
None,
Top,
Left,
Bottom,
Right,
};
struct Edge
{
EdgeType type;
Rect rect;
};
struct Monitor
{
std::string os_name;
std::string hw_name;
Rect rect;
};
struct Machine
{
std::vector<Monitor> monitors;
std::vector<Edge> escape_edges;
};
}
rflClass(mks::Rect)
rflField(top),
rflField(left),
rflField(bottom),
rflField(right)
rflEndClass()
rflEnum(mks::EdgeType)
rflEnumValue(None),
rflEnumValue(Top),
rflEnumValue(Left),
rflEnumValue(Bottom),
rflEnumValue(Right)
rflEndEnum()
rflClass(mks::Edge)
rflField(type),
rflField(rect)
rflEndClass()
rflClass(mks::Monitor)
rflField(os_name),
rflField(hw_name),
rflField(rect)
rflEndClass()
rflClass(mks::Machine)
rflField(monitors),
rflField(escape_edges)
rflEndClass()
int main()
{
mks::Machine machine;
json11::Json json = rfl::ToJson(machine);
rfl::JsonParseErrors errors;
rfl::FromJson(json, machine, errors);
}
#pragma once
//
// An exploratory static/compile-time reflection implementation for C++11.
// Supports: class/struct, enum, int, double, bool, std::string, std::vector.
// This implementation is here purely to find out how and why static reflection is popular and implies
// no endorsement of what follows.
// Any help to make this simpler gratefully received.
//
#include <json11/json11.hpp>
// Convert a compile-time string literal to a unique type
#define rflTypedString(str) rfl::TypedString<sizeof(str) - 1, \
rfl::prv::GetChar<0>(str), \
rfl::prv::GetChar<1>(str), \
rfl::prv::GetChar<2>(str), \
rfl::prv::GetChar<3>(str), \
rfl::prv::GetChar<4>(str), \
rfl::prv::GetChar<5>(str), \
rfl::prv::GetChar<6>(str), \
rfl::prv::GetChar<7>(str), \
rfl::prv::GetChar<8>(str), \
rfl::prv::GetChar<9>(str), \
rfl::prv::GetChar<10>(str), \
rfl::prv::GetChar<11>(str), \
rfl::prv::GetChar<12>(str), \
rfl::prv::GetChar<13>(str), \
rfl::prv::GetChar<14>(str), \
rfl::prv::GetChar<15>(str), \
rfl::prv::GetChar<16>(str), \
rfl::prv::GetChar<17>(str), \
rfl::prv::GetChar<18>(str), \
rfl::prv::GetChar<19>(str), \
rfl::prv::GetChar<20>(str), \
rfl::prv::GetChar<21>(str), \
rfl::prv::GetChar<22>(str), \
rfl::prv::GetChar<23>(str), \
rfl::prv::GetChar<24>(str), \
rfl::prv::GetChar<25>(str), \
rfl::prv::GetChar<26>(str), \
rfl::prv::GetChar<27>(str), \
rfl::prv::GetChar<28>(str), \
rfl::prv::GetChar<29>(str), \
rfl::prv::GetChar<30>(str), \
rfl::prv::GetChar<31>(str) \
>
#define rflClass(cls) \
template <> \
struct rfl::ClassReflector<cls> \
{ \
using Class = cls; \
using Name = rflTypedString(#cls); \
using FirstFieldNode = rfl::ListNode<
#define rflField(field) rfl::Field<Class, decltype(Class::field), &Class::field, rflTypedString(#field)>
#define rflEndClass() >; };
#define rflEnum(enm) \
template <> \
struct rfl::EnumReflector<enm> \
{ \
using Enum = enm; \
using Name = rflTypedString(#enm); \
using FirstEnumNode = rfl::ListNode <
#define rflEnumValue(value) rfl::EnumValue<Enum, rflTypedString(#value), Enum::value>
#define rflEndEnum() >; };
namespace rfl
{
// Compile-time typed string
template <std::size_t Size, char... C>
struct TypedString
{
static constexpr const std::size_t size = Size;
static constexpr const char string[sizeof...(C) + 1] = { C..., '\0' };
};
// Compile-time field description
template <typename ClassType, typename FieldType, FieldType ClassType::*FieldPtr, typename NameType>
struct Field
{
using Class = ClassType;
using Type = FieldType;
using Name = NameType;
static const Type& Get(const Class& object)
{
return object.*FieldPtr;
}
static Type& Get(Class& object)
{
return object.*FieldPtr;
}
};
// Compile-time enum value description
template <typename EnumType, typename NameType, EnumType Value>
struct EnumValue
{
using Type = EnumType;
using Name = NameType;
static const EnumType Value = Value;
// For quick string comparisons
static const uint32_t Hash = prv::HashString(Name::string, Name::size);
};
// A typed forward linked list
// Repeatedly split a type list into (i, i+1...N) pairs, recursively inheriting
// from the second half until there's nothing left; similar to Lisp's car/cdr.
template <typename... Types> struct ListNode;
template <typename First, typename... Rest>
struct ListNode<First, Rest...> : public ListNode<Rest...>
{
// Links within the list
using Item = First;
using Next = ListNode<Rest...>;
};
template <> struct ListNode<>
{
};
// Reflectors for all classes in the system
template <typename Class> struct ClassReflector
{
};
// Reflectors for all enums in the system
template <typename Enum> struct EnumReflector
{
};
// Convert basic type values to JSON
inline json11::Json ToJson(double value) { return json11::Json(value); }
inline json11::Json ToJson(int value) { return json11::Json(value); }
inline json11::Json ToJson(bool value) { return json11::Json(value); }
inline json11::Json ToJson(const char* value) { return json11::Json(value); }
inline json11::Json ToJson(const std::string& value) { return json11::Json(value); }
// Convert class objects to JSON
template <typename Class,
typename std::enable_if< !std::is_enum<Class>::value, int >::type* = nullptr>
json11::Json ToJson(const Class& object)
{
// Package everything into a map so that it can be returned as a json object
std::map<std::string, json11::Json> map;
// Kick-off linked list traversal
using FieldNode = typename ClassReflector<Class>::FirstFieldNode;
prv::AddJsonField<FieldNode>::Do(object, map);
return map;
}
// Convert enum class values to JSON
template <typename Class,
typename std::enable_if< std::is_enum<Class>::value, int >::type* = nullptr>
json11::Json ToJson(const Class& object)
{
// find the object value in the enum list and emit as string
using EnumValueNode = typename EnumReflector<Class>::FirstEnumNode;
return prv::EnumValueToString<EnumValueNode>::Do(object);
}
// Convert std::vector containers to JSON
template <typename Type>
json11::Json ToJson(const std::vector<Type>& array)
{
std::vector<json11::Json> json_array;
for (auto&& item : array)
json_array.push_back(ToJson(item));
return json_array;
}
struct JsonParseException
{
JsonParseException(const char* error)
: error(error)
{
}
const char* error;
};
// Parser tries to read as much as possible in the presence of unexpected data
typedef std::vector<std::string> JsonParseErrors;
#define PARSE_ASSIGN(condition, value_type, error) \
if (json.condition()) \
value = json.value_type(); \
else \
errors.push_back(error);
// Convert basic type values from JSON
inline void FromJson(const json11::Json& json, double& value, JsonParseErrors& errors)
{
PARSE_ASSIGN(is_number, number_value, "Expecting number for double value");
}
inline void FromJson(const json11::Json& json, int& value, JsonParseErrors& errors)
{
PARSE_ASSIGN(is_number, int_value, "Expecting number for int value");
}
inline void FromJson(const json11::Json& json, bool& value, JsonParseErrors& errors)
{
PARSE_ASSIGN(is_bool, bool_value, "Expecting bool");
}
inline void FromJson(const json11::Json& json, std::string& value, JsonParseErrors& errors)
{
PARSE_ASSIGN(is_string, string_value, "Expecting string");
}
#undef PARSE_ASSIGN
template <typename Class,
typename std::enable_if< !std::is_enum<Class>::value, int >::type* = nullptr>
void FromJson(const json11::Json& json, Class& object, JsonParseErrors& errors)
{
// Need an object for classes
if (!json.is_object())
{
errors.push_back("Expecting object for class value");
return;
}
// Match class fields to json object entries
using FieldNode = typename ClassReflector<Class>::FirstFieldNode;
prv::GetFieldJson<FieldNode>::Do(object, json.object_items(), errors);
}
template <typename Class,
typename std::enable_if< std::is_enum<Class>::value, int >::type* = nullptr>
void FromJson(const json11::Json& json, Class& object, JsonParseErrors& errors)
{
// Enums can only be stored as strings
if (!json.is_string())
{
errors.push_back("Expecting string for enum value");
return;
}
// Hash the enum name being looked up
const std::string& name = json.string_value();
uint32_t hash = prv::HashString(name.c_str(), name.size());
// Search all possible enums for a matching hash
using EnumValueNode = typename EnumReflector<Class>::FirstEnumNode;
using EnumType = typename EnumValueNode::Item::Type;
prv::EnumValueLookupResult<EnumType> result = prv::HashToEnumValue<EnumValueNode>::Do<EnumType>(hash);
// Apply the result
if (result.found)
object = result.value;
else
errors.push_back("Couldn't find enum value matching " + name);
}
template <typename Type>
void FromJson(const json11::Json& json, std::vector<Type>& array, JsonParseErrors& errors)
{
// Expecting an array for vectors
if (!json.is_array())
{
errors.push_back("Expecting array for std::vector value");
return;
}
// Convert each item in the JSON array
const std::vector<json11::Json>& json_array = json.array_items();
for (const json11::Json& item_json : json_array)
{
Type item;
FromJson(item_json, item, errors);
array.push_back(item);
}
}
namespace prv
{
// Get a character from a string-literatal or null terminator on overflow
template <int N, int M>
constexpr char GetChar(char const(&c)[M])
{
static_assert(M > 0, "Zero-size strings not allowed");
static_assert(M <= 32, "String too long");
return N < M ? c[N] : 0;
}
// FNV-1a 32-bit compile-time string hash
// NOTE: Converting multiply to 64-bit to silence warning C4307: '*': integral constant overflow
inline constexpr uint32_t HashString(const char* string, std::size_t pos)
{
return ((pos ? HashString(string, pos - 1) : 0x811C9DC5) ^ string[pos]) * 0x01000193ull;
}
template <typename CurrentFieldNode>
struct AddJsonField
{
template <typename Class>
static void Do(const Class& object, std::map<std::string, json11::Json>& map)
{
// Add current field
using Field = CurrentFieldNode::Item;
map[Field::Name::string] = ToJson(Field::Get(object));
// Chain to next node in the list
using NextFieldNode = typename CurrentFieldNode::Next;
AddJsonField<NextFieldNode>::Do(object, map);
}
};
// Terminate AddJsonField iteration
template <>
struct AddJsonField<ListNode<>>
{
template <typename Class>
static void Do(const Class&, const std::map<std::string, json11::Json>&)
{
}
};
template <typename CurrentFieldNode>
struct GetFieldJson
{
template <typename Class>
static void Do(Class& object, const std::map<std::string, json11::Json>& map, JsonParseErrors& errors)
{
// Lookup current field in the json map
using Field = CurrentFieldNode::Item;
auto i = map.find(Field::Name::string);
if (i != map.end())
{
// Copy the json object if it's there
const json11::Json& json = i->second;
FromJson(json, Field::Get(object), errors);
}
// Chain to next node in the list
using NextFieldNode = typename CurrentFieldNode::Next;
GetFieldJson<NextFieldNode>::Do(object, map, errors);
}
};
// Terminate GetFieldJson iteration
template <>
struct GetFieldJson<ListNode<>>
{
template <typename Class>
static void Do(Class&, const std::map<std::string, json11::Json>&, JsonParseErrors&)
{
}
};
// Convert an enumeration value to string literal
template <typename CurrentEnumNode>
struct EnumValueToString
{
template <typename EnumValueType>
static constexpr const char* Do(EnumValueType value)
{
// Check for equality or chain to next node in the list
using EnumValue = CurrentEnumNode::Item;
return EnumValue::Value == value ? EnumValue::Name::string : EnumValueToString<CurrentEnumNode::Next>::Do(value);
}
};
// Terminate EnumValueToString iteration
template <>
struct EnumValueToString<ListNode<>>
{
template <typename EnumValueType>
static constexpr const char* Do(EnumValueType)
{
// TODO: Return nullptr and some errors
return "None";
}
};
// Prevents the need to have a default None return value in each enum that's reflected while working
// within the statement limits of C++11 constexpr
template <typename EnumValueType>
struct EnumValueLookupResult
{
EnumValueLookupResult()
: found(false)
{
}
EnumValueLookupResult(EnumValueType value)
: value(value)
, found(true)
{
}
EnumValueType value;
bool found;
};
// Map a hash value to an enum value
template <typename CurrentEnumNode>
struct HashToEnumValue
{
template <typename EnumValueType>
static constexpr EnumValueLookupResult<EnumValueType> Do(uint32_t hash)
{
// Uses exact comparison of hash integers to lookup a matching enum value with no confirming string compare
// This will fail in the presence of hash integer collisions within the enum class itself
// Collisions with other system-wide hash integers will not matter
using EnumValue = CurrentEnumNode::Item;
using Result = EnumValueLookupResult<EnumValueType>;
return EnumValue::Hash == hash ? Result(EnumValue::Value) : HashToEnumValue<CurrentEnumNode::Next>::Do<EnumValueType>(hash);
}
};
// Terminate HashToEnumValue iteration
template <>
struct HashToEnumValue<ListNode<>>
{
template <typename EnumValueType>
static constexpr EnumValueLookupResult<EnumValueType> Do(uint32_t)
{
return EnumValueLookupResult<EnumValueType>();
}
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment