-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(serialization): add Deserializer
- Loading branch information
Showing
9 changed files
with
318 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/// @file | ||
/// @brief Class @ref cubos::core::data::Deserializer. | ||
/// @ingroup core-data-des | ||
|
||
#pragma once | ||
|
||
#include <unordered_map> | ||
|
||
#include <cubos/core/memory/function.hpp> | ||
#include <cubos/core/reflection/reflect.hpp> | ||
|
||
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<bool(void*)>; | ||
|
||
/// @brief Function type for deserialization hooks. | ||
/// @tparam T Type. | ||
template <typename T> | ||
using TypedHook = memory::Function<bool(T&)>; | ||
|
||
/// @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 <typename T> | ||
bool read(T& value) | ||
{ | ||
return this->read(reflection::reflect<T>(), &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 <typename T> | ||
void hook(TypedHook<T> hook) | ||
{ | ||
this->hook(reflection::reflect<T>(), | ||
[hook = memory::move(hook)](void* value) mutable { return hook(*static_cast<T*>(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<const reflection::Type*, Hook> mHooks; | ||
}; | ||
} // namespace cubos::core::data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
#include <cubos/core/log.hpp> | ||
#include <cubos/core/memory/stream.hpp> | ||
|
||
using cubos::core::memory::Stream; | ||
|
||
/// [Include] | ||
#include <cubos/core/data/des/deserializer.hpp> | ||
|
||
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 <cubos/core/reflection/external/primitives.hpp> | ||
|
||
using cubos::core::reflection::reflect; | ||
|
||
MyDeserializer::MyDeserializer() | ||
{ | ||
this->hook<int32_t>([](int32_t& value) { | ||
Stream::stdOut.print("enter an int32_t: "); | ||
Stream::stdIn.parse(value); | ||
return true; | ||
}); | ||
} | ||
/// [Setting up hooks] | ||
|
||
/// [Decomposing types] | ||
#include <cubos/core/reflection/traits/array.hpp> | ||
#include <cubos/core/reflection/traits/fields.hpp> | ||
#include <cubos/core/reflection/type.hpp> | ||
|
||
using cubos::core::reflection::ArrayTrait; | ||
using cubos::core::reflection::FieldsTrait; | ||
|
||
bool MyDeserializer::decompose(const Type& type, void* value) | ||
{ | ||
if (type.has<ArrayTrait>()) | ||
{ | ||
const auto& arrayTrait = type.get<ArrayTrait>(); | ||
auto arrayView = arrayTrait.view(value); | ||
|
||
auto length = static_cast<uint64_t>(arrayView.length()); | ||
Stream::stdOut.printf("enter array size: ", length); | ||
Stream::stdIn.parse(length); | ||
|
||
for (std::size_t i = 0; i < static_cast<std::size_t>(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<std::size_t>(length)) | ||
{ | ||
arrayView.erase(static_cast<std::size_t>(length)); | ||
} | ||
|
||
return true; | ||
} | ||
/// [Decomposing types] | ||
|
||
/// [Decomposing types with fields] | ||
if (type.has<FieldsTrait>()) | ||
{ | ||
for (const auto& [field, fieldValue] : type.get<FieldsTrait>().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 <glm/vec3.hpp> | ||
|
||
#include <cubos/core/reflection/external/glm.hpp> | ||
#include <cubos/core/reflection/external/vector.hpp> | ||
|
||
int main() | ||
{ | ||
std::vector<glm::ivec3> 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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<glm::ivec3>`, 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
#include <cubos/core/data/des/deserializer.hpp> | ||
#include <cubos/core/log.hpp> | ||
#include <cubos/core/reflection/type.hpp> | ||
|
||
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)); | ||
} |