From 5482884c92daf193fc50eda0103975f2dbcf2324 Mon Sep 17 00:00:00 2001 From: Ricardo Antunes Date: Fri, 3 Nov 2023 16:50:40 +0000 Subject: [PATCH] feat(serialization): add Deserializer --- core/CMakeLists.txt | 1 + .../cubos/core/data/des/deserializer.hpp | 93 ++++++++++++++ core/include/cubos/core/data/des/module.dox | 9 ++ core/samples/CMakeLists.txt | 1 + core/samples/data/des/custom/main.cpp | 118 ++++++++++++++++++ core/samples/data/des/custom/page.md | 56 +++++++++ core/samples/data/des/page.md | 5 + core/samples/data/page.md | 1 + core/src/cubos/core/data/des/deserializer.cpp | 34 +++++ 9 files changed, 318 insertions(+) create mode 100644 core/include/cubos/core/data/des/deserializer.hpp create mode 100644 core/include/cubos/core/data/des/module.dox create mode 100644 core/samples/data/des/custom/main.cpp create mode 100644 core/samples/data/des/custom/page.md create mode 100644 core/samples/data/des/page.md create mode 100644 core/src/cubos/core/data/des/deserializer.cpp diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 1c93c0af3..9032238f3 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -60,6 +60,7 @@ set(CUBOS_CORE_SOURCE "src/cubos/core/data/fs/embedded_archive.cpp" "src/cubos/core/data/ser/serializer.cpp" "src/cubos/core/data/ser/debug.cpp" + "src/cubos/core/data/des/deserializer.cpp" "src/cubos/core/io/window.cpp" "src/cubos/core/io/cursor.cpp" diff --git a/core/include/cubos/core/data/des/deserializer.hpp b/core/include/cubos/core/data/des/deserializer.hpp new file mode 100644 index 000000000..cd2dffaac --- /dev/null +++ b/core/include/cubos/core/data/des/deserializer.hpp @@ -0,0 +1,93 @@ +/// @file +/// @brief Class @ref cubos::core::data::Deserializer. +/// @ingroup core-data-des + +#pragma once + +#include + +#include +#include + +namespace cubos::core::data +{ + /// @brief Base class for deserializers, which defines the interface for deserializing + /// arbitrary data using its reflection metadata. + /// + /// Deserializers are type visitors which allow overriding the default deserialization + /// behaviour for each type using hooks. Hooks are functions which are called when the + /// deserializer encounters a type, and can be used to customize the deserialization process. + /// + /// If a type which can't be further decomposed is encountered for which no hook is defined, + /// the deserializer will emit a warning and fail. Implementations should set default hooks for + /// at least the primitive types. + /// + /// @ingroup core-data-des + class Deserializer + { + public: + virtual ~Deserializer() = default; + + /// @brief Constructs. + Deserializer() = default; + + /// @name Deleted copy and move constructors. + /// @brief Deleted as the hooks may contain references to the deserializer. + /// @{ + Deserializer(Deserializer&&) = delete; + Deserializer(const Deserializer&) = delete; + /// @} + + /// @brief Function type for deserialization hooks. + using Hook = memory::Function; + + /// @brief Function type for deserialization hooks. + /// @tparam T Type. + template + using TypedHook = memory::Function; + + /// @brief Deserialize the given value. + /// @param type Type. + /// @param value Value. + /// @return Whether the value was successfully deserialized. + bool read(const reflection::Type& type, void* value); + + /// @brief Deserialize the given value. + /// @tparam T Type. + /// @param value Value. + /// @return Whether the value was successfully deserialized. + template + bool read(T& value) + { + return this->read(reflection::reflect(), &value); + } + + /// @brief Sets the hook to be called on deserialization of the given type. + /// @param type Type. + /// @param hook Hook. + void hook(const reflection::Type& type, Hook hook); + + /// @brief Sets the hook to be called on deserialization of the given type. + /// @tparam T Type. + /// @param hook Hook. + template + void hook(TypedHook hook) + { + this->hook(reflection::reflect(), + [hook = memory::move(hook)](void* value) mutable { return hook(*static_cast(value)); }); + } + + protected: + /// @brief Called for each type with no hook defined. + /// + /// Should recurse by calling @ref read() again as appropriate. + /// + /// @param type Type. + /// @param value Value. + /// @return Whether the value was successfully deserialized. + virtual bool decompose(const reflection::Type& type, void* value) = 0; + + private: + std::unordered_map mHooks; + }; +} // namespace cubos::core::data diff --git a/core/include/cubos/core/data/des/module.dox b/core/include/cubos/core/data/des/module.dox new file mode 100644 index 000000000..3dd74916c --- /dev/null +++ b/core/include/cubos/core/data/des/module.dox @@ -0,0 +1,9 @@ +/// @dir +/// @brief @ref core-data-des directory. + +namespace cubos::core::data +{ + /// @defgroup core-data-des Deserialization + /// @ingroup core-data + /// @brief Provides deserialization utilities. +} diff --git a/core/samples/CMakeLists.txt b/core/samples/CMakeLists.txt index 28dea7f79..843680df3 100644 --- a/core/samples/CMakeLists.txt +++ b/core/samples/CMakeLists.txt @@ -36,6 +36,7 @@ make_sample(DIR "reflection/traits/string_conversion") make_sample(DIR "data/fs/embedded_archive" SOURCES "embed.cpp") make_sample(DIR "data/fs/standard_archive") make_sample(DIR "data/ser/custom") +make_sample(DIR "data/des/custom") make_sample(DIR "data/serialization") make_sample(DIR "ecs/events") make_sample(DIR "ecs/general") diff --git a/core/samples/data/des/custom/main.cpp b/core/samples/data/des/custom/main.cpp new file mode 100644 index 000000000..918cab7fd --- /dev/null +++ b/core/samples/data/des/custom/main.cpp @@ -0,0 +1,118 @@ +#include +#include + +using cubos::core::memory::Stream; + +/// [Include] +#include + +using cubos::core::data::Deserializer; +using cubos::core::reflection::Type; +/// [Include] + +/// [Your own deserializer] +class MyDeserializer : public Deserializer +{ +public: + MyDeserializer(); + +protected: + bool decompose(const Type& type, void* value) override; +}; +/// [Your own deserializer] + +/// [Setting up hooks] +#include + +using cubos::core::reflection::reflect; + +MyDeserializer::MyDeserializer() +{ + this->hook([](int32_t& value) { + Stream::stdOut.print("enter an int32_t: "); + Stream::stdIn.parse(value); + return true; + }); +} +/// [Setting up hooks] + +/// [Decomposing types] +#include +#include +#include + +using cubos::core::reflection::ArrayTrait; +using cubos::core::reflection::FieldsTrait; + +bool MyDeserializer::decompose(const Type& type, void* value) +{ + if (type.has()) + { + const auto& arrayTrait = type.get(); + auto arrayView = arrayTrait.view(value); + + auto length = static_cast(arrayView.length()); + Stream::stdOut.printf("enter array size: ", length); + Stream::stdIn.parse(length); + + for (std::size_t i = 0; i < static_cast(length); ++i) + { + if (i == arrayView.length()) + { + arrayView.insertDefault(i); + } + + Stream::stdOut.printf("writing array[{}]: ", i); + this->read(arrayTrait.elementType(), arrayView.get(i)); + } + + while (arrayView.length() > static_cast(length)) + { + arrayView.erase(static_cast(length)); + } + + return true; + } + /// [Decomposing types] + + /// [Decomposing types with fields] + if (type.has()) + { + for (const auto& [field, fieldValue] : type.get().view(value)) + { + Stream::stdOut.printf("writing field '{}': ", field->name()); + if (!this->read(field->type(), fieldValue)) + { + return false; + } + } + + return true; + } + + CUBOS_WARN("Cannot decompose '{}'", type.name()); + return false; +} +/// [Decomposing types with fields] + +/// [Usage] +#include + +#include +#include + +int main() +{ + std::vector vec{}; + MyDeserializer des{}; + des.read(vec); + + Stream::stdOut.print("-----------\n"); + Stream::stdOut.print("Resulting vec: [ "); + for (const auto& v : vec) + { + Stream::stdOut.printf("({}, {}, {}) ", v.x, v.y, v.z); + } + Stream::stdOut.print("]\n"); +} +/// [Usage] diff --git a/core/samples/data/des/custom/page.md b/core/samples/data/des/custom/page.md new file mode 100644 index 000000000..62f10725b --- /dev/null +++ b/core/samples/data/des/custom/page.md @@ -0,0 +1,56 @@ +# Custom Deserializer {#examples-core-data-des-custom} + +@brief Implementing your own @ref cubos::core::data::Deserializer "Deserializer". + +@see Full source code [here](https://github.com/GameDevTecnico/cubos/tree/main/core/samples/data/des/custom). + +To define your own deserializer type, you'll need to include +@ref core/data/des/deserializer.hpp. For simplicity, in this sample we'll use +the following aliases: + +@snippet data/des/custom/main.cpp Include + +We'll define a deserializer that will print the data to the standard output. + +@snippet data/des/custom/main.cpp Your own deserializer + +In the constructor, we should set hooks to be called for deserializing primitive +types or any other type we want to handle specifically. + +In this example, we'll only handle `int32_t`, but usually you should at least +cover all primitive types. + +@snippet data/des/custom/main.cpp Setting up hooks + +The only other thing you need to do is implement the @ref +cubos::core::data::deserializer::decompose "deserializer::decompose" method, +which acts as a catch-all for any type without a specific hook. + +Here, we can use traits such as @ref cubos::core::reflection::FieldsTrait +"FieldsTrait" to access the fields of a type and write to them. + +In this sample, we'll only be handling fields and arrays, but you should try to +cover as many kinds of data as possible. + +@snippet data/des/custom/main.cpp Decomposing types + +We start by checking if the type can be viewed as an array. If it can, we'll +ask the user how many elements they want the array to have. We resize it, and +then, we recurse into the elements. If the type doesn't have this trait, we'll +fallback into checking if it has fields. + +@snippet data/des/custom/main.cpp Decomposing types with fields + +If the type has fields, we'll iterate over them and ask the user to enter +values for them. Otherwise, we'll fail by returning `false`. + +Using our deserializer is as simple as constructing it and calling @ref +cubos::core::data::Deserializer::read "Deserializer::read" on the data we want +to deserialize. + +In this case, we'll be deserializing a `std::vector`, which is +an array of objects with three `int32_t` fields. + +@snippet data/des/custom/main.cpp Usage + +This should output the values you enter when you execute it. diff --git a/core/samples/data/des/page.md b/core/samples/data/des/page.md new file mode 100644 index 000000000..bf4a733f9 --- /dev/null +++ b/core/samples/data/des/page.md @@ -0,0 +1,5 @@ +# Deserialization {#examples-core-data-des} + +@brief Using the @ref core-data-des module. + +- @subpage examples-core-data-des-custom - @copybrief examples-core-data-des-custom diff --git a/core/samples/data/page.md b/core/samples/data/page.md index cab52f11a..f92057e73 100644 --- a/core/samples/data/page.md +++ b/core/samples/data/page.md @@ -3,3 +3,4 @@ @brief Using the @ref core-data module. - @subpage examples-core-data-ser - @copybrief examples-core-data-ser +- @subpage examples-core-data-des - @copybrief examples-core-data-des diff --git a/core/src/cubos/core/data/des/deserializer.cpp b/core/src/cubos/core/data/des/deserializer.cpp new file mode 100644 index 000000000..0e18baa28 --- /dev/null +++ b/core/src/cubos/core/data/des/deserializer.cpp @@ -0,0 +1,34 @@ +#include +#include +#include + +using cubos::core::data::Deserializer; + +bool Deserializer::read(const reflection::Type& type, void* value) +{ + if (auto it = mHooks.find(&type); it != mHooks.end()) + { + if (!it->second(value)) + { + CUBOS_WARN("Deserialization hook for type '{}' failed", type.name()); + return false; + } + } + else if (!this->decompose(type, value)) + { + CUBOS_WARN("Deserialization decomposition for type '{}' failed", type.name()); + return false; + } + + return true; +} + +void Deserializer::hook(const reflection::Type& type, Hook hook) +{ + if (auto it = mHooks.find(&type); it != mHooks.end()) + { + CUBOS_WARN("Hook for type '{}' already exists, overwriting", type.name()); + } + + mHooks.emplace(&type, memory::move(hook)); +}