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
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,13 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT)
protocol_reflection_smoke_test
VERSION
26
LINK_LIBRARIES
protocol
FILES
protocol.h
protocol_reflection.hxx
interface_A.h
interface_A_Subset.h
interface_B.h
interface_C.h
tracking_allocator.h
Expand Down
114 changes: 114 additions & 0 deletions protocol_reflection.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,48 @@ using protocol_bases = typename[:protocol_bases_type<Interface, Allocator>():];

} // namespace reflection_detail

// protocol<Interface, Allocator>'s own nested vtable already is the owning
// vtable used for narrowing; this trait just names it the way
// protocol.h's get_owning_vtable expects, matching how the Python/libclang
// backend's own generated protocol_owning_vtable_traits<::xyz::A,
// Allocator> is a bare alias to protocol<::xyz::A, Allocator>::vtable, not
// a separately-designed type.
template <typename Interface, typename Allocator>
struct protocol_owning_vtable_traits {
using vtable = typename protocol<Interface, Allocator>::vtable;
};

// Narrows a source owning vtable to a target one by copying every entry
// the target has (by exact name -- including xyz_protocol_clone/_move/
// _destroy, which every protocol's own vtable always declares) from the
// matching entry in the source. FromVtable/ToVtable are deduced directly
// from the pointer arguments, not from an Interface/Allocator template
// argument list: unlike the Python/libclang backend, where a per-interface
// generated overload hardcodes its own target type, this one generic
// definition works for any (FromInterface, ToInterface, Allocator), since
// it reflects on the two concrete vtable struct types themselves rather
// than needing to know which interfaces they came from. Found via ADL
// from get_owning_vtable (protocol.h): FromVtable/ToVtable are nested
// types of xyz::protocol<...>, so this must live in namespace xyz, not
// xyz::reflection_detail, to be visible there.
template <typename FromVtable, typename ToVtable>
void map_owning_vtable_members(const FromVtable* from, ToVtable* to) {
template for (constexpr std::meta::info to_member :
std::define_static_array(std::meta::nonstatic_data_members_of(
^^ToVtable, std::meta::access_context::current()))) {
constexpr std::string_view name = std::meta::identifier_of(to_member);
constexpr std::meta::info from_member =
reflection_detail::find_data_member(^^FromVtable, name);
to->[:to_member:] = from->[:from_member:];
}
}

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

using clone_or_move_fn = void* (*)(void*, const Allocator&);
using destroy_fn = void (*)(void*, const Allocator&);
Expand Down Expand Up @@ -339,6 +377,82 @@ class protocol : public reflection_detail::protocol_bases<T, Allocator> {
allocator_traits::is_always_equal::value)
: protocol(std::allocator_arg_t{}, other.alloc_, std::move(other)) {}

template <typename Other>
requires(!std::same_as<Other, T>)
constexpr protocol(std::allocator_arg_t, const Allocator& alloc,
const protocol<Other, Allocator>& other)
: alloc_(alloc) {
if (!other.valueless_after_move()) {
p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_);
vtable_ = get_owning_vtable<Other, T, Allocator>(other.vtable_);
} else {
p_ = nullptr;
vtable_ = nullptr;
}
}

template <typename Other>
requires(!std::same_as<Other, T>)
constexpr protocol(const protocol<Other, Allocator>& other)
: alloc_(allocator_traits::select_on_container_copy_construction(
other.alloc_)) {
if (!other.valueless_after_move()) {
p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_);
vtable_ = get_owning_vtable<Other, T, Allocator>(other.vtable_);
} else {
p_ = nullptr;
vtable_ = nullptr;
}
}

template <typename Other>
requires(!std::same_as<Other, T>)
constexpr protocol(
std::allocator_arg_t, const Allocator& alloc,
protocol<Other, Allocator>&&
other) noexcept(allocator_traits::is_always_equal::value)
: alloc_(alloc) {
if (alloc_ == other.alloc_) {
p_ = std::exchange(other.p_, nullptr);
vtable_ = get_owning_vtable<Other, T, Allocator>(
std::exchange(other.vtable_, nullptr));
} else {
if (!other.valueless_after_move()) {
p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_);
vtable_ = get_owning_vtable<Other, T, Allocator>(other.vtable_);
other.vtable_->xyz_protocol_destroy(other.p_, other.alloc_);
other.p_ = nullptr;
other.vtable_ = nullptr;
} else {
p_ = nullptr;
vtable_ = nullptr;
}
}
}

template <typename Other>
requires(!std::same_as<Other, T>)
constexpr protocol(protocol<Other, Allocator>&& other) noexcept(
allocator_traits::is_always_equal::value)
: alloc_(other.alloc_) {
if (alloc_ == other.alloc_) {
p_ = std::exchange(other.p_, nullptr);
vtable_ = get_owning_vtable<Other, T, Allocator>(
std::exchange(other.vtable_, nullptr));
} else {
if (!other.valueless_after_move()) {
p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_);
vtable_ = get_owning_vtable<Other, T, Allocator>(other.vtable_);
other.vtable_->xyz_protocol_destroy(other.p_, other.alloc_);
other.p_ = nullptr;
other.vtable_ = nullptr;
} else {
p_ = nullptr;
vtable_ = nullptr;
}
}
}

constexpr bool valueless_after_move() const noexcept { return p_ == nullptr; }

~protocol() {
Expand Down
34 changes: 26 additions & 8 deletions protocol_reflection_detail/naming.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include <string>
#include <string_view>

#include "protocol_reflection_detail/types.hxx"

namespace xyz::reflection_detail {

// `identifier_safe_string` must run at compile time.
Expand Down Expand Up @@ -68,15 +70,31 @@ consteval std::string vtable_entry_name(std::meta::info member) {
return std::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.
// A vtable struct's entry name for `member`, unique per exact overload
// (unlike vtable_entry_name above): interface C's overloaded `compute`s
// need one distinct entry each despite sharing one forwarder name.
//
// Built from the return type, parameter types, and constness individually,
// not from std::meta::display_string_of(member): a member's own display
// string is qualified by its declaring class (e.g. "int A::compute(int)"),
// which would make A::name and A_Subset::name get different names despite
// being the same conceptual member for narrowing's cross-interface matching
// to find.
consteval std::string vtable_slot_name(std::meta::info member) {
return identifier_safe_string(std::meta::display_string_of(member));
std::string result =
identifier_safe_string(std::string(std::meta::identifier_of(member)));
result += "_";
result += identifier_safe_string(std::meta::display_string_of(
std::meta::dealias(std::meta::return_type_of(member))));
for (std::meta::info parameter_type : parameter_types_of(member)) {
result += "_";
result +=
identifier_safe_string(std::meta::display_string_of(parameter_type));
}
if (std::meta::is_const(member)) {
result += "_const";
}
return result;
}

// The data member of `struct_type` named `name`, for reading or writing
Expand Down
170 changes: 170 additions & 0 deletions protocol_reflection_smoke_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// interface, keeping this file's compile/link surface minimal.
#include <gtest/gtest.h>

#include <atomic>
#include <cstddef>
#include <memory>
#include <string>
#include <string_view>
#include <thread>
#include <type_traits>
#include <vector>

#include "interface_A.h"
#include "interface_A_Subset.h"
#include "interface_B.h"
#include "interface_C.h"
#include "protocol.h"
Expand Down Expand Up @@ -297,4 +300,171 @@ TEST(ProtocolReflectionSmoke, AllocatorExtendedMoveFromValueless) {
EXPECT_TRUE(ppp.valueless_after_move());
}

// Narrowing conversions: A (name, count) -> A_Subset (name only), the
// machinery wired into protocol_reflection.hxx's converting constructors
// and get_owning_vtable/get_mapped_vtable (protocol.cc, backend-agnostic).

TEST(ProtocolReflectionSmoke, NarrowingCopyConversion) {
xyz::protocol<xyz::A, std::allocator<std::byte>> p(
std::allocator_arg, std::allocator<std::byte>(),
std::in_place_type<ALike>);
xyz::protocol<xyz::A_Subset, std::allocator<std::byte>> p_subset(p);
EXPECT_FALSE(p.valueless_after_move());
EXPECT_FALSE(p_subset.valueless_after_move());
EXPECT_EQ(p.name(), "ALike");
EXPECT_EQ(p_subset.name(), "ALike");
}

TEST(ProtocolReflectionSmoke, NarrowingMoveConversionEqualAllocators) {
xyz::protocol<xyz::A, std::allocator<std::byte>> p(
std::allocator_arg, std::allocator<std::byte>(),
std::in_place_type<ALike>);
xyz::protocol<xyz::A_Subset, std::allocator<std::byte>> p_subset =
std::move(p);
EXPECT_TRUE(p.valueless_after_move());
EXPECT_FALSE(p_subset.valueless_after_move());
EXPECT_EQ(p_subset.name(), "ALike");
}

TEST(ProtocolReflectionSmoke, NarrowingCopyConversionFromValueless) {
xyz::protocol<xyz::A, std::allocator<std::byte>> p(
std::allocator_arg, std::allocator<std::byte>(),
std::in_place_type<ALike>);
xyz::protocol<xyz::A, std::allocator<std::byte>> p2 = std::move(p);
EXPECT_TRUE(p.valueless_after_move());
xyz::protocol<xyz::A_Subset, std::allocator<std::byte>> p_subset(p);
EXPECT_TRUE(p_subset.valueless_after_move());
}

TEST(ProtocolReflectionSmoke, CountAllocationsForNarrowingCopyConversion) {
unsigned alloc_counter = 0;
unsigned dealloc_counter = 0;
{
xyz::protocol<xyz::A, xyz::TrackingAllocator<std::byte>> p(
std::allocator_arg,
xyz::TrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
EXPECT_EQ(alloc_counter, 1u);
xyz::protocol<xyz::A_Subset, xyz::TrackingAllocator<std::byte>> p_subset(p);
EXPECT_EQ(alloc_counter, 2u);
EXPECT_EQ(dealloc_counter, 0u);
}
EXPECT_EQ(alloc_counter, 2u);
EXPECT_EQ(dealloc_counter, 2u);
}

TEST(ProtocolReflectionSmoke, CountAllocationsForNarrowingMoveConversion) {
unsigned alloc_counter = 0;
unsigned dealloc_counter = 0;
{
xyz::protocol<xyz::A, xyz::TrackingAllocator<std::byte>> a(
std::allocator_arg,
xyz::TrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
// Equal allocators: the pointer is stolen, no new allocation.
xyz::protocol<xyz::A_Subset, xyz::TrackingAllocator<std::byte>> aa(
std::move(a));
EXPECT_TRUE(a.valueless_after_move());
EXPECT_FALSE(aa.valueless_after_move());
EXPECT_EQ(alloc_counter, 1u);
EXPECT_EQ(dealloc_counter, 0u);
}
EXPECT_EQ(alloc_counter, 1u);
EXPECT_EQ(dealloc_counter, 1u);
}

TEST(ProtocolReflectionSmoke, NarrowingCopyConversionNonEqualAllocators) {
unsigned alloc_counter = 0;
unsigned dealloc_counter = 0;
{
xyz::protocol<xyz::A, NonEqualTrackingAllocator<std::byte>> p(
std::allocator_arg,
NonEqualTrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
EXPECT_EQ(alloc_counter, 1u);
NonEqualTrackingAllocator<std::byte> target_alloc(&alloc_counter,
&dealloc_counter);
xyz::protocol<xyz::A_Subset, NonEqualTrackingAllocator<std::byte>> p_subset(
std::allocator_arg, target_alloc, p);
EXPECT_FALSE(p.valueless_after_move());
EXPECT_FALSE(p_subset.valueless_after_move());
EXPECT_EQ(p_subset.name(), "ALike");
// 1 allocation for the source, 1 more for the cloned target.
EXPECT_EQ(alloc_counter, 2u);
EXPECT_EQ(dealloc_counter, 0u);
}
EXPECT_EQ(alloc_counter, 2u);
EXPECT_EQ(dealloc_counter, 2u);
}

TEST(ProtocolReflectionSmoke, NarrowingMoveConversionNonEqualAllocators) {
unsigned alloc_counter = 0;
unsigned dealloc_counter = 0;
{
xyz::protocol<xyz::A, NonEqualTrackingAllocator<std::byte>> p(
std::allocator_arg,
NonEqualTrackingAllocator<std::byte>(&alloc_counter, &dealloc_counter),
std::in_place_type<ALike>);
EXPECT_EQ(alloc_counter, 1u);
// Non-equal allocators force move-construction into new storage on the
// target's allocator, and the source is destroyed immediately.
xyz::protocol<xyz::A_Subset, NonEqualTrackingAllocator<std::byte>> p_subset(
std::move(p));
EXPECT_TRUE(p.valueless_after_move());
EXPECT_FALSE(p_subset.valueless_after_move());
EXPECT_EQ(p_subset.name(), "ALike");
EXPECT_EQ(alloc_counter, 2u);
EXPECT_EQ(dealloc_counter, 1u);
}
EXPECT_EQ(alloc_counter, 2u);
EXPECT_EQ(dealloc_counter, 2u);
}

TEST(ProtocolReflectionSmoke, NarrowingMoveConversionFromValueless) {
xyz::protocol<xyz::A, std::allocator<std::byte>> p(
std::allocator_arg, std::allocator<std::byte>(),
std::in_place_type<ALike>);
xyz::protocol<xyz::A, std::allocator<std::byte>> p2 = std::move(p);
EXPECT_TRUE(p.valueless_after_move());
xyz::protocol<xyz::A_Subset, std::allocator<std::byte>> p_subset =
std::move(p);
EXPECT_TRUE(p_subset.valueless_after_move());
}

// Owning-conversion slice of protocol_test.cc's
// NarrowingConversionConcurrentStressing: exercises protocol.cc's
// get_mapped_vtable caching/locking under contention.
TEST(ProtocolReflectionSmoke, NarrowingConversionConcurrentStressing) {
constexpr int kNumThreads = 20;
constexpr int kIterationsPerThread = 50;
std::vector<std::thread> threads;
std::atomic<bool> start_signal{false};

for (int i = 0; i < kNumThreads; ++i) {
threads.emplace_back([&start_signal]() {
while (!start_signal.load()) {
std::this_thread::yield();
}
for (int iter = 0; iter < kIterationsPerThread; ++iter) {
xyz::protocol<xyz::A, std::allocator<std::byte>> p(
std::in_place_type<ALike>);
xyz::protocol<xyz::A_Subset, std::allocator<std::byte>> p_subset =
std::move(p);
EXPECT_EQ(p_subset.name(), "ALike");

xyz::protocol<xyz::A, std::allocator<char>> p_char(
std::in_place_type<ALike>);
xyz::protocol<xyz::A_Subset, std::allocator<char>> p_subset_char =
std::move(p_char);
EXPECT_EQ(p_subset_char.name(), "ALike");
}
});
}

start_signal.store(true);
for (auto& thread : threads) {
thread.join();
}
}

} // namespace
Loading