Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT)
protocol.h
protocol_reflection.hxx
interface_A.h
interface_C.h
protocol_reflection_smoke_test.cc)
target_compile_options(protocol_reflection_smoke_test
PRIVATE -freflection)
Expand Down
138 changes: 97 additions & 41 deletions protocol_reflection.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -47,63 +47,109 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
namespace xyz {
namespace reflection_detail {

// The data member of `struct_type` named `name`, as produced by
// naming.hxx's escaping: for splicing a value into a reflection-built
// vtable's entry, once the entry's own name (not just its position) is
// the only thing identifying it.
consteval std::meta::info find_data_member(std::meta::info struct_type,
std::string_view name) {
for (std::meta::info member : std::meta::nonstatic_data_members_of(
struct_type, std::meta::access_context::current())) {
if (std::meta::identifier_of(member) == name) return member;
// Every member of `interface` sharing `member`'s own name, in declaration
// order: the sibling overloads a forwarder's single named data member has
// to merge into one callable (e.g. interface C's overloaded `compute`s).
consteval std::vector<std::meta::info> members_named_like(
std::meta::info interface, std::meta::info member) {
std::string_view name = std::meta::identifier_of(member);
std::vector<std::meta::info> result;
for (std::meta::info candidate : interface_member_functions(interface)) {
if (std::meta::has_identifier(candidate) &&
std::meta::identifier_of(candidate) == name) {
result.push_back(candidate);
}
}
throw std::meta::exception("data member not found", ^^void);
return result;
}

// A per-interface-member forwarder, giving protocol<Interface, Allocator>
// ordinary call syntax for that member with no splicing at any call site.
// operator() is a member function template because its return type and
// arguments vary per Member, which a single fixed declaration can't
// express; unlike conformance.hxx's candidate forwarders, there is exactly
// one operator() per Wrapper here, so a generic template creates no
// overload-resolution ambiguity to worry about.
template <typename Interface, typename Allocator, std::meta::info Member>
struct protocol_member_wrapper {
template <typename... Args>
auto operator()(Args&&... args) const
noexcept(std::meta::is_noexcept(Member));
// A concrete, non-generic forwarder for one interface overload: its
// operator() has that overload's own parameter types rather than a
// forwarding template, so overload resolution across several such
// forwarders (protocol_member_wrapper_combinator) ranks them the way it
// would rank the interface's own overloads. Same reason
// conformance.hxx's single_candidate_forwarder is concrete, one level up:
// there it merges an implementation's candidates for one interface member;
// here it merges an interface's own overloads sharing one name.
template <typename Interface, typename Allocator, std::meta::info Member,
typename R, typename... Ps>
struct protocol_single_overload_wrapper {
R operator()(Ps... ps) const noexcept(std::meta::is_noexcept(Member));
};

// Merges N single-overload forwarders into one callable type via
// `using Bases::operator()...`, exactly conformance.hxx's
// candidate_overload_set technique, applied to an interface's own
// overloads instead of an implementation's candidates.
template <typename... Bases>
struct protocol_member_wrapper_combinator : Bases... {
using Bases::operator()...;
};

// The merged forwarder type for the whole name-group `member` belongs to
// (a single overload if `member` isn't overloaded). A plain function, not
// a function template keyed only on Member: every member in the group
// produces the same result, so protocol_single_overload_wrapper's
// operator() can recompute it from just its own Member, without
// protocol_bases_type threading the whole group through separately.
consteval std::meta::info protocol_member_wrapper_type(
std::meta::info interface, std::meta::info allocator,
std::meta::info member) {
std::vector<std::meta::info> base_types;
for (std::meta::info sibling : members_named_like(interface, member)) {
std::vector<std::meta::info> args{
interface, allocator, std::meta::reflect_constant(sibling),
std::meta::dealias(std::meta::return_type_of(sibling))};
for (std::meta::info parameter_type : parameter_types_of(sibling)) {
args.push_back(parameter_type);
}
base_types.push_back(
std::meta::substitute(^^protocol_single_overload_wrapper, args));
}
return std::meta::substitute(^^protocol_member_wrapper_combinator,
base_types);
}

template <typename Interface, typename Allocator, std::meta::info Member>
using protocol_member_wrapper =
typename[:protocol_member_wrapper_type(^^Interface, ^^Allocator, Member):];

template <typename Interface, typename Allocator>
consteval std::meta::info protocol_bases_type() {
std::vector<std::meta::info> bases;
for (std::meta::info member : interface_member_functions(^^Interface)) {
std::meta::info wrapper_type = std::meta::substitute(
^^protocol_member_wrapper,
{
^^Interface, ^^Allocator, std::meta::reflect_constant(member)});
// Every member in a name-group produces the same
// members_named_like(...)[0]; only process the group once, at its
// first member, so an overloaded name doesn't get a forwarder_base
// (and hence a same-named data member) built once per overload.
std::meta::info representative = members_named_like(^^Interface, member)[0];
if (representative != member) {
continue;
}
std::meta::info wrapper_type =
protocol_member_wrapper_type(^^Interface, ^^Allocator, member);
bases.push_back(std::meta::substitute(
^^forwarder_base,
{
wrapper_type, std::meta::reflect_constant(member)}));
wrapper_type, std::meta::reflect_constant(representative)}));
}
return forwarders_type(bases);
}

// protocol<Interface, Allocator>'s own base list: one forwarder_base per
// interface member, combined via forwarders.hxx's combinator. protocol
// inherits this directly (not through an intermediate type) so that each
// distinct member name, combined via forwarders.hxx's combinator. protocol
// inherits this directly, not through an intermediate type, so each
// Wrapper's base-to-derived static_cast to protocol<Interface, Allocator>
// (defined out of line below, once protocol is complete) is valid.
// is valid once protocol is complete.
template <typename Interface, typename Allocator>
using protocol_bases = typename[:protocol_bases_type<Interface, Allocator>():];

} // namespace reflection_detail

template <typename T, typename Allocator>
class protocol : public reflection_detail::protocol_bases<T, Allocator> {
template <typename, typename, std::meta::info>
friend struct reflection_detail::protocol_member_wrapper;
template <typename, typename, std::meta::info, typename, typename...>
friend struct reflection_detail::protocol_single_overload_wrapper;

using clone_or_move_fn = void* (*)(void*, const Allocator&);
using destroy_fn = void (*)(void*, const Allocator&);
Expand Down Expand Up @@ -184,7 +230,7 @@ class protocol : public reflection_detail::protocol_bases<T, Allocator> {
template for (constexpr std::meta::info member : std::define_static_array(
reflection_detail::interface_member_functions(^^T))) {
constexpr std::meta::info entry = reflection_detail::find_data_member(
^^vtable, reflection_detail::vtable_entry_name(member));
^^vtable, reflection_detail::vtable_slot_name(member));
constexpr std::meta::info merged_type =
reflection_detail::candidate_overload_set_type(
reflection_detail::resolve_implementation_candidates(
Expand Down Expand Up @@ -277,17 +323,27 @@ class protocol : public reflection_detail::protocol_bases<T, Allocator> {

namespace reflection_detail {

template <typename Interface, typename Allocator, std::meta::info Member>
template <typename... Args>
auto protocol_member_wrapper<Interface, Allocator, Member>::operator()(
Args&&... args) const noexcept(std::meta::is_noexcept(Member)) {
template <typename Interface, typename Allocator, std::meta::info Member,
typename R, typename... Ps>
R protocol_single_overload_wrapper<Interface, Allocator, Member, R,
Ps...>::operator()(Ps... ps) const
noexcept(std::meta::is_noexcept(Member)) {
using Owner = protocol<Interface, Allocator>;
using Base = forwarder_base<protocol_member_wrapper, Member>;
const auto* base = static_cast<const Base*>(static_cast<const void*>(this));
using Combined = protocol_member_wrapper<Interface, Allocator, Member>;
// The same representative every group member recomputes identically, so
// this names the exact forwarder_base specialization Combined is wrapped
// in as its sole member, regardless of which sibling overload this
// wrapper is for.
constexpr std::meta::info representative =
members_named_like(^^Interface, Member)[0];
using Base = forwarder_base<Combined, representative>;
const auto* combined = static_cast<const Combined*>(this);
const auto* base =
static_cast<const Base*>(static_cast<const void*>(combined));
const auto* owner = static_cast<const Owner*>(base);
constexpr std::meta::info entry =
find_data_member(^^typename Owner::vtable, vtable_entry_name(Member));
return owner->vtable_->[:entry:](owner->p_, std::forward<Args>(args)...);
find_data_member(^^typename Owner::vtable, vtable_slot_name(Member));
return owner->vtable_->[:entry:](owner->p_, std::forward<Ps>(ps)...);
}

} // namespace reflection_detail
Expand Down
29 changes: 28 additions & 1 deletion protocol_reflection_detail/naming.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,38 @@ constexpr std::string identifier_safe_string(std::string_view s) {

// The generated vtable-entry name for a reflected member function or data
// member, found by name. Operators, which have no identifier_of, need
// separate handling.
// separate handling. Deliberately identifier-only, not signature-qualified:
// this names a forwarder's single public-facing data member (forwarders.hxx),
// which must stay callable as e.g. `.compute(...)` regardless of how many
// overloads `compute` has. Every overload of the same name shares this
// name on purpose.
consteval std::string vtable_entry_name(std::meta::info member) {
return identifier_safe_string(std::meta::identifier_of(member));
}

// A generated vtable struct's entry name for `member`, unique per exact
// overload. Unlike vtable_entry_name above, this qualifies by the member's
// full display string (return type, parameter types, constness), not just
// its identifier: a vtable struct is an internal, never user-visible
// implementation detail with one data member per overload, so two
// overloads sharing a name (e.g. interface C's three `compute`s) need two
// distinct entries here, even though they share one forwarder name.
consteval std::string vtable_slot_name(std::meta::info member) {
return identifier_safe_string(std::meta::display_string_of(member));
}

// The data member of `struct_type` named `name`, for reading or writing
// a reflection-built struct's entry by name (a vtable_slot_name or
// vtable_entry_name result) rather than position.
consteval std::meta::info find_data_member(std::meta::info struct_type,
std::string_view name) {
for (std::meta::info member : std::meta::nonstatic_data_members_of(
struct_type, std::meta::access_context::current())) {
if (std::meta::identifier_of(member) == name) return member;
}
throw std::meta::exception("data member not found", ^^void);
}

} // namespace xyz::reflection_detail

#endif // XYZ_PROTOCOL_REFLECTION_DETAIL_NAMING_HXX_
37 changes: 37 additions & 0 deletions protocol_reflection_detail/naming_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace {

using xyz::reflection_detail::identifier_safe_string;
using xyz::reflection_detail::vtable_entry_name;
using xyz::reflection_detail::vtable_slot_name;

TEST(IdentifierSafeString, AlphanumericPassesThroughUnchanged) {
EXPECT_EQ(identifier_safe_string("abcXYZ123"), "abcXYZ123");
Expand Down Expand Up @@ -83,4 +84,40 @@ TEST(VtableEntryName, NamesAnOrdinaryMemberByItsIdentifier) {
EXPECT_STREQ(set_value_name, "set_5fvalue");
}

struct Overloaded {
int compute(int x) { return x; }

double compute(double x) const { return x; }
};

consteval std::meta::info compute_overload(bool take_double) {
for (std::meta::info member : std::meta::members_of(
^^Overloaded, std::meta::access_context::current())) {
if (std::meta::has_identifier(member) &&
std::meta::identifier_of(member) == "compute" &&
std::meta::is_const(member) == take_double) {
return member;
}
}
throw std::meta::exception("overload not found", ^^void);
}

TEST(VtableSlotName, DistinctOverloadsGetDistinctNames) {
constexpr const char* int_overload =
std::define_static_string(vtable_slot_name(compute_overload(false)));
constexpr const char* double_overload =
std::define_static_string(vtable_slot_name(compute_overload(true)));

EXPECT_STRNE(int_overload, double_overload);
}

TEST(VtableSlotName, SameOverloadGivesTheSameNameEveryTime) {
constexpr const char* first =
std::define_static_string(vtable_slot_name(compute_overload(false)));
constexpr const char* second =
std::define_static_string(vtable_slot_name(compute_overload(false)));

EXPECT_STREQ(first, second);
}

} // namespace
8 changes: 6 additions & 2 deletions protocol_reflection_detail/vtable_layout.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ namespace xyz::reflection_detail {

// One data_member_spec per dispatchable member of `interface` (const-only
// if `const_only`), each a named function pointer of the member's vtable
// entry type, erased through `erased_pointer_type`.
// entry type, erased through `erased_pointer_type`. Named by
// vtable_slot_name, not vtable_entry_name: a vtable is an internal struct
// with one data member per exact overload, so overloads sharing an
// identifier (e.g. interface C's overloaded `compute`s) still need
// distinct entries here.
consteval std::vector<std::meta::info> define_vtable_entries(
std::meta::info interface, std::meta::info erased_pointer_type,
bool const_only) {
Expand All @@ -55,7 +59,7 @@ consteval std::vector<std::meta::info> define_vtable_entries(
}
specs.push_back(std::meta::data_member_spec(
vtable_entry_pointer_type(member, erased_pointer_type),
{.name = vtable_entry_name(member)}));
{.name = vtable_slot_name(member)}));
}
return specs;
}
Expand Down
65 changes: 47 additions & 18 deletions protocol_reflection_detail/vtable_layout_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,44 +27,73 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
namespace {

using xyz::reflection_detail::const_view_vtable;
using xyz::reflection_detail::find_data_member;
using xyz::reflection_detail::view_vtable;
using xyz::reflection_detail::vtable_slot_name;

struct Fixture {
int value() const { return 0; }

void set_value(int) {}
};

// A template-id (e.g. const_view_vtable<Fixture>) can't be reflected
// directly with ^^ when the template is itself an alias template ("'^^'
// cannot be applied to a using-declaration"). Naming the instantiation
// through a plain, non-template alias first works around it.
using ConstViewVtableFixture = const_view_vtable<Fixture>;
using ViewVtableFixture = view_vtable<Fixture>;

// Entries are looked up by name rather than typed as literal members,
// because vtable_slot_name's output isn't a fixed, hand-writable identifier
// once an interface has overloads: it qualifies by full signature
// (naming.hxx). These tests use the same lookup real callers use in
// dispatch, rather than assuming a nice literal name.

TEST(ConstViewVtable, HasOneEntryPerConstMemberTakingAConstErasedPointer) {
constexpr std::meta::info value_entry = find_data_member(
^^ConstViewVtableFixture, vtable_slot_name(^^Fixture::value));
static_assert(
std::is_same_v<decltype(std::declval<const_view_vtable<Fixture>>().value),
int (*)(const void*)>);
std::is_same_v<
decltype(std::declval<const_view_vtable<Fixture>>().[:value_entry:]),
int (*)(const void*)>);
static_assert(sizeof(const_view_vtable<Fixture>) == sizeof(void (*)()));
}

TEST(ViewVtable, HasAConstViewSubobjectAndOneEntryPerMember) {
static_assert(
std::is_same_v<decltype(std::declval<view_vtable<Fixture>>().const_view),
const_view_vtable<Fixture>>);
static_assert(
std::is_same_v<decltype(std::declval<view_vtable<Fixture>>().value),
int (*)(void*)>);
// set_value's own literal underscore is escaped too (naming.hxx escapes
// every non-alphanumeric byte, including "_" itself): set_5fvalue.
static_assert(
std::is_same_v<decltype(std::declval<view_vtable<Fixture>>().set_5fvalue),
void (*)(void*, int)>);

constexpr std::meta::info value_entry =
find_data_member(^^ViewVtableFixture, vtable_slot_name(^^Fixture::value));
static_assert(std::is_same_v<
decltype(std::declval<view_vtable<Fixture>>().[:value_entry:]),
int (*)(void*)>);

constexpr std::meta::info set_value_entry = find_data_member(
^^ViewVtableFixture, vtable_slot_name(^^Fixture::set_value));
static_assert(std::is_same_v<decltype(std::declval<view_vtable<Fixture>>()
.[:set_value_entry:]),
void (*)(void*, int)>);
}

TEST(ViewVtable, EntriesAreCallableFunctionPointers) {
view_vtable<Fixture> vtable{
.const_view = {.value = +[](const void*) { return 42; }},
.value = +[](void*) { return 42; },
.set_5fvalue = +[](void*, int) {}};

EXPECT_EQ(vtable.const_view.value(nullptr), 42);
EXPECT_EQ(vtable.value(nullptr), 42);
vtable.set_5fvalue(nullptr, 1);
constexpr std::meta::info const_value_entry = find_data_member(
^^ConstViewVtableFixture, vtable_slot_name(^^Fixture::value));
constexpr std::meta::info value_entry =
find_data_member(^^ViewVtableFixture, vtable_slot_name(^^Fixture::value));
constexpr std::meta::info set_value_entry = find_data_member(
^^ViewVtableFixture, vtable_slot_name(^^Fixture::set_value));

view_vtable<Fixture> vtable{};
vtable.const_view.[:const_value_entry:] = +[](const void*) { return 42; };
vtable.[:value_entry:] = +[](void*) { return 42; };
vtable.[:set_value_entry:] = +[](void*, int) {};

EXPECT_EQ(vtable.const_view.[:const_value_entry:](nullptr), 42);
EXPECT_EQ(vtable.[:value_entry:](nullptr), 42);
vtable.[:set_value_entry:](nullptr, 1);
}

} // namespace
Loading
Loading