From cda7af431313bc8d79e0a4d4b89e9ec0e47f2f43 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Fri, 11 Sep 2026 21:12:47 +0100 Subject: [PATCH 1/6] Clean up comments and eradicate unused noexcept from fn_ptr_t From 04aa3daac9fff2805a5db0c5a3e49d90282d8b24 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Fri, 11 Sep 2026 21:46:20 +0100 Subject: [PATCH 2/6] Split detail out of protocol header From f91b5c9c87f9acead9830ca8f9eceab63b31e9b3 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Fri, 11 Sep 2026 22:37:21 +0100 Subject: [PATCH 3/6] Add support for operator[] From ab7cc948a6380e2dcb66eca49e3527ec04bb7068 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Fri, 11 Sep 2026 23:00:52 +0100 Subject: [PATCH 4/6] Add operator -> and operator * From cf8ef9e679cc358de51cfdd81c8d75a57d04d939 Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Sat, 12 Sep 2026 00:02:31 +0100 Subject: [PATCH 5/6] Code cleanup (remove unusable args) and add missing tests for mangling From e4fec91e1f804ae6bc06ba83ba7e89b9344bff2b Mon Sep 17 00:00:00 2001 From: "Jonathan B. Coe" Date: Fri, 11 Sep 2026 20:30:01 -0600 Subject: [PATCH 6/6] Split detail into logical parts --- BUILD.bazel | 10 +- CMakeLists.txt | 8 +- conformance.hh | 205 ++++++++++ detail.hh | 838 -------------------------------------- member_function_thunks.hh | 150 +++++++ operator_thunks.hh | 269 ++++++++++++ overload_spec.hh | 38 ++ protocol.hh | 42 +- protocol_traits.hh | 68 ++++ protocol_wrappers.hh | 198 +++++++++ vtable.hh | 173 ++++++++ 11 files changed, 1123 insertions(+), 876 deletions(-) create mode 100644 conformance.hh delete mode 100644 detail.hh create mode 100644 member_function_thunks.hh create mode 100644 operator_thunks.hh create mode 100644 overload_spec.hh create mode 100644 protocol_traits.hh create mode 100644 protocol_wrappers.hh create mode 100644 vtable.hh diff --git a/BUILD.bazel b/BUILD.bazel index 84b8693e..960c45f2 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -9,7 +9,15 @@ cc_library( cc_library( name = "protocol", - srcs = ["detail.hh"], + srcs = [ + "conformance.hh", + "member_function_thunks.hh", + "operator_thunks.hh", + "overload_spec.hh", + "protocol_traits.hh", + "protocol_wrappers.hh", + "vtable.hh", + ], hdrs = ["protocol.hh"], deps = [ ":name_mangling", diff --git a/CMakeLists.txt b/CMakeLists.txt index 52c8c924..b5c7f1d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,7 +132,13 @@ target_sources( "${CMAKE_CURRENT_SOURCE_DIR}" FILES protocol.hh - detail.hh + conformance.hh + member_function_thunks.hh + operator_thunks.hh + overload_spec.hh + protocol_traits.hh + protocol_wrappers.hh + vtable.hh name_mangling.h) # Consumers need the reflection flags of whichever compiler they build with, # so the options are generator expressions evaluated at their configure time diff --git a/conformance.hh b/conformance.hh new file mode 100644 index 00000000..c724b6c7 --- /dev/null +++ b/conformance.hh @@ -0,0 +1,205 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +#ifndef XYZ_PROTOCOL_CONFORMANCE_HH_ +#define XYZ_PROTOCOL_CONFORMANCE_HH_ + +#include +#include +#include +#include +#include +#include + +namespace xyz::detail { + +template +consteval bool is_operator(std::meta::info function) { + return is_operator_function(function) && operator_of(function) == Operator; +} + +// Returns `true` if `a` and `b` are the same operators or have the same +// identifier. Otherwise returns false. +consteval bool same_name(std::meta::info a, std::meta::info b) { + if (has_identifier(a) && has_identifier(b)) { + return identifier_of(a) == identifier_of(b); + } + if (is_operator_function(a) && is_operator_function(b)) { + return operator_of(a) == operator_of(b); + } + return false; +} + +// Returns `true` if the member functions `candidate` and `interface` have +// the same name, de-aliased return type and de-aliased parameter types. +consteval bool same_name_and_parameters(std::meta::info candidate, + std::meta::info interface) { + if (!same_name(candidate, interface)) return false; + if (dealias(return_type_of(interface)) != dealias(return_type_of(candidate))) + return false; + auto dealiased_type_of = [](std::meta::info parameter) { + return dealias(type_of(parameter)); + }; + return std::ranges::equal(parameters_of(interface), parameters_of(candidate), + {}, dealiased_type_of, dealiased_type_of); +} + +// Returns `true` if the member functions `candidate` and `interface` have +// the same name, reference qualifiers, de-aliased return type and de-aliased +// parameter types; const and noexcept are not compared. +consteval bool same_signature_ignoring_const(std::meta::info candidate, + std::meta::info interface) { + if (is_lvalue_reference_qualified(interface) != + is_lvalue_reference_qualified(candidate)) + return false; + if (is_rvalue_reference_qualified(interface) != + is_rvalue_reference_qualified(candidate)) + return false; + return same_name_and_parameters(candidate, interface); +} + +// Returns `true` if `candidate is a function with an explicit object parameter, +// the member functions `candidate` and `interface` have the +// same name, de-aliased return type and parameter types, and const / reference +// qualification. +consteval bool same_function_with_explicit_object(std::meta::info candidate, + std::meta::info interface) { + if (!same_name(candidate, interface)) return false; + if (dealias(return_type_of(candidate)) != dealias(return_type_of(interface))) + return false; + + const auto params = parameters_of(candidate); + if (params.empty() || !is_explicit_object_parameter(params.front())) + return false; + + if (is_const(interface) != + is_const(remove_reference(type_of(params.front())))) + return false; + + return std::ranges::equal(parameters_of(interface), + params | std::views::drop(1), {}, + std::meta::type_of, std::meta::type_of); +} + +// Returns `true` if the `candidate` member function is consistent with the +// `interface` member function for the purposes of structural subtyping; +// otherwise returns `false`. +consteval bool member_function_conforms_to(std::meta::info candidate, + std::meta::info interface) { + if (is_static_member(candidate)) { + // A static candidate has no object parameter, so it satisfies any const + // or reference qualification of `interface`. + if (!same_name_and_parameters(candidate, interface)) return false; + } else if (same_function_with_explicit_object(candidate, interface)) { + // No additional conditions. + } else { + if (!same_signature_ignoring_const(candidate, interface)) return false; + // `const` qualifiers must match. + if (is_const(interface) != is_const(candidate)) return false; + } + // If interface is `noexcept`, `candidate` must be noexcept. + return !is_noexcept(interface) || is_noexcept(candidate); +} + +// Per ISO C++ ([expr.prim.lambda.closure]), closure types are unique, unnamed, +// non-union class types. +// The closure type is not an aggregate type. +// This concept will also match an unnamed class type with a single +// `operator()`. +// +// In practice a lambda has no base classes and no template arguments, although +// there is no wording in the standard to guarantee this. +// +// TODO(jbcoe): Refine this concept to match only lambdas. +template +concept is_maybe_lambda = + is_class_type(dealias(^^T)) && !has_identifier(dealias(^^T)) && + !is_aggregate_type(dealias(^^T)) && !has_template_arguments(dealias(^^T)) && + bases_of(dealias(^^T), std::meta::access_context::unprivileged()).empty() && + requires { &T::operator(); }; + +// The named, non-special member functions and call operators of `Type`, +// static or not, in declaration order. +template +consteval auto conformance_candidate_infos() { + auto named = + members_of(Type, std::meta::access_context::unprivileged()) | + std::views::filter(std::meta::is_function) | + std::views::filter([](std::meta::info member) consteval { + return has_identifier(member) || + is_operator(member) || + is_operator(member) || + is_operator(member) || + is_operator(member); + }); + std::vector result(std::ranges::begin(named), + std::ranges::end(named)); + // GCC Workaround: Per [meta.reflection.member.queries], a closure type's + // function call operator is members-of-eligible, but GCC16's `members_of` + // does not yet enumerate it, leaving `result` empty for lambdas; name the + // operator directly as a fallback. + using T = typename[:Type:]; + if constexpr (is_maybe_lambda) { + if (result.empty()) result.push_back(^^T::operator()); + } + return result; +} + +template +constexpr inline auto conformance_candidates_of = + std::define_static_array(conformance_candidate_infos()); + +// The named, non-static, non-special member functions and call operators of +// `Type`, static or not, in declaration order. Ref-qualified functions are +// unsupported on protocol interfaces. +template +consteval std::vector protocol_interface_function_infos() { + std::vector result; + for (std::meta::info member : conformance_candidates_of) { + if (is_static_member(member)) continue; + if (is_lvalue_reference_qualified(member) || + is_rvalue_reference_qualified(member)) { + std::string name = has_identifier(member) + ? std::string(identifier_of(member)) + : std::string(display_string_of(member)); + throw std::runtime_error("ref-qualified member function '" + name + + "' is not supported in a protocol interface"); + } + result.push_back(member); + } + return result; +} + +template +constexpr inline auto protocol_interface_functions_of = + std::define_static_array(protocol_interface_function_infos()); + +// Finds the member of `CandidateType` that structurally conforms to +// `Member`. +template +consteval std::meta::info find_conforming_member() { + for (std::meta::info candidate : conformance_candidates_of) { + if (member_function_conforms_to(candidate, Member)) return candidate; + } + std::unreachable(); +} + +} // namespace xyz::detail + +#endif // XYZ_PROTOCOL_CONFORMANCE_HH_ diff --git a/detail.hh b/detail.hh deleted file mode 100644 index 009247e8..00000000 --- a/detail.hh +++ /dev/null @@ -1,838 +0,0 @@ -/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -==============================================================================*/ -#ifndef XYZ_PROTOCOL_DETAIL_HH_ -#define XYZ_PROTOCOL_DETAIL_HH_ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "name_mangling.h" - -// clang-p2996 deprecates data_member_options::no_unique_address in favour of -// a fork-specific attributes member that GCC does not have, so the warning is -// silenced for this file rather than moving off the standard field. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - -namespace xyz::detail { - -template -consteval bool is_operator(std::meta::info function) { - return is_operator_function(function) && operator_of(function) == Operator; -} - -// Returns `true` if `a` and `b` are the same operators or have the same -// identifier. Otherwise returns false. -consteval bool same_name(std::meta::info a, std::meta::info b) { - if (has_identifier(a) && has_identifier(b)) { - return identifier_of(a) == identifier_of(b); - } - if (is_operator_function(a) && is_operator_function(b)) { - return operator_of(a) == operator_of(b); - } - return false; -} - -// Returns `true` if the member functions `candidate` and `interface` have -// the same name, de-aliased return type and de-aliased parameter types. -consteval bool same_name_and_parameters(std::meta::info candidate, - std::meta::info interface) { - if (!same_name(candidate, interface)) return false; - if (dealias(return_type_of(interface)) != dealias(return_type_of(candidate))) - return false; - auto dealiased_type_of = [](std::meta::info parameter) { - return dealias(type_of(parameter)); - }; - return std::ranges::equal(parameters_of(interface), parameters_of(candidate), - {}, dealiased_type_of, dealiased_type_of); -} - -// Returns `true` if the member functions `candidate` and `interface` have -// the same name, reference qualifiers, de-aliased return type and de-aliased -// parameter types; const and noexcept are not compared. -consteval bool same_signature_ignoring_const(std::meta::info candidate, - std::meta::info interface) { - if (is_lvalue_reference_qualified(interface) != - is_lvalue_reference_qualified(candidate)) - return false; - if (is_rvalue_reference_qualified(interface) != - is_rvalue_reference_qualified(candidate)) - return false; - return same_name_and_parameters(candidate, interface); -} - -// Returns `true` if `candidate is a function with an explicit object parameter, -// the member functions `candidate` and `interface` have the -// same name, de-aliased return type and parameter types, and const / reference -// qualification. -consteval bool same_function_with_explicit_object(std::meta::info candidate, - std::meta::info interface) { - if (!same_name(candidate, interface)) return false; - if (dealias(return_type_of(candidate)) != dealias(return_type_of(interface))) - return false; - - const auto params = parameters_of(candidate); - if (params.empty() || !is_explicit_object_parameter(params.front())) - return false; - - if (is_const(interface) != - is_const(remove_reference(type_of(params.front())))) - return false; - - return std::ranges::equal(parameters_of(interface), - params | std::views::drop(1), {}, - std::meta::type_of, std::meta::type_of); -} - -// Returns `true` if the `candidate` member function is consistent with the -// `interface` member function for the purposes of structural subtyping; -// otherwise returns `false`. -consteval bool member_function_conforms_to(std::meta::info candidate, - std::meta::info interface) { - if (is_static_member(candidate)) { - // A static candidate has no object parameter, so it satisfies any const - // or reference qualification of `interface`. - if (!same_name_and_parameters(candidate, interface)) return false; - } else if (same_function_with_explicit_object(candidate, interface)) { - // No additional conditions. - } else { - if (!same_signature_ignoring_const(candidate, interface)) return false; - // `const` qualifiers must match. - if (is_const(interface) != is_const(candidate)) return false; - } - // If interface is `noexcept`, `candidate` must be noexcept. - return !is_noexcept(interface) || is_noexcept(candidate); -} - -// Per ISO C++ ([expr.prim.lambda.closure]), closure types are unique, unnamed, -// non-union class types. -// The closure type is not an aggregate type. -// This concept will also match an unnamed class type with a single -// `operator()`. -// -// In practice a lambda has no base classes and no template arguments, although -// there is no wording in the standard to guarantee this. -// -// TODO(jbcoe): Refine this concept to match only lambdas. -template -concept is_maybe_lambda = - is_class_type(dealias(^^T)) && !has_identifier(dealias(^^T)) && - !is_aggregate_type(dealias(^^T)) && !has_template_arguments(dealias(^^T)) && - bases_of(dealias(^^T), std::meta::access_context::unprivileged()).empty() && - requires { &T::operator(); }; - -// The named, non-special member functions and call operators of `Type`, -// static or not, in declaration order. -template -consteval auto conformance_candidate_infos() { - auto named = - members_of(Type, std::meta::access_context::unprivileged()) | - std::views::filter(std::meta::is_function) | - std::views::filter([](std::meta::info member) consteval { - return has_identifier(member) || - is_operator(member) || - is_operator(member) || - is_operator(member) || - is_operator(member); - }); - std::vector result(std::ranges::begin(named), - std::ranges::end(named)); - // GCC Workaround: Per [meta.reflection.member.queries], a closure type's - // function call operator is members-of-eligible, but GCC16's `members_of` - // does not yet enumerate it, leaving `result` empty for lambdas; name the - // operator directly as a fallback. - using T = typename[:Type:]; - if constexpr (is_maybe_lambda) { - if (result.empty()) result.push_back(^^T::operator()); - } - return result; -} - -template -constexpr inline auto conformance_candidates_of = - std::define_static_array(conformance_candidate_infos()); - -// The named, non-static, non-special member functions and call operators of -// `Type`, static or not, in declaration order. Ref-qualified functions are -// unsupported on protocol interfaces. -template -consteval std::vector protocol_interface_function_infos() { - std::vector result; - for (std::meta::info member : conformance_candidates_of) { - if (is_static_member(member)) continue; - if (is_lvalue_reference_qualified(member) || - is_rvalue_reference_qualified(member)) { - std::string name = has_identifier(member) - ? std::string(identifier_of(member)) - : std::string(display_string_of(member)); - throw std::runtime_error("ref-qualified member function '" + name + - "' is not supported in a protocol interface"); - } - result.push_back(member); - } - return result; -} - -template -constexpr inline auto protocol_interface_functions_of = - std::define_static_array(protocol_interface_function_infos()); - -// The mangled name of `Member`, computed once per distinct `Member`. -template -constexpr inline auto mangled_name_of = - std::define_static_string(xyz::name_mangling::mangle(Member)); - -// The `VtableType` entry named after the mangled member function `Member`. -template -consteval std::meta::info find_vtable_entry() { - std::string_view name = mangled_name_of; - std::vector entries = nonstatic_data_members_of( - VtableType, std::meta::access_context::unprivileged()); - for (std::meta::info entry : entries) { - if (identifier_of(entry) == name) return entry; - } - throw std::runtime_error("find_vtable_entry: no entry named '" + - std::string(name) + "'"); -} - -// Vanishing-this-pointer thunk for one overload of a synthesised named -// member function. A generated `member_base` holds it as its sole data -// member, named after the interface method, giving -// `p.member_function_name(args)` call syntax. -template -struct member_function_thunk; - -template -struct member_function_thunk { - static constexpr std::meta::info vtable_entry = - find_vtable_entry<^^Vtable, Member>(); - - R operator()(Args... args) noexcept(IsNoexcept) - requires(!IsConst) - { - auto* enclosing = reinterpret_cast(this); - auto* protocol_object = static_cast(enclosing); - if constexpr (requires { protocol_object->valueless_after_move(); }) { - assert(!protocol_object->valueless_after_move() && - "cannot call member function of valueless protocol"); - } - - const Vtable* vtable = protocol_object->vtable_; - return vtable->[:vtable_entry:](protocol_object->object_, - std::forward(args)...); - } - - R operator()(Args... args) const noexcept(IsNoexcept) - requires(IsConst) - { - const auto* enclosing = reinterpret_cast(this); - const auto* protocol_object = static_cast(enclosing); - if constexpr (requires { protocol_object->valueless_after_move(); }) { - assert(!protocol_object->valueless_after_move() && - "cannot call member function of valueless protocol"); - } - - const Vtable* vtable = protocol_object->vtable_; - return vtable->[:vtable_entry:](protocol_object->object_, - std::forward(args)...); - } - - protected: - // Only `member_function_overload_set` may create or copy a thunk. - member_function_thunk() = default; - ~member_function_thunk() = default; - member_function_thunk(const member_function_thunk&) = default; - member_function_thunk(member_function_thunk&&) = default; - member_function_thunk& operator=(const member_function_thunk&) = default; - member_function_thunk& operator=(member_function_thunk&&) = default; -}; - -template -using fn_ptr_t = R (*)(Args...); - -// One overload of a synthesised member function: the interface member (which -// names its vtable entry) and the const-qualification of the generated -// wrapper. -template -struct overload_spec {}; - -// The `member_function_thunk` specialisation for an `overload_spec`. -template -struct member_function_thunk_for; - -template -struct member_function_thunk_for, EnclosingType, - ProtocolType, Vtable> { - // Build the function-pointer type R(*)(Args...) from the method's return - // type and parameter types. - static consteval std::meta::info fn_ptr_type() { - std::vector fn_args{dealias(return_type_of(Member))}; - fn_args.append_range(parameters_of(Member) | - std::views::transform(std::meta::type_of)); - return substitute(^^fn_ptr_t, fn_args); - } - - // clang-format off - using type = typename[:substitute( - ^^member_function_thunk, {fn_ptr_type(), ^^EnclosingType, ^^ProtocolType, ^^Vtable, - std::meta::reflect_constant(Member), - std::meta::reflect_constant(IsConst), - std::meta::reflect_constant(is_noexcept(Member))}):]; - // clang-format on -}; - -template -using member_function_thunk_t = - member_function_thunk_for::type; - -// The overload set for one synthesised named member function: a -// `member_function_thunk` per overload, with every operator() brought into -// scope so that overload resolution among them works as for a member function -// of the interface. -template -struct member_function_overload_set - : member_function_thunk_t... { - using member_function_thunk_t::operator()...; - - private: - friend EnclosingType; - member_function_overload_set() = default; - ~member_function_overload_set() = default; - member_function_overload_set(const member_function_overload_set&) = default; - member_function_overload_set(member_function_overload_set&&) = default; - member_function_overload_set& operator=(const member_function_overload_set&) = - default; - member_function_overload_set& operator=(member_function_overload_set&&) = - default; -}; - -// Thunk for one overload of a synthesised call operator. `operator()` can't -// be reached through a named member, so `operator_overload_set` -// derives from this thunk directly instead of holding it as a data member, -// letting `ProtocolType` be recovered with a plain static_cast. -template -struct operator_thunk { - operator_thunk() = - delete ("Unspecialized operator thunk cannot be instantiated"); -}; - -// operator() -template -struct operator_thunk { - static constexpr std::meta::info vtable_entry = - find_vtable_entry<^^Vtable, Member>(); - - R operator()(Args... args) noexcept(IsNoexcept) - requires(!IsConst) - { - auto* protocol_object = static_cast(this); - if constexpr (requires { protocol_object->valueless_after_move(); }) { - assert(!protocol_object->valueless_after_move() && - "cannot call member function of valueless protocol"); - } - - const Vtable* vtable = protocol_object->vtable_; - return vtable->[:vtable_entry:](protocol_object->object_, - std::forward(args)...); - } - - R operator()(Args... args) const noexcept(IsNoexcept) - requires(IsConst) - { - const auto* protocol_object = static_cast(this); - if constexpr (requires { protocol_object->valueless_after_move(); }) { - assert(!protocol_object->valueless_after_move() && - "cannot call member function of valueless protocol"); - } - - const Vtable* vtable = protocol_object->vtable_; - return vtable->[:vtable_entry:](protocol_object->object_, - std::forward(args)...); - } -}; - -// operator [] -template -struct operator_thunk { - static constexpr std::meta::info vtable_entry = - find_vtable_entry<^^Vtable, Member>(); - - R operator[](Args... args) noexcept(IsNoexcept) - requires(!IsConst) - { - auto* protocol_object = static_cast(this); - const Vtable* vtable = protocol_object->vtable_; - return vtable->[:vtable_entry:](protocol_object->object_, - std::forward(args)...); - } - - R operator[](Args... args) const noexcept(IsNoexcept) - requires(IsConst) - { - const auto* protocol_object = static_cast(this); - const Vtable* vtable = protocol_object->vtable_; - return vtable->[:vtable_entry:](protocol_object->object_, - std::forward(args)...); - } -}; - -// operator -> -template -struct operator_thunk { - static constexpr std::meta::info vtable_entry = - find_vtable_entry<^^Vtable, Member>(); - - R operator->() noexcept(IsNoexcept) - requires(!IsConst) - { - auto* protocol_object = static_cast(this); - const Vtable* vtable = protocol_object->vtable_; - return vtable->[:vtable_entry:](protocol_object->object_); - } - - R operator->() const noexcept(IsNoexcept) - requires(IsConst) - { - const auto* protocol_object = static_cast(this); - const Vtable* vtable = protocol_object->vtable_; - return vtable->[:vtable_entry:](protocol_object->object_); - } -}; - -// operator * -template -struct operator_thunk { - static constexpr std::meta::info vtable_entry = - find_vtable_entry<^^Vtable, Member>(); - - R operator*() noexcept(IsNoexcept) - requires(!IsConst) - { - auto* protocol_object = static_cast(this); - const Vtable* vtable = protocol_object->vtable_; - return vtable->[:vtable_entry:](protocol_object->object_); - } - - R operator*() const noexcept(IsNoexcept) - requires(IsConst) - { - const auto* protocol_object = static_cast(this); - const Vtable* vtable = protocol_object->vtable_; - return vtable->[:vtable_entry:](protocol_object->object_); - } -}; - -// The `operator_thunk` specialisation for an `overload_spec`. -template -struct operator_thunk_for; - -template -struct operator_thunk_for, ProtocolType, - Vtable> { - // Build the function-pointer type R(*)(Args...) from the method's return - // type and parameter types. - static consteval std::meta::info fn_ptr_type() { - std::vector fn_args{dealias(return_type_of(Member))}; - fn_args.append_range(parameters_of(Member) | - std::views::transform(std::meta::type_of)); - return substitute(^^fn_ptr_t, fn_args); - } - - // clang-format off - using type = - typename[:substitute(^^operator_thunk, - { - std::meta::reflect_constant(operator_of(Member)), - fn_ptr_type(), - ^^ProtocolType, ^^Vtable, - std::meta::reflect_constant(Member), - std::meta::reflect_constant(IsConst), - std::meta::reflect_constant(is_noexcept(Member)) - }):]; - // clang-format on -}; - -template -using operator_thunk_t = - operator_thunk_for::type; - -// The overload set for a synthesised operator X inheriting from an -// `operator_thunk` for each overload. -// `std::meta::operators` is not specified as it can be derived from -// OverloadSpec. -template -struct operator_overload_set { - operator_overload_set() = - delete ("Unspecialized operator overload set cannot be instantiated"); -}; - -// operator() -template -struct operator_overload_set - : operator_thunk_t... { - using operator_thunk_t::operator()...; -}; - -// operator[] -template -struct operator_overload_set - : operator_thunk_t... { - using operator_thunk_t::operator[]...; -}; - -// operator-> -template -struct operator_overload_set - : operator_thunk_t... { - using operator_thunk_t::operator->...; -}; - -// operator* -template -struct operator_overload_set - : operator_thunk_t... { - using operator_thunk_t::operator*...; -}; - -// How generated wrappers treat the const-qualification of interface members. -enum class const_policy { - // `protocol`: as declared in `I`, so `const protocol` exposes only the - // const member functions of `I` (const propagates). - propagate, - // `protocol_view`: every wrapper is const-qualified regardless of `I` - // (shallow const, as for `std::span`). - all_const, - // `protocol_view`: only the const member functions of `I`. - const_only, -}; - -// Returns `true` if `member` if forwarded under `ConstPolicy`. -template -consteval bool is_forwarded_member_function( - std::meta::info member, std::span members) { - switch (ConstPolicy) { - case const_policy::propagate: - // `protocol` propagates const through forwarded member function calls - // so supports const and non-const qualified member functions. - return true; - case const_policy::const_only: - // `protocol_view` forwards only const-qualified member - // functions. - return is_const(member); - case const_policy::all_const: - // `protocol_view` is a view type: overload resolution through - // a const and non-const access path must yield the same result. - // A const-qualified member function is only given a forwarding wrapper if - // no non-const-qualified with an otherwise identical signature exists. - // (Aside: Oh the double negatives! If only `mutable` was the keyword.) - if (!is_const(member)) { - return true; - } else { - return std::ranges::none_of(members, [&](std::meta::info other) { - return member != other && - same_signature_ignoring_const(member, other); - }); - } - } - std::unreachable(); -} - -// A single-member base wrapping the overload set for one interface member -// function name, named after that method (giving the -// `p.member_function_name(args)` call syntax). -template -struct member_base_generator { - struct type; - consteval { - // clang-format off - std::meta::info thunk_type = substitute( - ^^member_function_overload_set, {^^type, ^^ProtocolType, ^^Vtable, ^^OverloadSpecs...}); - - define_aggregate( - ^^type, {data_member_spec(thunk_type, - std::meta::data_member_options{ - .name = identifier_of(Member), - .no_unique_address = true - })}); - // clang-format on - } -}; - -template -using member_base_t = - member_base_generator::type; - -// Combines the single-member base types and overload sets produced by -// `member_base_generator` and `X_operator_overload_set` into one type via -// multiple inheritance. -template -struct member_bases_wrapper : MemberBases... {}; - -// Returns a `member_bases_wrapper` specialisation with one base per public, -// non-special, member function name of `interface_type`, giving named members -// with an `operator()` for each overload selected by `ConstPolicy`, plus a -// `operator_overload_set` if `interface_type` has call operators. -// TODO: Rewrite this hard-to-read function. -template -consteval std::meta::info generate_member_bases_wrapper() { - std::span members = - protocol_interface_functions_of; - std::vector member_base_types; - std::vector matched_members; - for (std::meta::info member : members) { - // Find unique names/operators. - if (std::ranges::any_of(matched_members, - [&](std::meta::info matched_member) { - return same_name(member, matched_member); - })) - continue; - matched_members.push_back(member); - - // Collect overloads. - std::vector overload_specs; - for (std::meta::info overload : members) { - if (!same_name(overload, member) || - !is_forwarded_member_function(overload, members)) - continue; - const bool wrapper_is_const = - ConstPolicy == const_policy::propagate ? is_const(overload) : true; - // clang-format off - overload_specs.push_back(substitute( - ^^overload_spec, {reflect_constant(overload), - std::meta::reflect_constant(wrapper_is_const)})); - // clang-format on - } - if (overload_specs.empty()) continue; - - std::vector member_base_args; - if (has_identifier(member)) { - member_base_args.push_back(reflect_constant(member)); - } else if (is_operator(member)) { - member_base_args.push_back( - reflect_constant(std::meta::operators::op_parentheses)); - } else if (is_operator(member)) { - member_base_args.push_back( - reflect_constant(std::meta::operators::op_square_brackets)); - } else if (is_operator(member)) { - member_base_args.push_back( - reflect_constant(std::meta::operators::op_star)); - } else if (is_operator(member)) { - member_base_args.push_back( - reflect_constant(std::meta::operators::op_arrow)); - } - member_base_args.push_back(^^ProtocolType); - member_base_args.push_back(^^Vtable); - member_base_args.append_range(overload_specs); - if (has_identifier(member)) { - member_base_types.push_back( - substitute(^^member_base_t, member_base_args)); - } else if (is_operator_function(member)) { - member_base_types.push_back( - substitute(^^operator_overload_set, member_base_args)); - } else { - std::unreachable(); - } - } - return substitute(^^member_bases_wrapper, member_base_types); -} - -// The generated wrapper type for `T`: a `member_bases_wrapper` specialisation -// with named members with `operator()` for each public, non-special, member -// function from `T` selected by `ConstPolicy`. -template -using protocol_wrappers_t = - typename[:generate_member_bases_wrapper<^^T, ProtocolType, Vtable, - ConstPolicy>():]; - -// Returns a list of data_member_spec values, one for each member function -// implemented by `protocol`, each describing a vtable function pointer with -// signature R(*)(void*, Args...) for a mutable interface method, or -// R(*)(const void*, Args...) for a const one, named by the member function's -// mangled signature (see `xyz::name_mangling::mangle`). -template -consteval std::vector generate_vtable_specs() { - std::vector function_pointer_specs; - - function_pointer_specs.push_back(data_member_spec( - ^^const std::type_info*, { - .name = "xyz_protocol_typeid"})); - - template for (constexpr std::meta::info member : - protocol_interface_functions_of) { - // Build the function-pointer type R(*)(void*, Args...) noexcept(...) - // from the method's return type, parameter types and noexcept-ness; a - // const method takes `const void*` instead, matching the constness of - // the access path it's called through. - std::vector fn_args{dealias(return_type_of(member))}; - fn_args.push_back(is_const(member) ? ^^const void* : ^^void*); - std::vector member_parameters = parameters_of(member); - for (std::meta::info parameter : member_parameters) { - fn_args.push_back(dealias(type_of(parameter))); - } - std::meta::info fn_ptr_type = substitute(^^fn_ptr_t, fn_args); - - // GCC UBSAN workaround: `std::string`'s pointer-taking constructors have a - // null check GCC trunk can't constant-fold under `-fsanitize=undefined`, - // even though `mangled_name_of` is never null. The iterator-pair - // constructor has no such check. - std::string_view cached_name = mangled_name_of; - function_pointer_specs.push_back(data_member_spec( - fn_ptr_type, - std::meta::data_member_options{ - .name = std::string(cached_name.begin(), cached_name.end())})); - } - return function_pointer_specs; -} - -// Generates a vtable with named function pointers for each public, -// non-special, member function from `T`. Name mangling ensures that -// names for overloads are unique. -template -struct vtable_generator { - struct type; - consteval { define_aggregate(^^type, generate_vtable_specs<^^T>()); } -}; - -template -using vtable_t = typename vtable_generator::type; - -// Finds the member of `CandidateType` that structurally conforms to -// `Member`. -template -consteval std::meta::info find_conforming_member() { - for (std::meta::info candidate : conformance_candidates_of) { - if (member_function_conforms_to(candidate, Member)) return candidate; - } - std::unreachable(); -} - -// Trampolines translate type-erased calls to vtable functions to member -// function calls on the underlying (owned or viewed) object. -template -struct mutable_view_trampoline; - -template -struct mutable_view_trampoline { - static R operator()(void* ptr, Args... args) noexcept(Noexcept) { - return static_cast(ptr)->[:CandidateMember:]( - std::forward(args)...); - } -}; - -template -struct const_view_trampoline; - -template -struct const_view_trampoline { - static R operator()(const void* ptr, Args... args) noexcept(Noexcept) { - return static_cast(ptr)->[:CandidateMember:]( - std::forward(args)...); - } -}; - -// Builds a vtable for `T` whose entries call through to the corresponding -// member of `U`. -// For `protocol_view` some vtable entries may be unused due to const -// qualifiers. We opt for simple vtable construction logic and rely on -// generation of forwarding wrappers to determine which functions are forwarded. -template -consteval vtable_t make_view_vtable() { - vtable_t vtable{}; - - vtable.xyz_protocol_typeid = &typeid(U); - - template for (constexpr std::meta::info member : - protocol_interface_functions_of<^^T>) { - constexpr std::meta::info vtable_member = - find_vtable_entry<^^vtable_t, member>(); - using FnPtrType = typename[:type_of(vtable_member):]; - if constexpr (is_const(member)) { - constexpr std::meta::info candidate = - find_conforming_member(); - vtable.[:vtable_member:] = &const_view_trampoline::operator(); - } else /*constexpr*/ { - constexpr std::meta::info candidate = - find_conforming_member(); - vtable.[:vtable_member:] = &mutable_view_trampoline< - FnPtrType, U, candidate>::operator(); - } - } - return vtable; -} - -// The shared, compile-time vtable every protocol_view (or -// protocol_view) that views a `U` points to. -template -inline constexpr vtable_t view_vtable_for = make_view_vtable(); - -} // namespace xyz::detail - -#pragma GCC diagnostic pop -#endif // XYZ_PROTOCOL_DETAIL_HH_ diff --git a/member_function_thunks.hh b/member_function_thunks.hh new file mode 100644 index 00000000..302a5e2b --- /dev/null +++ b/member_function_thunks.hh @@ -0,0 +1,150 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +#ifndef XYZ_PROTOCOL_MEMBER_FUNCTION_THUNKS_HH_ +#define XYZ_PROTOCOL_MEMBER_FUNCTION_THUNKS_HH_ + +#include +#include +#include +#include +#include + +#include "overload_spec.hh" +#include "protocol_traits.hh" +#include "vtable.hh" + +namespace xyz::detail { + +// Vanishing-this-pointer thunk for one overload of a synthesised named +// member function. A generated `member_base` holds it as its sole data +// member, named after the interface method, giving +// `p.member_function_name(args)` call syntax. +template +struct member_function_thunk; + +template +struct member_function_thunk { + static constexpr std::meta::info vtable_entry = + find_vtable_entry<^^Vtable, Member>(); + + R operator()(Args... args) noexcept(IsNoexcept) + requires(!IsConst) + { + auto* enclosing = reinterpret_cast(this); + auto* protocol_object = static_cast(enclosing); + if constexpr (xyz::reflection::is_protocol_v) { + assert(!protocol_object->valueless_after_move() && + "cannot call member function of valueless protocol"); + } + + const Vtable* vtable = protocol_object->vtable_; + return vtable->[:vtable_entry:](protocol_object->object_, + std::forward(args)...); + } + + R operator()(Args... args) const noexcept(IsNoexcept) + requires(IsConst) + { + const auto* enclosing = reinterpret_cast(this); + const auto* protocol_object = static_cast(enclosing); + if constexpr (xyz::reflection::is_protocol_v) { + assert(!protocol_object->valueless_after_move() && + "cannot call member function of valueless protocol"); + } + + const Vtable* vtable = protocol_object->vtable_; + return vtable->[:vtable_entry:](protocol_object->object_, + std::forward(args)...); + } + + protected: + // Only `member_function_overload_set` may create or copy a thunk. + member_function_thunk() = default; + ~member_function_thunk() = default; + member_function_thunk(const member_function_thunk&) = default; + member_function_thunk(member_function_thunk&&) = default; + member_function_thunk& operator=(const member_function_thunk&) = default; + member_function_thunk& operator=(member_function_thunk&&) = default; +}; + +// The `member_function_thunk` specialisation for an `overload_spec`. +template +struct member_function_thunk_for; + +template +struct member_function_thunk_for, EnclosingType, + ProtocolType, Vtable> { + // Build the function-pointer type R(*)(Args...) from the method's return + // type and parameter types. + static consteval std::meta::info fn_ptr_type() { + std::vector fn_args{dealias(return_type_of(Member))}; + fn_args.append_range(parameters_of(Member) | + std::views::transform(std::meta::type_of)); + return substitute(^^fn_ptr_t, fn_args); + } + + // clang-format off + using type = typename[:substitute( + ^^member_function_thunk, {fn_ptr_type(), ^^EnclosingType, ^^ProtocolType, ^^Vtable, + std::meta::reflect_constant(Member), + std::meta::reflect_constant(IsConst), + std::meta::reflect_constant(is_noexcept(Member))}):]; + // clang-format on +}; + +template +using member_function_thunk_t = + member_function_thunk_for::type; + +// The overload set for one synthesised named member function: a +// `member_function_thunk` per overload, with every operator() brought into +// scope so that overload resolution among them works as for a member function +// of the interface. +template +struct member_function_overload_set + : member_function_thunk_t... { + using member_function_thunk_t::operator()...; + + private: + friend EnclosingType; + member_function_overload_set() = default; + ~member_function_overload_set() = default; + member_function_overload_set(const member_function_overload_set&) = default; + member_function_overload_set(member_function_overload_set&&) = default; + member_function_overload_set& operator=(const member_function_overload_set&) = + default; + member_function_overload_set& operator=(member_function_overload_set&&) = + default; +}; + +} // namespace xyz::detail + +#endif // XYZ_PROTOCOL_MEMBER_FUNCTION_THUNKS_HH_ diff --git a/operator_thunks.hh b/operator_thunks.hh new file mode 100644 index 00000000..ef8e00a6 --- /dev/null +++ b/operator_thunks.hh @@ -0,0 +1,269 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +#ifndef XYZ_PROTOCOL_OPERATOR_THUNKS_HH_ +#define XYZ_PROTOCOL_OPERATOR_THUNKS_HH_ + +#include +#include +#include +#include +#include + +#include "overload_spec.hh" +#include "protocol_traits.hh" +#include "vtable.hh" + +namespace xyz::detail { + +// Thunk for one overload of a synthesised call operator. `operator()` can't +// be reached through a named member, so `operator_overload_set` +// derives from this thunk directly instead of holding it as a data member, +// letting `ProtocolType` be recovered with a plain static_cast. +template +struct operator_thunk { + operator_thunk() = + delete ("Unspecialized operator thunk cannot be instantiated"); +}; + +// operator() +template +struct operator_thunk { + static constexpr std::meta::info vtable_entry = + find_vtable_entry<^^Vtable, Member>(); + + R operator()(Args... args) noexcept(IsNoexcept) + requires(!IsConst) + { + auto* protocol_object = static_cast(this); + if constexpr (xyz::reflection::is_protocol_v) { + assert(!protocol_object->valueless_after_move() && + "cannot call member function of valueless protocol"); + } + + const Vtable* vtable = protocol_object->vtable_; + return vtable->[:vtable_entry:](protocol_object->object_, + std::forward(args)...); + } + + R operator()(Args... args) const noexcept(IsNoexcept) + requires(IsConst) + { + const auto* protocol_object = static_cast(this); + if constexpr (xyz::reflection::is_protocol_v) { + assert(!protocol_object->valueless_after_move() && + "cannot call member function of valueless protocol"); + } + + const Vtable* vtable = protocol_object->vtable_; + return vtable->[:vtable_entry:](protocol_object->object_, + std::forward(args)...); + } +}; + +// operator [] +template +struct operator_thunk { + static constexpr std::meta::info vtable_entry = + find_vtable_entry<^^Vtable, Member>(); + + R operator[](Args... args) noexcept(IsNoexcept) + requires(!IsConst) + { + auto* protocol_object = static_cast(this); + if constexpr (xyz::reflection::is_protocol_v) { + assert(!protocol_object->valueless_after_move() && + "cannot call member function of valueless protocol"); + } + + const Vtable* vtable = protocol_object->vtable_; + return vtable->[:vtable_entry:](protocol_object->object_, + std::forward(args)...); + } + + R operator[](Args... args) const noexcept(IsNoexcept) + requires(IsConst) + { + const auto* protocol_object = static_cast(this); + if constexpr (xyz::reflection::is_protocol_v) { + assert(!protocol_object->valueless_after_move() && + "cannot call member function of valueless protocol"); + } + + const Vtable* vtable = protocol_object->vtable_; + return vtable->[:vtable_entry:](protocol_object->object_, + std::forward(args)...); + } +}; + +// operator -> +template +struct operator_thunk { + static constexpr std::meta::info vtable_entry = + find_vtable_entry<^^Vtable, Member>(); + + R operator->() noexcept(IsNoexcept) + requires(!IsConst) + { + auto* protocol_object = static_cast(this); + if constexpr (xyz::reflection::is_protocol_v) { + assert(!protocol_object->valueless_after_move() && + "cannot call member function of valueless protocol"); + } + + const Vtable* vtable = protocol_object->vtable_; + return vtable->[:vtable_entry:](protocol_object->object_); + } + + R operator->() const noexcept(IsNoexcept) + requires(IsConst) + { + const auto* protocol_object = static_cast(this); + if constexpr (xyz::reflection::is_protocol_v) { + assert(!protocol_object->valueless_after_move() && + "cannot call member function of valueless protocol"); + } + + const Vtable* vtable = protocol_object->vtable_; + return vtable->[:vtable_entry:](protocol_object->object_); + } +}; + +// operator * +template +struct operator_thunk { + static constexpr std::meta::info vtable_entry = + find_vtable_entry<^^Vtable, Member>(); + + R operator*() noexcept(IsNoexcept) + requires(!IsConst) + { + auto* protocol_object = static_cast(this); + if constexpr (xyz::reflection::is_protocol_v) { + assert(!protocol_object->valueless_after_move() && + "cannot call member function of valueless protocol"); + } + + const Vtable* vtable = protocol_object->vtable_; + return vtable->[:vtable_entry:](protocol_object->object_); + } + + R operator*() const noexcept(IsNoexcept) + requires(IsConst) + { + const auto* protocol_object = static_cast(this); + if constexpr (xyz::reflection::is_protocol_v) { + assert(!protocol_object->valueless_after_move() && + "cannot call member function of valueless protocol"); + } + + const Vtable* vtable = protocol_object->vtable_; + return vtable->[:vtable_entry:](protocol_object->object_); + } +}; + +// The `operator_thunk` specialisation for an `overload_spec`. +template +struct operator_thunk_for; + +template +struct operator_thunk_for, ProtocolType, + Vtable> { + // Build the function-pointer type R(*)(Args...) from the method's return + // type and parameter types. + static consteval std::meta::info fn_ptr_type() { + std::vector fn_args{dealias(return_type_of(Member))}; + fn_args.append_range(parameters_of(Member) | + std::views::transform(std::meta::type_of)); + return substitute(^^fn_ptr_t, fn_args); + } + + // clang-format off + using type = + typename[:substitute(^^operator_thunk, + { + std::meta::reflect_constant(operator_of(Member)), + fn_ptr_type(), + ^^ProtocolType, ^^Vtable, + std::meta::reflect_constant(Member), + std::meta::reflect_constant(IsConst), + std::meta::reflect_constant(is_noexcept(Member)) + }):]; + // clang-format on +}; + +template +using operator_thunk_t = + operator_thunk_for::type; + +// The overload set for a synthesised operator X inheriting from an +// `operator_thunk` for each overload. +template +struct operator_overload_set { + operator_overload_set() = + delete ("Unspecialized operator overload set cannot be instantiated"); +}; + +// operator() +template +struct operator_overload_set + : operator_thunk_t... { + using operator_thunk_t::operator()...; +}; + +// operator[] +template +struct operator_overload_set + : operator_thunk_t... { + using operator_thunk_t::operator[]...; +}; + +// operator-> +template +struct operator_overload_set + : operator_thunk_t... { + using operator_thunk_t::operator->...; +}; + +// operator* +template +struct operator_overload_set + : operator_thunk_t... { + using operator_thunk_t::operator*...; +}; + +} // namespace xyz::detail + +#endif // XYZ_PROTOCOL_OPERATOR_THUNKS_HH_ diff --git a/overload_spec.hh b/overload_spec.hh new file mode 100644 index 00000000..78320961 --- /dev/null +++ b/overload_spec.hh @@ -0,0 +1,38 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +#ifndef XYZ_PROTOCOL_OVERLOAD_SPEC_HH_ +#define XYZ_PROTOCOL_OVERLOAD_SPEC_HH_ + +#include + +namespace xyz::detail { + +template +using fn_ptr_t = R (*)(Args...); + +// One overload of a synthesised member function or operator: the interface +// member (which names its vtable entry) and the const-qualification of the +// generated wrapper. +template +struct overload_spec {}; + +} // namespace xyz::detail + +#endif // XYZ_PROTOCOL_OVERLOAD_SPEC_HH_ diff --git a/protocol.hh b/protocol.hh index a019dec3..de8a7927 100644 --- a/protocol.hh +++ b/protocol.hh @@ -48,8 +48,13 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include #include -#include "detail.hh" +#include "conformance.hh" +#include "member_function_thunks.hh" #include "name_mangling.h" +#include "operator_thunks.hh" +#include "protocol_traits.hh" +#include "protocol_wrappers.hh" +#include "vtable.hh" // clang-p2996 deprecates data_member_options::no_unique_address in favour of // a fork-specific attributes member that GCC does not have, so the warning is @@ -59,41 +64,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. namespace xyz::reflection { -template -concept is_valid_interface = - is_class_type(^^I) && std::same_as> && - std::ranges::none_of( - members_of(^^I, std::meta::access_context::unprivileged()), - std::meta::is_volatile); - -template -concept is_valid_view_interface = - is_valid_interface || is_valid_interface>; - -template -class protocol; - -template -class protocol_view; - -template -struct is_protocol : std::false_type {}; - -template -struct is_protocol> : std::true_type {}; - -template -inline constexpr bool is_protocol_v = is_protocol::value; - -template -struct is_protocol_view : std::false_type {}; - -template -struct is_protocol_view> : std::true_type {}; - -template -inline constexpr bool is_protocol_view_v = is_protocol_view::value; - // Returns `true` if `Candidate` is a structural subtype of `Interface`; // otherwise returns `false`. // TODO(jbcoe) Move is_protocol_conformant into the detail header. diff --git a/protocol_traits.hh b/protocol_traits.hh new file mode 100644 index 00000000..8b943547 --- /dev/null +++ b/protocol_traits.hh @@ -0,0 +1,68 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +#ifndef XYZ_PROTOCOL_PROTOCOL_TRAITS_HH_ +#define XYZ_PROTOCOL_PROTOCOL_TRAITS_HH_ + +#include +#include +#include +#include +#include + +namespace xyz::reflection { + +template +concept is_valid_interface = + is_class_type(^^I) && std::same_as> && + std::ranges::none_of( + members_of(^^I, std::meta::access_context::unprivileged()), + std::meta::is_volatile); + +template +concept is_valid_view_interface = + is_valid_interface || is_valid_interface>; + +template +class protocol; + +template +class protocol_view; + +template +struct is_protocol : std::false_type {}; + +template +struct is_protocol> : std::true_type {}; + +template +inline constexpr bool is_protocol_v = is_protocol::value; + +template +struct is_protocol_view : std::false_type {}; + +template +struct is_protocol_view> : std::true_type {}; + +template +inline constexpr bool is_protocol_view_v = is_protocol_view::value; + +} // namespace xyz::reflection + +#endif // XYZ_PROTOCOL_PROTOCOL_TRAITS_HH_ diff --git a/protocol_wrappers.hh b/protocol_wrappers.hh new file mode 100644 index 00000000..7194f70d --- /dev/null +++ b/protocol_wrappers.hh @@ -0,0 +1,198 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +#ifndef XYZ_PROTOCOL_PROTOCOL_WRAPPERS_HH_ +#define XYZ_PROTOCOL_PROTOCOL_WRAPPERS_HH_ + +#include +#include +#include +#include + +#include "conformance.hh" +#include "member_function_thunks.hh" +#include "operator_thunks.hh" +#include "overload_spec.hh" + +// clang-p2996 deprecates data_member_options::no_unique_address in favour of +// a fork-specific attributes member that GCC does not have, so the warning is +// silenced for this file rather than moving off the standard field. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +namespace xyz::detail { + +// How generated wrappers treat the const-qualification of interface members. +enum class const_policy { + // `protocol`: as declared in `I`, so `const protocol` exposes only the + // const member functions of `I` (const propagates). + propagate, + // `protocol_view`: every wrapper is const-qualified regardless of `I` + // (shallow const, as for `std::span`). + all_const, + // `protocol_view`: only the const member functions of `I`. + const_only, +}; + +// Returns `true` if `member` if forwarded under `ConstPolicy`. +template +consteval bool is_forwarded_member_function( + std::meta::info member, std::span members) { + switch (ConstPolicy) { + case const_policy::propagate: + // `protocol` propagates const through forwarded member function calls + // so supports const and non-const qualified member functions. + return true; + case const_policy::const_only: + // `protocol_view` forwards only const-qualified member + // functions. + return is_const(member); + case const_policy::all_const: + // `protocol_view` is a view type: overload resolution through + // a const and non-const access path must yield the same result. + // A const-qualified member function is only given a forwarding wrapper if + // no non-const-qualified with an otherwise identical signature exists. + // (Aside: Oh the double negatives! If only `mutable` was the keyword.) + if (!is_const(member)) { + return true; + } else { + return std::ranges::none_of(members, [&](std::meta::info other) { + return member != other && + same_signature_ignoring_const(member, other); + }); + } + } + std::unreachable(); +} + +// A single-member base wrapping the overload set for one interface member +// function name, named after that method (giving the +// `p.member_function_name(args)` call syntax). +template +struct member_base_generator { + struct type; + consteval { + // clang-format off + std::meta::info thunk_type = substitute( + ^^member_function_overload_set, {^^type, ^^ProtocolType, ^^Vtable, ^^OverloadSpecs...}); + + define_aggregate( + ^^type, {data_member_spec(thunk_type, + std::meta::data_member_options{ + .name = identifier_of(Member), + .no_unique_address = true + })}); + // clang-format on + } +}; + +template +using member_base_t = + member_base_generator::type; + +// Combines the single-member base types and overload sets produced by +// `member_base_generator` and `X_operator_overload_set` into one type via +// multiple inheritance. +template +struct member_bases_wrapper : MemberBases... {}; + +// Returns a `member_bases_wrapper` specialisation with one base per public, +// non-special, member function name of `interface_type`, giving named members +// with an `operator()` for each overload selected by `ConstPolicy`, plus a +// `operator_overload_set` if `interface_type` has call operators. +// TODO: Rewrite this hard-to-read function. +template +consteval std::meta::info generate_member_bases_wrapper() { + std::span members = + protocol_interface_functions_of; + std::vector member_base_types; + std::vector matched_members; + for (std::meta::info member : members) { + // Find unique names/operators. + if (std::ranges::any_of(matched_members, + [&](std::meta::info matched_member) { + return same_name(member, matched_member); + })) + continue; + matched_members.push_back(member); + + // Collect overloads. + std::vector overload_specs; + for (std::meta::info overload : members) { + if (!same_name(overload, member) || + !is_forwarded_member_function(overload, members)) + continue; + const bool wrapper_is_const = + ConstPolicy == const_policy::propagate ? is_const(overload) : true; + // clang-format off + overload_specs.push_back(substitute( + ^^overload_spec, {reflect_constant(overload), + std::meta::reflect_constant(wrapper_is_const)})); + // clang-format on + } + if (overload_specs.empty()) continue; + + std::vector member_base_args; + if (has_identifier(member)) { + member_base_args.push_back(reflect_constant(member)); + } else if (is_operator(member)) { + member_base_args.push_back( + reflect_constant(std::meta::operators::op_parentheses)); + } else if (is_operator(member)) { + member_base_args.push_back( + reflect_constant(std::meta::operators::op_square_brackets)); + } else if (is_operator(member)) { + member_base_args.push_back( + reflect_constant(std::meta::operators::op_star)); + } else if (is_operator(member)) { + member_base_args.push_back( + reflect_constant(std::meta::operators::op_arrow)); + } + member_base_args.push_back(^^ProtocolType); + member_base_args.push_back(^^Vtable); + member_base_args.append_range(overload_specs); + if (has_identifier(member)) { + member_base_types.push_back( + substitute(^^member_base_t, member_base_args)); + } else if (is_operator_function(member)) { + member_base_types.push_back( + substitute(^^operator_overload_set, member_base_args)); + } else { + std::unreachable(); + } + } + return substitute(^^member_bases_wrapper, member_base_types); +} + +// The generated wrapper type for `T`: a `member_bases_wrapper` specialisation +// with named members with `operator()` for each public, non-special, member +// function from `T` selected by `ConstPolicy`. +template +using protocol_wrappers_t = + typename[:generate_member_bases_wrapper<^^T, ProtocolType, Vtable, + ConstPolicy>():]; + +} // namespace xyz::detail + +#pragma GCC diagnostic pop +#endif // XYZ_PROTOCOL_PROTOCOL_WRAPPERS_HH_ diff --git a/vtable.hh b/vtable.hh new file mode 100644 index 00000000..d2ef0866 --- /dev/null +++ b/vtable.hh @@ -0,0 +1,173 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +#ifndef XYZ_PROTOCOL_VTABLE_HH_ +#define XYZ_PROTOCOL_VTABLE_HH_ + +#include +#include +#include +#include +#include +#include +#include + +#include "conformance.hh" +#include "name_mangling.h" +#include "overload_spec.hh" + +namespace xyz::detail { + +// The mangled name of `Member`, computed once per distinct `Member`. +template +constexpr inline auto mangled_name_of = + std::define_static_string(xyz::name_mangling::mangle(Member)); + +// The `VtableType` entry named after the mangled member function `Member`. +template +consteval std::meta::info find_vtable_entry() { + std::string_view name = mangled_name_of; + std::vector entries = nonstatic_data_members_of( + VtableType, std::meta::access_context::unprivileged()); + for (std::meta::info entry : entries) { + if (identifier_of(entry) == name) return entry; + } + throw std::runtime_error("find_vtable_entry: no entry named '" + + std::string(name) + "'"); +} + +// Returns a list of data_member_spec values, one for each member function +// implemented by `protocol`, each describing a vtable function pointer with +// signature R(*)(void*, Args...) for a mutable interface method, or +// R(*)(const void*, Args...) for a const one, named by the member function's +// mangled signature (see `xyz::name_mangling::mangle`). +template +consteval std::vector generate_vtable_specs() { + std::vector function_pointer_specs; + + function_pointer_specs.push_back(data_member_spec( + ^^const std::type_info*, { + .name = "xyz_protocol_typeid"})); + + template for (constexpr std::meta::info member : + protocol_interface_functions_of) { + // Build the function-pointer type R(*)(void*, Args...) noexcept(...) + // from the method's return type, parameter types and noexcept-ness; a + // const method takes `const void*` instead, matching the constness of + // the access path it's called through. + std::vector fn_args{dealias(return_type_of(member))}; + fn_args.push_back(is_const(member) ? ^^const void* : ^^void*); + std::vector member_parameters = parameters_of(member); + for (std::meta::info parameter : member_parameters) { + fn_args.push_back(dealias(type_of(parameter))); + } + std::meta::info fn_ptr_type = substitute(^^fn_ptr_t, fn_args); + + // GCC UBSAN workaround: `std::string`'s pointer-taking constructors have a + // null check GCC trunk can't constant-fold under `-fsanitize=undefined`, + // even though `mangled_name_of` is never null. The iterator-pair + // constructor has no such check. + std::string_view cached_name = mangled_name_of; + function_pointer_specs.push_back(data_member_spec( + fn_ptr_type, + std::meta::data_member_options{ + .name = std::string(cached_name.begin(), cached_name.end())})); + } + return function_pointer_specs; +} + +// Generates a vtable with named function pointers for each public, +// non-special, member function from `T`. Name mangling ensures that +// names for overloads are unique. +template +struct vtable_generator { + struct type; + consteval { define_aggregate(^^type, generate_vtable_specs<^^T>()); } +}; + +template +using vtable_t = typename vtable_generator::type; + +// Trampolines translate type-erased calls to vtable functions to member +// function calls on the underlying (owned or viewed) object. +template +struct mutable_view_trampoline; + +template +struct mutable_view_trampoline { + static R operator()(void* ptr, Args... args) noexcept(Noexcept) { + return static_cast(ptr)->[:CandidateMember:]( + std::forward(args)...); + } +}; + +template +struct const_view_trampoline; + +template +struct const_view_trampoline { + static R operator()(const void* ptr, Args... args) noexcept(Noexcept) { + return static_cast(ptr)->[:CandidateMember:]( + std::forward(args)...); + } +}; + +// Builds a vtable for `T` whose entries call through to the corresponding +// member of `U`. +// For `protocol_view` some vtable entries may be unused due to const +// qualifiers. We opt for simple vtable construction logic and rely on +// generation of forwarding wrappers to determine which functions are forwarded. +template +consteval vtable_t make_view_vtable() { + vtable_t vtable{}; + + vtable.xyz_protocol_typeid = &typeid(U); + + template for (constexpr std::meta::info member : + protocol_interface_functions_of<^^T>) { + constexpr std::meta::info vtable_member = + find_vtable_entry<^^vtable_t, member>(); + using FnPtrType = typename[:type_of(vtable_member):]; + if constexpr (is_const(member)) { + constexpr std::meta::info candidate = + find_conforming_member(); + vtable.[:vtable_member:] = &const_view_trampoline::operator(); + } else /*constexpr*/ { + constexpr std::meta::info candidate = + find_conforming_member(); + vtable.[:vtable_member:] = &mutable_view_trampoline< + FnPtrType, U, candidate>::operator(); + } + } + return vtable; +} + +// The shared, compile-time vtable every protocol_view (or +// protocol_view) that views a `U` points to. +template +inline constexpr vtable_t view_vtable_for = make_view_vtable(); + +} // namespace xyz::detail + +#endif // XYZ_PROTOCOL_VTABLE_HH_