Last active
May 10, 2021 09:09
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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