Last active
May 3, 2025 19:37
-
-
Save Alrecenk/0bd87c773f44a68e8043fe93d9f6ff53 to your computer and use it in GitHub Desktop.
A header only registry for classes and their void methods to allow automatic serialization and facilitate remote procedure calls.
This file contains hidden or 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
#ifndef _REGISTRY_H_ | |
#define _REGISTRY_H_ 1 | |
#include <iostream> | |
#include <vector> | |
#include <tuple> | |
#include <string> | |
#include <cstring> | |
#include <type_traits> | |
#include <memory> | |
#include <unordered_map> | |
#include <functional> | |
#include <typeindex> | |
//Prints anything that properly handles the << operator with a comma and space after | |
template<typename T> | |
inline void printArg(const T& arg) { | |
std::cout << arg << ", "; | |
} | |
//Prints an arbitrary set of multiple args | |
template<typename... Args> | |
inline void printArgs(const Args&... args) { | |
(printArg(args), ...); | |
std::cout << '\n'; | |
} | |
// Prints a tuple of arbitrary structure | |
template<typename... Args> | |
inline void printTuple(const std::tuple<Args...> tuple) { | |
std::apply([&](const auto&... args) { | |
(printArg(args), ...); | |
}, tuple); | |
} | |
// Helper for assignTuple that uses an index_sequence to create the proper fold expression for the copy | |
template<typename... ArgsSrc, typename... ArgsDst, std::size_t... I> | |
inline void assignTupleImpl(const std::tuple<ArgsDst&...>& dst, const std::tuple<ArgsSrc...>& src, std::index_sequence<I...>) { | |
((std::get<I>(dst) = std::get<I>(src)), ...); | |
} | |
// Assigns from tuple `src` into tuple `dst` (which holds references). | |
template<typename... ArgsSrc, typename... ArgsDst> | |
inline void assignTuple(const std::tuple<ArgsDst&...>& dst, const std::tuple<ArgsSrc...>& src) { | |
static_assert(sizeof...(ArgsSrc) == sizeof...(ArgsDst), "Tuples must be same size"); | |
assignTupleImpl(dst, src, std::index_sequence_for<ArgsSrc...>{}); | |
} | |
// Given tuple type full of reference types, these three type templates work to get a tuple type of the matching value types | |
// Usage: removeTupleRefs<decltype(ref_tuple)> and then put that in < > to a templated function that needs it | |
template<typename Tuple> | |
struct remove_refs_from_tuple; | |
// Remove references in a tuple | |
template<typename... Args> | |
struct remove_refs_from_tuple<std::tuple<Args...>> { | |
using type = std::tuple<std::remove_cvref_t<Args>...>; | |
}; | |
//Creates a more convenient alias of above that is templated on the Tuple instead of its args | |
template<typename Tuple> | |
using removeTupleRefs = typename remove_refs_from_tuple<Tuple>::type; | |
// Runs a class method on a shared_ptr to an object with the given arguments | |
template <typename T, typename Ret, typename... Args> | |
inline Ret execute(std::shared_ptr<T> obj, Ret(T::* method)(Args...), Args&&... args) { | |
return ((*(obj.get())).*method)(std::forward<Args>(args)...); | |
} | |
// Runs a class method on a shared_ptr to an object with the arguments given in the tuple | |
template <typename T, typename Ret, typename... Args> | |
inline Ret executeTuple(std::shared_ptr<T> obj, Ret(T::* method)(Args...), const std::tuple<Args...>& args_tuple) { | |
return std::apply([&](const auto&... args) { | |
return ((*(obj.get())).*method)(std::forward<decltype(args)>(args)...); | |
}, args_tuple); | |
} | |
// Appends the given argument to the end of the byte buffer | |
// Function arguments and class member types must be in this list for them to be serializable | |
template<typename T> | |
inline void serializeArg(std::vector<char>& buffer, const T& arg) { | |
if constexpr (std::is_same_v<T, std::string>) { | |
//Serialize string | |
const std::string& str = static_cast<std::string> (arg); | |
size_t len = str.size(); | |
buffer.insert(buffer.end(), reinterpret_cast<const char*>(&len), reinterpret_cast<const char*>(&len) + sizeof(len)); | |
buffer.insert(buffer.end(), str.begin(), str.end()); | |
} | |
else if constexpr (std::is_trivially_copyable_v<T>) { | |
//serialize plain old data types | |
const char* data = reinterpret_cast<const char*>(&arg); | |
buffer.insert(buffer.end(), data, data + sizeof(T)); | |
} | |
else { | |
static_assert(std::is_trivially_copyable_v<T>, "Unsupported type for serialization"); | |
} | |
} | |
// Deserializes an argument serialized with SerializeArg | |
// Uses a pointer into the the vector<char> data and increments after each read, | |
// so the current arg always starts at the pointer | |
// Function arguments and class member types must be in this list for them to be deserializable | |
template<typename T> | |
inline T deserializeArg(char*& data) { | |
if constexpr (std::is_same_v<T, std::string>) { | |
//Deserialize string | |
size_t len; | |
std::memcpy(&len, data, sizeof(len)); | |
data += sizeof(len); | |
std::string result(data, len); | |
data += len; | |
return result; | |
} | |
else if constexpr (std::is_trivially_copyable_v<T>) { | |
// Deserialize plain old data types | |
T value; | |
std::memcpy(&value, data, sizeof(T)); | |
data += sizeof(T); | |
return value; | |
} | |
else { | |
printf("Error unrecongized element type in deserializeArg!\n"); | |
} | |
} | |
//serializes an arbitrary list of arguments into a byte array | |
// Types must be supported in serializeArg | |
template<typename... Args> | |
inline std::vector<char> serialize(const Args&... args) { | |
std::vector<char> serial; | |
(serializeArg(serial, args), ...); // Fold expression runs serialize on every arg in order, return values ignored (data is appended to serial) | |
return serial; | |
} | |
//Same as serialize but the args are passed in wrapped in a Tuple | |
template<typename... Args> | |
inline std::vector<char> serializeTuple(const std::tuple<Args...>& tuple_args) { | |
std::vector<char> serial; | |
std::apply([&](const auto&... args) { | |
(serializeArg(serial, args), ...); // Fold expression runs serialize on every arg in order, return values ignored (data is appended to serial) | |
}, tuple_args); | |
return serial; | |
} | |
//Deserializes a byte array made with serialize into a Tuple | |
// Types must be supported in deserializeArg | |
template<typename... Args> | |
inline std::tuple<Args...> deserialize(std::vector<char> serial) { | |
char* data = serial.data(); // get pointer to start of data that will be incremented as we deserialize | |
return std::tuple{ deserializeArg<Args>(data)... }; // Note: using an initializer list guarantees order, but make_tuple does not | |
} | |
// Same as deseralize, but allows hinting the return structure by passing in a tuple of the desired structure | |
template<typename... Args> | |
inline std::tuple<Args...> deserialize(const std::vector<char>& serial, const std::tuple<Args...>&) { | |
return deserialize<Args...>(serial); // normal call | |
} | |
// Helper for DeserializeToTuple to unwrap the Tuple and call the general deserialize<Args...> | |
template<typename Tuple, std::size_t... I> | |
inline auto deserializeToTuple(std::vector<char> serial, std::index_sequence<I...>) { | |
return deserialize<std::tuple_element_t<I, Tuple>...>(std::move(serial)); | |
} | |
//deserializeToTuple works like deserialize but the template arguments are wrapped in a Tuple | |
template<typename Tuple> | |
inline auto deserializeToTuple(std::vector<char> serial) { | |
return deserializeToTuple<Tuple>(std::move(serial), std::make_index_sequence<std::tuple_size_v<Tuple>>{}); | |
} | |
// Object types that want to support direct serialization must have a copy of | |
// auto getStructure(ObjectType& o) implemented that returns a tuple of references into their data | |
template<typename T, typename Ret> | |
Ret getStructure(T&); | |
// Deserializes the given data (produced with one of the serialize functions) and writes it over the given object | |
// Must override getStructure<object_type> for any object type using this method to know where to write | |
template<typename T> | |
inline void deserializeInto(T& obj, const std::vector<char>& serial) { | |
auto ref_tuple = getStructure(obj); | |
assignTuple( | |
ref_tuple, | |
deserializeToTuple<removeTupleRefs<decltype(ref_tuple)>>(serial) | |
); | |
} | |
// Runs a class method on a shared_ptr to an object with the arguments given as bytes generated from serialize(args) | |
template <typename T, typename Ret, typename... Args> | |
inline Ret executeSerialized(std::shared_ptr<T> obj, Ret(T::* method)(Args...), const std::vector<char>& args_serial) { | |
auto args_tuple = deserialize<Args...>(args_serial); | |
return std::apply([&](const auto&... args) { | |
return ((*(obj.get())).*method)(std::forward<decltype(args)>(args)...); | |
}, args_tuple); | |
} | |
//AbstractVoidMethod type allows differently templated methods to live in the same map in the registry | |
class AbstractVoidMethod { | |
public: | |
virtual void execute(std::shared_ptr<void> obj, const std::vector<char>& args_serial) const = 0; | |
}; | |
// A wrapper for a void method with a function to execute it on a shared_ptr to an appropriately typed object | |
// Used to hold and label executable functions in the registry | |
template <typename T, typename Ret, typename... Args> | |
struct VoidMethod : AbstractVoidMethod { | |
Ret(T::* method)(Args...); | |
VoidMethod(Ret(T::* m)(Args...)) : method(m) {} | |
inline void execute(std::shared_ptr<void> obj, const std::vector<char>& args_serial) const override { | |
auto args_tuple = deserialize<Args...>(args_serial); | |
auto typed_obj = std::static_pointer_cast<T>(obj); | |
std::apply([&](const auto&... args) { | |
((*(typed_obj.get())).*method)(std::forward<decltype(args)>(args)...); | |
}, args_tuple); | |
} | |
}; | |
// The Registry holds classes and functions for automatic serialization, deserialization, and execution | |
class Registry { | |
public: | |
std::unordered_map<int, std::unique_ptr<AbstractVoidMethod>> methods; | |
std::unordered_map<int, std::function<std::vector<char>(void*)>> serializers; | |
std::unordered_map<int, std::function<std::shared_ptr<void>(const std::vector<char>&)>> deserializers; | |
std::unordered_map<std::type_index, int64_t> type_to_id; | |
// Adds a class to the registry | |
template<typename T> | |
inline int registerClass() { | |
std::cout << "register class: " << typeid(T).name() << "\n"; | |
//check if the class being registered has a getStructure implementation that returns a nonempty Tuple | |
T new_object; | |
auto structure = getStructure(new_object); | |
static_assert(std::tuple_size<decltype(structure)>() > 0, "A class with no data is being registered for serialization!"); | |
int id = (int)(type_to_id.size()); | |
serializers[id] = [](void* objPtr) -> std::vector<char> { | |
auto& obj = *static_cast<T*>(objPtr); | |
return serializeTuple(getStructure(obj)); | |
}; | |
deserializers[id] = [](const std::vector<char>& serial) -> std::shared_ptr<void> { | |
T typed_obj; | |
if (serial.size() > 0) { // deserializer can be called with no data to return a default object of the given type | |
deserializeInto(typed_obj, serial); | |
} | |
return std::make_shared<T>(typed_obj); | |
}; | |
type_to_id[std::type_index(typeid(T))] = id; | |
return id; | |
} | |
// Adds a void method on a class to the registry | |
template <typename T, typename Ret, typename... Args> | |
inline int registerMethod(Ret(T::* method)(Args...)) { | |
int method_id = (int)(methods.size()); | |
static_assert(std::is_same_v<Ret, void>, "registerMethod only supports void functions"); | |
methods[method_id] = std::make_unique<VoidMethod<T, Ret, Args...>>(method); | |
return method_id; | |
} | |
//Executes a method in the registry on an object of the appropriate type | |
// Serialized args are assumed generated with serialize(args...) | |
inline void execute(std::shared_ptr<void> obj, int method_id, const std::vector<char>& args_serial) const { | |
auto it = methods.find(method_id); | |
if (it == methods.end()) throw std::runtime_error("Unknown method ID"); | |
it->second->execute(obj, args_serial); | |
} | |
// Returns the id for the given class infered from a type passed by template | |
template<typename T> | |
inline int getIdForType() const { | |
std::cout << "getid: " << typeid(T).name() << "\n"; | |
auto it = type_to_id.find(std::type_index(typeid(T))); | |
if (it != type_to_id.end()) { | |
return (int)(it->second); | |
} | |
throw std::runtime_error("Type not registered in ClassRegistry"); | |
} | |
//For internal use | |
//Serialize an object with the type matching type id | |
inline std::vector<char> serializeObj(int type_id, void* objPtr) const { | |
auto it = serializers.find(type_id); | |
if (it != serializers.end()) { | |
return it->second(objPtr); | |
} | |
throw std::runtime_error("Unknown class id during serialization"); | |
} | |
//Serialize an object into type_id and raw data | |
template<typename T> | |
inline std::pair<int, std::vector<char>> serializeObj(std::shared_ptr<T>& obj) const { | |
int id = getIdForType<T>(); | |
return { id, serializeObj(id, obj.get()) }; | |
} | |
// Deserialize an object | |
inline std::shared_ptr<void> deserializeObj(int type_id, const std::vector<char>& serial) const { | |
auto it = deserializers.find(type_id); | |
if (it != deserializers.end()) { | |
return it->second(serial); | |
} | |
throw std::runtime_error("Unknown class id during deserialization"); | |
} | |
// Deserialize an object, but with the arguments in a pair so you can call it with the output of serializeObj | |
inline std::shared_ptr<void> deserializeObj(const std::pair<int, const std::vector<char>>& id_serial) const { | |
return deserializeObj(id_serial.first, id_serial.second); | |
} | |
//Make a deep copy of an object by serializing it and deserializing it | |
template<typename T> | |
inline std::shared_ptr<void> deepCopy(std::shared_ptr<T>& obj) const{ | |
auto serial = serializeObj(obj); | |
return deserializeObj(serial); | |
} | |
}; | |
class MyClass { | |
public: | |
std::string msg = ""; | |
int value = 0; | |
inline void update(std::string s, int v) { | |
msg += s; | |
value += v; | |
} | |
inline void print() const { | |
std::cout << msg << " (" << value << ")\n"; | |
} | |
}; | |
// Returns the structure of the class data as a reference tuple | |
auto static getStructure(MyClass& obj) { | |
return std::tie(obj.msg, obj.value); | |
} | |
inline void testFunctionRegistry() { | |
// Register references to all classes and functions we want to serialize | |
Registry registry; | |
int MYCLASS_ID = registry.registerClass<MyClass>(); | |
int UPDATE_ID = registry.registerMethod(&MyClass::update); | |
std::shared_ptr<MyClass> obj = std::make_shared<MyClass>(MyClass{ "Hello", 10 }); | |
// serialize arbitrary parameters into raw bytes | |
std::vector<char> args_serial = serialize(std::string(" World"), 5); | |
// Execute a function by purely serialized info | |
registry.execute(obj, UPDATE_ID, args_serial); | |
// serialize object into raw bytes | |
std::pair<int, const std::vector<char>> obj_serial = registry.serializeObj(obj); | |
// duplicate the object from the raw bytes | |
std::shared_ptr<MyClass> obj2 = std::static_pointer_cast<MyClass>(registry.deserializeObj(obj_serial)); | |
obj2->print(); // prints Hello World (15) | |
} | |
#endif // #ifndef _REGISTRY_H_ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment