diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 66e7a610e0..6b88302129 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -47,11 +47,13 @@ set(CUBOS_CORE_SOURCE "src/cubos/core/data/old/binary_serializer.cpp" "src/cubos/core/data/old/binary_deserializer.cpp" "src/cubos/core/data/old/package.cpp" + "src/cubos/core/data/old/context.cpp" + "src/cubos/core/data/fs/file.cpp" "src/cubos/core/data/fs/file_system.cpp" "src/cubos/core/data/fs/standard_archive.cpp" "src/cubos/core/data/fs/embedded_archive.cpp" - "src/cubos/core/data/old/context.cpp" + "src/cubos/core/data/ser/serializer.cpp" "src/cubos/core/io/window.cpp" "src/cubos/core/io/cursor.cpp" diff --git a/core/include/cubos/core/data/ser/module.dox b/core/include/cubos/core/data/ser/module.dox new file mode 100644 index 0000000000..73139557ce --- /dev/null +++ b/core/include/cubos/core/data/ser/module.dox @@ -0,0 +1,9 @@ +/// @dir +/// @brief @ref core-data-ser directory. + +namespace cubos::core::data +{ + /// @defgroup core-data-ser Serialization + /// @ingroup core-data + /// @brief Provides serialization utilities. +} diff --git a/core/include/cubos/core/data/ser/serializer.hpp b/core/include/cubos/core/data/ser/serializer.hpp new file mode 100644 index 0000000000..833d159512 --- /dev/null +++ b/core/include/cubos/core/data/ser/serializer.hpp @@ -0,0 +1,80 @@ +/// @file +/// @brief Class @ref cubos::core::data::Serializer. +/// @ingroup core-data-ser + +#pragma once + +#include + +#include + +namespace cubos::core::data +{ + /// @brief Base class for serializers, which defines the interface for serializing arbitrary + /// data using its reflection metadata. + /// + /// Serializers are type visitors which allow overriding the default serialization behaviour + /// for each type using hooks. Hooks are functions which are called when the serializer + /// encounters a type, and can be used to customize the serialization process. + /// + /// If a type which can't be further decomposed is encountered for which no hook is defined, + /// the serializer will emit a warning and fail. Implementations should set default hooks for + /// at least the primitive types. + /// + /// @ingroup core-data-ser + class Serializer + { + public: + virtual ~Serializer() = default; + + /// @brief Function type for serialization hooks. + /// @param ser Serializer. + /// @param type Type. + /// @param value Value. + /// @return Whether the value was successfully serialized. + using Hook = bool (*)(Serializer& ser, const reflection::Type& type, const void* value); + + /// @brief Serialize the given value. + /// @param type Type. + /// @param value Value. + /// @return Whether the value was successfully serialized. + bool write(const reflection::Type& type, const void* value); + + /// @brief Serialize the given value. + /// @tparam T Type. + /// @param value Value. + /// @return Whether the value was successfully serialized. + template + bool write(const T& value) + { + return this->write(reflection::reflect(), &value); + } + + /// @brief Sets the hook to be called on serialization 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 serialization of the given type. + /// @tparam T Type. + /// @param hook Hook. + template + void hook(Hook hook) + { + this->hook(reflection::reflect(), hook); + } + + protected: + /// @brief Called for each type with no hook defined. + /// + /// Should recurse by calling @ref write() again as appropriate. + /// + /// @param type Type. + /// @param value Value. + /// @return Whether the value was successfully serialized. + virtual bool decompose(const reflection::Type& type, const void* value) = 0; + + private: + std::unordered_map mHooks; + }; +} // namespace cubos::core::data diff --git a/core/samples/CMakeLists.txt b/core/samples/CMakeLists.txt index fcb31042d8..1bade9e1ff 100644 --- a/core/samples/CMakeLists.txt +++ b/core/samples/CMakeLists.txt @@ -34,6 +34,7 @@ make_sample(DIR "reflection/traits/array") make_sample(DIR "reflection/traits/dictionary") 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/serialization") make_sample(DIR "ecs/events") make_sample(DIR "ecs/general") diff --git a/core/samples/data/page.md b/core/samples/data/page.md new file mode 100644 index 0000000000..cab52f11a2 --- /dev/null +++ b/core/samples/data/page.md @@ -0,0 +1,5 @@ +# Data {#examples-core-data} + +@brief Using the @ref core-data module. + +- @subpage examples-core-data-ser - @copybrief examples-core-data-ser diff --git a/core/samples/data/ser/custom/main.cpp b/core/samples/data/ser/custom/main.cpp new file mode 100644 index 0000000000..1b862052a3 --- /dev/null +++ b/core/samples/data/ser/custom/main.cpp @@ -0,0 +1,107 @@ +#include +#include + +using cubos::core::memory::Stream; + +/// [Include] +#include + +using cubos::core::data::Serializer; +using cubos::core::reflection::Type; +/// [Include] + +/// [Your own serializer] +class MySerializer : public Serializer +{ +public: + MySerializer(); + +protected: + bool decompose(const Type& type, const void* value) override; +}; +/// [Your own serializer] + +/// [Setting up hooks] +#include + +using cubos::core::reflection::reflect; + +MySerializer::MySerializer() +{ + this->hook([](Serializer&, const Type&, const void* value) { + Stream::stdOut.print(*static_cast(value)); + return true; + }); +} +/// [Setting up hooks] + +/// [Decomposing types] +#include +#include +#include + +using cubos::core::reflection::ArrayTrait; +using cubos::core::reflection::FieldsTrait; + +bool MySerializer::decompose(const Type& type, const void* value) +{ + if (type.has()) + { + const auto& arrayTrait = type.get(); + + Stream::stdOut.put('['); + for (const auto* element : arrayTrait.view(value)) + { + if (!this->write(arrayTrait.elementType(), element)) + { + return false; + } + Stream::stdOut.print(", "); + } + Stream::stdOut.put(']'); + + return true; + } + /// [Decomposing types] + + /// [Decomposing types with fields] + if (type.has()) + { + Stream::stdOut.put('{'); + for (const auto& [field, fieldValue] : type.get().view(value)) + { + Stream::stdOut.printf("{}: ", field->name()); + if (!this->write(field->type(), fieldValue)) + { + return false; + } + Stream::stdOut.print(", "); + } + Stream::stdOut.put('}'); + + return true; + } + + CUBOS_WARN("Cannot decompose '{}'", type.name()); + return false; +} +/// [Decomposing types with fields] + +/// [Usage] +#include + +#include +#include + +int main() +{ + std::vector vec{{1, 2, 3}, {4, 5, 6}}; + + MySerializer ser{}; + ser.write(vec); +} +/// [Usage] + +/// [Output] +// [{x: 1, y: 2, z: 3, }, {x: 4, y: 5, z: 6, }, ] +/// [Output] diff --git a/core/samples/data/ser/custom/page.md b/core/samples/data/ser/custom/page.md new file mode 100644 index 0000000000..3abb146593 --- /dev/null +++ b/core/samples/data/ser/custom/page.md @@ -0,0 +1,55 @@ +# Custom Serializer {#examples-core-data-ser-custom} + +@brief Implementing your own @ref cubos::core::data::Serializer "Serializer". + +To define your own serializer type, you'll need to include +@ref core/data/ser/serializer.hpp. For simplicity, in this sample we'll use +the following aliases: + +@snippet data/ser/custom/main.cpp Include + +We'll define a serializer that will print the data to the standard output. + +@snippet data/ser/custom/main.cpp Your own serializer + +In the constructor, we should set hooks to be called for serializing 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/ser/custom/main.cpp Setting up hooks + +The only other thing you need to do is implement the @ref +cubos::core::data::Serializer::decompose "Serializer::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 get the fields of a type and print 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/ser/custom/main.cpp Decomposing types + +We start by checking if the type can be viewed as an array. If it can, we +recurse into its elements. +Otherwise, we'll fallback to the fields of the type. + +@snippet data/ser/custom/main.cpp Decomposing types with fields + +If the type has fields, we'll iterate over them and print them. +Otherwise, we'll fail by returning `false`. + +Using our serializer is as simple as constructing it and calling @ref +cubos::core::data::Serializer::write "Serializer::write" on the data we want to +serialize. + +In this case, we'll be serializing a `std::vector`, which is +an array of objects with three `int32_t` fields. + +@snippet data/ser/custom/main.cpp Usage + +This should output: + +@snippet data/ser/custom/main.cpp Output diff --git a/core/samples/data/ser/page.md b/core/samples/data/ser/page.md new file mode 100644 index 0000000000..7737e70b47 --- /dev/null +++ b/core/samples/data/ser/page.md @@ -0,0 +1,5 @@ +# Serialization {#examples-core-data-ser} + +@brief Using the @ref core-data-ser module. + +- @subpage examples-core-data-ser-custom - @copybrief examples-core-data-ser-custom diff --git a/core/src/cubos/core/data/ser/serializer.cpp b/core/src/cubos/core/data/ser/serializer.cpp new file mode 100644 index 0000000000..663b424943 --- /dev/null +++ b/core/src/cubos/core/data/ser/serializer.cpp @@ -0,0 +1,34 @@ +#include +#include +#include + +using cubos::core::data::Serializer; + +bool Serializer::write(const reflection::Type& type, const void* value) +{ + if (auto it = mHooks.find(&type); it != mHooks.end()) + { + if (!it->second(*this, type, value)) + { + CUBOS_WARN("Serialization hook for type '{}' failed", type.name()); + return false; + } + } + else if (!this->decompose(type, value)) + { + CUBOS_WARN("Serialization decomposition for type '{}' failed", type.name()); + return false; + } + + return true; +} + +void Serializer::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, hook); +} diff --git a/docs/pages/3_examples/1_core/main.md b/docs/pages/3_examples/1_core/main.md index 9a48f9137e..761b5d59b6 100644 --- a/docs/pages/3_examples/1_core/main.md +++ b/docs/pages/3_examples/1_core/main.md @@ -10,3 +10,4 @@ The following examples have fully documented tutorials: - @subpage examples-core-logging - @copybrief examples-core-logging - @subpage examples-core-reflection - @copybrief examples-core-reflection +- @subpage examples-core-data - @copybrief examples-core-data