A collection of basic/generally desirable code I use across multiple C++ projects.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

214 lines
9.8 KiB

#pragma once
#ifdef ULE_CONFIG_OPTION_SERIALIZATION
#ifndef ULE_SERIALIZE_H
#define ULE_SERIALIZE_H
#include "config.h"
#include "print.h"
#include "types.h"
#include "string.h"
/*
NOTES ON SERIALIZATION
after wrestling with various reflection libraries for a week, I decided to use none of them.
there are two-ish ways to do serialization:
A. write or use a 'reflection' system which is able to introspect types in the program, and generate the code to serialize/deserialize using this introspection
B. define functions manually which do the work, and modify them whenever the underlying data structures change
you can do a mixture of option A and option B. We lean towards option B.
primitive types used in the program, (defined in types.h) will automatically know how to serialize and deserialize themselves.
this can optionally include our String type, and/or vector, matrix and quaternion types (default glm).
for most structs, there is a bit more elbow grease required.
define a function with this signature:
// serialize a T
void serialize(String* str, T v);
and one with this signature:
// serialize a pointer to T
void serialize(String* str, T* v);
and for deserialization:
// in this case, we assume the address |v| has been allocated, and we fill in the slots
void deserialize(char** buffer, T* v);
// in this case, we're deserializing a pointer, so we have to allocate memory ourselves.
// that's up to the implementer to do correctly.
// it's possible to deserialize 'null' here
void deserialize(char** buffer, T** v);
you have to manually implement these functions and change them when the struct members change.
the body of the serialize function bodies will essentially call 'serialize' on each of the members you
wish to serialize. if you want to skip certain members, just do not call 'serialize' on them.
similarly, for deserialize you should call deserialize on each member.
the order matters. if you wish to be able deserialize something you have serialized, the order which you process
members must be the same between the serialization/deserialization calls.
that is essentially it, with some caveats in the circumstance where you could be serializing/deserializing a NULL,
and in the case where you are deserializing a pointer to a data structure (you have to perform allocation in this case).
these cases are explained below.
*/
// in an #include-guarded header you can use this helper macro to define a few function stubs
// + potentially helpful function definitions, with no templates required.
//
// having serialization allows trivial/naive implementations of things like cloning and equality.
// these definitions can be done with macros, provided you have written an implementation of |serialize|
// and |deserialize| for your data type.
//
#define SERIALIZE_H_HELPER_FUNCTION_DEFS(T) \
extern void serialize(String* str, T* v); \
extern void serialize(String* str, T v); \
extern void deserialize(char** buffer, T* v); \
extern void deserialize(char** buffer, T** v); \
static void serializePrint(T* v) { \
ULE_TYPES_H_FTAG; \
String str = String(""); \
serialize(&str, v); \
println(str.c_str()); \
} \
static bool serializeEquals(T* t1, T* t2) { \
ULE_TYPES_H_FTAG; \
String s1 = String128f(""); \
String s2 = String128f(""); \
serialize(&s1, t1); \
serialize(&s2, t2); \
return String::eq(s1.c_str(), s2.c_str()); \
}
// if you implement deserialize with a T*.
#define SERIALIZE_H_HELPER_CLONE_T_POINTER(T) \
static void serializeClone(T* orig, T* destination) { \
ULE_TYPES_H_FTAG; \
String str = String128f(""); \
serialize(&str, orig); \
char* buffer = str.c_str(); \
deserialize(&buffer, destination); \
}
// if you implement deserialize with a T**.
#define SERIALIZE_H_HELPER_CLONE_T_DOUBLE_POINTER(T) \
static void serializeClone(T* orig, T** destination) { \
ULE_TYPES_H_FTAG; \
String str = String128f(""); \
serialize(&str, orig); \
char* buffer = str.c_str(); \
deserialize(&buffer, destination); \
}
// The body of the serialize function should simply call `serialize(str, member)` for each member of the struct that you wish to serialize.
// If the struct is nullable (pointer to struct), you have to check for that. this helper will do that for you:
#define SERIALIZE_NULL_SENTINEL "null\n"
#define SERIALIZE_HANDLE_NULL(str, pointer) \
if (pointer == null) { \
str->append(SERIALIZE_NULL_SENTINEL); \
return; \
}
// Similarly, for deserializing pointers, you need to check for our null sentinel value first
// make sure to write your serialized data out in binary mode if you use fwrite, otherwise you're likely
// to run into an annoying bug where the '\n' in the null sentinel will be converted to '\r\n' on windows,
// causing all null sentinels to be ignored (the in-memory "null\n" will never be equal to the on-disk "null\r\n",
// obviously)
#define DESERIALIZE_HANDLE_NULL(buffer, pointer) \
char* _buffer = *buffer; \
while (String::isAsciiWhitespace(*_buffer)) _buffer++; \
if (String::memeq((unsigned char*)_buffer, (unsigned char*)SERIALIZE_NULL_SENTINEL, (sizeof(SERIALIZE_NULL_SENTINEL) - 1))) { \
*pointer = null; \
*buffer += (sizeof(SERIALIZE_NULL_SENTINEL) - 1); \
return; \
}
//
// Our HashTable implementation (Table.hpp) and Array implementation (Array.hpp) knows how to serialize/deserialize, but
// for your own custom containers or standard containers you'll have to define serialize/deserialize for them.
//
// What our system does is basically, for serialization:
// - the code that wants to serialize something initializes a String type which has an interface that lets you concat stuff to it.
// - call 'serialize' on the thing you want to serialize, passing in this String type as well
// - the behavior is defined by you from this point, but probably you recursively call 'serialize' on members of the type you're serializing, and data just gets concatenated to the string as you go.
// at the end the string will have the serialized data.
//
// For deserialization:
// - get some big buffer of serialized data, probably from reading a file, as a char*.
// - define a variable of the type you want to initialize from this serialized data
// - pass the address of the buffer (type char**) and the address of the variable you want to initialize to 'deserialize'
// - behavior at this point is defined by you, but probably you recursively call 'deserialize' on the members of the type you are initializing.
// - at each step, the code will essentially assert that data of a particular type exists in serialized form at the address |buffer| points to, and try to read that data from that address. if it succeeds, it will increment the buffer pointer by the number of characters read, so it should be pointing at the next data member if it exists, or pointing to the null character if it's the end of the stream. if it ever finds something unexpected, it will crash.
// - IMPORTANT: you have to know the order of the data in the serialized form. You cannot try to deserialize out of order.
// - if you want to skip certain members, either don't serialize them at all, or deserialize them into a dummy address.
//
extern void serialize(String* str, u8 v);
extern void serialize(String* str, u16 v);
extern void serialize(String* str, u32 v);
extern void serialize(String* str, u64 v);
extern void serialize(String* str, s8 v);
extern void serialize(String* str, s16 v);
extern void serialize(String* str, s32 v);
extern void serialize(String* str, s64 v);
extern void serialize(String* str, float v);
extern void serialize(String* str, double v);
extern void deserialize(char** buffer, u8* v);
extern void deserialize(char** buffer, u16* v);
extern void deserialize(char** buffer, u32* v);
extern void deserialize(char** buffer, u64* v);
extern void deserialize(char** buffer, s8* v);
extern void deserialize(char** buffer, s16* v);
extern void deserialize(char** buffer, s32* v);
extern void deserialize(char** buffer, s64* v);
extern void deserialize(char** buffer, float* v);
extern void deserialize(char** buffer, double* v);
// these serialize and deserialize strings, not single characters.
extern void serialize(String* str, char* v);
extern void serialize(String* str, const char* v);
// read the string notes above in the large block comment for gotchas involving these functions.
extern void deserialize(char** buffer, char* v);
extern void deserialize(char** buffer, const char* v);
extern void deserialize(char** buffer, char** v);
extern void deserialize(char** buffer, const char** v);
#ifndef _WIN32
extern void serialize(String* str, size_t v);
extern void deserialize(char** buffer, size_t* v);
#endif
#ifdef ULE_CONFIG_OPTION_USE_GLM
extern void serialize(String* str, glm::vec<2, float, (glm::qualifier) 3> v);
extern void serialize(String* str, glm::vec<3, float, (glm::qualifier) 3> v);
extern void serialize(String* str, glm::vec<4, float, (glm::qualifier) 3> v);
extern void serialize(String* str, glm::mat<2, 2, float, (glm::qualifier) 3> v);
extern void serialize(String* str, glm::mat<3, 3, float, (glm::qualifier) 3> v);
extern void serialize(String* str, glm::mat<4, 4, float, (glm::qualifier) 3> v);
extern void deserialize(char** buffer, glm::vec<2, float, (glm::qualifier) 3>* v);
extern void deserialize(char** buffer, glm::vec<3, float, (glm::qualifier) 3>* v);
extern void deserialize(char** buffer, glm::vec<4, float, (glm::qualifier) 3>* v);
extern void deserialize(char** buffer, glm::mat<2, 2, float, (glm::qualifier) 3>* v);
extern void deserialize(char** buffer, glm::mat<3, 3, float, (glm::qualifier) 3>* v);
extern void deserialize(char** buffer, glm::mat<4, 4, float, (glm::qualifier) 3>* v);
#endif
#endif
#endif