diff --git a/README.md b/README.md index 2064fbb..6dc71e2 100644 --- a/README.md +++ b/README.md @@ -353,6 +353,51 @@ An example for such custom (de)serialization can be found in the file `json/reflector-chronoutilities.h`. It provides (de)serialization of `DateTime` and `TimeSpan` objects from the C++ utilities library mentioned under dependencies. +### Versioning +#### JSON (de)serializer +The JSON (de)serializer doesn't support versioning at this point. It'll simply read/write the +members present in the struct. Additional members (which were e.g. present in older/newer +versions of the struct) are ignored when reading and in consequence dropped when writing. + +#### Binary (de)serializer +The binary (de)serializer supports *very* experimental versioning. Otherwise adding/removing +members is a breaking change. The versioning looks like this: + +
+// enable definition of the macros shown below (otherwise use long macros defined  in
+// `lib/versioning.h`)
+#define REFLECTIVE_RAPIDJSON_SHORT_MACROS
+
+#include <reflective_rapidjson/binary/serializable.h>
+
+// example struct where version is *not* serialized/deserialized; defaults to version from
+// outer scope when reading/writing, defaults to version 0 on top-level
+struct Nested : public BinarySerializable<Nested> { //
+    std::uint32_t foo; // will be read/written in any case
+
+as_of_version(3):
+    std::uint32_t bar; // will be read/written if outer scope version is >= 3
+};
+
+// example struct where version is serialized/deserialized; defaults to version when writing
+struct Example : public BinarySerializable<Example, 3> {
+    Nested nested;      // will be read/written in any case, version is "propagated down"
+    std::uint32_t a, b; // will be read/written in any case
+
+until_version(2):
+    std::uint32_t c, d; // will be read/written if version is <= 2
+
+as_of_version(3):
+    std::uint32_t e, f; // will be read/written if version is >= 3
+
+as_of_version(4):
+    std::uint32_t g;    // will be read/written if version is >= 4
+};
+
+ +A mechanism to catch unsupported versions during deserialization is yet to be implemented. +Additionally, the versioning is completely untested at this point. + ### Remarks * Static member variables and member functions are currently ignored by the generator. * It is currently not possible to ignore a specific member variable. diff --git a/generator/binaryserializationcodegenerator.cpp b/generator/binaryserializationcodegenerator.cpp index a527104..b8b9b9b 100644 --- a/generator/binaryserializationcodegenerator.cpp +++ b/generator/binaryserializationcodegenerator.cpp @@ -5,6 +5,10 @@ #include #include #include +#include +#include + +#include #include @@ -35,36 +39,159 @@ BinarySerializationCodeGenerator::BinarySerializationCodeGenerator(CodeFactory & } /*! - * \brief Returns the qualified name of the specified \a record if it is considered relevant. + * \brief Checks whether \a possiblyRelevantClass is actually relevant. */ -string BinarySerializationCodeGenerator::qualifiedNameIfRelevant(clang::CXXRecordDecl *record) const +void BinarySerializationCodeGenerator::computeRelevantClass(RelevantClass &possiblyRelevantClass) const { - const string qualifiedName(record->getQualifiedNameAsString()); - switch (isQualifiedNameIfRelevant(record, qualifiedName)) { - case IsRelevant::Yes: - return qualifiedName; - case IsRelevant::No: - return string(); - default:; + SerializationCodeGenerator::computeRelevantClass(possiblyRelevantClass); + if (possiblyRelevantClass.isRelevant != IsRelevant::Maybe) { + return; } // consider all classes specified via "--additional-classes" argument relevant if (!m_options.additionalClassesArg.isPresent()) { - return string(); + return; } - for (const char *className : m_options.additionalClassesArg.values()) { - if (className == qualifiedName) { - return qualifiedName; + for (const char *const className : m_options.additionalClassesArg.values()) { + if (className == possiblyRelevantClass.qualifiedName) { + possiblyRelevantClass.isRelevant = IsRelevant::Yes; + return; } } +} - return string(); +/// \brief The RetrieveIntegerLiteralFromDeclaratorDecl struct is used to traverse a variable declaration to get the integer value. +struct RetrieveIntegerLiteralFromDeclaratorDecl : public clang::RecursiveASTVisitor { + explicit RetrieveIntegerLiteralFromDeclaratorDecl(const clang::ASTContext &ctx); + bool VisitStmt(clang::Stmt *st); + const clang::ASTContext &ctx; + std::uint64_t res; + bool success; +}; + +/// \brief Constructs a new instance for the specified AST context. +RetrieveIntegerLiteralFromDeclaratorDecl::RetrieveIntegerLiteralFromDeclaratorDecl(const clang::ASTContext &ctx) + : ctx(ctx) + , res(0) + , success(false) +{ +} + +/// \brief Reads the integer value of \a st for integer literals. +bool RetrieveIntegerLiteralFromDeclaratorDecl::VisitStmt(clang::Stmt *st) +{ + if (st->getStmtClass() != clang::Stmt::IntegerLiteralClass) { + return true; + } + const auto *const integerLiteral = static_cast(st); + auto evaluation = clang::Expr::EvalResult(); + integerLiteral->EvaluateAsInt(evaluation, ctx, clang::Expr::SE_NoSideEffects, true); + if (!evaluation.Val.isInt()) { + return true; + } + const auto &asInt = evaluation.Val.getInt(); + if (asInt.getActiveBits() > 64) { + return true; + } + res = asInt.getZExtValue(); + success = true; + return false; +} + +/// \brief The MemberTracking struct is an internal helper for BinarySerializationCodeGenerator::generate(). +struct MemberTracking { + bool membersWritten = false, withinCondition = false; + BinaryVersion asOfVersion = BinaryVersion(), lastAsOfVersion = BinaryVersion(); + BinaryVersion untilVersion = BinaryVersion(), lastUntilVersion = BinaryVersion(); + + bool checkForVersionMarker(clang::Decl *decl); + void concludeCondition(std::ostream &os); + void writeVersionCondition(std::ostream &os); + void writeExtraPadding(std::ostream &os); +}; + +/*! + * \brief Returns whether \a delc is a static member variable and processes special static member variables + * for versioning. + */ +bool MemberTracking::checkForVersionMarker(clang::Decl *decl) +{ + if (decl->getKind() != clang::Decl::Kind::Var) { + return false; + } + auto *const declarator = static_cast(decl); + const auto declarationName = declarator->getName(); + const auto isAsOfVersion = declarationName.startswith("rrjAsOfVersion"); + if (isAsOfVersion || declarationName.startswith("rrjUntilVersion")) { + auto v = RetrieveIntegerLiteralFromDeclaratorDecl(declarator->getASTContext()); + v.TraverseDecl(declarator); + if (v.success) { + if (isAsOfVersion) { + asOfVersion = v.res; + if (asOfVersion > untilVersion) { + untilVersion = 0; + } + } else { + untilVersion = v.res; + if (untilVersion < asOfVersion) { + asOfVersion = 0; + } + } + } + } + return true; +} + +/*! + * \brief Concludes an unfinished version condition if-block. + */ +void MemberTracking::concludeCondition(std::ostream &os) +{ + if (withinCondition) { + os << " }\n"; + } +} + +/*! + * \brief Starts a new version condition if-block if versioning parameters have changed. + */ +void MemberTracking::writeVersionCondition(std::ostream &os) +{ + if (asOfVersion == lastAsOfVersion && untilVersion == lastUntilVersion) { + return; + } + concludeCondition(os); + lastAsOfVersion = asOfVersion; + lastUntilVersion = untilVersion; + if ((withinCondition = asOfVersion || untilVersion)) { + os << " if ("; + if (asOfVersion) { + os << "version >= " << asOfVersion; + if (untilVersion) { + os << " && "; + } + } + if (untilVersion) { + os << "version <= " << untilVersion; + } + os << ") {\n"; + } +} + +/*! + * \brief Writes extra padding (if within a version condition). + */ +void MemberTracking::writeExtraPadding(std::ostream &os) +{ + if (withinCondition) { + os << " "; + } } /*! * \brief Generates pull() and push() helper functions in the ReflectiveRapidJSON::BinaryReflector namespace for the relevant classes. */ -void BinarySerializationCodeGenerator::generate(ostream &os) const +void BinarySerializationCodeGenerator::generate(std::ostream &os) const { // initialize source manager to make use of isOnlyIncluded() for skipping records which are only included lazyInitializeSourceManager(); @@ -118,22 +245,55 @@ void BinarySerializationCodeGenerator::generate(ostream &os) const // print writeCustomType method os << "template <> " << visibility << " void writeCustomType<::" << relevantClass.qualifiedName - << ">(BinarySerializer &serializer, const ::" << relevantClass.qualifiedName - << " &customObject)\n{\n" - " // write base classes\n"; + << ">(BinarySerializer &serializer, const ::" << relevantClass.qualifiedName << " &customObject, BinaryVersion version)\n{\n"; + if (!relevantClass.relevantBase.empty()) { + os << " // write version\n" + " using V = Versioning<" + << relevantClass.relevantBase + << ">;\n" + " if constexpr (V::enabled) {\n" + " serializer.writeVariableLengthUIntBE(V::applyDefault(version));\n" + " }\n"; + } + os << " // write base classes\n"; for (const RelevantClass *baseClass : relevantBases) { - os << " serializer.write(static_castqualifiedName << " &>(customObject));\n"; + os << " serializer.write(static_castqualifiedName << " &>(customObject), version);\n"; } os << " // write members\n"; - auto membersWritten = false; - for (const clang::FieldDecl *field : relevantClass.record->fields()) { - if (writePrivateMembers || field->getAccess() == clang::AS_public) { - os << " serializer.write(customObject." << field->getName() << ");\n"; - membersWritten = true; + auto mt = MemberTracking(); + for (clang::Decl *const decl : relevantClass.record->decls()) { + // check static member variables for version markers + if (mt.checkForVersionMarker(decl)) { + continue; } + + // skip all further declarations but fields + if (decl->getKind() != clang::Decl::Kind::Field) { + continue; + } + + // skip const members + const auto *const field = static_cast(decl); + if (field->getType().isConstant(field->getASTContext())) { + continue; + } + + // skip private members conditionally + if (!writePrivateMembers && field->getAccess() != clang::AS_public) { + continue; + } + + // write version markers + mt.writeVersionCondition(os); + mt.writeExtraPadding(os); + + // write actual code for serialization + os << " serializer.write(customObject." << field->getName() << ", version);\n"; + mt.membersWritten = true; } - if (relevantBases.empty() && !membersWritten) { - os << " (void)serializer;\n (void)customObject;\n"; + mt.concludeCondition(os); + if (relevantBases.empty() && !mt.membersWritten) { + os << " (void)serializer;\n (void)customObject;\n \n(void)version;"; } os << "}\n"; @@ -143,28 +303,53 @@ void BinarySerializationCodeGenerator::generate(ostream &os) const } // print readCustomType method - os << "template <> " << visibility << " void readCustomType<::" << relevantClass.qualifiedName - << ">(BinaryDeserializer &deserializer, ::" << relevantClass.qualifiedName - << " &customObject)\n{\n" - " // read base classes\n"; + mt = MemberTracking(); + os << "template <> " << visibility << " BinaryVersion readCustomType<::" << relevantClass.qualifiedName + << ">(BinaryDeserializer &deserializer, ::" << relevantClass.qualifiedName << " &customObject, BinaryVersion version)\n{\n"; + if (!relevantClass.relevantBase.empty()) { + os << " // read version\n" + " if constexpr (Versioning<" + << relevantClass.relevantBase + << ">::enabled) {\n" + " version = deserializer.readVariableLengthUIntBE();\n" + " }\n"; + } + os << " // read base classes\n"; for (const RelevantClass *baseClass : relevantBases) { os << " deserializer.read(static_cast<::" << baseClass->qualifiedName << " &>(customObject));\n"; } os << " // read members\n"; - auto membersRead = false; - for (const clang::FieldDecl *field : relevantClass.record->fields()) { + for (clang::Decl *const decl : relevantClass.record->decls()) { + // check static member variables for version markers + if (mt.checkForVersionMarker(decl)) { + continue; + } + + // skip all further declarations but fields + if (decl->getKind() != clang::Decl::Kind::Field) { + continue; + } + // skip const members + const auto *const field = static_cast(decl); if (field->getType().isConstant(field->getASTContext())) { continue; } + + // write version markers + mt.writeVersionCondition(os); + mt.writeExtraPadding(os); + if (readPrivateMembers || field->getAccess() == clang::AS_public) { - os << " deserializer.read(customObject." << field->getName() << ");\n"; - membersRead = true; + os << " deserializer.read(customObject." << field->getName() << ", version);\n"; + mt.membersWritten = true; } } - if (relevantBases.empty() && !membersRead) { + mt.concludeCondition(os); + if (relevantBases.empty() && !mt.membersWritten) { os << " (void)deserializer;\n (void)customObject;\n"; } + os << " return version;\n"; os << "}\n\n"; } diff --git a/generator/binaryserializationcodegenerator.h b/generator/binaryserializationcodegenerator.h index 2563db8..4bbc851 100644 --- a/generator/binaryserializationcodegenerator.h +++ b/generator/binaryserializationcodegenerator.h @@ -27,7 +27,7 @@ public: void generate(std::ostream &os) const override; protected: - std::string qualifiedNameIfRelevant(clang::CXXRecordDecl *record) const override; + void computeRelevantClass(RelevantClass &possiblyRelevantClass) const override; const Options &m_options; }; diff --git a/generator/codegenerator.cpp b/generator/codegenerator.cpp index 0694ae6..f2f3acc 100644 --- a/generator/codegenerator.cpp +++ b/generator/codegenerator.cpp @@ -44,17 +44,18 @@ bool CodeGenerator::isOnlyIncluded(const clang::Decl *declaration) const /*! * \brief Returns whether the specified \a record inherits from an instantiation of the specified \a templateClass. + * \returns Returns the relevant base class if that's the case and otherwise nullptr. * \remarks The specified \a record must be defined (not only forward-declared). */ -bool CodeGenerator::inheritsFromInstantiationOf(clang::CXXRecordDecl *const record, const char *const templateClass) +clang::CXXBaseSpecifier *CodeGenerator::inheritsFromInstantiationOf(clang::CXXRecordDecl *const record, const char *const templateClass) { - for (const clang::CXXBaseSpecifier &base : record->bases()) { - const clang::CXXRecordDecl *const baseDecl = base.getType()->getAsCXXRecordDecl(); + for (clang::CXXBaseSpecifier &base : record->bases()) { + clang::CXXRecordDecl *const baseDecl = base.getType()->getAsCXXRecordDecl(); if (baseDecl && baseDecl->getQualifiedNameAsString() == templateClass) { - return true; + return &base; } } - return false; + return nullptr; } } // namespace ReflectiveRapidJSON diff --git a/generator/codegenerator.h b/generator/codegenerator.h index 0c5de9d..a206b87 100644 --- a/generator/codegenerator.h +++ b/generator/codegenerator.h @@ -8,6 +8,7 @@ namespace clang { class Decl; class CXXRecordDecl; +class CXXBaseSpecifier; class SourceManager; } // namespace clang @@ -32,7 +33,7 @@ protected: CodeFactory &factory() const; void lazyInitializeSourceManager() const; bool isOnlyIncluded(const clang::Decl *declaration) const; - static bool inheritsFromInstantiationOf(clang::CXXRecordDecl *record, const char *templateClass); + static clang::CXXBaseSpecifier *inheritsFromInstantiationOf(clang::CXXRecordDecl *record, const char *templateClass); private: CodeFactory &m_factory; diff --git a/generator/jsonserializationcodegenerator.cpp b/generator/jsonserializationcodegenerator.cpp index 3f90657..700d96d 100644 --- a/generator/jsonserializationcodegenerator.cpp +++ b/generator/jsonserializationcodegenerator.cpp @@ -35,30 +35,25 @@ JsonSerializationCodeGenerator::JsonSerializationCodeGenerator(CodeFactory &fact } /*! - * \brief Returns the qualified name of the specified \a record if it is considered relevant. + * \brief Checks whether \a possiblyRelevantClass is actually relevant. */ -string JsonSerializationCodeGenerator::qualifiedNameIfRelevant(clang::CXXRecordDecl *record) const +void JsonSerializationCodeGenerator::computeRelevantClass(RelevantClass &possiblyRelevantClass) const { - const string qualifiedName(record->getQualifiedNameAsString()); - switch (isQualifiedNameIfRelevant(record, qualifiedName)) { - case IsRelevant::Yes: - return qualifiedName; - case IsRelevant::No: - return string(); - default:; + SerializationCodeGenerator::computeRelevantClass(possiblyRelevantClass); + if (possiblyRelevantClass.isRelevant != IsRelevant::Maybe) { + return; } // consider all classes specified via "--additional-classes" argument relevant if (!m_options.additionalClassesArg.isPresent()) { - return string(); + return; } - for (const char *className : m_options.additionalClassesArg.values()) { - if (className == qualifiedName) { - return qualifiedName; + for (const char *const className : m_options.additionalClassesArg.values()) { + if (className == possiblyRelevantClass.qualifiedName) { + possiblyRelevantClass.isRelevant = IsRelevant::Yes; + return; } } - - return string(); } /*! diff --git a/generator/jsonserializationcodegenerator.h b/generator/jsonserializationcodegenerator.h index 67e8a19..f94024a 100644 --- a/generator/jsonserializationcodegenerator.h +++ b/generator/jsonserializationcodegenerator.h @@ -27,7 +27,7 @@ public: void generate(std::ostream &os) const override; protected: - std::string qualifiedNameIfRelevant(clang::CXXRecordDecl *record) const override; + void computeRelevantClass(RelevantClass &possiblyRelevantClass) const override; const Options &m_options; }; diff --git a/generator/serializationcodegenerator.cpp b/generator/serializationcodegenerator.cpp index b7fffe2..6c6a936 100644 --- a/generator/serializationcodegenerator.cpp +++ b/generator/serializationcodegenerator.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include @@ -66,12 +68,12 @@ void SerializationCodeGenerator::addDeclaration(clang::Decl *decl) } } -SerializationCodeGenerator::IsRelevant SerializationCodeGenerator::isQualifiedNameIfRelevant( - clang::CXXRecordDecl *record, const std::string &qualifiedName) const +void SerializationCodeGenerator::computeRelevantClass(RelevantClass &possiblyRelevantClass) const { // skip all classes which are only forward-declared - if (!record->isCompleteDefinition()) { - return IsRelevant::No; + if (!possiblyRelevantClass.record->isCompleteDefinition()) { + possiblyRelevantClass.isRelevant = IsRelevant::No; + return; } // consider all classes for which a specialization of the "AdaptedJsonSerializable" struct is available @@ -80,31 +82,40 @@ SerializationCodeGenerator::IsRelevant SerializationCodeGenerator::isQualifiedNa if (isOnlyIncluded(adaptionRecord.record)) { continue; } - if (adaptionRecord.qualifiedName == qualifiedName) { - return IsRelevant::Yes; + if (adaptionRecord.qualifiedName == possiblyRelevantClass.qualifiedName) { + possiblyRelevantClass.isRelevant = IsRelevant::Yes; + return; } } // skip all classes which are only included - if (isOnlyIncluded(record)) { - return IsRelevant::No; + if (isOnlyIncluded(possiblyRelevantClass.record)) { + possiblyRelevantClass.isRelevant = IsRelevant::No; + return; } // consider all classes inheriting from an instantiation of "JsonSerializable" relevant - if (inheritsFromInstantiationOf(record, m_qualifiedNameOfRecords)) { - return IsRelevant::Yes; + if (const auto *const relevantBase = inheritsFromInstantiationOf(possiblyRelevantClass.record, m_qualifiedNameOfRecords)) { + auto policy = clang::PrintingPolicy(possiblyRelevantClass.record->getLangOpts()); + policy.FullyQualifiedName = true; + policy.SuppressScope = false; + policy.SuppressUnwrittenScope = false; + policy.SplitTemplateClosers = false; + possiblyRelevantClass.relevantBase + = clang::TypeName::getFullyQualifiedName(relevantBase->getType(), possiblyRelevantClass.record->getASTContext(), policy, true); + possiblyRelevantClass.isRelevant = IsRelevant::Yes; + return; } - - return IsRelevant::Maybe; } std::vector SerializationCodeGenerator::findRelevantClasses() const { std::vector relevantClasses; - for (clang::CXXRecordDecl *record : m_records) { - string qualifiedName(qualifiedNameIfRelevant(record)); - if (!qualifiedName.empty()) { - relevantClasses.emplace_back(move(qualifiedName), record); + for (clang::CXXRecordDecl *const record : m_records) { + auto &relevantClass = relevantClasses.emplace_back(record->getQualifiedNameAsString(), record); + computeRelevantClass(relevantClass); + if (relevantClass.isRelevant != IsRelevant::Yes) { + relevantClasses.pop_back(); } } return relevantClasses; diff --git a/generator/serializationcodegenerator.h b/generator/serializationcodegenerator.h index ba171c3..23a3236 100644 --- a/generator/serializationcodegenerator.h +++ b/generator/serializationcodegenerator.h @@ -5,6 +5,8 @@ #include +#include + namespace ReflectiveRapidJSON { std::ostream &operator<<(std::ostream &os, llvm::StringRef str); @@ -15,11 +17,14 @@ std::ostream &operator<<(std::ostream &os, llvm::StringRef str); */ class SerializationCodeGenerator : public CodeGenerator { public: + enum class IsRelevant { Yes, No, Maybe }; struct RelevantClass { explicit RelevantClass(std::string &&qualifiedName, clang::CXXRecordDecl *record); std::string qualifiedName; - clang::CXXRecordDecl *record; + std::string relevantBase; + clang::CXXRecordDecl *record = nullptr; + IsRelevant isRelevant = IsRelevant::Maybe; }; SerializationCodeGenerator(CodeFactory &factory); @@ -27,9 +32,7 @@ public: void addDeclaration(clang::Decl *decl) override; protected: - enum class IsRelevant { Yes, No, Maybe }; - IsRelevant isQualifiedNameIfRelevant(clang::CXXRecordDecl *record, const std::string &qualifiedName) const; - virtual std::string qualifiedNameIfRelevant(clang::CXXRecordDecl *record) const = 0; + virtual void computeRelevantClass(RelevantClass &possiblyRelevantClass) const; std::vector findRelevantClasses() const; static std::vector findRelevantBaseClasses( const RelevantClass &relevantClass, const std::vector &relevantBases); diff --git a/generator/tests/morestructs.h b/generator/tests/morestructs.h index d355236..c98a9f8 100644 --- a/generator/tests/morestructs.h +++ b/generator/tests/morestructs.h @@ -1,8 +1,11 @@ #ifndef REFLECTIVE_RAPIDJSON_TESTS_MORE_STRUCTS_H #define REFLECTIVE_RAPIDJSON_TESTS_MORE_STRUCTS_H +#define REFLECTIVE_RAPIDJSON_SHORT_MACROS + #include "../../lib/binary/serializable.h" #include "../../lib/json/serializable.h" +#include "../../lib/versioning.h" using namespace std; using namespace ReflectiveRapidJSON; @@ -67,4 +70,29 @@ struct PointerStruct : public BinarySerializable { std::shared_ptr s3; }; +/*! + * \brief All of this is supposed to work if classes are within a namespace so let's use a namespace here. + */ +namespace SomeNamespace { + +/*! + * \brief The PointerStruct struct is used to test the behavior of the binary (de)serialization with smart pointer. + */ +// clang-format off +struct VersionedStruct : public BinarySerializable { + std::uint32_t a, b; + +until_version(2): + std::uint32_t c, d; + +as_of_version(3): + std::uint32_t e, f; + +as_of_version(4): + std::uint32_t g; +}; +// clang-format on + +} // namespace SomeNamespace + #endif // REFLECTIVE_RAPIDJSON_TESTS_MORE_STRUCTS_H diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index f555b2a..7ff73cb 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -5,7 +5,7 @@ set(META_PROJECT_TYPE library) set(META_HEADER_ONLY_LIB ON) # add project files -set(HEADER_FILES traits.h) +set(HEADER_FILES traits.h versioning.h) set(SRC_FILES) set(TEST_HEADER_FILES) set(TEST_SRC_FILES) diff --git a/lib/binary/reflector-boosthana.h b/lib/binary/reflector-boosthana.h index fd57de2..ea64add 100644 --- a/lib/binary/reflector-boosthana.h +++ b/lib/binary/reflector-boosthana.h @@ -24,16 +24,17 @@ namespace ReflectiveRapidJSON { namespace BinaryReflector { -template > *> void readCustomType(BinaryDeserializer &deserializer, Type &customType) +template > *> +BinaryVersion readCustomType(BinaryDeserializer &deserializer, Type &customType, BinaryVersion version) { - boost::hana::for_each( - boost::hana::keys(customType), [&deserializer, &customType](auto key) { deserializer.read(boost::hana::at_key(customType, key)); }); + boost::hana::for_each(boost::hana::keys(customType), [&](auto key) { deserializer.read(boost::hana::at_key(customType, key), version); }); + return 0; } -template > *> void writeCustomType(BinarySerializer &serializer, const Type &customType) +template > *> +void writeCustomType(BinarySerializer &serializer, const Type &customType, BinaryVersion version) { - boost::hana::for_each( - boost::hana::keys(customType), [&serializer, &customType](auto key) { serializer.write(boost::hana::at_key(customType, key)); }); + boost::hana::for_each(boost::hana::keys(customType), [&](auto key) { serializer.write(boost::hana::at_key(customType, key), version); }); } } // namespace BinaryReflector diff --git a/lib/binary/reflector-chronoutilities.h b/lib/binary/reflector-chronoutilities.h index 79c911e..e600ebf 100644 --- a/lib/binary/reflector-chronoutilities.h +++ b/lib/binary/reflector-chronoutilities.h @@ -16,23 +16,33 @@ namespace ReflectiveRapidJSON { namespace BinaryReflector { -template <> inline void readCustomType(BinaryDeserializer &deserializer, CppUtilities::DateTime &dateTime) +template <> +inline BinaryVersion readCustomType(BinaryDeserializer &deserializer, CppUtilities::DateTime &dateTime, BinaryVersion version) { + CPP_UTILITIES_UNUSED(version) deserializer.read(dateTime.ticks()); + return 0; } -template <> inline void writeCustomType(BinarySerializer &serializer, const CppUtilities::DateTime &dateTime) +template <> +inline void writeCustomType(BinarySerializer &serializer, const CppUtilities::DateTime &dateTime, BinaryVersion version) { + CPP_UTILITIES_UNUSED(version) serializer.write(dateTime.totalTicks()); } -template <> inline void readCustomType(BinaryDeserializer &deserializer, CppUtilities::TimeSpan &timeSpan) +template <> +inline BinaryVersion readCustomType(BinaryDeserializer &deserializer, CppUtilities::TimeSpan &timeSpan, BinaryVersion version) { + CPP_UTILITIES_UNUSED(version) deserializer.read(timeSpan.ticks()); + return 0; } -template <> inline void writeCustomType(BinarySerializer &serializer, const CppUtilities::TimeSpan &timeSpan) +template <> +inline void writeCustomType(BinarySerializer &serializer, const CppUtilities::TimeSpan &timeSpan, BinaryVersion version) { + CPP_UTILITIES_UNUSED(version) serializer.write(timeSpan.totalTicks()); } diff --git a/lib/binary/reflector.h b/lib/binary/reflector.h index 32aedc9..b5eb38e 100644 --- a/lib/binary/reflector.h +++ b/lib/binary/reflector.h @@ -8,6 +8,7 @@ */ #include "../traits.h" +#include "../versioning.h" #include #include @@ -33,7 +34,8 @@ template struct AdaptedBinarySerializable : public Traits::Bool struct BinarySerializable; +using BinaryVersion = std::uint64_t; +template struct BinarySerializable; /*! * \brief The BinaryReflector namespace contains BinaryReader and BinaryWriter for automatic binary (de)serialization. @@ -50,9 +52,22 @@ template using IsCustomType = Traits::Not>; class BinaryDeserializer; class BinarySerializer; -template > * = nullptr> void readCustomType(BinaryDeserializer &deserializer, Type &customType); -template > * = nullptr> void writeCustomType(BinarySerializer &serializer, const Type &customType); +/// \brief Reads \a customType via \a deserializer. +/// \remarks +/// - If \tp Type is versioned, the version is determined from the data. Otherwise \a version is assumed. +/// - The specified \a version shall be passed to nested invocations. +/// \returns Returns the determined/assumed version. +template > * = nullptr> +BinaryVersion readCustomType(BinaryDeserializer &deserializer, Type &customType, BinaryVersion version = 0); +/// \brief Writes \a customType via \a serializer. +/// \remarks +/// - If \tp Type is versioned, \a version is prepended to the data. +/// - The specified \a version shall be passed to nested invocations. +template > * = nullptr> +void writeCustomType(BinarySerializer &serializer, const Type &customType, BinaryVersion version = 0); + +/// \brief The BinaryDeserializer class can read various data types, including custom ones, from an std::istream. class BinaryDeserializer : public CppUtilities::BinaryReader { friend class ::BinaryReflectorTests; @@ -71,12 +86,14 @@ public: void read(Type &iteratable); template > * = nullptr> void read(Type &enumValue); template > * = nullptr> void read(Type &variant); - template > * = nullptr> void read(Type &customType); + template > * = nullptr> BinaryVersion read(Type &builtInType, BinaryVersion version); + template > * = nullptr> BinaryVersion read(Type &customType, BinaryVersion version = 0); private: std::unordered_map m_pointer; }; +/// \brief The BinarySerializer class can write various data types, including custom ones, to an std::ostream. class BinarySerializer : public CppUtilities::BinaryWriter { friend class ::BinaryReflectorTests; @@ -90,7 +107,8 @@ public: template , Traits::HasSize> * = nullptr> void write(const Type &iteratable); template > * = nullptr> void write(const Type &enumValue); template > * = nullptr> void write(const Type &variant); - template > * = nullptr> void write(const Type &customType); + template > * = nullptr> void write(const Type &builtInType, BinaryVersion version); + template > * = nullptr> void write(const Type &customType, BinaryVersion version = 0); private: std::unordered_map m_pointer; @@ -207,9 +225,15 @@ template > *> void BinaryDeseria Detail::readVariantValueByRuntimeIndex(readByte(), variant, *this); } -template > *> void BinaryDeserializer::read(Type &customType) +template > *> BinaryVersion BinaryDeserializer::read(Type &builtInType, BinaryVersion version) { - readCustomType(*this, customType); + read(builtInType); + return version; +} + +template > *> BinaryVersion BinaryDeserializer::read(Type &customType, BinaryVersion version) +{ + return readCustomType(*this, customType, version); } inline BinarySerializer::BinarySerializer(std::ostream *stream) @@ -286,9 +310,15 @@ template > *> void BinarySeriali variant); } -template > *> void BinarySerializer::write(const Type &customType) +template > *> void BinarySerializer::write(const Type &builtInType, BinaryVersion version) { - writeCustomType(*this, customType); + CPP_UTILITIES_UNUSED(version) + write(builtInType); +} + +template > *> void BinarySerializer::write(const Type &customType, BinaryVersion version) +{ + writeCustomType(*this, customType, version); } } // namespace BinaryReflector diff --git a/lib/binary/serializable.h b/lib/binary/serializable.h index 9e65633..ff0cd26 100644 --- a/lib/binary/serializable.h +++ b/lib/binary/serializable.h @@ -17,25 +17,26 @@ namespace ReflectiveRapidJSON { /*! * \brief The BinarySerializable class provides the CRTP-base for (de)serializable objects. */ -template struct BinarySerializable { - void toBinary(std::ostream &outputStream) const; - void restoreFromBinary(std::istream &inputStream); +template struct BinarySerializable { + void toBinary(std::ostream &outputStream, BinaryVersion version = 0) const; + BinaryVersion restoreFromBinary(std::istream &inputStream); static Type fromBinary(std::istream &inputStream); static constexpr const char *qualifiedName = "ReflectiveRapidJSON::BinarySerializable"; + static constexpr auto version = v; }; -template inline void BinarySerializable::toBinary(std::ostream &outputStream) const +template inline void BinarySerializable::toBinary(std::ostream &outputStream, BinaryVersion version) const { - BinaryReflector::BinarySerializer(&outputStream).write(static_cast(*this)); + BinaryReflector::BinarySerializer(&outputStream).write(static_cast(*this), version); } -template inline void BinarySerializable::restoreFromBinary(std::istream &inputStream) +template inline BinaryVersion BinarySerializable::restoreFromBinary(std::istream &inputStream) { - BinaryReflector::BinaryDeserializer(&inputStream).read(static_cast(*this)); + return BinaryReflector::BinaryDeserializer(&inputStream).read(static_cast(*this)); } -template Type BinarySerializable::fromBinary(std::istream &inputStream) +template Type BinarySerializable::fromBinary(std::istream &inputStream) { Type object; static_cast &>(object).restoreFromBinary(inputStream); diff --git a/lib/tests/binaryreflector.cpp b/lib/tests/binaryreflector.cpp index e9258f8..50ee263 100644 --- a/lib/tests/binaryreflector.cpp +++ b/lib/tests/binaryreflector.cpp @@ -75,8 +75,9 @@ struct ObjectWithVariantsBinary : public BinarySerializable void readCustomType(BinaryDeserializer &deserializer, TestObjectBinary &customType) +template <> BinaryVersion readCustomType(BinaryDeserializer &deserializer, TestObjectBinary &customType, BinaryVersion version) { + CPP_UTILITIES_UNUSED(version) deserializer.read(customType.number); deserializer.read(customType.number2); deserializer.read(customType.numbers); @@ -92,10 +93,12 @@ template <> void readCustomType(BinaryDeserializer &deserializ deserializer.read(customType.someEnumClass); deserializer.read(customType.timeSpan); deserializer.read(customType.dateTime); + return 0; } -template <> void writeCustomType(BinarySerializer &serializer, const TestObjectBinary &customType) +template <> void writeCustomType(BinarySerializer &serializer, const TestObjectBinary &customType, BinaryVersion version) { + CPP_UTILITIES_UNUSED(version) serializer.write(customType.number); serializer.write(customType.number2); serializer.write(customType.numbers); @@ -113,27 +116,35 @@ template <> void writeCustomType(BinarySerializer &serializer, serializer.write(customType.dateTime); } -template <> void readCustomType(BinaryDeserializer &deserializer, NestingArrayBinary &customType) +template <> BinaryVersion readCustomType(BinaryDeserializer &deserializer, NestingArrayBinary &customType, BinaryVersion version) { + CPP_UTILITIES_UNUSED(version) deserializer.read(customType.name); deserializer.read(customType.testObjects); + return 0; } -template <> void writeCustomType(BinarySerializer &serializer, const NestingArrayBinary &customType) +template <> void writeCustomType(BinarySerializer &serializer, const NestingArrayBinary &customType, BinaryVersion version) { + CPP_UTILITIES_UNUSED(version) serializer.write(customType.name); serializer.write(customType.testObjects); } -template <> void readCustomType(BinaryDeserializer &deserializer, ObjectWithVariantsBinary &customType) +template <> +BinaryVersion readCustomType(BinaryDeserializer &deserializer, ObjectWithVariantsBinary &customType, BinaryVersion version) { + CPP_UTILITIES_UNUSED(version) deserializer.read(customType.someVariant); deserializer.read(customType.anotherVariant); deserializer.read(customType.yetAnotherVariant); + return 0; } -template <> void writeCustomType(BinarySerializer &serializer, const ObjectWithVariantsBinary &customType) +template <> +void writeCustomType(BinarySerializer &serializer, const ObjectWithVariantsBinary &customType, BinaryVersion version) { + CPP_UTILITIES_UNUSED(version) serializer.write(customType.someVariant); serializer.write(customType.anotherVariant); serializer.write(customType.yetAnotherVariant); diff --git a/lib/tests/traits.cpp b/lib/tests/traits.cpp index f996cc8..6689957 100644 --- a/lib/tests/traits.cpp +++ b/lib/tests/traits.cpp @@ -1,13 +1,27 @@ #include "../traits.h" +#include "../versioning.h" + +#include "../binary/serializable.h" #include #include -// treat some types differently to test Treat... traits +// define structs for testing REFLECTIVE_RAPIDJSON_TREAT_AS_… struct Foo { }; struct Bar { }; + +// define structs for testing versioning +struct VersionlessBase : public ReflectiveRapidJSON::BinarySerializable { +}; +struct VersionedDerived : public VersionlessBase, public ReflectiveRapidJSON::BinarySerializable { +}; +struct VersionedBase : public ReflectiveRapidJSON::BinarySerializable { +}; +struct VersionlessDerived : public VersionedBase, public ReflectiveRapidJSON::BinarySerializable { +}; + namespace ReflectiveRapidJSON { REFLECTIVE_RAPIDJSON_TREAT_AS_MAP_OR_HASH(Foo); REFLECTIVE_RAPIDJSON_TREAT_AS_MULTI_MAP_OR_HASH(Foo); @@ -48,3 +62,14 @@ static_assert(IsIteratableExceptString>::value, "vector is iter static_assert(!IsIteratableExceptString::value, "string not iteratable"); static_assert(!IsIteratableExceptString::value, "wstring not iteratable"); static_assert(!IsIteratableExceptString::value, "string not iteratable"); + +// test versioning traits +static_assert(!Versioning::enabled, "versioning for built-in types not enabled"); +static_assert(!Versioning::enabled, "versioning for standard types not enabled"); +static_assert(!Versioning::enabled, "versioning not enabled by default"); +static_assert(Versioning>::enabled, "versioning enabled if non-zero version parameter specified (derived)"); +static_assert(Versioning::enabled, "versioning enabled if non-zero version parameter specified (base)"); +static_assert(!Versioning>::enabled, "versioning disabled for derived, even if base is versioned"); +static_assert(!Versioning>::enabled, "versioning disabled if zero-version specified"); +static_assert(Versioning>::applyDefault(0) == 3, "default version returned"); +static_assert(Versioning>::applyDefault(2) == 2, "default version overridden"); diff --git a/lib/versioning.h b/lib/versioning.h new file mode 100644 index 0000000..ec7fa39 --- /dev/null +++ b/lib/versioning.h @@ -0,0 +1,44 @@ +#ifndef REFLECTIVE_RAPIDJSON_VERSIONING +#define REFLECTIVE_RAPIDJSON_VERSIONING + +#include + +namespace ReflectiveRapidJSON { + +#ifdef REFLECTIVE_RAPIDJSON_GENERATOR +#define REFLECTIVE_RAPIDJSON_CAT_1(a, b) a##b +#define REFLECTIVE_RAPIDJSON_CAT_2(a, b) REFLECTIVE_RAPIDJSON_CAT_1(a, b) +#define REFLECTIVE_RAPIDJSON_AS_OF_VERSION(version) \ + static constexpr std::size_t REFLECTIVE_RAPIDJSON_CAT_2(rrjAsOfVersion, __COUNTER__) = version; \ +public +#define REFLECTIVE_RAPIDJSON_UNTIL_VERSION(version) \ + static constexpr std::size_t REFLECTIVE_RAPIDJSON_CAT_2(rrjUntilVersion, __COUNTER__) = version; \ +public +#else +#define REFLECTIVE_RAPIDJSON_AS_OF_VERSION(version) public +#define REFLECTIVE_RAPIDJSON_UNTIL_VERSION(version) public +#endif + +#ifdef REFLECTIVE_RAPIDJSON_SHORT_MACROS +#define as_of_version(version) REFLECTIVE_RAPIDJSON_AS_OF_VERSION(version) +#define until_version(version) REFLECTIVE_RAPIDJSON_UNTIL_VERSION(version) +#endif + +CPP_UTILITIES_TRAITS_DEFINE_TYPE_CHECK(IsVersioned, T::version); + +template ::value> struct Versioning { + static constexpr auto enabled = false; +}; + +template struct Versioning { + static constexpr auto enabled = Type::version != 0; + static constexpr auto serializationDefault = Type::version; + static constexpr auto applyDefault(decltype(serializationDefault) version) + { + return version ? version : serializationDefault; + } +}; + +} // namespace ReflectiveRapidJSON + +#endif // REFLECTIVE_RAPIDJSON_TRAITS