Skip to content

Instantly share code, notes, and snippets.

@Alrecenk
Last active May 3, 2025 19:37
Show Gist options
  • Save Alrecenk/0bd87c773f44a68e8043fe93d9f6ff53 to your computer and use it in GitHub Desktop.
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.
#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