diff --git a/score/static_reflection_with_serialization/serialization/include/serialization/visit_serialize.h b/score/static_reflection_with_serialization/serialization/include/serialization/visit_serialize.h index cfdd225988..b444b6eeef 100644 --- a/score/static_reflection_with_serialization/serialization/include/serialization/visit_serialize.h +++ b/score/static_reflection_with_serialization/serialization/include/serialization/visit_serialize.h @@ -80,11 +80,65 @@ struct vector struct optional { }; +// Distinct tag for enum-typed fields whose enumerator name table is known via a specialization +// of ::score::common::visitor::EnumTraits. Carries the same wire-level integer encoding as +// signed_le / unsigned_le (see enum_serialized_descriptor::wire_tag) but lets downstream +// consumers (e.g. FIBEX generation) detect "this is a named enum" instead of falling through to +// the generic integral overloads. +struct enum_le +{ +}; } // namespace payload_tags +// One value <-> name mapping of an enumerator, as declared in the originating proto `enum`. +struct Enumerator +{ + std::int64_t value; + const char* name; +}; + +// Per-enum-type metadata carrier, used by the serialization visitor and, downstream, by FIBEX +// generation to recover the enumerator name<->value table that would otherwise be lost once the +// enum is folded into a plain integral wire type. +// +// The primary (unspecialized) template intentionally carries no names: any C++ enum works with +// the existing signed/unsigned integral serialization path unless a specialization of this trait +// is provided for it. Code generators are expected to emit a specialization for every proto +// `enum` they generate, reusing the enumerator list they already have in memory during codegen. +// This is a compile-time-only, zero-runtime-cost side channel: it does not alter the wire format +// of the enum itself. +template +struct EnumTraits +{ + static constexpr bool kHasNames = false; +}; + namespace details { +/// \brief Compile-time predicate for "is T an enum with a signed underlying type?". +/// +/// std::is_signed is only meaningful for arithmetic types and is always false for enum types +/// (enums are not arithmetic types per the standard), even when their underlying type is signed. +/// This trait instead inspects the enum's underlying type, and is only instantiated for enum +/// types (via the bool non-type template parameter) so it stays safe to use unconditionally in +/// enable_if_t expressions alongside is_enum for non-enum T. +/// \tparam T Candidate type; only its is_enum-ness is inspected here (this primary/false-type +/// template is selected for all non-enum T). +template ::value> +struct is_signed_enum : std::false_type +{ +}; + +/// \brief Specialization selected for enum types: inherits from +/// std::is_signed>, i.e. ::value is true iff T's underlying +/// integer type is signed. +/// \tparam T Enum type to inspect. +template +struct is_signed_enum : std::is_signed::type> +{ +}; + template auto cast_to_source_serializable_data_span(const T* data, size_t size) -> score::cpp::span { @@ -972,6 +1026,21 @@ struct memcpy_serialized_descriptor using payload_type = memcpy_serialized; }; +// Descriptor for enum-typed fields whose enumerator names are known via EnumTraits. Keeps the +// same on-wire integer representation as memcpy_serialized_descriptor (Tag is signed_le or +// unsigned_le, matching the enum's underlying signedness) but exposes the original enum type so +// that downstream consumers (e.g. the FIBEX generator's visitor) can look up EnumTraits and +// recover the enumerator name<->value table. This does not change the serialized byte layout. +template +// coverity[autosar_cpp14_a11_0_2_violation] +struct enum_serialized_descriptor +{ + using payload_tag = payload_tags::enum_le; + using payload_type = memcpy_serialized; + using wire_tag = Tag; + using enum_type = T; +}; + template ::value) && (std::is_signed::value), std::int32_t> = 0> @@ -1003,9 +1072,34 @@ inline auto visit_as(serialized_visitor& /*unused*/, T& /*unused*/) return memcpy_serialized_descriptor(); } -template ::value) && (std::is_signed::value), std::int32_t> = 0> +template < + typename A, + typename T, + std::enable_if_t<(std::is_enum::value) && (details::is_signed_enum::value) && (EnumTraits::kHasNames), + std::int32_t> = 0> +/// \brief Dispatches a signed enum with a registered EnumTraits specialization to the +/// enum-aware descriptor. +/// \param T Enum type with a signed underlying type and EnumTraits::kHasNames == true. +/// \return enum_serialized_descriptor, i.e. the same on-wire +/// signed integer representation as memcpy_serialized_descriptor, but additionally +/// exposing T so that EnumTraits can be looked up by downstream consumers. +// This is false positive, Overload signatures are different. +// coverity[autosar_cpp14_m3_2_3_violation : FALSE] +// coverity[autosar_cpp14_a2_10_4_violation : FALSE] +inline auto visit_as(serialized_visitor& /*unused*/, T& /*unused*/) +{ + return enum_serialized_descriptor(); +} + +template < + typename A, + typename T, + std::enable_if_t<(std::is_enum::value) && (details::is_signed_enum::value) && (!EnumTraits::kHasNames), + std::int32_t> = 0> +/// \brief Dispatches a signed enum without a registered EnumTraits specialization to the +/// plain integral descriptor (previous behavior, unchanged). +/// \param T Enum type with a signed underlying type and EnumTraits::kHasNames == false. +/// \return memcpy_serialized_descriptor. // This is false positive, Overload signatures are different. // coverity[autosar_cpp14_m3_2_3_violation : FALSE] // coverity[autosar_cpp14_a2_10_4_violation : FALSE] @@ -1014,9 +1108,34 @@ inline auto visit_as(serialized_visitor& /*unused*/, T& /*unused*/) return memcpy_serialized_descriptor(); } -template ::value) && (!std::is_signed::value), std::int32_t> = 0> +template < + typename A, + typename T, + std::enable_if_t<(std::is_enum::value) && (!details::is_signed_enum::value) && (EnumTraits::kHasNames), + std::int32_t> = 0> +/// \brief Dispatches an unsigned enum with a registered EnumTraits specialization to the +/// enum-aware descriptor. +/// \param T Enum type with an unsigned underlying type and EnumTraits::kHasNames == true. +/// \return enum_serialized_descriptor, i.e. the same on-wire +/// unsigned integer representation as memcpy_serialized_descriptor, but additionally +/// exposing T so that EnumTraits can be looked up by downstream consumers. +// This is false positive, Overload signatures are different. +// coverity[autosar_cpp14_m3_2_3_violation : FALSE] +// coverity[autosar_cpp14_a2_10_4_violation : FALSE] +inline auto visit_as(serialized_visitor& /*unused*/, T& /*unused*/) +{ + return enum_serialized_descriptor(); +} + +template < + typename A, + typename T, + std::enable_if_t<(std::is_enum::value) && (!details::is_signed_enum::value) && (!EnumTraits::kHasNames), + std::int32_t> = 0> +/// \brief Dispatches an unsigned enum without a registered EnumTraits specialization to the +/// plain integral descriptor (previous behavior, unchanged). +/// \param T Enum type with an unsigned underlying type and EnumTraits::kHasNames == false. +/// \return memcpy_serialized_descriptor. // This is false positive, Overload signatures are different. // coverity[autosar_cpp14_m3_2_3_violation : FALSE] // coverity[autosar_cpp14_a2_10_4_violation : FALSE] diff --git a/score/static_reflection_with_serialization/serialization/test/ut/test_serializer_visitor.cpp b/score/static_reflection_with_serialization/serialization/test/ut/test_serializer_visitor.cpp index 456eab5e3f..2e59855552 100644 --- a/score/static_reflection_with_serialization/serialization/test/ut/test_serializer_visitor.cpp +++ b/score/static_reflection_with_serialization/serialization/test/ut/test_serializer_visitor.cpp @@ -971,4 +971,66 @@ TEST(clear_functionality_test, test_that_clear_function_can_clear_vector_of_int3 score::common::visitor::detail::clear(vector_wrapper_instance); } +// Enum used to verify that registering ::score::common::visitor::EnumTraits only adds +// compile-time enumerator name metadata and leaves the serialized wire representation unchanged. +enum class EnumWithNames : std::int32_t +{ + kFirst = 0, + kSecond = 1, +}; + } // namespace test + +template <> +struct score::common::visitor::EnumTraits +{ + static constexpr bool kHasNames = true; + // Enumerator name<->value table, as a real code generator would emit it, so the test can + // verify that names are actually retrievable through the specialized trait (not just that + // kHasNames is set). + static constexpr std::array kEnumerators = {{ + {static_cast(test::EnumWithNames::kFirst), "kFirst"}, + {static_cast(test::EnumWithNames::kSecond), "kSecond"}, + }}; +}; + +TEST(serializer_visitor, named_enum_wire_format_unchanged) +{ + RecordProperty("ParentRequirement", "SCR-1633893"); + RecordProperty("ASIL", "B"); + RecordProperty("Description", + "Check that registering EnumTraits for an enum type only adds compile-time " + "enumerator name metadata and does not alter its serialized wire representation."); + RecordProperty("TestingTechnique", "Requirements-based test"); + RecordProperty("DerivationTechnique", "requirements-analysis"); // requirements + + static_assert(::score::common::visitor::EnumTraits::kHasNames, + "EnumTraits::kHasNames must be true once specialized"); + static_assert(!::score::common::visitor::EnumTraits::kHasNames, + "EnumTraits::kHasNames must default to false for enums without a registered trait"); + + // Wire format (serialized size) must stay identical to the plain integral representation. + EXPECT_EQ(check_serialized(), sizeof(test::EnumWithNames)); + + using Descriptor = decltype(::score::common::visitor::visit_as( + std::declval<::score::common::visitor::serialized_visitor&>(), std::declval())); + static_assert( + std::is_same::value, + "an enum with a registered EnumTraits specialization must dispatch to the enum_le payload tag"); + static_assert(std::is_same::value, + "enum_serialized_descriptor must expose the original enum type"); + // test::EnumWithNames has a signed underlying type (std::int32_t): the wire tag must be + // signed_le, not unsigned_le (regression check for is_signed being applied to the enum type + // itself rather than its underlying type). + static_assert(std::is_same::value, + "a named enum with a signed underlying type must use the signed_le wire tag"); + + // The whole point of EnumTraits is to make the enumerator name<->value table retrievable + // through the trait, so verify the actual entries, not just that kHasNames is set. + const auto& enumerators = ::score::common::visitor::EnumTraits::kEnumerators; + ASSERT_EQ(enumerators.size(), 2U); + EXPECT_EQ(enumerators[0].value, static_cast(test::EnumWithNames::kFirst)); + EXPECT_STREQ(enumerators[0].name, "kFirst"); + EXPECT_EQ(enumerators[1].value, static_cast(test::EnumWithNames::kSecond)); + EXPECT_STREQ(enumerators[1].name, "kSecond"); +}