| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Utility functions for stringifying enum values. NOT PORTABLE C++. Do NOT use |
| // for implementing production features! Only for logging. Tested in GCC and |
| // Clang. |
| |
| #ifndef SERVICES_NETWORK_STRINGIFY_ENUM_H_ |
| #define SERVICES_NETWORK_STRINGIFY_ENUM_H_ |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <array> |
| #include <limits> |
| #include <ostream> |
| #include <string_view> |
| #include <type_traits> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/compiler_specific.h" |
| #include "base/strings/string_util.h" |
| #include "base/types/cxx23_to_underlying.h" |
| |
| namespace network { |
| |
| namespace internal { |
| |
| // MaxValue() finds the max value of an enum, compensating for the variety of |
| // conventions used in Chromium. Returns the maximum size_t value if no max |
| // could be found. May also return a ridiculously large value if the max is |
| // actually negative, but by limiting the maximum value we accept we can protect |
| // ourselves and avoid generating an enormous table. |
| template <typename Enum> |
| consteval size_t MaxValue() { |
| if constexpr (requires { Enum::kMaxValue; }) { |
| // The convention used by mojo. |
| return static_cast<size_t>(Enum::kMaxValue); |
| } else if constexpr (requires { Enum::MAX; }) { |
| // Used in //net |
| return static_cast<size_t>(Enum::MAX); |
| } else if constexpr (requires { Enum::kLast; }) { |
| // Used in a few places. |
| return static_cast<size_t>(Enum::kLast); |
| } else if constexpr (requires { Enum::kMax; }) { |
| // Somewhat common. |
| return static_cast<size_t>(Enum::kMax); |
| } else if constexpr (requires { Enum::kCount; }) { |
| // Popular because the compiler will assign it for you. |
| return static_cast<size_t>(Enum::kCount) - 1; |
| } else if constexpr (requires { Enum::COUNT; }) { |
| // Historically popular. |
| return static_cast<size_t>(Enum::COUNT) - 1; |
| } else { |
| // The maximum value for this enum could not be determined. Give up. |
| return std::numeric_limits<size_t>::max(); |
| } |
| } |
| |
| // Returns the name of the compile-time-constant enum value |
| // `uniquely_named_value` as a std::string_view. If `Enum` is a class enum type, |
| // then the returned name will include the enum type, ie. "EnumType::kValue", |
| // otherwise it will just include the value, matching normal usage. Works by |
| // parsing the name of the function as supplied by the compiler. This is the |
| // non-portable part. Returns a string_view which references into the function |
| // name. The referenced string can be extremely long, so should ideally not be |
| // referenced in the binary. |
| template <typename Enum, Enum uniquely_named_value> |
| consteval std::string_view GetEnumName() { |
| constexpr std::string_view kMatchString = "uniquely_named_value = "; |
| constexpr std::string_view kNamespaceSeparator = "::"; |
| // `PRETTY_FUNCTION` on GCC returns a string like |
| // "constexpr std::string_view GetEnumName() [with Enum = |
| // network::{anonymous}::EnumName; Enum uniquely_named_value = |
| // network::<unnamed>::EnumName::kValue; std::string_view = |
| // std::basic_string_view<char>]" |
| // |
| // Clang returns a string like |
| // "std::string_view GetEnumName() [Enum = network::(anonymous |
| // namespace)::EnumName, uniquely_named_value = network::(anonymous |
| // namespace)::EnumName::kValue]" |
| // |
| // Both GCC and Clang don't include "EnumName::" before "kValue" when it is |
| // not a class enum. |
| // |
| // TODO(crbug.com/373461931): MSVC returns a string like |
| // "class std::basic_string_view<char,struct std::char_traits<char> > __cdecl |
| // GetEnumName<enum |
| // network::`anonymous-namespace'::EnumName,network::`anonymous-namespace'::EnumName::kValue>(void)" |
| // Add support to parse this format if needed. |
| constexpr std::string_view kPrettyFunction = PRETTY_FUNCTION; |
| const size_t match_pos = kPrettyFunction.find(kMatchString); |
| if (match_pos == std::string_view::npos) { |
| // Compiler is not clang or GCC, or they have changed their |
| // PRETTY_FUNCTION format. |
| return {}; |
| } |
| const auto is_cpp_identifier_char = [](char c) { |
| // "()<> " are included for anonymous namespace support. |
| return base::IsAsciiAlphaNumeric(c) || c == '_' || c == ':' || c == '(' || |
| c == ')' || c == '<' || c == '>' || c == ' '; |
| }; |
| const std::string_view remaining_chars = |
| kPrettyFunction.substr(match_pos + kMatchString.size()); |
| |
| const auto end_pos = |
| std::ranges::find_if_not(remaining_chars, is_cpp_identifier_char); |
| |
| const std::string_view including_namespaces = |
| remaining_chars.substr(0, end_pos - remaining_chars.begin()); |
| const size_t final_namespace_separator_pos = |
| including_namespaces.rfind(kNamespaceSeparator); |
| if (final_namespace_separator_pos == std::string_view::npos) { |
| // No namespace after all. |
| return including_namespaces; |
| } |
| |
| std::string_view value_name = including_namespaces.substr( |
| final_namespace_separator_pos + kNamespaceSeparator.size()); |
| |
| // If the compiler didn't have a name for this value, then we'd prefer to |
| // return nothing. In this case, GCC outputs something like |
| // "(network::<unnamed>::EnumName)1" and Clang outputs something like |
| // "(network::(anonymous namespace)::EnumName)1". We can detect both formats |
| // by seeing if the string ends in ")\d+". |
| if (const size_t final_closing_paren = value_name.rfind(')'); |
| final_closing_paren != std::string_view::npos) { |
| const std::string_view after_final_paren = |
| value_name.substr(final_closing_paren + 1); |
| if (std::ranges::all_of(after_final_paren, base::IsAsciiDigit<char>)) { |
| return {}; |
| } |
| } |
| |
| if constexpr (!std::is_convertible_v<Enum, std::underlying_type_t<Enum>>) { |
| // This is a class enum. Include the name of the enum in the output. |
| if (final_namespace_separator_pos > 0) { |
| const size_t previous_namespace_separator_pos = |
| including_namespaces.rfind(kNamespaceSeparator, |
| final_namespace_separator_pos - 1); |
| if (previous_namespace_separator_pos != std::string_view::npos) { |
| value_name = including_namespaces.substr( |
| previous_namespace_separator_pos + kNamespaceSeparator.size()); |
| } |
| } |
| } |
| return value_name; |
| } |
| |
| // Copies a string view into an array so that the original backing string does |
| // not have to be retained in the binary. Does not include a trailing '\0'. |
| template <size_t size> |
| class ExtractStringView { |
| public: |
| // `view.size()` must match `size`. |
| consteval explicit ExtractStringView(std::string_view view) { |
| CHECK(view.size() == size); |
| std::ranges::copy(view, storage_.begin()); |
| } |
| |
| // Returns the stored string as a string view. |
| constexpr std::string_view AsStringView() const { |
| return std::string_view(storage_.data(), storage_.size()); |
| } |
| |
| private: |
| std::array<char, size> storage_; |
| }; |
| |
| // Storage for an array of value names for `Enum`, with `size` entries. |
| template <typename Enum, size_t size = MaxValue<Enum>() + 1> |
| class EnumTable final { |
| public: |
| constexpr EnumTable() = default; |
| |
| // Copy and assignment shouldn't be needed. |
| EnumTable(const EnumTable&) = delete; |
| EnumTable& operator=(const EnumTable&) = delete; |
| |
| constexpr std::string_view GetNameForValue(Enum value) const { |
| const size_t n = static_cast<size_t>(value); |
| return n < size ? storage_[n] : std::string_view(); |
| } |
| |
| private: |
| static constexpr auto kIndexSequence = std::make_index_sequence<size>(); |
| |
| template <size_t index> |
| static constexpr std::string_view ValueName() { |
| return GetEnumName<Enum, static_cast<Enum>(index)>(); |
| } |
| |
| // Constructs a tuple of enum value names from `Enum` consisting of one |
| // ExtractStringView object per `index`. |
| template <size_t... index> |
| static constexpr auto MakeExtractStringViewTuple( |
| std::index_sequence<index...>) { |
| return std::make_tuple( |
| ExtractStringView<ValueName<index>().size()>(ValueName<index>())...); |
| } |
| |
| // The return type of MakeExtractStringViewTuple(). |
| using TupleType = decltype(MakeExtractStringViewTuple(kIndexSequence)); |
| |
| // Given a tuple created by MakeExtractStringViewTuple(), returns an array of |
| // one string_view per tuple entry. The value at index 0 of the array is the |
| // name of the enum entry with value 0, etc. |
| template <size_t... index> |
| constexpr std::array<std::string_view, size> MakeStringViewArrayFromTuple( |
| const TupleType& tuple, |
| std::index_sequence<index...>) { |
| return {std::get<index>(tuple).AsStringView()...}; |
| } |
| |
| TupleType storage_tuple_ = MakeExtractStringViewTuple(kIndexSequence); |
| std::array<std::string_view, size> storage_ = |
| MakeStringViewArrayFromTuple(storage_tuple_, kIndexSequence); |
| }; |
| |
| // Precalculated storage for arrays of value names. Accessing |
| // kEnumTable<SomeEnum> will cause the value names to be calculated for SomeEnum |
| // at compile time and stored in the binary. |
| template <typename Enum> |
| inline constexpr EnumTable<Enum> kEnumTable; |
| |
| namespace enum_stream_concepts { |
| |
| // A non-class enum is implicitly convertible to an integer type, and so will |
| // use an integer stream operator by default. It is difficult to distinguish |
| // between a use of the integer stream operator and a user-defined stream |
| // operator, so we override all the integer stream operators inside this |
| // namespace. |
| |
| // A dummy type to indicate that we shouldn't use a streaming operator. |
| struct UnwantedConversion {}; |
| |
| // Fake definitions for all the integer operators defined by the C++ standard, |
| // allowing us to detect if one of these would be used. These need to be inside |
| // their own namespace so they are only seen by the concepts here. These should |
| // match exactly the types specified by the standard. This includes types not |
| // normally permitted in Chromium code, so they need "NOLINT" annotations. |
| UnwantedConversion operator<<(std::ostream&, char); |
| UnwantedConversion operator<<(std::ostream&, signed char); // NOLINT |
| UnwantedConversion operator<<(std::ostream&, unsigned char); // NOLINT |
| UnwantedConversion operator<<(std::ostream&, short value); // NOLINT |
| UnwantedConversion operator<<(std::ostream&, unsigned short value); // NOLINT |
| UnwantedConversion operator<<(std::ostream&, int value); |
| UnwantedConversion operator<<(std::ostream&, unsigned int value); // NOLINT |
| UnwantedConversion operator<<(std::ostream&, long value); // NOLINT |
| UnwantedConversion operator<<(std::ostream&, unsigned long value); // NOLINT |
| UnwantedConversion operator<<(std::ostream&, long long value); // NOLINT |
| // NOLINTNEXTLINE |
| UnwantedConversion operator<<(std::ostream&, unsigned long long value); |
| |
| template <typename T> |
| concept OutputStreamable = |
| requires(std::ostream& os, T&& t) { os << std::forward<T>(t); }; |
| |
| template <typename T> |
| concept StreamOperatorIsUnwanted = requires(std::ostream& os, T t) { |
| { os << t } -> std::same_as<UnwantedConversion>; |
| }; |
| |
| } // namespace enum_stream_concepts |
| |
| template <typename T> |
| concept EnumHasUserDefinedStreamOperator = |
| std::is_enum_v<T> && enum_stream_concepts::OutputStreamable<T> && |
| !enum_stream_concepts::StreamOperatorIsUnwanted<T>; |
| |
| // To avoid accidentally creating large tables for sparse enums, avoid |
| // stringifying any enum where the maximum value is too large. |
| inline constexpr size_t kMaxMaxValue = 50; |
| |
| } // namespace internal |
| |
| // Returns the name of the enum entry `value` as it appears in the source code. |
| // Returns an empty string if the name could not be determined. A side-effect of |
| // calling this function is that a table of the names of all the enum entries |
| // for this enum will be generated and stored in the binary. |
| // |
| // Relies on non-standard C++ features, but should fail gracefully (ie. return |
| // an empty string) on other compilers. |
| template <typename Enum> |
| constexpr std::string_view GetEnumValueName(Enum value) |
| requires(std::is_enum_v<Enum> && |
| internal::MaxValue<Enum>() <= internal::kMaxMaxValue) |
| { |
| return internal::kEnumTable<Enum>.GetNameForValue(value); |
| } |
| |
| // As a convenience for generic code, this version just returns an empty |
| // string_view if the requirements of GetEnumValueName() are not met. |
| template <typename Enum> |
| constexpr std::string_view GetEnumValueNameForGenericCode(Enum value) { |
| if constexpr (requires { GetEnumValueName(value); }) { |
| return GetEnumValueName(value); |
| } else { |
| return {}; |
| } |
| } |
| |
| // Streams the stringification of `value` to `os`. It has a couple of extra |
| // features: |
| // 1. It will use an existing operator<< for the enum if one exists, avoiding |
| // generating the table and wasting binary space. |
| // 2. It will output "Unknown (n)" where the name of `value` could not be |
| // determined, where "n" is `value` as a number. |
| template <typename Enum> |
| requires(std::is_enum_v<std::remove_cvref_t<Enum>>) |
| void StreamEnumValueTo(std::ostream& os, Enum&& value) { |
| // Require "operator<<" to be specialized for "Enum" so we don't accidentally |
| // trigger the implicit conversion to int for non-class enums. |
| if constexpr (internal::EnumHasUserDefinedStreamOperator< |
| std::remove_cvref_t<Enum>>) { |
| os << std::forward<Enum>(value); |
| } else { |
| const std::string_view name = GetEnumValueNameForGenericCode(value); |
| if (name.empty()) { |
| os << "Unknown (" << base::to_underlying(value) << ")"; |
| } else { |
| os << name; |
| } |
| } |
| } |
| |
| } // namespace network |
| |
| #endif // SERVICES_NETWORK_STRINGIFY_ENUM_H_ |