diff --git a/CHANGELOG.md b/CHANGELOG.md index c1faff1783..3752f9c5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Components to support rigid body rotation (#865, **@fallenatlas**). - Compute contact points and contact manifold for collision between boxes (#1243, **@fallenatlas**). - Handle body rotation on physics integration (#1242, **&fallenatlas**). +- Binary Serializer and Deserializer (#1306, **@RiscadoA**). ### Fixed diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 18389ac75b..6df7c58e68 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -67,8 +67,10 @@ set(CUBOS_CORE_SOURCE "src/data/ser/serializer.cpp" "src/data/ser/json.cpp" "src/data/ser/debug.cpp" + "src/data/ser/binary.cpp" "src/data/des/deserializer.cpp" "src/data/des/json.cpp" + "src/data/des/binary.cpp" "src/io/window.cpp" "src/io/cursor.cpp" diff --git a/core/include/cubos/core/data/des/binary.hpp b/core/include/cubos/core/data/des/binary.hpp new file mode 100644 index 0000000000..b3e5930577 --- /dev/null +++ b/core/include/cubos/core/data/des/binary.hpp @@ -0,0 +1,27 @@ +/// @file +/// @brief Class @ref cubos::core::data::BinaryDeserializer. +/// @ingroup core-data-des + +#pragma once + +#include +#include + +namespace cubos::core::data +{ + /// @brief Implementation of the abstract Deserializer class meant to deserialize data written by a @ref + /// BinarySerializer. + class CUBOS_CORE_API BinaryDeserializer : public Deserializer + { + public: + /// @brief Constructs. + /// @param stream Stream to read from. + BinaryDeserializer(memory::Stream& stream); + + protected: + bool decompose(const reflection::Type& type, void* value) override; + + private: + memory::Stream& mStream; ///< Stream to deserialize from. + }; +} // namespace cubos::core::data \ No newline at end of file diff --git a/core/include/cubos/core/data/ser/binary.hpp b/core/include/cubos/core/data/ser/binary.hpp new file mode 100644 index 0000000000..38aca59aa2 --- /dev/null +++ b/core/include/cubos/core/data/ser/binary.hpp @@ -0,0 +1,27 @@ +/// @file +/// @brief Class @ref cubos::core::data::BinarySerializer. +/// @ingroup core-data-ser + +#pragma once + +#include +#include + +namespace cubos::core::data +{ + /// @brief Implementation of the abstract Serializer class meant to serialize data in a non-readable but fast and + /// compact way. + class CUBOS_CORE_API BinarySerializer : public Serializer + { + public: + /// @brief Constructs. + /// @param stream Stream to write to. + BinarySerializer(memory::Stream& stream); + + protected: + bool decompose(const reflection::Type& type, const void* value) override; + + private: + memory::Stream& mStream; ///< Stream to serialize to. + }; +} // namespace cubos::core::data \ No newline at end of file diff --git a/core/src/data/des/binary.cpp b/core/src/data/des/binary.cpp new file mode 100644 index 0000000000..5316778db8 --- /dev/null +++ b/core/src/data/des/binary.cpp @@ -0,0 +1,184 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using cubos::core::data::BinaryDeserializer; +using cubos::core::memory::AnyValue; +using cubos::core::reflection::ArrayTrait; +using cubos::core::reflection::DictionaryTrait; +using cubos::core::reflection::EnumTrait; +using cubos::core::reflection::FieldsTrait; +using cubos::core::reflection::StringConversionTrait; +using cubos::core::reflection::Type; + +// NOLINTBEGIN(bugprone-macro-parentheses) +#define AUTO_HOOK(casted, type) \ + this->hook([this](type& value) { \ + casted castedValue; \ + if (!mStream.readExact(&castedValue, sizeof(castedValue))) \ + { \ + return false; \ + } \ + value = static_cast(memory::fromLittleEndian(castedValue)); \ + return true; \ + }) +// NOLINTEND(bugprone-macro-parentheses) + +BinaryDeserializer::BinaryDeserializer(memory::Stream& stream) + : mStream(stream) +{ + this->hook([this](bool& value) { + value = mStream.get() != '\0'; + return !mStream.eof(); + }); + this->hook([this](char& value) { return mStream.readExact(&value, 1); }); + + AUTO_HOOK(std::int8_t, signed char); + AUTO_HOOK(std::int16_t, short); + AUTO_HOOK(std::int32_t, int); + AUTO_HOOK(std::int64_t, long); + AUTO_HOOK(std::int64_t, long long); + + AUTO_HOOK(std::uint8_t, unsigned char); + AUTO_HOOK(std::uint16_t, unsigned short); + AUTO_HOOK(std::uint32_t, unsigned int); + AUTO_HOOK(std::uint64_t, unsigned long); + AUTO_HOOK(std::uint64_t, unsigned long long); + + AUTO_HOOK(float, float); + AUTO_HOOK(double, double); + + this->hook([this](std::string& value) { + std::size_t length; + if (!this->read(length)) + { + return false; + } + + value.resize(length); + return mStream.readExact(value.data(), length); + }); +} + +bool BinaryDeserializer::decompose(const Type& type, void* value) +{ + if (type.has()) + { + const auto& trait = type.get(); + auto view = trait.view(value); + view.clear(); + + std::size_t length; + if (!this->read(length)) + { + CUBOS_ERROR("Could not deserialize dictionary length"); + return false; + } + + auto key = AnyValue::defaultConstruct(trait.keyType()); + for (std::size_t i = 0; i < length; ++i) + { + if (!this->read(trait.keyType(), key.get())) + { + CUBOS_ERROR("Could not deserialize dictionary key"); + return false; + } + + view.insertDefault(key.get()); + auto entry = view.find(key.get()); + CUBOS_ASSERT(entry != view.end(), "We just inserted the entry, where did it go?"); + + if (!this->read(trait.valueType(), entry->value)) + { + CUBOS_ERROR("Could not deserialize dictionary value"); + return false; + } + } + } + else if (type.has()) + { + const auto& trait = type.get(); + auto view = trait.view(value); + + std::size_t length; + if (!this->read(length)) + { + CUBOS_ERROR("Could not deserialize array length"); + return false; + } + view.resize(length); + + for (auto* element : view) + { + if (!this->read(trait.elementType(), element)) + { + CUBOS_ERROR("Could not deserialize array element"); + return false; + } + } + } + else if (type.has()) + { + const auto& trait = type.get(); + + std::string name; + if (!this->read(name)) + { + CUBOS_ERROR("Could not deserialize enum"); + return false; + } + + if (!trait.contains(name)) + { + CUBOS_ERROR("Could not deserialize enum, no such variant '{}'", name); + return false; + } + + trait.at(name).set(value); + } + else if (type.has()) + { + const auto& trait = type.get(); + for (const auto& [field, fieldValue] : trait.view(value)) + { + if (!this->read(field->type(), fieldValue)) + { + CUBOS_ERROR("Could not deserialize field '{}'", field->name()); + return false; + } + } + } + else if (type.has()) + { + const auto& trait = type.get(); + + std::string string; + if (!this->read(string)) + { + CUBOS_ERROR("Could not deserialize string conversion"); + return false; + } + + if (!trait.from(value, string)) + { + CUBOS_ERROR("Could not deserialize string conversion, string '{}' is not a valid conversion", string); + return false; + } + } + else + { + CUBOS_WARN("Type {} doesn't have any of the supported traits", type.name()); + return false; + } + + return true; +} diff --git a/core/src/data/ser/binary.cpp b/core/src/data/ser/binary.cpp new file mode 100644 index 0000000000..8ba2ead5e4 --- /dev/null +++ b/core/src/data/ser/binary.cpp @@ -0,0 +1,143 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using cubos::core::data::BinarySerializer; +using cubos::core::reflection::ArrayTrait; +using cubos::core::reflection::DictionaryTrait; +using cubos::core::reflection::EnumTrait; +using cubos::core::reflection::FieldsTrait; +using cubos::core::reflection::StringConversionTrait; +using cubos::core::reflection::Type; + +#define AUTO_HOOK(casted, type) \ + this->hook([this](const type& value) { \ + auto castedValue = memory::toLittleEndian(static_cast(value)); \ + return mStream.writeExact(&castedValue, sizeof(castedValue)); \ + }) + +BinarySerializer::BinarySerializer(memory::Stream& stream) + : mStream(stream) +{ + this->hook([this](const bool& value) { return mStream.writeExact(value ? "\1" : "\0", 1); }); + this->hook([this](const char& value) { return mStream.writeExact(&value, 1); }); + + AUTO_HOOK(std::int8_t, signed char); + AUTO_HOOK(std::int16_t, short); + AUTO_HOOK(std::int32_t, int); + AUTO_HOOK(std::int64_t, long); + AUTO_HOOK(std::int64_t, long long); + + AUTO_HOOK(std::uint8_t, unsigned char); + AUTO_HOOK(std::uint16_t, unsigned short); + AUTO_HOOK(std::uint32_t, unsigned int); + AUTO_HOOK(std::uint64_t, unsigned long); + AUTO_HOOK(std::uint64_t, unsigned long long); + + AUTO_HOOK(float, float); + AUTO_HOOK(double, double); + + this->hook([this](const std::string& value) { + if (!this->write(value.length())) + { + return false; + } + + return mStream.writeExact(value.data(), value.length()); + }); +} + +bool BinarySerializer::decompose(const Type& type, const void* value) +{ + if (type.has()) + { + const auto& trait = type.get(); + auto view = trait.view(value); + + if (!this->write(view.length())) + { + CUBOS_WARN("Could not serialize dictionary length"); + return false; + } + + for (const auto& [entryKey, entryValue] : view) + { + if (!this->write(trait.keyType(), entryKey)) + { + CUBOS_WARN("Could not serialize dictionary key"); + return false; + } + + if (!this->write(trait.valueType(), entryValue)) + { + CUBOS_WARN("Could not serialize dictionary value"); + return false; + } + } + } + else if (type.has()) + { + const auto& trait = type.get(); + auto view = trait.view(value); + + if (!this->write(view.length())) + { + CUBOS_WARN("Could not serialize array length"); + return false; + } + + for (const auto* element : view) + { + if (!this->write(trait.elementType(), element)) + { + CUBOS_WARN("Could not serialize array element"); + return false; + } + } + } + else if (type.has()) + { + const auto& trait = type.get(); + if (!this->write(trait.variant(value).name())) + { + CUBOS_WARN("Could not serialize enum variant"); + return false; + } + } + else if (type.has()) + { + const auto& trait = type.get(); + for (const auto& [field, fieldValue] : trait.view(value)) + { + if (!this->write(field->type(), fieldValue)) + { + CUBOS_WARN("Could not serialize field '{}'", field->name()); + return false; + } + } + } + else if (type.has()) + { + const auto& trait = type.get(); + if (!this->write(trait.into(value))) + { + CUBOS_WARN("Could not serialize string conversion"); + return false; + } + } + else + { + CUBOS_WARN("Type {} doesn't have any of the supported traits", type.name()); + return false; + } + + return true; +} diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index 61d5a54e1e..756d0f4abb 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable( data/ser/debug.cpp data/ser/json.cpp data/des/json.cpp + data/serdes/binary.cpp memory/any_value.cpp memory/any_vector.cpp diff --git a/core/tests/data/serdes/binary.cpp b/core/tests/data/serdes/binary.cpp new file mode 100644 index 0000000000..93855e89f3 --- /dev/null +++ b/core/tests/data/serdes/binary.cpp @@ -0,0 +1,81 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../utils.hpp" + +using cubos::core::data::BinaryDeserializer; +using cubos::core::data::BinarySerializer; +using cubos::core::data::Serializer; +using cubos::core::memory::BufferStream; +using cubos::core::memory::SeekOrigin; +using cubos::core::reflection::Type; + +#define AUTO_TEST(type, value) \ + { \ + type result{}; \ + stream.seek(0, SeekOrigin::Begin); \ + CHECK(ser.write(value)); \ + stream.seek(0, SeekOrigin::Begin); \ + CHECK(des.read(result)); \ + CHECK_EQ(value, result); \ + } + +#define AUTO_TEST_IMPL(...) AUTO_TEST(decltype(__VA_ARGS__), (__VA_ARGS__)) + +namespace +{ + enum class Color + { + Red, + Green, + Blue + }; +} // namespace + +CUBOS_REFLECT_EXTERNAL_DECL(CUBOS_EMPTY, Color); +CUBOS_REFLECT_EXTERNAL_IMPL(Color) +{ + return Type::create("Color").with( + EnumTrait{}.withVariant("Red").withVariant("Green").withVariant("Blue")); +} + +TEST_CASE("data::Binary(De)Serializer") +{ + BufferStream stream{}; + BinarySerializer ser{stream}; + BinaryDeserializer des{stream}; + + AUTO_TEST(bool, false); + AUTO_TEST(bool, true); + AUTO_TEST(char, 'a'); + AUTO_TEST(int8_t, -120); + AUTO_TEST(int16_t, 30000); + AUTO_TEST(int32_t, -2000000000); + AUTO_TEST(int64_t, 9000000000000000000); + AUTO_TEST(uint8_t, 120); + AUTO_TEST(uint16_t, 60000); + AUTO_TEST(uint32_t, 4000000000); + AUTO_TEST(uint64_t, 18000000000000000000ULL); + AUTO_TEST(float, 1.5F); + AUTO_TEST(double, 1.0); + AUTO_TEST(std::string, "Hello, world!"); + AUTO_TEST(uuids::uuid, *uuids::uuid::from_string("f7063cd4-de44-47b5-b0a9-f0e7a558c9e5")); + + AUTO_TEST_IMPL(std::map{{"false", false}, {"true", true}}); + AUTO_TEST_IMPL(std::vector{1, 2, 3}); + AUTO_TEST_IMPL(glm::tvec3{1, 2, 3}); + AUTO_TEST_IMPL(Color::Red); + AUTO_TEST_IMPL(Color::Green); + AUTO_TEST_IMPL(Color::Blue); +}