diff --git a/reflection/protocol.hh b/reflection/protocol.hh index 1bed2ef..3557f50 100644 --- a/reflection/protocol.hh +++ b/reflection/protocol.hh @@ -22,11 +22,13 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // A C++26-reflection-based implementation of protocol and protocol_view. // -// Member function stubs are synthesised at compile time for every public -// non-special member function declared in the Interface type. +// protocol_view has a minimal but working implementation: functions are +// dispatched at runtime using a vtable. // -// The stubs are currently unimplemented beyond providing member function -// signatures. +// protocol currently supports only compile-time signature checks. +// +// Neither implementation currently supports overloaded member functions or +// operators. #include #include @@ -101,8 +103,9 @@ consteval bool member_function_conforms_to(std::meta::info candidate, } // The named, non-static, non-special member functions of `Type`. -// TODO(jbcoe): Handle static functions as they can be used to satisfy interface -// conformance. +// +// TODO(jbcoe): Handle static functions as they can be used to satisfy +// interface conformance. template constexpr inline auto protocol_interface_functions_of = std::define_static_array( @@ -111,39 +114,79 @@ constexpr inline auto protocol_interface_functions_of = std::views::filter(std::not_fn(std::meta::is_static_member)) | std::views::filter(std::meta::has_identifier)); -// Vanishing-this-pointer thunk for a synthesised member stub. The thunk -// carries a single operator() whose signature mirrors one method of the -// Interface type. -template ::vtable data member with the same name as +// `Member`. vtable_generator and generate_wrapper_bases enumerate the same +// interface members using the same identifier, so a match always exists. +// `VtableType`/`Member` are template parameters for the same reason as +// protocol_interface_functions_of's `Type`. +template +consteval std::meta::info find_vtable_member() { + std::string_view target_name = identifier_of(Member); + for (std::meta::info m : + members_of(VtableType, std::meta::access_context::unprivileged())) { + if (has_identifier(m) && identifier_of(m) == target_name) return m; + } + std::unreachable(); +} + +// --------------------------------------------------------------------------- +// Vanishing-this-pointer thunk for a synthesised member function. +// +// The thunk carries a single operator() whose signature mirrors one method +// of the Interface type. +// --------------------------------------------------------------------------- +template struct method_thunk; // TODO(jbcoe): Extend this approach to handle lvalue and rvalue qualifiers. -template -struct method_thunk { - R operator()(Args... /*args*/) noexcept(IsNoexcept) +template +struct method_thunk { + static consteval std::meta::info vtable_entry() { + return find_vtable_member<^^Vtable, Member>(); + } + + // Provides member-function call syntax. Recovers the EnclosingType pointer + // through the vanishing-this-pointer cast, widens it to the enclosing + // protocol/protocol_view object, then calls through its stored vtable + // pointer's matching function pointer, passing the viewed/owned object + // (not the protocol/protocol_view wrapper itself). + R operator()(Args... args) noexcept(IsNoexcept) requires(!IsConst) { - [[maybe_unused]] auto* base = reinterpret_cast(this); - std::unreachable(); // vtable dispatch not yet implemented + auto* enclosing = reinterpret_cast(this); + auto* protocol_object = static_cast(enclosing); + const Vtable* vtable = protocol_object->vtable_; + constexpr std::meta::info entry = vtable_entry(); + return (*vtable).[:entry:](protocol_object->object_, args...); } - R operator()(Args... /*args*/) const noexcept(IsNoexcept) + R operator()(Args... args) const noexcept(IsNoexcept) requires(IsConst) { - [[maybe_unused]] const auto* base = - reinterpret_cast(this); - std::unreachable(); // vtable dispatch not yet implemented + const auto* enclosing = reinterpret_cast(this); + const auto* protocol_object = static_cast(enclosing); + const Vtable* vtable = protocol_object->vtable_; + constexpr std::meta::info entry = vtable_entry(); + // IsConst is true on this overload, so vtable_generator generated this + // entry with a leading `const void*` parameter; object_ (a plain + // void*) converts to that implicitly. + return (*vtable).[:entry:](protocol_object->object_, args...); } }; -template -using fn_ptr_t = R (*)(Args...); +template +using fn_ptr_t = R (*)(Args...) noexcept(Noexcept); // A single-member base wrapping the thunk for one interface member function, // named after that method (giving the `p.method_name(args)` call syntax). -template +// `ProtocolType` and `Vtable` are threaded through to `method_thunk`; see its +// comment for why they can't be recovered from `member_base` itself. +template struct member_base_generator { struct member_base; consteval { @@ -151,7 +194,8 @@ struct member_base_generator { // Build the function-pointer type R(*)(Args...) from the method's // return type and parameter types. - std::vector fn_args{dealias(return_type_of(Member))}; + std::vector fn_args{std::meta::reflect_constant(false), + dealias(return_type_of(Member))}; std::vector member_parameters = parameters_of(Member); fn_args.append_range(member_parameters | std::views::transform(std::meta::type_of)); @@ -159,7 +203,8 @@ struct member_base_generator { // clang-format off std::meta::info thunk_type = substitute( - ^^method_thunk, {fn_ptr_type, ^^member_base, + ^^method_thunk, {fn_ptr_type, ^^member_base, ^^ProtocolType, ^^Vtable, + std::meta::reflect_constant(Member), std::meta::reflect_constant(is_const(Member)), std::meta::reflect_constant(is_noexcept(Member))}); @@ -173,40 +218,169 @@ struct member_base_generator { } }; -template -using member_base_generator_t = member_base_generator::member_base; +template +using member_base_generator_t = + member_base_generator::member_base; // Combines the single-member base types produced by `member_base_generator` // into one type via multiple inheritance. template -struct stub_bases : MemberBases... {}; +struct wrapper_bases : MemberBases... {}; -// Returns a `stub_bases` specialisation with one base per public, non-special, -// member function of `interface_type`, giving named members with `operator()` -// for each. +// Returns a `wrapper_bases` specialisation with one base per public, +// non-special, member function of `interface_type`, giving named members +// with `operator()` for each. `ProtocolType`/`Vtable` are forwarded to +// `member_base_generator`. // // Two bases defining a member of the same name make that name ambiguous to // look up through the derived class, so overloaded methods are unsupported // for now. -template -consteval std::meta::info generate_stub_bases() { +template +consteval std::meta::info generate_wrapper_bases() { std::vector member_base_types; for (std::meta::info member : protocol_interface_functions_of) { // clang-format off member_base_types.push_back(dealias( - substitute(^^member_base_generator_t, {reflect_constant(member)})) + substitute(^^member_base_generator_t, + {reflect_constant(member), ^^ProtocolType, ^^Vtable})) ); // clang-format on } - return substitute(^^stub_bases, member_base_types); + return substitute(^^wrapper_bases, member_base_types); +} + +// The generated wrapper type for `T`: a `wrapper_bases` specialisation with +// named members with `operator()` for each public, non-special, member +// function from `T`. `ProtocolType` is the enclosing protocol/protocol_view +// specialisation (protocol or protocol_view) and `Vtable` +// is its vtable_generator::vtable: each thunk needs both to reach +// ProtocolType's `vtable_` pointer and call through it. +template +using protocol_wrappers_t = + typename[:generate_wrapper_bases<^^T, ProtocolType, Vtable>():]; + +// --------------------------------------------------------------------------- +// 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. +// +// Because C++ disallows two data members with the same name inside the same +// class, overloaded methods will cause compile-time errors. +// We will address this limitation in a follow-up PR. +// --------------------------------------------------------------------------- +consteval std::vector generate_vtable_specs( + std::meta::info interface_type) { + std::vector function_pointer_specs; + + std::ranges::range auto members = + std::define_static_array(members_of( + interface_type, std::meta::access_context::unprivileged())) | + std::views::filter(std::meta::is_function) | + std::views::filter(std::not_fn(std::meta::is_static_member)) | + std::views::filter(std::meta::has_identifier); + + for (std::meta::info member : members) { + std::string_view name = identifier_of(member); + + // 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{ + std::meta::reflect_constant(is_noexcept(member)), + 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); + + function_pointer_specs.push_back(data_member_spec( + fn_ptr_type, std::meta::data_member_options{.name = name})); + } + return function_pointer_specs; } -// The generated stub type for `T`: a `stub_bases` specialisation with named -// members with `operator()` for each public, non-special, member function -// from `T`. +// Generates a vtable with named function pointers for each public, +// non-special, member function from `T`. template -using protocol_stubs_t = typename[:generate_stub_bases<^^T>():]; +struct vtable_generator { + struct vtable; + consteval { define_aggregate(^^vtable, generate_vtable_specs(^^T)); } +}; + +// Finds the member of `CandidateType` that structurally conforms to +// `Member`, using the same matching rule as is_protocol_conformant. +template +consteval std::meta::info find_conforming_member() { + for (std::meta::info candidate : + protocol_interface_functions_of) { + if (member_function_conforms_to(candidate, Member)) return candidate; + } + std::unreachable(); +} + +// Recovers a `U*`/`const U*` from the type-erased pointer a vtable entry is +// called with, then calls the matching member of `U`. +template +struct mutable_view_trampoline; + +template +struct mutable_view_trampoline { + static R call(void* ptr, Args... args) noexcept(Noexcept) { + return static_cast(ptr)->[:CandidateMember:](args...); + } +}; + +template +struct const_view_trampoline; + +template +struct const_view_trampoline { + static R call(const void* ptr, Args... args) noexcept(Noexcept) { + return static_cast(ptr)->[:CandidateMember:](args...); + } +}; + +// Builds a vtable for `T` whose entries call through to the corresponding +// member of `U`. Every entry is populated: protocol_view's constructor only +// accepts a non-const U (see its `!std::is_const_v` constraint), so a +// sound pointer to call any member, const or mutating, through is always +// available. +template +consteval typename vtable_generator::vtable make_view_vtable() { + using Vtable = typename vtable_generator::vtable; + Vtable result{}; + + template for (constexpr std::meta::info member : + protocol_interface_functions_of<^^T>) { + constexpr std::meta::info candidate = find_conforming_member(); + constexpr std::meta::info vtable_member = + find_vtable_member<^^Vtable, member>(); + using FnPtrType = typename[:type_of(vtable_member):]; + if constexpr (is_const(member)) { + result.[:vtable_member:] = &const_view_trampoline::call; + } else { + result.[:vtable_member:] = &mutable_view_trampoline::call; + } + } + return result; +} + +// The shared, compile-time vtable every protocol_view that views a `U` +// points to. +template +inline constexpr typename vtable_generator::vtable view_vtable_for = + make_view_vtable(); } // namespace detail @@ -245,7 +419,9 @@ inline constexpr bool is_protocol_conformant_v = is_protocol_conformant(); template > -class protocol : public detail::protocol_stubs_t { +class protocol : public detail::protocol_wrappers_t< + T, protocol, + typename detail::vtable_generator::vtable> { public: protocol() = delete; // Deleted as `T` is used as an interface type. @@ -273,10 +449,22 @@ class protocol : public detail::protocol_stubs_t { requires is_protocol_conformant_v> && (!is_protocol_v>) explicit protocol(std::in_place_type_t, Ts&&... ts); + + private: + // Grants the synthesised member thunks access to `vtable_` so they can + // locate and call through the matching vtable entry. + template + friend struct detail::method_thunk; + + const typename detail::vtable_generator::vtable* vtable_; }; template -class protocol_view : public detail::protocol_stubs_t { +class protocol_view + : public detail::protocol_wrappers_t< + T, protocol_view, typename detail::vtable_generator::vtable> { public: // The default constructor is deleted as a default constructed // `protocol_view` would be empty. @@ -289,11 +477,31 @@ class protocol_view : public detail::protocol_stubs_t { protocol_view& operator=(protocol_view&&) noexcept = default; ~protocol_view() = default; - // Construct from any type U that conforms to the Interface T. + // Construct from any non-const type U that conforms to the Interface T. + // U being const is rejected unconditionally, regardless of whether T + // actually declares any non-const methods: a simple, T-independent rule + // is easier to reason about than one that only rejects const U when it + // would actually be unsound. template requires is_protocol_conformant_v> && - (!is_protocol_view_v>) - explicit protocol_view(const U& object); + (!is_protocol_view_v>) && + (!std::is_const_v) + explicit protocol_view(U& object) + : object_(static_cast(std::addressof(object))), + vtable_(&detail::view_vtable_for) {} + + private: + // Grants the synthesised member thunks access to `object_`/`vtable_` so + // they can locate and call through the matching vtable entry. + template + friend struct detail::method_thunk; + + // Non-owning pointer to the viewed object. + void* object_ = nullptr; + + const typename detail::vtable_generator::vtable* vtable_; }; } // namespace xyz::reflection diff --git a/reflection/protocol_test.cc b/reflection/protocol_test.cc index 0781161..22e7e4c 100644 --- a/reflection/protocol_test.cc +++ b/reflection/protocol_test.cc @@ -359,9 +359,29 @@ TEST(ReflectionProtocolViewTest, IsConstructibleFromConformingType) { struct NonConforming {}; - static_assert(std::is_constructible_v, Conforming>); + // protocol_view's constructor takes U&, so constructibility is checked + // from an lvalue, not a prvalue. + static_assert(std::is_constructible_v, Conforming&>); static_assert( - !std::is_constructible_v, NonConforming>); + !std::is_constructible_v, NonConforming&>); +} + +TEST(ReflectionProtocolViewTest, NotConstructibleFromConstObject) { + // protocol_view rejects a const object unconditionally: even though + // Interface has no non-const methods (so a const object would actually + // be safe to dispatch through), construction from one is still rejected, + // because the rule doesn't inspect Interface at all. + struct Interface { + int get() const; + }; + + struct Conforming { + int get() const { return 0; } + }; + + static_assert(std::is_constructible_v, Conforming&>); + static_assert( + !std::is_constructible_v, const Conforming&>); } TEST(ReflectionProtocolTest, IsConstructibleInPlaceFromConformingType) { @@ -403,10 +423,13 @@ TEST(ReflectionProtocolViewTest, ConstMemberFunction) { int get_value() const; }; - // TODO(jbcoe): replace static assertion with runtime test. - static_assert(requires(const protocol_view& p) { - { p.get_value() } -> std::same_as; - }); + struct Conforming { + int get_value() const { return 42; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.get_value(), 42); } TEST(ReflectionProtocolViewTest, NonConstMemberFunctionNotInvocableFromConst) { @@ -427,10 +450,16 @@ TEST(ReflectionProtocolViewTest, SingleParameterMemberFunction) { void update(int value); }; - // TODO(jbcoe): replace static assertion with runtime test. - static_assert(requires(protocol_view& p) { - { p.update(0) } -> std::same_as; - }); + struct Conforming { + int last_value = 0; + + void update(int value) { last_value = value; } + }; + + Conforming c; + protocol_view p(c); + p.update(42); + EXPECT_EQ(c.last_value, 42); } TEST(ReflectionProtocolViewTest, NoexceptMemberFunction) { @@ -438,10 +467,13 @@ TEST(ReflectionProtocolViewTest, NoexceptMemberFunction) { double compute(double input) noexcept; }; - // TODO(jbcoe): replace static assertion with runtime test. - static_assert(requires(protocol_view& p) { - { p.compute(0.0) } noexcept -> std::same_as; - }); + struct Conforming { + double compute(double input) noexcept { return input * 2.0; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.compute(21.0), 42.0); } TEST(ReflectionProtocolViewTest, MultiParameterMemberFunction) { @@ -449,10 +481,13 @@ TEST(ReflectionProtocolViewTest, MultiParameterMemberFunction) { int add(int a, int b) const; }; - // TODO(jbcoe): replace static assertion with runtime test. - static_assert(requires(const protocol_view& p) { - { p.add(1, 2) } -> std::same_as; - }); + struct Conforming { + int add(int a, int b) const { return a + b; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.add(1, 2), 3); } TEST(ReflectionProtocolViewTest, VoidMemberFunction) { @@ -460,10 +495,16 @@ TEST(ReflectionProtocolViewTest, VoidMemberFunction) { void reset(); }; - // TODO(jbcoe): replace static assertion with runtime test. - static_assert(requires(protocol_view& p) { - { p.reset() } -> std::same_as; - }); + struct Conforming { + bool was_reset = false; + + void reset() { was_reset = true; } + }; + + Conforming c; + protocol_view p(c); + p.reset(); + EXPECT_TRUE(c.was_reset); } TEST(ReflectionProtocolViewTest, MultipleMemberFunctions) { @@ -472,11 +513,38 @@ TEST(ReflectionProtocolViewTest, MultipleMemberFunctions) { double multiply(double x, double y) const noexcept; }; - // TODO(jbcoe): replace static assertion with runtime test. - static_assert(requires(const protocol_view& p) { - { p.add(1.0, 2.0) } noexcept -> std::same_as; - { p.multiply(3.0, 4.0) } noexcept -> std::same_as; - }); + struct Conforming { + double add(double x, double y) const noexcept { return x + y; } + + double multiply(double x, double y) const noexcept { return x * y; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.add(1.0, 2.0), 3.0); + EXPECT_EQ(p.multiply(3.0, 4.0), 12.0); +} + +TEST(ReflectionProtocolViewTest, MixedConstAndMutatingMemberFunctions) { + struct Interface { + int get() const; + void set(int value); + }; + + struct Conforming { + int value = 0; + + int get() const { return value; } + + void set(int new_value) { value = new_value; } + }; + + Conforming c; + protocol_view p(c); + EXPECT_EQ(p.get(), 0); + p.set(7); + EXPECT_EQ(p.get(), 7); + EXPECT_EQ(c.value, 7); } // Member function signature tests for protocol.