Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Serializer #652

Merged
merged 4 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions core/include/cubos/core/data/ser/module.dox
Original file line number Diff line number Diff line change
@@ -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.
}
80 changes: 80 additions & 0 deletions core/include/cubos/core/data/ser/serializer.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/// @file
/// @brief Class @ref cubos::core::data::Serializer.
/// @ingroup core-data-ser

#pragma once

#include <unordered_map>

#include <cubos/core/reflection/reflect.hpp>

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 <typename T>
bool write(const T& value)
{
return this->write(reflection::reflect<T>(), &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 <typename T>
void hook(Hook hook)
{
this->hook(reflection::reflect<T>(), 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<const reflection::Type*, Hook> mHooks;
};
} // namespace cubos::core::data
1 change: 1 addition & 0 deletions core/samples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
5 changes: 5 additions & 0 deletions core/samples/data/page.md
Original file line number Diff line number Diff line change
@@ -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
107 changes: 107 additions & 0 deletions core/samples/data/ser/custom/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include <cubos/core/log.hpp>
#include <cubos/core/memory/stream.hpp>

using cubos::core::memory::Stream;

/// [Include]
#include <cubos/core/data/ser/serializer.hpp>

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 <cubos/core/reflection/external/primitives.hpp>

using cubos::core::reflection::reflect;

MySerializer::MySerializer()
{
this->hook<int32_t>([](Serializer&, const Type&, const void* value) {
Stream::stdOut.print(*static_cast<const int32_t*>(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 MySerializer::decompose(const Type& type, const void* value)
{
if (type.has<ArrayTrait>())
{
const auto& arrayTrait = type.get<ArrayTrait>();

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<FieldsTrait>())
{
Stream::stdOut.put('{');
for (const auto& [field, fieldValue] : type.get<FieldsTrait>().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 <glm/vec3.hpp>

#include <cubos/core/reflection/external/glm.hpp>
#include <cubos/core/reflection/external/vector.hpp>

int main()
{
std::vector<glm::ivec3> 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]
55 changes: 55 additions & 0 deletions core/samples/data/ser/custom/page.md
Original file line number Diff line number Diff line change
@@ -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<glm::ivec3>`, 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
5 changes: 5 additions & 0 deletions core/samples/data/ser/page.md
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions core/src/cubos/core/data/ser/serializer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include <cubos/core/data/ser/serializer.hpp>
#include <cubos/core/log.hpp>
#include <cubos/core/reflection/type.hpp>

using cubos::core::data::Serializer;

bool Serializer::write(const reflection::Type& type, const void* value)

Check warning on line 7 in core/src/cubos/core/data/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.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/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.cpp#L9

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

Check warning on line 11 in core/src/cubos/core/data/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.cpp#L11

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

Check warning on line 14 in core/src/cubos/core/data/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.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/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.cpp#L17

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

Check warning on line 20 in core/src/cubos/core/data/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.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/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.cpp#L23

Added line #L23 was not covered by tests
}

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

Check warning on line 26 in core/src/cubos/core/data/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.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/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.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/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.cpp#L30

Added line #L30 was not covered by tests
}

mHooks.emplace(&type, hook);

Check warning on line 33 in core/src/cubos/core/data/ser/serializer.cpp

View check run for this annotation

Codecov / codecov/patch

core/src/cubos/core/data/ser/serializer.cpp#L33

Added line #L33 was not covered by tests
}
1 change: 1 addition & 0 deletions docs/pages/3_examples/1_core/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading