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 4, 2023
1 parent ebc345c commit 6e28bc0
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 @@ -61,6 +61,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)

Check warning on line 7 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L7

Added line #L7 was not covered by tests
{
if (auto it = mHooks.find(&type); it != mHooks.end())

Check warning on line 9 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L9

Added line #L9 was not covered by tests
{
if (!it->second(value))

Check warning on line 11 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L11

Added line #L11 was not covered by tests
{
CUBOS_WARN("Deserialization hook for type '{}' failed", type.name());
return false;

Check warning on line 14 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L13-L14

Added lines #L13 - L14 were not covered by tests
}
}
else if (!this->decompose(type, value))

Check warning on line 17 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L17

Added line #L17 was not covered by tests
{
CUBOS_WARN("Deserialization decomposition for type '{}' failed", type.name());
return false;

Check warning on line 20 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L19-L20

Added lines #L19 - L20 were not covered by tests
}

return true;

Check warning on line 23 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L23

Added line #L23 was not covered by tests
}

void Deserializer::hook(const reflection::Type& type, Hook hook)

Check warning on line 26 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L26

Added line #L26 was not covered by tests
{
if (auto it = mHooks.find(&type); it != mHooks.end())

Check warning on line 28 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L28

Added line #L28 was not covered by tests
{
CUBOS_WARN("Hook for type '{}' already exists, overwriting", type.name());

Check warning on line 30 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L30

Added line #L30 was not covered by tests
}

mHooks.emplace(&type, memory::move(hook));

Check warning on line 33 in core/src/cubos/core/data/des/deserializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/des/deserializer.cpp#L33

Added line #L33 was not covered by tests
}

0 comments on commit 6e28bc0

Please sign in to comment.