Skip to content

Commit

Permalink
feat(serialization): add Deserializer
Browse files Browse the repository at this point in the history
  • Loading branch information
RiscadoA committed Nov 10, 2023
1 parent 0fb3cd0 commit 5482884
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 0 deletions.
1 change: 1 addition & 0 deletions core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
93 changes: 93 additions & 0 deletions core/include/cubos/core/data/des/deserializer.hpp
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
9 changes: 9 additions & 0 deletions core/include/cubos/core/data/des/module.dox
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.
}
1 change: 1 addition & 0 deletions core/samples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
118 changes: 118 additions & 0 deletions core/samples/data/des/custom/main.cpp
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]
56 changes: 56 additions & 0 deletions core/samples/data/des/custom/page.md
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.
5 changes: 5 additions & 0 deletions core/samples/data/des/page.md
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
1 change: 1 addition & 0 deletions core/samples/data/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
34 changes: 34 additions & 0 deletions core/src/cubos/core/data/des/deserializer.cpp
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));
}

0 comments on commit 5482884

Please sign in to comment.