diff --git a/.clang-format b/.clang-format index 1a7aa7a..b59b976 100644 --- a/.clang-format +++ b/.clang-format @@ -1,5 +1,7 @@ ---- -Language: Cpp +# No `Language: Cpp` key: clang-format's language guesser classifies +# protocol_reflection.h (C++26 `^^` reflection syntax) as Objective-C, and a +# Cpp-only configuration makes it error out on that file. A single +# language-agnostic document applies to every language clang-format detects. # BasedOnStyle: Google AccessModifierOffset: -1 AlignAfterOpenBracket: Align @@ -153,4 +155,3 @@ StatementMacros: - QT_REQUIRE_VERSION TabWidth: 8 UseTab: Never -... diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index bcad3fd..fc3b83f 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -34,7 +34,7 @@ jobs: strategy: fail-fast: false matrix: - configuration: ["Release", "Debug"] + configuration: ["release", "debug"] settings: - { name: "Ubuntu GCC-11", @@ -211,6 +211,20 @@ jobs: std: 20, }, } + - { + name: "Ubuntu GCC-16 (C++26 Reflection)", + os: ubuntu-24.04, + compiler: + { + type: GCC16, + version: 16, + cc: "gcc-16", + cxx: "g++-16", + std: 26, + }, + lib: "libstdc++16", + implementation: "reflection", + } steps: - uses: actions/checkout@v4 - uses: seanmiddleditch/gha-setup-ninja@master @@ -231,9 +245,17 @@ jobs: with: version: ${{ matrix.settings.compiler.version }} platform: x64 + - name: Install GCC 16 from ubuntu-toolchain-r PPA + if: matrix.settings.compiler.type == 'GCC16' + run: | + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get update + sudo apt-get install -y --no-install-recommends g++-16 gcc-16 + echo "CC=gcc-16" >> "$GITHUB_ENV" + echo "CXX=g++-16" >> "$GITHUB_ENV" - name: Install uv uses: astral-sh/setup-uv@v5 - name: Set up Python run: uv sync - name: Run CMake - run: ./scripts/cmake.sh --${{ matrix.configuration == 'Debug' && 'debug' || 'release' }} + run: ./scripts/cmake.sh --${{ matrix.configuration }} --implementation=${{ matrix.settings.implementation || 'Python' }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 0626252..f1931ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,36 @@ option(ENABLE_UBSAN "Enable Undefined Behaviour Sanitizer" OFF) option(ENABLE_TSAN "Enable Thread Sanitizer" OFF) option(ENABLE_MSAN "Enable Memory Sanitizer" OFF) +option( + XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND + "Also build and test the C++26-reflection code-generation \ +backend (protocol_reflection.h). Requires a compiler with P2996 reflection \ +support (GCC 16+ with -freflection). The default Python code-generation \ +path is unaffected." + OFF) + +if(XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND) + include(CheckCXXSourceCompiles) + set(CMAKE_REQUIRED_FLAGS "-std=c++26 -freflection") + check_cxx_source_compiles( + " + #include + constexpr std::meta::info reflection_of_int = ^^int; + int main() {} + " + XYZ_PROTOCOL_REFLECTION_SUPPORTED) + unset(CMAKE_REQUIRED_FLAGS) + if(NOT XYZ_PROTOCOL_REFLECTION_SUPPORTED) + message( + FATAL_ERROR + "XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND is ON but the compiler " + "(${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}) does not " + "accept '-std=c++26 -freflection'. C++26 reflection currently " + "requires GCC 16 or newer; configure with e.g. " + "CXX=g++-16 CC=gcc-16 and a separate build directory (-B).") + endif() +endif() + if(ENABLE_ASAN OR ENABLE_UBSAN OR ENABLE_TSAN OR ENABLE_MSAN) set(ENABLE_SANITIZERS ON) endif() @@ -148,6 +178,47 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) target_include_directories(protocol_test PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + if(XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND) + # protocol_test.cc includes the per-interface headers directly (no + # Python-generated "generated/protocol_.h") when + # XYZ_PROTOCOL_ENABLE_REFLECTION is defined: protocol_reflection.h + # synthesizes the same machinery from the plain interface struct at + # compile time, so no code generation step or shim headers are needed. + xyz_add_test( + NAME + protocol_test_reflection + VERSION + 26 + LINK_LIBRARIES + protocol + FILES + protocol_test.cc + protocol_reflection_detail/naming_test.cc + protocol_reflection_detail/members_test.cc + protocol_reflection_detail/types_test.cc + protocol_reflection_detail/thunk_test.cc + protocol_reflection_detail/conformance_test.cc + protocol_reflection_detail/vtable_layout_test.cc + protocol_reflection.h + protocol_reflection_detail/naming.h + protocol_reflection_detail/members.h + protocol_reflection_detail/types.h + protocol_reflection_detail/thunk.h + protocol_reflection_detail/conformance.h + protocol_reflection_detail/vtable_layout.h + protocol_reflection_detail/operator_forwarders.h + interface_A.h + interface_A_Subset.h + interface_B.h + interface_C.h + interface_D.h) + target_include_directories(protocol_test_reflection + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_options(protocol_test_reflection PRIVATE -freflection) + target_compile_definitions(protocol_test_reflection + PRIVATE XYZ_PROTOCOL_ENABLE_REFLECTION) + endif() + add_executable(protocol_benchmark protocol_benchmark.cc) target_link_libraries(protocol_benchmark PRIVATE protocol benchmark::benchmark) add_dependencies(protocol_benchmark generate_protocols) @@ -170,6 +241,23 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) --flags=-I${CMAKE_CURRENT_BINARY_DIR} --flags=-I${CMAKE_CURRENT_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + + if(XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND) + # No generated-header include path is needed here (unlike + # concept_errors_test above): the reflection backend synthesizes its + # machinery from a plain struct at compile time. + add_test( + NAME concept_errors_test_reflection + COMMAND + ${Python3_EXECUTABLE} -m pytest + ${CMAKE_CURRENT_SOURCE_DIR}/scripts/test_concept_errors_reflection.py + --compiler=${CMAKE_CXX_COMPILER} + --flags=-std=c++26 + --flags=-freflection + --flags=-DXYZ_PROTOCOL_ENABLE_REFLECTION + --flags=-I${CMAKE_CURRENT_SOURCE_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + endif() endif() endif(${BUILD_TESTING}) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e77735..6abd0f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -114,6 +114,21 @@ The `xyz_generate_protocol` CMake macro automates code generation and supports e This macro ensures that the Python script runs during the CMake configuration or build phase to generate the necessary C++ source files for the protocol. +### C++26 Reflection Backend + +An opt-in second backend, `protocol_reflection.h`, generates the same +machinery inside the compiler using C++26 reflection instead of a build step. +It requires GCC 16+ (`-freflection`) and is enabled with a CMake option: + +```bash +CXX=g++-16 CC=gcc-16 ./scripts/cmake.sh --release \ + -DXYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND=ON -B build-reflection +``` + +This builds an additional test binary, `protocol_test_reflection`, running +the same tests against the reflection backend. See `implementation-notes.md` +(section 5) for the backend's design and known limitations. + ## Performance and Benchmarks You can measure the performance of member function dispatch, copying, and moving by running the `protocol_benchmark` target via the wrapper script: @@ -144,6 +159,21 @@ This library is an active proof of concept and is subject to change. - Development: Use this library for understanding its concepts and contributing to its development. Avoid using it in production code. +## Interactive Docker Shell + +To launch an interactive bash shell in the pre-configured Docker container (which includes GCC 16, Python, CMake, and `uv`) without running an AI agent, use: + +```bash +./scripts/docker-shell.sh [--rebuild-docker] +``` + +Inside the shell, you can experiment with the C++26 reflection backend directly: + +```bash +CXX=g++-16 CC=gcc-16 ./scripts/cmake.sh --release \ + -DXYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND=ON -B build-reflection +``` + ## AI Coding Sandboxes The repository includes a Docker-based sandbox script for AI coding diff --git a/GEMINI.md b/GEMINI.md index 5e03711..4e54bbf 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,51 +1,44 @@ -# Gemini Project Context: protocol - -This file provides project-specific mandates and conventions that override -general defaults for this repository. - -## Engineering Standards - -- **C++ Specification:** Target C++20/26. Prioritize value semantics, type - erasure, and allocator-aware designs consistent with P3019 - (`std::polymorphic`). -- **Naming Conventions:** NEVER use abbreviations in public or internal variable - names. Use descriptive names like `XYZ_GENERATE_MANUAL_VTABLE` instead of - `XYZ_GEN_MAN_VT`. -- **Stability:** Ensure that generated symbols (e.g., vtable entry names) remain - deterministic and stable between runs by using MD5 hashing of function - signatures. -- **WG21 Style:** `DRAFT.md` must adhere to ISO C++ standardization proposal - norms -- **Paper Format:** We use pure Markdown, no YAML frontmatter, and no HTML blocks. - -## Workflow Mandates - -- **Tooling:** Always use `uv` for Python dependency management (`uv run ...`). -- **Build & Test:** Use `scripts/cmake.sh` for all build and test operations. - The `scripts/cmake.sh` entrypoint supports `--debug`, `--release`, - `--asan`, `--ubsan`, `--tsan`, and `--msan`. -- **Compiler Preferences:** Prefer Clang 19+ for sanitizer-based verification - and CI, as it provides superior support for MSAN and TSAN compared to - older GCC versions. -- **Verification:** All changes must be verified using the `scripts/cmake.sh` - script to build and test the implementation. -- **Sanitizer Verification:** When modifying memory-sensitive or concurrent - code, verify changes locally using at least one sanitizer (e.g., - `./scripts/cmake.sh --asan` or `--tsan`). Note that ASAN, TSAN, and MSAN - are mutually exclusive. -- **Post-Change Checks:** Tests and pre-commit checks MUST be run after any - modifications to the codebase. - -## Git Usage - -- **Source Control:** This repository uses git. -- **History Integrity:** NEVER use git commands that affect the git history. -- **Commit & Branching:** Never commit changes, create, or delete branches. -- **Human Intervention:** If git commands must be run, you MUST ask for human - intervention. - -## Critical Paths - -- Generation Script: `scripts/generate_protocol.py` -- Proposal Draft: `DRAFT.md` -- Build Entrypoint: `scripts/cmake.sh` +# protocol: project conventions + +Project-specific mandates that override defaults for this repo. + +## Engineering standards + +- C++20/26. Prioritize value semantics, type erasure, allocator-aware design + (P3019 `std::polymorphic`). +- No abbreviations in variable names (`XYZ_GENERATE_MANUAL_VTABLE`, not + `XYZ_GEN_MAN_VT`). +- Generated symbols (e.g. vtable entry names) must be deterministic, + stable, and overload-disambiguating. The Python backend mangles by + hashing the function signature (MD5); the reflection backend instead + escapes the signature string byte-for-byte into a valid identifier + (`identifier_safe_string`), which is injective by construction rather than + probabilistically collision-resistant. +- `DRAFT.md` follows WG21 proposal norms + (). Pure Markdown + only — no YAML frontmatter, no HTML. + +## Workflow + +- Python deps: always `uv run ...`. +- Build/test only via `scripts/cmake.sh`: `--debug`/`--release`, + `--asan`/`--ubsan`/`--tsan`/`--msan` (mutually exclusive), + `--implementation=Python|reflection`. `reflection` needs a P2996 compiler + (e.g. `CXX=g++-16 CC=gcc-16`) and hard-fails rather than falling back. +- Prefer Clang 19+ for sanitizers/CI (better MSAN/TSAN than GCC). +- While iterating, a targeted compile/test is enough. Before calling a + change done, run the full suite via `scripts/cmake.sh` plus pre-commit + checks. If the change touches allocators or manual object lifetime + (clone/move/destroy, pointer casts), also run `--asan --ubsan`. If it + touches the vtable registry's shared cache/mutex (`get_mapped_vtable`), + also run `--tsan`. + +## Git + +- Never commit, branch, or run history-altering git commands. Ask the human + if one is needed. + +## Critical paths + +- Generator: `scripts/generate_protocol.py` · Proposal: `DRAFT.md` · Build: + `scripts/cmake.sh` diff --git a/README.md b/README.md index 4c65884..5f02097 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,36 @@ We propose the addition of two class templates, `protocol` and `protocol_view`, to the C++ Standard Library. Both classes support -structural-subtyping, `protocol` is owning, `protocol_view` is non-owning. +structural subtyping: `protocol` is owning (allocator-aware), and +`protocol_view` is non-owning. -See [DRAFT.md](DRAFT.md) for more details on design. +See [DRAFT.md](DRAFT.md) for complete details on the proposal design and library specifications. -This repository contains both the ISO C++ proposal to add these new library -types and a reference implementation. The reference implementation is currently -reliant on a Python code-generation step as C++26 reflection is missing some of -the features needed to generate code needed by these types at compile time. +## Implementation Architecture & Backends + +This repository contains both the ISO C++ proposal paper and a reference implementation supporting two code-generation backends: + +1. Python Build-Step Backend (Default): Uses `libclang` (`py_cppmodel`) and Jinja2 templates ([scripts/protocol.j2](scripts/protocol.j2)) to parse C++ interface headers and generate static vtable specializations at build time. Supported on all target C++20 compilers (GCC 11-14, Clang 17-21, MSVC, Apple Clang). +2. C++26 Reflection Backend (Opt-In): Uses C++26 reflection ([P2996R13]) via [protocol_reflection.h](protocol_reflection.h) to synthesize vtables, forwarding wrappers, and concepts entirely inside the compiler at compile time without any external Python generation step. Requires GCC 16+ with `-freflection`. + +Both backends share the same public API in [protocol.h](protocol.h), support narrowing conversions, and pass the identical test suite. Full removal of the Python code-generation step remains future work as compiler reflection implementations mature. + +For deep architectural details, see [implementation-notes.md](implementation-notes.md). + +## Quick Start & Building + +Ensure you have [CMake](https://cmake.org/), a modern C++ compiler, and [uv](https://docs.astral.sh/uv/) installed. + +```bash +# Build and run tests with default Python backend +./scripts/cmake.sh + +# Build and run tests with the C++26 Reflection backend (requires GCC 16+) +CXX=g++-16 CC=gcc-16 ./scripts/cmake.sh --release \ + -DXYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND=ON -B build-reflection +``` + +For complete build options, benchmarking, and development practices, see [CONTRIBUTING.md](CONTRIBUTING.md). ## Standardization @@ -22,14 +44,6 @@ The paper [P4148R2](https://wg21.link/P4148R2.pdf) (derived from working group in Brno on June 11th 2026. The authors have been encouraged to continue work. -## Contributing and Development - -For build instructions, testing, contributing guidelines, and a deeper look into -the code generation architecture, see [CONTRIBUTING.md](CONTRIBUTING.md). - -## GitHub codespaces +## GitHub Codespaces -Press `.` or visit [https://github.dev/jbcoe/cc-protocol] to open the project in -an instant, cloud-based, development environment. We have defined a -[devcontainer](.devcontainer/devcontainer.json) that will automatically install -the dependencies required to build and test the project. +Press `.` or visit [https://github.dev/jbcoe/cc-protocol](https://github.dev/jbcoe/cc-protocol) to open the project in an instant, cloud-based development environment. We provide a [devcontainer](.devcontainer/devcontainer.json) pre-configured with all required dependencies, including GCC 16 for C++26 reflection experiments. diff --git a/cmake/xyz_add_test.cmake b/cmake/xyz_add_test.cmake index 00cf265..ff9f4a6 100644 --- a/cmake/xyz_add_test.cmake +++ b/cmake/xyz_add_test.cmake @@ -50,7 +50,7 @@ function(xyz_add_test) if(NOT XYZ_VERSION) set(XYZ_VERSION 20) else() - set(VALID_TARGET_VERSIONS 11 14 17 20 23) + set(VALID_TARGET_VERSIONS 11 14 17 20 23 26) list(FIND VALID_TARGET_VERSIONS ${XYZ_VERSION} index) if(index EQUAL -1) message(FATAL_ERROR "TYPE must be one of <${VALID_TARGET_VERSIONS}>") diff --git a/docker/Dockerfile b/docker/Dockerfile index 1f92f6b..6578bf7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,6 +26,16 @@ RUN wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/nul && apt-get update && apt-get install -y --no-install-recommends cmake \ && rm -rf /var/lib/apt/lists/* +# Install GCC 16 (experimental C++26 reflection support via -std=c++26 +# -freflection, see https://gcc.gnu.org/gcc-16/changes.html) from the Ubuntu +# Toolchain Test PPA, since Ubuntu 24.04's own repos only go up to GCC 14. +# This is additive: the default g++ (13.3.0) installed above remains the +# toolchain used for the default build; g++-16/gcc-16 are opt-in, invoked +# explicitly (e.g. via CMAKE_CXX_COMPILER=g++-16) for the reflection backend. +RUN add-apt-repository -y ppa:ubuntu-toolchain-r/test \ +&& apt-get update && apt-get install -y --no-install-recommends g++-16 gcc-16 \ +&& rm -rf /var/lib/apt/lists/* + # Install bazelisk. RUN ARCH=$(dpkg --print-architecture) && \ wget https://github.com/bazelbuild/bazelisk/releases/download/v1.25.0/bazelisk-linux-${ARCH} -O /usr/local/bin/bazelisk \ diff --git a/implementation-notes.md b/implementation-notes.md index 640e653..3fe6fb4 100644 --- a/implementation-notes.md +++ b/implementation-notes.md @@ -1,6 +1,6 @@ # C++ Protocol Reference Implementation Notes -This document details the design and implementation of the `protocol` and `protocol_view` types, focusing on code generation, virtual dispatch, narrowing conversions, and concurrent safety. +This document details the design and implementation of the `protocol` and `protocol_view` types, focusing on code generation, virtual dispatch, narrowing conversions, and concurrent safety. Sections 1-4 describe the default, Python/libclang build-step backend; section 5 describes the opt-in C++26-reflection backend, which reuses the narrowing and concurrency machinery in sections 3-4 unchanged. --- @@ -66,7 +66,7 @@ constexpr protocol_view(const protocol_view& other) : ptr_(other.ptr_), vptr_(get_mutable_vtable(other.vptr_)) {} ``` -Conversions are fully transitive (for example, `protocol_view` to `protocol_view` to `protocol_view`). In each step, the registry maps the current vtable pointer to the target interface vtable. Since the mapping registry resolves type transitions directly, intermediate conversions do not create chain-linked redirects. +Conversions are fully transitive (for example, `protocol_view` to `protocol_view` to `protocol_view`). Each step is an independent call into the registry that maps straight from the vtable pointer it's given to the target interface's vtable, so a multi-step conversion produces a sequence of directly-mapped, independently-cached vtables rather than a chain of vtables that each redirect through the previous one. ### Converting Owning Protocols Allocator-extended and standard converting constructors construct the target `protocol` from the source `protocol`. If the allocators are equal, the storage pointer `p_` is moved directly (`std::exchange`) and the target vtable is mapped. If the allocators are not equal, the source's `xyz_protocol_move` or `xyz_protocol_clone` function is called to construct the value in the target allocator's storage. @@ -86,7 +86,7 @@ const void* get_mapped_vtable( ``` ### The Cache and Lifetime Control (Intentional Leak) -Mapped vtables are cached in a static `std::unordered_map` keyed by `CacheKey{source_vtable_pointer, conversion_anchor}`. The `conversion_anchor` is the address of a static template local `conversion_anchor`, ensuring target vtable/allocator uniqueness. Values are stored as `std::unique_ptr`. Because the map is node-allocated, returned pointers to elements remain stable. +Mapped vtables are cached in a static `std::unordered_map` keyed by `CacheKey{source_vtable_pointer, conversion_anchor}`, where `conversion_anchor` is the address of a `static const char conversion_anchor = 0;` local declared inside each of `get_vtable`/`get_mutable_vtable`/`get_owning_vtable`. Those functions are templates, so each `` (or `<..., Allocator>`) instantiation gets its own copy of that local — its address is therefore a stable, unique stand-in for "which target type (and allocator) this conversion is for," without needing RTTI. Values are stored as `std::unique_ptr`. Because the map is node-allocated, returned pointers to elements remain stable. To ensure safety during program shutdown, the cache map and its protecting mutex are initialized as dynamic objects allocated via `new` on the heap and referenced statically (`static auto& cache = *new ...`). This deliberately prevents their destruction during program termination, avoiding Undefined Behavior (such as segfaults) if other global or static objects trigger protocol conversions during cleanup/destructor execution. @@ -94,7 +94,7 @@ Because active references to these static structures reside in the global data s Since the vtables are dynamically allocated and retained on the heap until program termination, memory growth is bounded by the total number of distinct conversion type pairs in the binary. This compile-time bound ensures that the cache does not require an eviction policy (such as LRU) or memory cap, as memory consumption remains flat after startup. -Pointer equality is used to compare the `CacheKey` components. This is safe because static vtable instances and anchor variables are guaranteed to have unique heap or data segment addresses. Compiler optimization techniques (such as COMDAT folding or duplicate variable consolidation) do not affect correctness because identical layouts that are folded share identical function pointer semantics. +Pointer equality is used to compare the `CacheKey` components. This is safe because static vtable instances and anchor variables are guaranteed to have unique heap or data segment addresses. A linker is free to fold two of these read-only statics into a single address when it can prove they're byte-for-byte identical (identical code folding); that doesn't break the cache, since two definitions only get folded when they were already identical, so folding can't make the cache conflate two conversions that are actually different. ### Split-Lock Pattern To prevent recursive deadlocks when nested conversions occur (e.g. mapping an owning vtable requires mapping its nested mutable vtable on the same thread), the mutex is not held during mapping. @@ -111,3 +111,41 @@ The lookup and population sequence is: 7. If the insertion fails (meaning another thread inserted the key concurrently), the local buffer is destroyed, and the already-cached pointer is returned. This guarantees that all threads always resolve to the identical vtable pointer for a given conversion key, eliminating data races and leaks under high contention. + +--- + +## 5. C++26 Reflection Code-Generation Backend + +[protocol_reflection.h](protocol_reflection.h) is an opt-in second backend that generates the same machinery as sections 1-2 above, but inside the compiler at compile time via C++26 reflection ([P2996R13]), instead of ahead of compilation via `libclang` and a Jinja2 template. It requires GCC 16+ (`-std=c++26 -freflection`) and the CMake option `XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND`. [protocol_reflection_guide.md](protocol_reflection_guide.md) is a full section-by-section walkthrough of the header for anyone about to read or modify it; this section gives the shorter design summary in the style of sections 1-4. + +### Reflection Primitives + +Three operations cover almost all of what the backend needs. `^^Type` lifts a type, function, or data member into a `std::meta::info` value. A library of `consteval std::meta::*` functions (`is_function`, `is_const`, `identifier_of`, `return_type_of`, and similar) then answers ordinary questions about that `info`. A splice, `[:member:]`, lowers an `info` back into code: `object.[:member:]()` calls the member it reflects, and `typename [:type:]` names the type it reflects. + +### Interface Enumeration and Vtable Generation + +`is_interface_member_function` and `interface_member_functions` enumerate an interface's public, non-static, non-special member functions in declaration order, playing the same role as the AST traversal in section 1 (the same restriction on template member functions applies, for the same reason: no fixed signature to give a vtable slot). Entry names are the member's own signature string (name/operator, parameter types, constness, noexcept), escaped byte-for-byte into a valid identifier: `identifier_safe_string` passes `[a-zA-Z0-9]` through unchanged and replaces everything else, including a literal `_`, with `_` followed by its two-digit hex value. Unlike section 1's MD5 suffix, this isn't a hash: it's injective by construction (no unescaped `_` ever survives from the input, so a `_` in the output always starts an unambiguous two-hex-digit escape), so two different signatures can never collide — a real, if academic, possibility a hash-based scheme (MD5, or this backend's original FNV-1a) can't fully rule out. Names are longer than a hash digest, but that's invisible outside compiler diagnostics. + +Rather than rendering a vtable struct from a template, `define_vtable_entries` builds a list of `data_member_spec`s (one function pointer per interface member). `std::meta::define_aggregate` takes a class that has only been forward-declared so far, with no members, plus that list, and gives the class exactly those data members — the reflection equivalent of generating and compiling a struct definition, except the "source" is the list of specs rather than text. `view_vtable` and `owning_vtable` wrap that generated `entries` struct in a handwritten shell that adds the fixed lifetime operations (`xyz_protocol_clone`/`move`/`destroy`), mirroring the two vtable layouts in section 2. Populating a vtable for a given `(Interface, Implementation)` pair resolves each interface member against the stored type (see below) and stores the address of an `erased_call_thunk::call` specialization. That function does the same job as one of section 2's per-type lambdas (`static_cast` the erased pointer back to the concrete type, then call the member on it) but as a plain static member function rather than a lambda, generated once per `(Interface, Implementation, member)` combination, and calling through a splice (`self->[:member:](args...)`) rather than a member name written literally in source. + +### Duck-Typed Member Resolution + +`resolve_implementation_candidates` finds every member of the implementation type matching an interface member by name (or operator kind) and constness — no parameter or return-type filtering. Each candidate is wrapped in an `implementation_candidate_call` keyed by its *own* signature, with `operator()` qualified const or not according to that specific candidate's own constness (not a single flag shared across the merged set) — the same way Ryan Keane's `rjk::duck` derives a per-candidate `Self` type. This matters: a stored type declaring both `foo()` and `foo() const` merges into ordinary C++ overloading-on-constness (as if both were declared directly in one class), so a non-const interface member correctly prefers the non-const overload instead of the merge becoming ambiguous. All candidates for one interface member are merged into a `candidate_overload_set` via `using Candidates::operator()...` — the same idiom `overloaded_calls` uses for an interface's own overload sets, applied here to the implementation's candidates instead. `make_candidate_overload_set` builds that merged type; `models_reflected_interface` checks it's invocable with the interface member's parameter types via `std::is_invocable_r_v`/`std::is_invocable_v`. That check is what `reflection_protocol_const_concept`/`reflection_protocol_concept` test, the reflection equivalents of the Python backend's generated `protocol_concept_`. + +Conformance is exactly what the concept says, and nothing more: a stored type either satisfies `reflection_protocol_concept`/`reflection_protocol_const_concept` or it doesn't, and `protocol`'s constructors are constrained on exactly that. What changed is how the concept checks callability — via real invocability against the merged candidate set, rather than hand-comparing signatures — exactly like the Python backend's `requires`-expression (itself a real call, e.g. `t.method(std::declval()...)`) and Ryan Keane's `rjk::duck` technique (https://ryanjk5.github.io/posts/rjk-duck/). `erased_call_thunk` uses the same merged type for the actual dispatch call, so whatever the concept accepted is exactly what runs. + +### Giving Vtable Entries Call Syntax + +C++26 reflection cannot splice a reflection as the name of a *new* declaration, so a member function actually named `name` cannot be spliced into existence the way a vtable entry's mangled name can. The backend works around this using the technique from Ryan Keane's `rjk::duck` library: since `define_aggregate` can declare a data member from a plain string, each interface member gets a data member (not a function) named after it, whose type is an empty `forwarding_call` wrapper with an exact-signature `operator()`. Because `a.name()` doesn't distinguish a member function from a data member whose type has `operator()`, this gives ordinary call syntax for free. The wrapper's `operator()` recovers its owning `protocol`/`protocol_view` via `static_cast(static_cast(this))`, valid only because every wrapper sits at offset zero of its owner; `forwarders_at_offset_zero` checks that layout assumption with `std::meta::offset_of` rather than assuming it. Operators (`operator+`, etc.) can't use this trick, since a data member can't be named `operator+`; each operator kind instead gets its own macro-stamped forwarder template with the operator symbol written literally in source (39 invocations, one per supported operator kind, including plain `operator=`). + +Assignment merges into `protocol`/`protocol_view` through the same mechanism as every other operator: several interface-declared `operator=` overloads, even of the same constness, resolve correctly through the merged set, exactly like `operator+` already does. Getting there needs one extra step that no other operator needs, though. Every class in the merge chain that doesn't declare any member of its own — `operator_join`, `combined_operator_joins`, and finally `protocol`/`protocol_view` themselves — still gets a compiler-generated `operator=`, whether it declares one explicitly or not. C++ hides an inherited member whenever a class has a member of the same name, and this rule applies even to that compiler-generated one with no source text at all. So each of those classes needs its own `using Base::operator=;`, bringing the inherited one back into scope alongside whatever that class declares itself. This is ordinary C++ name hiding, nothing specific to assignment or to reflection — it just never comes up for any other operator, because `operator=` is the only member name every level of this hierarchy ends up declaring one way or another. + +### Narrowing and Concurrency + +Sections 3 and 4 above apply unchanged: `protocol_vtable_traits` and `protocol_owning_vtable_traits` specializations plug the reflection-generated vtable types into the same `get_vtable`/`get_mutable_vtable`/`get_owning_vtable` registry that section 4 describes, so narrowing conversions, the vtable cache, and its concurrency guarantees are identical regardless of which backend produced the vtable being narrowed. + +### Known Limitations + +Interface members must be direct members of the stored type — members inherited from a base of the implementation are not found. An interface member function named `swap` or `valueless_after_move` would be silently hidden by `protocol`'s own member of that name (since the generated forwarders are inherited data members), so it is instead rejected with a `static_assert` naming the interface. Generated member functions are not marked `constexpr`, which is out of scope for this backend for now. + +[P2996R13]: https://isocpp.org/files/papers/P2996R13.html diff --git a/interface_D.h b/interface_D.h index de55025..22d7c66 100644 --- a/interface_D.h +++ b/interface_D.h @@ -17,6 +17,7 @@ struct D { int operator~() const; bool operator!() const; void operator=(int x); + void operator=(double x); bool operator<(int x) const; bool operator>(int x) const; void operator+=(int x); diff --git a/protocol.h b/protocol.h index f4d0403..17913c6 100644 --- a/protocol.h +++ b/protocol.h @@ -29,7 +29,7 @@ namespace xyz { template struct is_protocol : std::false_type {}; -template +template > class protocol; template @@ -58,7 +58,8 @@ const void* get_mapped_vtable(const void* source_vtable_pointer, void* target)); template -const typename protocol_vtable_traits::const_vtable* get_vtable( +const typename protocol_vtable_traits::const_vtable* +get_const_vtable( const typename protocol_vtable_traits::const_vtable* source_vtable_pointer) { using FromVtable = @@ -68,8 +69,8 @@ const typename protocol_vtable_traits::const_vtable* get_vtable( static const char conversion_anchor = 0; auto mapping_function = [](const void* source, void* target) { - map_vtable_members(static_cast(source), - static_cast(target)); + map_const_vtable_members(static_cast(source), + static_cast(target)); }; return static_cast( @@ -78,7 +79,7 @@ const typename protocol_vtable_traits::const_vtable* get_vtable( } template -const typename protocol_vtable_traits::vtable* get_mutable_vtable( +const typename protocol_vtable_traits::vtable* get_vtable( const typename protocol_vtable_traits::vtable* source_vtable_pointer) { using FromVtable = typename protocol_vtable_traits::vtable; @@ -87,8 +88,8 @@ const typename protocol_vtable_traits::vtable* get_mutable_vtable( static const char conversion_anchor = 0; auto mapping_function = [](const void* source, void* target) { - map_mutable_vtable_members(static_cast(source), - static_cast(target)); + map_vtable_members(static_cast(source), + static_cast(target)); }; return static_cast( @@ -123,7 +124,8 @@ get_owning_vtable(const typename protocol_owning_vtable_traits< sizeof(ToVtable), mapping_function)); } -template > +#ifndef XYZ_PROTOCOL_ENABLE_REFLECTION +template class protocol { static_assert( sizeof(T) == 0, @@ -164,7 +166,15 @@ class protocol_view { "Note: protocol_view specializations are automatically generated " "alongside protocol specializations."); }; +#endif } // namespace xyz +// If reflection is enabled, the real xyz::protocol / xyz::protocol_view +// implementations come from protocol_reflection.h instead of the +// placeholder definitions above. +#ifdef XYZ_PROTOCOL_ENABLE_REFLECTION +#include "protocol_reflection.h" +#endif + #endif // XYZ_PROTOCOL_H_ diff --git a/protocol_reflection.h b/protocol_reflection.h new file mode 100644 index 0000000..8a9a78d --- /dev/null +++ b/protocol_reflection.h @@ -0,0 +1,1041 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ +// C++26-reflection code-generation backend for xyz::protocol / +// xyz::protocol_view. Instead of generating a per-interface header at build +// time, this header generates the same machinery inside the compiler using +// P2996 reflection. Any plain struct, class, or template instantiation works +// as an interface type automatically; no macro, build step, or per-type +// opt-in annotation is required. +// +// Requires GCC 16+ with `-std=c++26 -freflection`; fails to compile +// otherwise, rather than silently no-op'ing. Defines xyz::protocol / +// xyz::protocol_view as primary templates (protocol.h's own placeholder +// primary templates must therefore not also be defined when this header +// is included, or the two would conflict; see protocol.h for how it +// arranges that). +// +// Conformance to an interface is checked by a concept +// (reflection_protocol_concept / reflection_protocol_const_concept), +// exactly as strictly as any C++ concept: a stored type either satisfies +// it or it doesn't, and protocol's constructors are constrained on +// exactly that. The concept is implemented via real invocability against +// the stored type's own overload set -- every candidate with the +// interface member's name is merged into one callable type and the +// compiler decides what's callable -- rather than this backend +// hand-comparing parameter types. This mirrors the Python backend's +// `requires`-expression-based concept (itself a real call, e.g. +// `t.method(std::declval()...)`) and Ryan Keane's rjk::duck technique +// (https://ryanjk5.github.io/posts/rjk-duck/). +// +// Documented, deliberate limitations of this backend: +// - Interface members must be implemented as direct members of the stored +// type: member functions inherited from a base class of the implementation +// are not found by the reflection-based member resolution. +// - An interface member function named `swap` or `valueless_after_move` +// would be hidden by protocol's own member of that name (undetectable via +// the forwarders, which are inherited), so it is instead rejected with a +// static_assert naming the interface. +// - Generated member functions are not marked constexpr (out of scope for +// this backend for now). +#ifndef XYZ_PROTOCOL_REFLECTION_H_ +#define XYZ_PROTOCOL_REFLECTION_H_ + +#ifndef __cpp_impl_reflection +#error \ + "This header requires a compiler with C++26 reflection support (GCC 16+, -std=c++26 -freflection)." +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "protocol.h" +#include "protocol_reflection_detail/conformance.h" +#include "protocol_reflection_detail/members.h" +#include "protocol_reflection_detail/naming.h" +#include "protocol_reflection_detail/operator_forwarders.h" +#include "protocol_reflection_detail/thunk.h" +#include "protocol_reflection_detail/types.h" +#include "protocol_reflection_detail/vtable_layout.h" + +namespace xyz { + +namespace reflection_detail { + +using std::meta::info; + +// --------------------------------------------------------------------------- +// Vtable instances per (interface, implementation) pair +// --------------------------------------------------------------------------- + +// Allocate storage for one Implementation via a rebound copy of `allocator` +// and construct it from `arguments`, deallocating if construction throws. +// Shared by the vtable lifetime operations and protocol's constructors. +template +void* allocate_and_construct(const Allocator& allocator, + Arguments&&... arguments) { + using implementation_allocator = typename std::allocator_traits< + Allocator>::template rebind_alloc; + using implementation_allocator_traits = + std::allocator_traits; + implementation_allocator rebound_allocator(allocator); + auto memory = implementation_allocator_traits::allocate(rebound_allocator, 1); + try { + implementation_allocator_traits::construct( + rebound_allocator, memory, std::forward(arguments)...); + return memory; + } catch (...) { + implementation_allocator_traits::deallocate(rebound_allocator, memory, 1); + throw; + } +} + +// The allocator-aware lifetime operations, forming the fixed part of +// vtable_impl. +template +struct allocator_lifetime { + using implementation_allocator = typename std::allocator_traits< + Allocator>::template rebind_alloc; + using implementation_allocator_traits = + std::allocator_traits; + + static void* clone(void* erased, const Allocator& allocator) { + return allocate_and_construct( + allocator, *static_cast(erased)); + } + + static void* move_construct(void* erased, const Allocator& allocator) { + return allocate_and_construct( + allocator, std::move(*static_cast(erased))); + } + + static void destroy(void* erased, const Allocator& allocator) { + auto* self = static_cast(erased); + implementation_allocator rebound_allocator(allocator); + implementation_allocator_traits::destroy(rebound_allocator, self); + implementation_allocator_traits::deallocate(rebound_allocator, self, 1); + } +}; + +// One vtable entry value: the address of the exactly-typed thunk for the +// Index-th interface member in the given selection. +template +consteval auto make_vtable_entry() { + constexpr info member = interface_members[Index]; + constexpr bool ConstErased = std::meta::is_const(member); + constexpr info merged = + make_candidate_overload_set(^^Implementation, member, ConstErased); + constexpr info erased_pointer_type = ConstErased ? ^^const void* : ^^void*; + using ThunkPointer = [:vtable_entry_pointer_type(member, + erased_pointer_type):]; + if constexpr (merged == info{}) { + return ThunkPointer(nullptr); + } else { + using Signature = [:member_function_type(member):]; + return ThunkPointer( + &erased_call_thunk::call); + } +} + +template +consteval auto make_view_entries(std::index_sequence) { + return typename view_vtable::view_entries{ + make_vtable_entry()...}; +} + +template +consteval auto make_owning_entries(std::index_sequence) { + return typename owning_vtable::owning_entries{ + make_vtable_entry()...}; +} + +template +inline constexpr typename view_vtable::vtable view_vtable_for = { + make_view_entries( + std::make_index_sequence.size()>())}; + +template +inline constexpr + typename owning_vtable::vtable owning_vtable_for = { + &allocator_lifetime::clone, + &allocator_lifetime::move_construct, + &allocator_lifetime::destroy, + &view_vtable_for, + make_owning_entries( + std::make_index_sequence.size()>())}; + +// --------------------------------------------------------------------------- +// Vtable narrowing maps, found by ADL from protocol.h's get_const_vtable / +// get_vtable / get_owning_vtable. Entries are copied by name: every entry of +// the target vtable must exist, identically named (same signature hash), in +// the source vtable — which is exactly the subset relationship narrowing +// conversions rely on. +// --------------------------------------------------------------------------- + +template +void copy_vtable_entries(const FromEntries& from, ToEntries& to) { + static constexpr auto to_members = + std::define_static_array(std::meta::nonstatic_data_members_of( + ^^ToEntries, std::meta::access_context::current())); + template for (constexpr info to_member : to_members) { + constexpr info from_member = + data_member_named(^^FromEntries, std::meta::identifier_of(to_member)); + static_assert(from_member != info{}, + "reflection backend: narrowing conversion requires every " + "target interface member to exist in the source interface " + "with an identical signature"); + to.[:to_member:] = from.[:from_member:]; + } +} + +template +concept reflection_view_vtable = + requires { typename Vtable::xyz_reflection_view_vtable_tag; }; + +template +concept reflection_owning_vtable = + requires { typename Vtable::xyz_reflection_owning_vtable_tag; }; + +template +void map_const_vtable_members(const FromVtable* from, ToVtable* to) { + copy_vtable_entries(from->entries, to->entries); +} + +template +void map_vtable_members(const FromVtable* from, ToVtable* to) { + copy_vtable_entries(from->entries, to->entries); +} + +template +void map_owning_vtable_members(const FromVtable* from, ToVtable* to) { + to->xyz_protocol_clone = from->xyz_protocol_clone; + to->xyz_protocol_move = from->xyz_protocol_move; + to->xyz_protocol_destroy = from->xyz_protocol_destroy; + to->view_vt = get_vtable(from->view_vt); + copy_vtable_entries(from->entries, to->entries); +} + +// --------------------------------------------------------------------------- +// Forwarding call wrappers +// +// GCC 16 cannot splice a reflection as the declarator-id of a new member +// function, so per-method forwarders are synthesized as data members (named +// by the interface member's identifier) whose type overloads operator() with +// the exact interface signature. Overloaded interface names become one data +// member whose type merges one wrapper per overload. +// +// Recovering the owning protocol / protocol_view object from inside that +// wrapper's operator() is two ordinary, always-well-defined casts, not one +// address-equality assumption. named_forwarders::single_member_wrapper holds exactly one +// data member: that group's (possibly overload-merged) forwarding_call. A +// standard-layout class with exactly one non-static data member shares that +// member's address ([class.mem]), so casting from the member back to its +// enclosing single_member_wrapper via void* is always well-defined, with no +// [[no_unique_address]] or layout hoping involved. From there, casting up +// from single_member_wrapper to Owner is an ordinary base-to-derived +// static_cast: Owner (transitively, through named_forwarders<...>::type = +// forwarder_group, ...>) really does derive from +// every single_member_wrapper, so the compiler applies whatever pointer +// adjustment that base's actual offset needs -- offset zero is never +// required or assumed for this step. This mirrors Duck's trace_to_duck +// (https://github.com/RyanJK5/rjk-duck), whose vtable_function recovers its +// owning duck the same two-step way, through its own vtable_function_wrapper +// (Duck's equivalent of single_member_wrapper, hand-written there rather +// than reflected, since Duck doesn't need one generated per interface member +// of an arbitrary type at compile time). +// +// define_aggregate can only run from inside a `consteval { ... }` block, and +// that block cannot have another class's scope intervening between it and +// the incomplete type it completes -- so single_member_wrapper is nested +// inside named_forwarders itself (one uniquely-named member, one nested +// class, completed by the consteval block right below it, sharing its +// scope), rather than living at namespace scope alongside forwarder_group. +// --------------------------------------------------------------------------- + +template +struct forwarding_call; + +template +struct forwarding_call { + ReturnType operator()(ParameterTypes... parameters) noexcept(IsNoexcept) { + void* voided = this; + auto* wrapper = + static_cast*>(voided); + auto* owner = static_cast(wrapper); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct forwarding_call { + ReturnType operator()(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const void* voided = this; + const auto* wrapper = + static_cast*>( + voided); + const auto* owner = + static_cast(wrapper); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct overloaded_calls : Overloads... { + using Overloads::operator()...; +}; + +// Interface members grouped for forwarder generation, preserving declaration +// order within and across groups: named members group by identifier, +// operator members by operator kind. Each call selects one population via +// group_operators — the two populations need different downstream mechanisms +// (a data member cannot be named `operator+`), but share this grouping. +consteval std::vector> grouped_interface_members( + const std::vector& members, bool group_operators) { + std::vector> groups; + std::vector grouped(members.size(), false); + for (std::size_t index = 0; index < members.size(); ++index) { + if (grouped[index]) continue; + if (std::meta::has_identifier(members[index]) == group_operators) continue; + std::vector group; + for (std::size_t other = index; other < members.size(); ++other) { + if (grouped[other]) continue; + if (std::meta::has_identifier(members[other]) == group_operators) { + continue; + } + bool same_group = group_operators + ? std::meta::operator_of(members[other]) == + std::meta::operator_of(members[index]) + : std::meta::identifier_of(members[other]) == + std::meta::identifier_of(members[index]); + if (!same_group) continue; + grouped[other] = true; + group.push_back(members[other]); + } + groups.push_back(group); + } + return groups; +} + +// Combines one single_member_wrapper per uniquely-named interface member +// through ordinary multiple inheritance. +template +struct forwarder_group : public Wrappers... {}; + +// One base per uniquely-named interface member (operators are handled +// separately, since a data member cannot be named `operator+`), combined +// via forwarder_group. ForceConstCall makes every wrapper's operator() const +// regardless of the interface member's constness, matching the generated +// protocol_view classes whose forwarders are all const. +template +struct named_forwarders { + using owner_type = Owner; + + // GroupKeyMember is a group's first interface member: its own info both + // identifies the group and, via identifier_of, names the data member + // below. Declared here, completed by the consteval block that follows, + // sharing this scope as define_aggregate requires. + template + struct single_member_wrapper; + + consteval static info wrapper_type_for(const std::vector& overloads) { + std::vector overload_wrappers; + for (info member : overloads) { + overload_wrappers.push_back(std::meta::substitute( + ^^forwarding_call, + { + ^^named_forwarders, std::meta::reflect_constant(member), + std::meta::reflect_constant(overloads.front()), + member_function_type(member), + std::meta::reflect_constant(ForceConstCall || + std::meta::is_const(member)), + std::meta::reflect_constant(std::meta::is_noexcept(member))})); + } + return overload_wrappers.size() == 1 + ? overload_wrappers.front() + : std::meta::substitute(^^overloaded_calls, overload_wrappers); + } + + consteval { + for (const std::vector& overloads : grouped_interface_members( + interface_member_functions(^^Interface, ConstOnly), false)) { + std::meta::define_aggregate( + std::meta::substitute( + ^^single_member_wrapper, + { + std::meta::reflect_constant(overloads.front())}), + {std::meta::data_member_spec( + wrapper_type_for(overloads), + {.name = std::meta::identifier_of(overloads.front()), + .no_unique_address = true})}); + } + } + + consteval static std::vector bases() { + std::vector result; + for (const std::vector& overloads : grouped_interface_members( + interface_member_functions(^^Interface, ConstOnly), false)) { + result.push_back(std::meta::substitute( + ^^single_member_wrapper, + { + std::meta::reflect_constant(overloads.front())})); + } + return result; + } + + using type = typename[:std::meta::substitute(^^forwarder_group, bases()):]; +}; + +// --------------------------------------------------------------------------- +// Operator forwarding +// +// A data member cannot be named `operator+`, so interface operators cannot +// use the named-forwarder mechanism above. Instead, operator_forwarder and +// operator_join (protocol_reflection_detail/operator_forwarders.h) are +// generated once per operator kind offline, rather than reimplemented here: +// forwarding an operator needs a real `operator` declaration, with +// the symbol itself a literal token in source, the same limitation +// https://github.com/RyanJK5/rjk-duck works around with its own +// generate_operators.py. The per-interface set of operator forwarders is +// combined into an empty base class of protocol / protocol_view below. +// +// operator= needs one extra thing the other operators don't: protocol and +// protocol_view each need their own operator= (hand-written for value +// semantics, or left to the compiler to generate), and in C++ a class's own +// operator=, even a compiler-generated one, hides any operator= it would +// otherwise inherit from a base class. So every class in this merge chain +// that doesn't declare a member of its own needs an explicit +// using-declaration bringing the inherited operator= back into scope: +// combined_operator_joins below, and each of protocol, protocol_view, and protocol_view further down this file. +// --------------------------------------------------------------------------- + +// Every level of this merge hierarchy that doesn't declare its own members +// gets an implicitly-declared operator= of its own (ordinary C++), which +// would otherwise hide any operator= a Join brings in from its Forwarders -- +// hence the using-declaration below, mirroring the one operator_join itself +// already needs for the same reason. +template +struct combined_operator_joins : Joins... { + using Joins::operator=...; +}; + +consteval info make_operator_forwarders(info interface_type, info owner_type, + bool const_only, + bool force_const_call) { + std::vector joins; + for (const std::vector& overloads : grouped_interface_members( + interface_member_functions(interface_type, const_only), true)) { + std::meta::operators kind = std::meta::operator_of(overloads.front()); + std::vector join_arguments; + join_arguments.push_back(std::meta::reflect_constant(kind)); + for (info member : overloads) { + join_arguments.push_back(std::meta::substitute( + ^^operator_forwarder, + { + owner_type, std::meta::reflect_constant(member), + member_function_type(member), std::meta::reflect_constant(kind), + std::meta::reflect_constant(force_const_call || + std::meta::is_const(member)), + std::meta::reflect_constant(std::meta::is_noexcept(member))})); + } + joins.push_back(std::meta::substitute(^^operator_join, join_arguments)); + } + return std::meta::substitute(^^combined_operator_joins, joins); +} + +template +struct operator_forwarders { + using type = typename[:make_operator_forwarders(^^Interface, ^^Owner, + ConstOnly, ForceConstCall):]; +}; + +// Every forwarder base subobject, every forwarder data member inside those +// bases (the named forwarding wrappers), and their bases recursively are +// expected to collapse to offset zero of the owning object: forwarding_call +// and operator_forwarder never hold state, so every wrapper is empty and +// [[no_unique_address]] should let them all overlap. Neither the two-step +// cast in forwarding_call nor the direct static_cast(this) in +// operator_forwarder depends on that collapse for correctness: both apply +// whatever adjustment the real offset needs. But [[no_unique_address]] is +// only a request, not a guarantee, so this asserts the collapse happened +// rather than silently paying for padding an implementation is free to +// insert if it ever doesn't. +consteval bool forwarders_at_offset_zero(info class_type) { + for (info base : + std::meta::bases_of(class_type, std::meta::access_context::current())) { + if (std::meta::offset_of(base).bytes != 0) return false; + info base_type = std::meta::dealias(std::meta::type_of(base)); + for (info data_member : std::meta::nonstatic_data_members_of( + base_type, std::meta::access_context::current())) { + if (std::meta::offset_of(data_member).bytes != 0) return false; + } + if (!forwarders_at_offset_zero(base_type)) return false; + } + return true; +} + +} // namespace reflection_detail + +// --------------------------------------------------------------------------- +// Registry hookup: matches the shape expected by get_const_vtable / +// get_vtable / get_owning_vtable in protocol.h, so they work unmodified +// regardless of backend. +// --------------------------------------------------------------------------- + +template +struct protocol_vtable_traits { + using const_vtable = typename reflection_detail::view_vtable::vtable; + using vtable = typename reflection_detail::view_vtable::vtable; +}; + +template +struct protocol_owning_vtable_traits { + using vtable = + typename reflection_detail::owning_vtable::vtable; +}; + +// --------------------------------------------------------------------------- +// protocol — primary template definition. The constructors, +// assignment, swap, and destructor are hand-written here (define_aggregate +// can only produce data members, so this part isn't reflection-generated); +// the per-interface concepts and vtable types they use are the reflection +// equivalents built above. Named-method forwarding comes from the +// named_forwarders empty base. +// --------------------------------------------------------------------------- + +template +class protocol + : public reflection_detail::named_forwarders, + false, false>::type, + public reflection_detail::operator_forwarders, + false, false>::type { + friend class protocol_view; + friend class protocol_view; + template + friend class protocol; + template + friend struct protocol_owning_vtable_traits; + template + friend struct reflection_detail::forwarding_call; + template + friend struct reflection_detail::operator_forwarder; + + static_assert(!reflection_detail::has_reserved_interface_member_name(^^T), + "xyz::protocol: interface must not declare a member function " + "named 'swap' or 'valueless_after_move' - these names are " + "reserved for protocol's own public members and would be " + "silently hidden by ordinary C++ name-hiding rules"); + static_assert( + !reflection_detail::has_rvalue_qualified_interface_member(^^T), + "xyz::protocol: interface must not declare an rvalue-qualified (&&) " + "member function - this backend does not support them"); + + using vtable = + typename reflection_detail::owning_vtable::vtable; + + template + decltype(auto) dispatch_reflected_member(Arguments&&... arguments) { + static_assert(reflection_detail::forwarders_at_offset_zero(^^protocol)); + constexpr std::meta::info entry = reflection_detail::data_member_named( + ^^typename reflection_detail::owning_vtable::owning_entries, + reflection_detail::vtable_entry_name(Member)); + return vtable_->entries.[:entry:](p_, + std::forward(arguments)...); + } + + template + decltype(auto) dispatch_reflected_member(Arguments&&... arguments) const { + static_assert(reflection_detail::forwarders_at_offset_zero(^^protocol)); + constexpr std::meta::info entry = reflection_detail::data_member_named( + ^^typename reflection_detail::owning_vtable::owning_entries, + reflection_detail::vtable_entry_name(Member)); + return vtable_->entries.[:entry:](p_, + std::forward(arguments)...); + } + + using allocator_traits = std::allocator_traits; + + template + [[nodiscard]] void* create_storage(Ts&&... ts) const { + return reflection_detail::allocate_and_construct( + alloc_, std::forward(ts)...); + } + + void* p_; + const vtable* vtable_; + [[no_unique_address]] Allocator alloc_; + + public: + template + requires(!std::same_as) + protocol(protocol&& 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( + 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.vtable_); + other.vtable_->xyz_protocol_destroy(other.p_, other.alloc_); + other.p_ = nullptr; + other.vtable_ = nullptr; + } else { + p_ = nullptr; + vtable_ = nullptr; + } + } + } + + template + requires(!std::same_as) + protocol(const protocol& 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.vtable_); + } else { + p_ = nullptr; + vtable_ = nullptr; + } + } + + template + requires(!std::same_as) + protocol(std::allocator_arg_t, const Allocator& alloc, + const protocol& other) + : alloc_(alloc) { + if (!other.valueless_after_move()) { + p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_); + vtable_ = get_owning_vtable(other.vtable_); + } else { + p_ = nullptr; + vtable_ = nullptr; + } + } + + template + requires(!std::same_as) + protocol(std::allocator_arg_t, const Allocator& alloc, + protocol&& + other) noexcept(allocator_traits::is_always_equal::value) + : alloc_(alloc) { + if (alloc_ == other.alloc_) { + p_ = std::exchange(other.p_, nullptr); + vtable_ = get_owning_vtable( + 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.vtable_); + other.vtable_->xyz_protocol_destroy(other.p_, other.alloc_); + other.p_ = nullptr; + other.vtable_ = nullptr; + } else { + p_ = nullptr; + vtable_ = nullptr; + } + } + } + + explicit protocol() + requires std::default_initializable && + reflection_protocol_concept && std::copy_constructible + : protocol(std::allocator_arg_t{}, Allocator{}) {} + + template + explicit protocol(U&& u) + requires(!std::same_as>) && + not_protocol_or_view && + std::copy_constructible> && + reflection_protocol_concept + : protocol(std::allocator_arg_t{}, Allocator{}, std::forward(u)) {} + + template + explicit protocol(std::in_place_type_t, Ts&&... ts) + requires std::same_as, U> && + not_protocol_or_view && std::constructible_from && + std::copy_constructible && + std::default_initializable && + reflection_protocol_concept + : protocol(std::allocator_arg_t{}, Allocator{}, std::in_place_type, + std::forward(ts)...) {} + + template + explicit protocol(std::in_place_type_t, std::initializer_list ilist, + Ts&&... ts) + requires std::same_as, U> && + not_protocol_or_view && + std::constructible_from, Ts&&...> && + std::copy_constructible && + std::default_initializable && + reflection_protocol_concept + : protocol(std::allocator_arg_t{}, Allocator{}, std::in_place_type, + ilist, std::forward(ts)...) {} + + protocol(const protocol& other) + : protocol(std::allocator_arg_t{}, + allocator_traits::select_on_container_copy_construction( + other.alloc_), + other) {} + + protocol(protocol&& other) noexcept(allocator_traits::is_always_equal::value) + : protocol(std::allocator_arg_t{}, other.alloc_, std::move(other)) {} + + explicit protocol(std::allocator_arg_t, const Allocator& alloc) + requires std::default_initializable && std::copy_constructible + : alloc_(alloc) { + p_ = create_storage(); + vtable_ = &reflection_detail::owning_vtable_for; + } + + template + explicit protocol(std::allocator_arg_t, const Allocator& alloc, U&& u) + requires(!std::same_as>) && + not_protocol_or_view && + std::copy_constructible> && + reflection_protocol_concept + : alloc_(alloc) { + p_ = create_storage>(std::forward(u)); + vtable_ = &reflection_detail::owning_vtable_for>; + } + + template + explicit protocol(std::allocator_arg_t, const Allocator& alloc, + std::in_place_type_t, Ts&&... ts) + requires std::same_as, U> && + not_protocol_or_view && std::constructible_from && + std::copy_constructible && reflection_protocol_concept + : alloc_(alloc) { + p_ = create_storage(std::forward(ts)...); + vtable_ = &reflection_detail::owning_vtable_for; + } + + template + explicit protocol(std::allocator_arg_t, const Allocator& alloc, + std::in_place_type_t, std::initializer_list ilist, + Ts&&... ts) + requires std::same_as, U> && + not_protocol_or_view && + std::constructible_from, Ts&&...> && + std::copy_constructible && reflection_protocol_concept + : alloc_(alloc) { + p_ = create_storage(ilist, std::forward(ts)...); + vtable_ = &reflection_detail::owning_vtable_for; + } + + protocol(std::allocator_arg_t, const Allocator& alloc, const protocol& other) + : alloc_(alloc) { + if (!other.valueless_after_move()) { + p_ = other.vtable_->xyz_protocol_clone(other.p_, alloc_); + vtable_ = other.vtable_; + } else { + p_ = nullptr; + vtable_ = nullptr; + } + } + + protocol(std::allocator_arg_t, const Allocator& alloc, + protocol&& other) noexcept(allocator_traits::is_always_equal::value) + : alloc_(alloc) { + if constexpr (allocator_traits::is_always_equal::value) { + p_ = std::exchange(other.p_, nullptr); + vtable_ = std::exchange(other.vtable_, nullptr); + } else { + if (alloc_ == other.alloc_) { + p_ = std::exchange(other.p_, nullptr); + vtable_ = std::exchange(other.vtable_, nullptr); + } else { + if (!other.valueless_after_move()) { + p_ = other.vtable_->xyz_protocol_move(other.p_, alloc_); + vtable_ = other.vtable_; + } else { + p_ = nullptr; + vtable_ = nullptr; + } + } + } + } + + bool valueless_after_move() const noexcept { return p_ == nullptr; } + + ~protocol() { + if (p_ != nullptr) { + vtable_->xyz_protocol_destroy(p_, alloc_); + } + } + + protocol& operator=(protocol other) noexcept( + allocator_traits::is_always_equal::value) { + std::swap(p_, other.p_); + std::swap(vtable_, other.vtable_); + if constexpr (!allocator_traits::is_always_equal::value) { + std::swap(alloc_, other.alloc_); + } + return *this; + } + + // Interface-declared operator= overloads are merged by operator_forwarders + // the same way every other operator is; the copy-assignment operator above + // would otherwise hide them entirely (ordinary C++ name hiding, not + // anything specific to assignment: a derived class's own operator=, even + // an implicitly-declared one, hides any operator= it would otherwise + // inherit, unless brought back into scope by a using-declaration). + using reflection_detail::operator_forwarders, false, + false>::type::operator=; + + void swap(protocol& other) noexcept( + allocator_traits::is_always_equal::value) { + std::swap(p_, other.p_); + std::swap(vtable_, other.vtable_); + if constexpr (!allocator_traits::is_always_equal::value) { + std::swap(alloc_, other.alloc_); + } + } + + friend void swap(protocol& lhs, protocol& rhs) noexcept( + allocator_traits::is_always_equal::value) { + lhs.swap(rhs); + } +}; + +// --------------------------------------------------------------------------- +// protocol_view — the const view. +// --------------------------------------------------------------------------- + +template +class protocol_view + : public reflection_detail::named_forwarders, + true, true>::type, + public reflection_detail::operator_forwarders, + true, true>::type { + template + friend class protocol_view; + template + friend struct reflection_detail::forwarding_call; + template + friend struct reflection_detail::operator_forwarder; + + using const_vtable = typename reflection_detail::view_vtable::vtable; + + template + decltype(auto) dispatch_reflected_member(Arguments&&... arguments) const { + static_assert( + reflection_detail::forwarders_at_offset_zero(^^protocol_view)); + constexpr std::meta::info entry = reflection_detail::data_member_named( + ^^typename reflection_detail::view_vtable::view_entries, + reflection_detail::vtable_entry_name(Member)); + return vptr_->entries.[:entry:](ptr_, + std::forward(arguments)...); + } + + const void* ptr_; + const const_vtable* vptr_; + + protocol_view(const void* ptr, const const_vtable* vptr) noexcept + : ptr_(ptr), vptr_(vptr) {} + + template + static const void* checked_ptr(const protocol& p) noexcept { + assert(!p.valueless_after_move()); + return p.p_; + } + + public: + template + requires reflection_protocol_const_concept && not_protocol_or_view + protocol_view(const U& obj) noexcept + : ptr_(std::addressof(obj)), + vptr_(&reflection_detail::view_vtable_for>) {} + + template + requires reflection_protocol_const_concept && not_protocol_or_view + protocol_view(const U&&) = delete; + + template + protocol_view(const protocol& p) noexcept + : ptr_(checked_ptr(p)), vptr_(p.vtable_->view_vt) {} + + template + protocol_view(const protocol&&) = delete; + + template + protocol_view(protocol& p) noexcept + : ptr_(checked_ptr(p)), vptr_(p.vtable_->view_vt) {} + + template + protocol_view(protocol&&) = delete; + + protocol_view(protocol_view other) noexcept; + + template + requires(!std::same_as) + protocol_view(const protocol_view& other) noexcept + : ptr_(other.ptr_), vptr_(get_const_vtable(other.vptr_)) {} + + template + requires(!std::same_as) + protocol_view(const protocol_view& other) noexcept + : ptr_(other.ptr_), vptr_(get_const_vtable(other.vptr_)) {} + + template + requires(!std::same_as) + protocol_view(const protocol& p) noexcept + : protocol_view(protocol_view(p)) {} + + template + requires(!std::same_as) + protocol_view(const protocol&&) = delete; + + template + requires(!std::same_as) + protocol_view(protocol& p) noexcept + : protocol_view(protocol_view(p)) {} + + template + requires(!std::same_as) + protocol_view(protocol&&) = delete; + + // See protocol's own operator= for why this using-declaration is needed: + // without it, the implicitly-declared copy-assignment operator this class + // relies on would hide any operator= inherited from operator_forwarders. + using reflection_detail::operator_forwarders, true, + true>::type::operator=; +}; + +// --------------------------------------------------------------------------- +// protocol_view — the mutable view. +// --------------------------------------------------------------------------- + +template +class protocol_view + : public reflection_detail::named_forwarders, false, + true>::type, + public reflection_detail::operator_forwarders, false, + true>::type { + template + friend class protocol_view; + template + friend struct reflection_detail::forwarding_call; + template + friend struct reflection_detail::operator_forwarder; + + using view_vtable = typename reflection_detail::view_vtable::vtable; + + template + decltype(auto) dispatch_reflected_member(Arguments&&... arguments) const { + static_assert( + reflection_detail::forwarders_at_offset_zero(^^protocol_view)); + constexpr std::meta::info entry = reflection_detail::data_member_named( + ^^typename reflection_detail::view_vtable::view_entries, + reflection_detail::vtable_entry_name(Member)); + return vptr_->entries.[:entry:](ptr_, + std::forward(arguments)...); + } + + void* ptr_; + const view_vtable* vptr_; + + template + static void* checked_ptr(protocol& p) noexcept { + assert(!p.valueless_after_move()); + return p.p_; + } + + public: + template + requires reflection_protocol_concept && not_protocol_or_view + protocol_view(U& obj) noexcept + : ptr_(std::addressof(obj)), + vptr_(&reflection_detail::view_vtable_for>) {} + + template + requires reflection_protocol_concept && not_protocol_or_view + protocol_view(const U&&) = delete; + + template + protocol_view(protocol& p) noexcept + : ptr_(checked_ptr(p)), vptr_(p.vtable_->view_vt) {} + + template + protocol_view(protocol&&) = delete; + + template + requires(!std::same_as) + protocol_view(const protocol_view& other) noexcept + : ptr_(other.ptr_), vptr_(get_vtable(other.vptr_)) {} + + template + requires(!std::same_as) + protocol_view(protocol& p) noexcept + : protocol_view(protocol_view(p)) {} + + template + requires(!std::same_as) + protocol_view(protocol&&) = delete; + + // See protocol's own operator= for why this using-declaration is needed: + // without it, the implicitly-declared copy-assignment operator this class + // relies on would hide any operator= inherited from operator_forwarders. + using reflection_detail::operator_forwarders, false, + true>::type::operator=; +}; + +template +inline protocol_view::protocol_view(protocol_view other) noexcept + : ptr_(other.ptr_), vptr_(other.vptr_) {} + +} // namespace xyz + +#endif // XYZ_PROTOCOL_REFLECTION_H_ diff --git a/protocol_reflection_detail/conformance.h b/protocol_reflection_detail/conformance.h new file mode 100644 index 0000000..6115f9e --- /dev/null +++ b/protocol_reflection_detail/conformance.h @@ -0,0 +1,284 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ +// Conformance checking for the C++26 reflection backend: does a stored +// implementation type satisfy an interface, member by member. +// Split out of protocol_reflection.h because this piece is self-contained +// (depends only on members.h and types.h, nothing from vtable-building, +// thunks, or forwarders) and independently testable: conformance_test.cc +// includes this header directly rather than the whole backend, checking the +// two public concepts against interface/implementation pairs with no +// xyz::protocol or real vtable involved. +#ifndef XYZ_PROTOCOL_REFLECTION_DETAIL_CONFORMANCE_H_ +#define XYZ_PROTOCOL_REFLECTION_DETAIL_CONFORMANCE_H_ + +#ifndef __cpp_impl_reflection +#error \ + "This header requires a compiler with C++26 reflection support (GCC 16+, -std=c++26 -freflection)." +#endif + +#include +#include +#include +#include +#include + +#include "protocol_reflection_detail/members.h" +#include "protocol_reflection_detail/types.h" + +namespace xyz { +namespace reflection_detail { + +using std::meta::info; + +// --------------------------------------------------------------------------- +// Resolving an interface member against a stored implementation type +// +// Duck-typed dispatch: given interface member M and implementation type U, +// find the member(s) of U a call could target, by name (or operator kind) +// and constness only, with no parameter or return type filtering. Rather +// than hand-comparing signatures, which would mean reimplementing overload +// resolution, every matching candidate is wrapped and merged into one +// callable type via `using Candidates::operator()...`, the same idiom +// `overloaded_calls` (protocol_reflection.h) uses for an interface's own +// overload sets, applied here to the implementation's candidates instead. +// The compiler's real overload resolution then picks the match, for both +// the conformance check and the real call: the concept (below) checks not +// "is there an exact signature match" but "is this merged candidate set +// callable", exactly what the Python backend's requires-expression-based +// concept (a real call, e.g. `t.method(std::declval()...)`) and Ryan +// Keane's rjk::duck technique (https://ryanjk5.github.io/posts/rjk-duck/) +// check. +// --------------------------------------------------------------------------- + +consteval bool same_member_name(info interface_member, + info implementation_member) { + if (std::meta::has_identifier(interface_member)) { + return std::meta::has_identifier(implementation_member) && + std::meta::identifier_of(interface_member) == + std::meta::identifier_of(implementation_member); + } + return std::meta::is_operator_function(implementation_member) && + std::meta::operator_of(interface_member) == + std::meta::operator_of(implementation_member); +} + +// Members of implementation_type matching interface_member by name (or +// operator kind), constness, and noexcept. A noexcept interface member can +// only ever be dispatched through a noexcept-qualified candidate: the vtable +// thunk and every forwarder are generated noexcept(true) for such a member +// (mirroring its exact declaration), so a throwing candidate is not merely +// non-conforming but would let an exception escape a noexcept function, +// terminating the program instead of failing to compile. is_invocable_r_v +// (used below to check the merge) does not itself inspect noexcept, so this +// filters candidates the same way the constness check above does, rather +// than relying on that trait to catch the mismatch. +// +// Rvalue-qualified candidates are excluded for an unrelated reason: self is +// always accessed as an lvalue here, and merging one in anyway would +// collide with a same-constness unqualified candidate's identical +// operator() signature (member_function_type discards ref-qualification), +// making the merge ambiguous rather than unusable. +consteval std::vector resolve_implementation_candidates( + info implementation_type, info interface_member) { + std::vector candidates; + for (info candidate : std::meta::members_of( + implementation_type, std::meta::access_context::current())) { + if (!is_interface_member_function(candidate)) continue; + if (!same_member_name(interface_member, candidate)) continue; + if (std::meta::is_const(interface_member) && + !std::meta::is_const(candidate)) { + continue; + } + if (std::meta::is_noexcept(interface_member) && + !std::meta::is_noexcept(candidate)) { + continue; + } + if (std::meta::is_rvalue_reference_qualified(candidate)) continue; + candidates.push_back(candidate); + } + return candidates; +} + +// One matching implementation candidate, callable with its own real +// parameter types, not the interface member's. Merging several of these +// (candidate_overload_set, below) and calling the merge with the interface +// member's argument types is what routes the call through the compiler's +// real overload resolution, implicit conversions included, instead of a +// hand-rolled comparison. +// +// operator() is const-qualified exactly when Candidate itself is const, not +// based on the outer erasure (ConstErased, which only picks self's pointee +// constness). This matters when a stored type declares both a const and a +// non-const overload of the same name and parameters: merging two wrappers +// whose operator()s differ only by this same cv-qualification is ordinary +// C++ overloading on constness, exactly as if the class had declared both +// directly. Real overload resolution then ranks them the same way it would +// rank calling the member directly: preferring non-const on a non-const +// access path, with only the const one viable at all on a const path. +template +struct implementation_candidate_call; + +template +struct implementation_candidate_call { + using SelfPointer = + std::conditional_t; + SelfPointer self; + + explicit implementation_candidate_call(SelfPointer self) : self(self) {} + + ReturnType operator()(ParameterTypes... parameters) { + return self->[:Candidate:](std::forward(parameters)...); + } +}; + +template +struct implementation_candidate_call { + using SelfPointer = + std::conditional_t; + SelfPointer self; + + explicit implementation_candidate_call(SelfPointer self) : self(self) {} + + ReturnType operator()(ParameterTypes... parameters) const { + return self->[:Candidate:](std::forward(parameters)...); + } +}; + +// Merges one wrapper per implementation candidate for a single interface +// member. This is the implementation-candidate axis; overloaded_calls +// (protocol_reflection.h) is the unrelated interface-overload axis, merging +// wrappers for an interface's own declared overloads. +template +struct candidate_overload_set : Candidates... { + using Candidates::operator()...; + + template + explicit candidate_overload_set(SelfPointer self) : Candidates(self)... {} +}; + +// The merged candidate_overload_set type (or the lone candidate's own +// wrapper type, or info{} if there are no name/constness-eligible +// candidates at all) for one interface member against implementation_type. +consteval info make_candidate_overload_set(info implementation_type, + info interface_member, + bool const_erased) { + std::vector candidates = + resolve_implementation_candidates(implementation_type, interface_member); + if (candidates.empty()) return info{}; + std::vector wrapper_types; + for (info candidate : candidates) { + wrapper_types.push_back(std::meta::substitute( + ^^implementation_candidate_call, + { + implementation_type, std::meta::reflect_constant(candidate), + member_function_type(candidate), + std::meta::reflect_constant(const_erased), + std::meta::reflect_constant(std::meta::is_const(candidate))})); + } + if (wrapper_types.size() == 1) return wrapper_types.front(); + return std::meta::substitute(^^candidate_overload_set, wrapper_types); +} + +// Peels R(Ps...) back into a real parameter pack, since is_invocable_r_v and +// is_invocable_v need a template argument pack, not a std::vector, to +// check whether MergedCandidates is callable with an interface member's +// parameter types. Same partial-specialization idiom erased_call_thunk +// (thunk.h) uses. +template +struct is_invocable_with_return; + +template +struct is_invocable_with_return { + static constexpr bool value = + std::is_void_v + ? std::is_invocable_v + : std::is_invocable_r_v< + ReturnType, typename[:MergedCandidates:], ParameterTypes...>; +}; + +template +consteval bool member_is_satisfiable(bool const_only) { + constexpr info member = interface_members[Index]; + if (const_only && !std::meta::is_const(member)) return true; + constexpr info merged = make_candidate_overload_set( + ^^Implementation, member, std::meta::is_const(member)); + if constexpr (merged == info{}) { + return false; + } else { + return is_invocable_with_return< + merged, typename[:member_function_type(member):]>::value; + } +} + +template +consteval bool all_members_satisfiable(bool const_only, + std::index_sequence) { + return (... && member_is_satisfiable( + const_only)); +} + +template +consteval bool models_reflected_interface(bool const_only = false) { + // Non-class candidates (e.g. an int offered to a converting constructor + // during overload resolution) have no members to enumerate, so they don't + // model the interface. This must be `if constexpr`, not a plain `if`: + // all_members_satisfiable is a template, and merely naming it in a + // live (non-discarded) statement forces its instantiation regardless of + // which branch would run at evaluation time. That instantiation includes + // make_candidate_overload_set's std::meta::members_of call on + // Implementation. Only a discarded if-constexpr branch is skipped. + if constexpr (!std::meta::is_class_type( + std::meta::dealias(^^Implementation)) || + !std::meta::is_complete_type( + std::meta::dealias(^^Implementation))) { + return false; + } else { + return all_members_satisfiable( + const_only, + std::make_index_sequence.size()>()); + } +} + +} // namespace reflection_detail + +// Reflection-backed equivalents of the per-interface concepts +// protocol_const_concept_ / protocol_concept_. +template +concept reflection_protocol_const_concept = + reflection_detail::models_reflected_interface< + std::remove_cvref_t, Interface>(true); + +template +concept reflection_protocol_concept = + reflection_detail::models_reflected_interface< + std::remove_cvref_t, Interface>(false); + +} // namespace xyz + +#endif // XYZ_PROTOCOL_REFLECTION_DETAIL_CONFORMANCE_H_ diff --git a/protocol_reflection_detail/conformance_test.cc b/protocol_reflection_detail/conformance_test.cc new file mode 100644 index 0000000..5578a98 --- /dev/null +++ b/protocol_reflection_detail/conformance_test.cc @@ -0,0 +1,104 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ + +#include "protocol_reflection_detail/conformance.h" + +#include + +// Direct tests of the reflection backend's conformance concepts +// (conformance.h): does an implementation type satisfy an interface, +// member by member. This file includes only the conformance header, not +// protocol.h, and no xyz::protocol or real vtable is ever involved. + +namespace { + +struct SimpleInterface { + int compute(int) const; +}; + +struct GoodImplementation { + int compute(int x) const { return x; } +}; + +TEST(ReflectionConformanceTest, SatisfiesWhenMemberMatches) { + static_assert(xyz::reflection_protocol_const_concept); + static_assert( + xyz::reflection_protocol_concept); +} + +struct MissingMethodImplementation { + int other() const { return 0; } +}; + +TEST(ReflectionConformanceTest, FailsWhenMethodMissing) { + static_assert( + !xyz::reflection_protocol_const_concept); +} + +struct NotConvertible {}; + +struct WrongReturnTypeImplementation { + NotConvertible compute(int) const { return {}; } +}; + +TEST(ReflectionConformanceTest, FailsWhenReturnTypeNotConvertible) { + static_assert( + !xyz::reflection_protocol_const_concept); +} + +struct NonConstInterface { + int compute(int); +}; + +struct ConstOnlyImplementation { + int compute(int) const { return 0; } +}; + +TEST(ReflectionConformanceTest, ConstCandidateSatisfiesNonConstMember) { + // A const-qualified implementation candidate can serve any interface + // member regardless of the member's own constness: calling a const + // method through a non-const access path is always fine. + static_assert(xyz::reflection_protocol_concept); +} + +struct ConstInterface { + int compute(int) const; +}; + +struct MutatingOnlyImplementation { + int compute(int) { return 0; } +}; + +TEST(ReflectionConformanceTest, MutatingCandidateFailsConstMember) { + // compute() const on the interface can only be satisfied by a const + // candidate, regardless of which concept is checked: a const interface + // member is unreachable through a non-const self either way. + static_assert( + !xyz::reflection_protocol_const_concept); + static_assert(!xyz::reflection_protocol_concept); +} + +} // namespace diff --git a/protocol_reflection_detail/members.h b/protocol_reflection_detail/members.h new file mode 100644 index 0000000..83562cc --- /dev/null +++ b/protocol_reflection_detail/members.h @@ -0,0 +1,118 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ +// Interface member enumeration for the C++26 reflection backend: deciding +// which of an interface type's members are dispatchable protocol members at +// all. Split out of protocol_reflection.h because this piece is +// self-contained (depends only on the standard library and , not on +// naming, candidate resolution, or vtable machinery) and independently +// testable: members_test.cc includes this header directly rather than the +// whole backend. +#ifndef XYZ_PROTOCOL_REFLECTION_MEMBERS_H_ +#define XYZ_PROTOCOL_REFLECTION_MEMBERS_H_ + +#ifndef __cpp_impl_reflection +#error \ + "This header requires a compiler with C++26 reflection support (GCC 16+, -std=c++26 -freflection)." +#endif + +#include +#include +#include + +namespace xyz { +namespace reflection_detail { + +using std::meta::info; + +// --------------------------------------------------------------------------- +// Interface member enumeration +// --------------------------------------------------------------------------- + +// Member function templates are deliberately excluded: a template has no +// fixed signature, so it cannot be mapped to a single vtable slot. This is +// currently implied by std::meta::is_function returning false for an +// uninstantiated function template, but is checked explicitly so the +// exclusion doesn't depend on that incidental behaviour. +consteval bool is_interface_member_function(info member) { + return std::meta::is_function(member) && + !std::meta::is_function_template(member) && + !std::meta::is_special_member_function(member) && + !std::meta::is_static_member(member) && std::meta::is_public(member) && + (std::meta::has_identifier(member) || + std::meta::is_operator_function(member)); +} + +// All interface member functions, in declaration order, optionally filtered +// by constness. Declaration order is load-bearing: vtable entry order and +// forwarder generation both follow it. +consteval std::vector interface_member_functions( + info interface_type, bool const_only = false) { + std::vector result; + for (info member : std::meta::members_of( + interface_type, std::meta::access_context::current())) { + if (is_interface_member_function(member)) { + if (!const_only || std::meta::is_const(member)) { + result.push_back(member); + } + } + } + return result; +} + +// Names that are always public members of protocol / protocol_view +// (see protocol_reflection.h's class definitions). An interface member +// function with one of these names would be silently hidden by C++ +// name-hiding rules rather than reachable through the generated forwarders, +// so it is rejected with a static_assert instead of compiling to a silently +// broken call. +consteval bool has_reserved_interface_member_name(info interface_type) { + for (info member : interface_member_functions(interface_type)) { + if (!std::meta::has_identifier(member)) continue; + std::string_view name = std::meta::identifier_of(member); + if (name == "swap" || name == "valueless_after_move") return true; + } + return false; +} + +// Rvalue-qualified interface members (declared `&&`) are rejected: this +// backend does not yet give a forwarder its own `&&`-qualified operator(). +// Left unrejected, such a member would silently compile to an ordinary, +// always-callable forwarder instead of one restricted to a moved-from +// protocol, misrepresenting what the interface declared. +consteval bool has_rvalue_qualified_interface_member(info interface_type) { + for (info member : interface_member_functions(interface_type)) { + if (std::meta::is_rvalue_reference_qualified(member)) return true; + } + return false; +} + +// The interface's member list, enumerated once per interface and persisted +// with define_static_array. Vtable construction (make_vtable_entry) and the +// concept checks (models_reflected_interface) index this array; forwarder +// generation (named_forwarders, make_operator_forwarders) instead +// re-enumerates via interface_member_functions directly. +template +inline constexpr auto interface_members = + std::define_static_array(interface_member_functions(^^Interface)); + +} // namespace reflection_detail +} // namespace xyz + +#endif // XYZ_PROTOCOL_REFLECTION_MEMBERS_H_ diff --git a/protocol_reflection_detail/members_test.cc b/protocol_reflection_detail/members_test.cc new file mode 100644 index 0000000..c7761d7 --- /dev/null +++ b/protocol_reflection_detail/members_test.cc @@ -0,0 +1,190 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ + +#include "members.h" + +#include + +#include + +// Direct tests of reflection_detail's member-enumeration helpers +// (members.h): which of a struct's members count as dispatchable interface +// members at all. This file includes only the members header, not +// protocol.h, and none of the structs below are ever wrapped in +// xyz::protocol. + +namespace { + +struct EnumerationProbe { + EnumerationProbe() = default; + + void public_method() {} + + private: + void private_method() {} + + public: + static void static_method() {} + + template + void template_method(T) {} + + int operator+(int x) { return x; } + + int data_member = 0; +}; + +consteval std::meta::info FindByName(std::string_view name) { + for (std::meta::info member : std::meta::members_of( + ^^EnumerationProbe, std::meta::access_context::unchecked())) { + if (std::meta::has_identifier(member) && + std::meta::identifier_of(member) == name) { + return member; + } + } + return std::meta::info{}; +} + +consteval std::meta::info FindSpecialMember() { + for (std::meta::info member : std::meta::members_of( + ^^EnumerationProbe, std::meta::access_context::unchecked())) { + if (std::meta::is_special_member_function(member)) return member; + } + return std::meta::info{}; +} + +consteval std::meta::info FindOperatorPlus() { + for (std::meta::info member : std::meta::members_of( + ^^EnumerationProbe, std::meta::access_context::unchecked())) { + if (std::meta::is_operator_function(member) && + std::meta::operator_of(member) == std::meta::operators::op_plus) { + return member; + } + } + return std::meta::info{}; +} + +TEST(ReflectionMembersTest, AcceptsOrdinaryPublicMethod) { + static_assert(xyz::reflection_detail::is_interface_member_function( + FindByName("public_method"))); +} + +TEST(ReflectionMembersTest, RejectsPrivateMethod) { + static_assert(!xyz::reflection_detail::is_interface_member_function( + FindByName("private_method"))); +} + +TEST(ReflectionMembersTest, RejectsStaticMethod) { + static_assert(!xyz::reflection_detail::is_interface_member_function( + FindByName("static_method"))); +} + +TEST(ReflectionMembersTest, RejectsFunctionTemplate) { + static_assert(!xyz::reflection_detail::is_interface_member_function( + FindByName("template_method"))); +} + +TEST(ReflectionMembersTest, RejectsSpecialMemberFunction) { + static_assert(!xyz::reflection_detail::is_interface_member_function( + FindSpecialMember())); +} + +TEST(ReflectionMembersTest, AcceptsOperatorFunction) { + static_assert( + xyz::reflection_detail::is_interface_member_function(FindOperatorPlus())); +} + +TEST(ReflectionMembersTest, RejectsDataMember) { + static_assert(!xyz::reflection_detail::is_interface_member_function( + std::meta::nonstatic_data_members_of( + ^^EnumerationProbe, std::meta::access_context::current())[0])); +} + +struct ConstFilterProbe { + void mutating() {} + + void observing() const {} +}; + +consteval std::size_t CountMembers(bool const_only) { + return xyz::reflection_detail::interface_member_functions(^^ConstFilterProbe, + const_only) + .size(); +} + +TEST(ReflectionMembersTest, InterfaceMemberFunctionsFiltersByConstness) { + static_assert(CountMembers(false) == 2); + static_assert(CountMembers(true) == 1); +} + +struct DeclarationOrderProbe { + void third() {} + + void first() {} + + void second() {} +}; + +consteval bool PreservesDeclarationOrder() { + auto members = xyz::reflection_detail::interface_member_functions( + ^^DeclarationOrderProbe); + return members.size() == 3 && + std::meta::identifier_of(members[0]) == "third" && + std::meta::identifier_of(members[1]) == "first" && + std::meta::identifier_of(members[2]) == "second"; +} + +TEST(ReflectionMembersTest, InterfaceMemberFunctionsPreservesDeclarationOrder) { + static_assert(PreservesDeclarationOrder()); +} + +struct CleanInterface { + void foo() {} +}; + +struct ReservedSwapInterface { + void swap(int) {} +}; + +struct ReservedValuelessInterface { + void valueless_after_move() {} +}; + +TEST(ReflectionMembersTest, HasReservedInterfaceMemberName) { + static_assert(!xyz::reflection_detail::has_reserved_interface_member_name( + ^^CleanInterface)); + static_assert(xyz::reflection_detail::has_reserved_interface_member_name( + ^^ReservedSwapInterface)); + static_assert(xyz::reflection_detail::has_reserved_interface_member_name( + ^^ReservedValuelessInterface)); +} + +struct RvalueQualifiedInterface { + void consume() && {} +}; + +TEST(ReflectionMembersTest, HasRvalueQualifiedInterfaceMember) { + static_assert(!xyz::reflection_detail::has_rvalue_qualified_interface_member( + ^^CleanInterface)); + static_assert(xyz::reflection_detail::has_rvalue_qualified_interface_member( + ^^RvalueQualifiedInterface)); +} + +} // namespace diff --git a/protocol_reflection_detail/naming.h b/protocol_reflection_detail/naming.h new file mode 100644 index 0000000..128676c --- /dev/null +++ b/protocol_reflection_detail/naming.h @@ -0,0 +1,127 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ +// Deterministic, stable vtable entry naming for the C++26 reflection backend. +// Split out of protocol_reflection.h because this piece is self-contained +// (every function here depends only on the standard library and , none +// on the rest of the backend's member-enumeration or vtable machinery) and +// independently testable: naming_test.cc includes this header directly +// rather than the whole backend. +#ifndef XYZ_PROTOCOL_REFLECTION_NAMING_H_ +#define XYZ_PROTOCOL_REFLECTION_NAMING_H_ + +#ifndef __cpp_impl_reflection +#error \ + "This header requires a compiler with C++26 reflection support (GCC 16+, -std=c++26 -freflection)." +#endif + +#include +#include +#include + +namespace xyz { +namespace reflection_detail { + +using std::meta::info; + +// --------------------------------------------------------------------------- +// Deterministic, stable entry naming +// +// Vtable entry names must be valid C++ identifiers, unique within one +// interface's vtable, and a deterministic function of a member's own +// signature, not of its position among other members. The last part +// matters because narrowing (see protocol_reflection.h's "Vtable narrowing +// maps") matches vtable entries between two *different* interfaces by name, +// so the same signature must always produce the same name regardless of +// what other members its interface happens to declare. +// +// Rather than hashing the signature (which only gives a probabilistic +// guarantee against collisions), identifier_safe_string encodes it exactly: +// every byte outside [a-zA-Z0-9], including a literal `_` itself, is +// replaced by `_` followed by its two-digit hex value. This is injective +// by construction: scanning left to right, a `_` in the output always +// starts a two-hex-digit escape, since no unescaped `_` from the input +// ever survives unescaped, so two different signatures can never collide +// the way two different hash inputs theoretically could. +// --------------------------------------------------------------------------- + +consteval std::string identifier_safe_string(std::string_view text) { + const char* hex_digits = "0123456789abcdef"; + std::string result; + for (unsigned char byte : text) { + bool is_identifier_safe = (byte >= 'a' && byte <= 'z') || + (byte >= 'A' && byte <= 'Z') || + (byte >= '0' && byte <= '9'); + if (is_identifier_safe) { + result += static_cast(byte); + } else { + result += '_'; + result += hex_digits[(byte >> 4) & 0xF]; + result += hex_digits[byte & 0xF]; + } + } + return result; +} + +// Identifier-safe spelling for an operator kind: every std::meta::operators +// enumerator is named op_, found here by reflection (instead of one +// hand-written switch case per operator) and returned with that prefix +// stripped. +consteval std::string_view operator_spelling(std::meta::operators kind) { + template for (constexpr info e : std::define_static_array( + std::meta::enumerators_of(^^std::meta::operators))) { + if (kind == [:e:]) { + return std::string_view(std::meta::identifier_of(e)).substr(3); + } + } + return ""; +} + +consteval std::string mangled_member_name(info member) { + if (std::meta::has_identifier(member)) { + return std::string(std::meta::identifier_of(member)); + } + return "operator_" + + std::string(operator_spelling(std::meta::operator_of(member))); +} + +consteval std::string member_signature_string(info member) { + std::string signature = mangled_member_name(member); + signature += "("; + bool first = true; + for (info parameter : std::meta::parameters_of(member)) { + if (!first) signature += ","; + first = false; + signature += std::meta::display_string_of( + std::meta::dealias(std::meta::type_of(parameter))); + } + signature += ")"; + if (std::meta::is_const(member)) signature += " const"; + if (std::meta::is_noexcept(member)) signature += " noexcept"; + return signature; +} + +consteval std::string vtable_entry_name(info member) { + return identifier_safe_string(member_signature_string(member)); +} + +} // namespace reflection_detail +} // namespace xyz + +#endif // XYZ_PROTOCOL_REFLECTION_NAMING_H_ diff --git a/protocol_reflection_detail/naming_test.cc b/protocol_reflection_detail/naming_test.cc new file mode 100644 index 0000000..31a4d80 --- /dev/null +++ b/protocol_reflection_detail/naming_test.cc @@ -0,0 +1,145 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ + +#include "naming.h" + +#include + +#include +#include +#include + +// Direct tests of reflection_detail's naming helpers (naming.h), which every +// vtable slot's identifier depends on. This file includes only the naming +// header, not protocol.h, and none of the structs below are ever wrapped in +// xyz::protocol: a collision or instability here is caught at its source +// instead of surfacing as a confusing dispatch or narrowing failure +// somewhere else. +// +// These helpers are consteval, so their properties are checked with +// static_assert rather than EXPECT_EQ/EXPECT_NE: the result is a compile-time +// fact, and comparing within one constant expression avoids the (unrelated) +// restriction on a heap-allocating std::string escaping constant evaluation +// to become an ordinary runtime value. Each check still sits inside a named +// TEST() so it shows up as its own passing test once the static_assert has +// compiled. + +namespace { + +TEST(ReflectionDetailTest, IdentifierSafeStringEscapesLiteralUnderscore) { + // Escaping a literal underscore, rather than leaving it alone, is what + // prevents this exact collision: under a scheme that left '_' unescaped, + // "a_20" (literal 'a', '_', '2', '0') and "a " (a space, itself escaped + // to "_20") would both encode to "a_20". + static_assert(xyz::reflection_detail::identifier_safe_string("a_20") != + xyz::reflection_detail::identifier_safe_string("a ")); +} + +TEST(ReflectionDetailTest, IdentifierSafeStringPassesAlnumThrough) { + static_assert(xyz::reflection_detail::identifier_safe_string("Abc123") == + "Abc123"); +} + +TEST(ReflectionDetailTest, IdentifierSafeStringEscapesNonAlnumAsHex) { + static_assert(xyz::reflection_detail::identifier_safe_string(" ") == "_20"); + static_assert(xyz::reflection_detail::identifier_safe_string("_") == "_5f"); + static_assert(xyz::reflection_detail::identifier_safe_string("(") == "_28"); +} + +consteval bool AllOperatorSpellingsAreUnique() { + std::vector spellings; + template for (constexpr std::meta::info e : std::define_static_array( + std::meta::enumerators_of(^^std::meta::operators))) { + std::string_view spelling = + xyz::reflection_detail::operator_spelling([:e:]); + for (std::string_view existing : spellings) { + if (existing == spelling) return false; + } + spellings.push_back(spelling); + } + return true; +} + +TEST(ReflectionDetailTest, OperatorSpellingIsUniquePerOperator) { + // operator_spelling feeds directly into every operator's mangled vtable + // entry name; two operator kinds sharing a spelling would collide on one + // vtable slot. + static_assert(AllOperatorSpellingsAreUnique()); +} + +struct ConstOverloadNamingProbe { + void call(int) {} + + void call(int) const {} +}; + +consteval std::string EntryNameForConstness(bool want_const) { + for (std::meta::info member : std::meta::members_of( + ^^ConstOverloadNamingProbe, std::meta::access_context::current())) { + if (!std::meta::has_identifier(member)) continue; + if (std::meta::identifier_of(member) != "call") continue; + if (std::meta::is_const(member) != want_const) continue; + return xyz::reflection_detail::vtable_entry_name(member); + } + return ""; +} + +TEST(ReflectionDetailTest, VtableEntryNameDistinguishesConstness) { + // call(int) and call(int) const share everything vtable_entry_name + // encodes except constness; if constness weren't part of the entry name, + // protocol would generate one colliding vtable slot for what should be + // two independently dispatchable overloads. + static_assert(EntryNameForConstness(false) != EntryNameForConstness(true)); +} + +TEST(ReflectionDetailTest, VtableEntryNameIsStableAcrossCalls) { + // Narrowing conversions (copy_vtable_entries) match entries between two + // different interfaces by name, so reflecting the same member twice must + // always produce the identical name. + static_assert(EntryNameForConstness(false) == EntryNameForConstness(false)); +} + +struct ParameterCountNamingProbe { + void call(int) {} + + void call(int, int) {} +}; + +consteval std::string EntryNameForParameterCount(std::size_t param_count) { + for (std::meta::info member : std::meta::members_of( + ^^ParameterCountNamingProbe, std::meta::access_context::current())) { + if (!std::meta::has_identifier(member)) continue; + if (std::meta::identifier_of(member) != "call") continue; + std::size_t count = 0; + for ([[maybe_unused]] std::meta::info parameter : + std::meta::parameters_of(member)) { + ++count; + } + if (count != param_count) continue; + return xyz::reflection_detail::vtable_entry_name(member); + } + return ""; +} + +TEST(ReflectionDetailTest, VtableEntryNameDistinguishesParameterCount) { + static_assert(EntryNameForParameterCount(1) != EntryNameForParameterCount(2)); +} + +} // namespace diff --git a/protocol_reflection_detail/operator_forwarders.h b/protocol_reflection_detail/operator_forwarders.h new file mode 100644 index 0000000..a8830db --- /dev/null +++ b/protocol_reflection_detail/operator_forwarders.h @@ -0,0 +1,1271 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ +// ============================================================================ +// AUTOMATICALLY GENERATED FILE - DO NOT MODIFY +// ============================================================================ +// This file was generated by scripts/generate_operator_forwarders.py. +// +// One operator_forwarder specialization pair (non-const, const) plus one +// operator_join specialization per operator kind, for the C++26 reflection +// backend: forwarding an interface operator requires a real `operator` +// declaration, and the symbol must appear as a literal token in source, so +// each kind is generated once here rather than reimplemented by hand. +// +// Any manual changes made to this file will be overwritten during the next +// generation run. +// ============================================================================ +#ifndef XYZ_PROTOCOL_REFLECTION_DETAIL_OPERATOR_FORWARDERS_H_ +#define XYZ_PROTOCOL_REFLECTION_DETAIL_OPERATOR_FORWARDERS_H_ + +#ifndef __cpp_impl_reflection +#error \ + "This header requires a compiler with C++26 reflection support (GCC 16+, -std=c++26 -freflection)." +#endif + +#include +#include + +namespace xyz { +namespace reflection_detail { + +using std::meta::info; + +template +struct operator_forwarder; + +// One join per operator kind gathers every overload of that kind into a +// single class: name lookup for e.g. `operator+` across multiple distinct +// bases would be ambiguous, so the overloads must be merged with +// using-declarations, which also require the operator symbol literally. +template +struct operator_join; + +// op_equals +template +struct operator_forwarder { + ReturnType operator=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator=...; +}; + +// op_parentheses +template +struct operator_forwarder { + ReturnType operator()(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator()(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator()...; +}; + +// op_square_brackets +template +struct operator_forwarder { + ReturnType operator[](ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator[](ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator[]...; +}; + +// op_arrow +template +struct operator_forwarder { + ReturnType operator->() noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member(); + } +}; + +template +struct operator_forwarder { + ReturnType operator->() const noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member(); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator->...; +}; + +// op_arrow_star +template +struct operator_forwarder { + ReturnType operator->*(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator->*(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator->*...; +}; + +// op_tilde +template +struct operator_forwarder { + ReturnType operator~() noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member(); + } +}; + +template +struct operator_forwarder { + ReturnType operator~() const noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member(); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator~...; +}; + +// op_exclamation +template +struct operator_forwarder { + ReturnType operator!() noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member(); + } +}; + +template +struct operator_forwarder { + ReturnType operator!() const noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member(); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator!...; +}; + +// op_plus +template +struct operator_forwarder { + ReturnType operator+(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator+(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator+...; +}; + +// op_minus +template +struct operator_forwarder { + ReturnType operator-(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator-(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator-...; +}; + +// op_star +template +struct operator_forwarder { + ReturnType operator*(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator*(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator*...; +}; + +// op_slash +template +struct operator_forwarder { + ReturnType operator/(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator/(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator/...; +}; + +// op_percent +template +struct operator_forwarder { + ReturnType operator%(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator%(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator%...; +}; + +// op_caret +template +struct operator_forwarder { + ReturnType operator^(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator^(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator^...; +}; + +// op_ampersand +template +struct operator_forwarder { + ReturnType operator&(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator&(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator&...; +}; + +// op_pipe +template +struct operator_forwarder { + ReturnType operator|(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator|(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator|...; +}; + +// op_plus_equals +template +struct operator_forwarder { + ReturnType operator+=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator+=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator+=...; +}; + +// op_minus_equals +template +struct operator_forwarder { + ReturnType operator-=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator-=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator-=...; +}; + +// op_star_equals +template +struct operator_forwarder { + ReturnType operator*=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator*=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator*=...; +}; + +// op_slash_equals +template +struct operator_forwarder { + ReturnType operator/=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator/=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator/=...; +}; + +// op_percent_equals +template +struct operator_forwarder { + ReturnType operator%=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator%=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator%=...; +}; + +// op_caret_equals +template +struct operator_forwarder { + ReturnType operator^=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator^=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator^=...; +}; + +// op_ampersand_equals +template +struct operator_forwarder { + ReturnType operator&=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator&=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator&=...; +}; + +// op_pipe_equals +template +struct operator_forwarder { + ReturnType operator|=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator|=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator|=...; +}; + +// op_equals_equals +template +struct operator_forwarder { + ReturnType operator==(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator==(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator==...; +}; + +// op_exclamation_equals +template +struct operator_forwarder { + ReturnType operator!=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator!=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator!=...; +}; + +// op_less +template +struct operator_forwarder { + ReturnType operator<(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator<(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator<...; +}; + +// op_greater +template +struct operator_forwarder { + ReturnType operator>(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator>(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator>...; +}; + +// op_less_equals +template +struct operator_forwarder { + ReturnType operator<=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator<=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator<=...; +}; + +// op_greater_equals +template +struct operator_forwarder { + ReturnType operator>=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator>=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator>=...; +}; + +// op_spaceship +template +struct operator_forwarder { + ReturnType operator<=>(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator<=>(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator<=>...; +}; + +// op_ampersand_ampersand +template +struct operator_forwarder { + ReturnType operator&&(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator&&(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join : Forwarders... { + using Forwarders::operator&&...; +}; + +// op_pipe_pipe +template +struct operator_forwarder { + ReturnType operator||(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator||(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator||...; +}; + +// op_less_less +template +struct operator_forwarder { + ReturnType operator<<(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator<<(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator<<...; +}; + +// op_greater_greater +template +struct operator_forwarder { + ReturnType operator>>(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator>>(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator>>...; +}; + +// op_less_less_equals +template +struct operator_forwarder { + ReturnType operator<<=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator<<=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator<<=...; +}; + +// op_greater_greater_equals +template +struct operator_forwarder { + ReturnType operator>>=(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator>>=(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join : Forwarders... { + using Forwarders::operator>>=...; +}; + +// op_plus_plus +template +struct operator_forwarder { + ReturnType operator++(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator++(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator++...; +}; + +// op_minus_minus +template +struct operator_forwarder { + ReturnType operator--(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator--(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator--...; +}; + +// op_comma +template +struct operator_forwarder { + ReturnType operator,(ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_forwarder { + ReturnType operator,(ParameterTypes... parameters) const + noexcept(IsNoexcept) { + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + } +}; + +template +struct operator_join + : Forwarders... { + using Forwarders::operator, ...; +}; + +} // namespace reflection_detail +} // namespace xyz + +#endif // XYZ_PROTOCOL_REFLECTION_DETAIL_OPERATOR_FORWARDERS_H_ diff --git a/protocol_reflection_detail/thunk.h b/protocol_reflection_detail/thunk.h new file mode 100644 index 0000000..88f706f --- /dev/null +++ b/protocol_reflection_detail/thunk.h @@ -0,0 +1,87 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ +// Erased-call thunk for the C++26 reflection backend: the static function +// shape a vtable entry points at. Split out of protocol_reflection.h +// because this piece is self-contained: it only needs an already-built +// Signature and a reflected MergedCandidates type, constructible from a +// pointer and callable with the given parameters. It never calls into +// member enumeration, naming, type synthesis, or candidate resolution +// itself; those are only used by callers deciding what to plug in. That +// makes it independently testable with a hand-rolled fake candidate type: +// thunk_test.cc includes this header directly rather than the whole +// backend. +#ifndef XYZ_PROTOCOL_REFLECTION_THUNK_H_ +#define XYZ_PROTOCOL_REFLECTION_THUNK_H_ + +#ifndef __cpp_impl_reflection +#error \ + "This header requires a compiler with C++26 reflection support (GCC 16+, -std=c++26 -freflection)." +#endif + +#include +#include +#include + +namespace xyz { +namespace reflection_detail { + +using std::meta::info; + +// --------------------------------------------------------------------------- +// Erased-call thunks: the static functions vtable entries point at. +// Signature exactly mirrors the interface member (exact parameter types, +// matching noexcept), with a leading erased pointer. ConstErased selects the +// const_view flavour (const void* + const implementation access). +// MergedCandidates is the make_candidate_overload_set type built +// for this interface member: the thunk constructs an instance bound to +// self and calls through it, so the compiler's real overload resolution, +// not this thunk, picks which candidate runs. +// --------------------------------------------------------------------------- + +template +struct erased_call_thunk; + +template +struct erased_call_thunk { + using ErasedPointer = std::conditional_t; + using SelfPointer = + std::conditional_t; + + static ReturnType call(ErasedPointer erased, + ParameterTypes... parameters) noexcept(IsNoexcept) { + auto* self = static_cast(erased); + using Candidates = [:MergedCandidates:]; + Candidates candidates(self); + if constexpr (std::is_void_v) { + candidates(std::forward(parameters)...); + } else { + return candidates(std::forward(parameters)...); + } + } +}; + +} // namespace reflection_detail +} // namespace xyz + +#endif // XYZ_PROTOCOL_REFLECTION_THUNK_H_ diff --git a/protocol_reflection_detail/thunk_test.cc b/protocol_reflection_detail/thunk_test.cc new file mode 100644 index 0000000..19f69d2 --- /dev/null +++ b/protocol_reflection_detail/thunk_test.cc @@ -0,0 +1,118 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ + +#include "thunk.h" + +#include + +#include + +// Direct tests of erased_call_thunk (thunk.h), using hand-rolled fake +// "candidate" types rather than the real candidate_overload_set machinery. +// No xyz::protocol is involved: the thunk's own contract only requires a +// reflected type constructible from a pointer and callable with the given +// parameters. + +namespace { + +struct FakeImplementation { + int value = 0; +}; + +struct FakeCandidate { + FakeImplementation* self; + + explicit FakeCandidate(FakeImplementation* self) : self(self) {} + + int operator()(int x) { return self->value + x; } +}; + +TEST(ReflectionThunkTest, ForwardsCallToMergedCandidate) { + FakeImplementation impl{10}; + void* erased = &impl; + int result = xyz::reflection_detail::erased_call_thunk< + FakeImplementation, ^^FakeCandidate, int(int), false, false>::call(erased, + 5); + EXPECT_EQ(result, 15); +} + +struct FakeConstCandidate { + const FakeImplementation* self; + + explicit FakeConstCandidate(const FakeImplementation* self) : self(self) {} + + int operator()(int x) const { return self->value + x; } +}; + +TEST(ReflectionThunkTest, ConstErasedUsesConstPointer) { + const FakeImplementation impl{20}; + const void* erased = &impl; + int result = + xyz::reflection_detail::erased_call_thunk::call(erased, 5); + EXPECT_EQ(result, 25); +} + +struct VoidCandidate { + FakeImplementation* self; + + explicit VoidCandidate(FakeImplementation* self) : self(self) {} + + void operator()(int x) { self->value = x; } +}; + +TEST(ReflectionThunkTest, HandlesVoidReturnType) { + FakeImplementation impl{0}; + void* erased = &impl; + xyz::reflection_detail::erased_call_thunk::call(erased, 42); + EXPECT_EQ(impl.value, 42); +} + +struct MultiParamCandidate { + FakeImplementation* self; + + explicit MultiParamCandidate(FakeImplementation* self) : self(self) {} + + int operator()(int a, int b) { return self->value + a + b; } +}; + +TEST(ReflectionThunkTest, ForwardsMultipleParametersInOrder) { + FakeImplementation impl{1}; + void* erased = &impl; + int result = xyz::reflection_detail::erased_call_thunk< + FakeImplementation, ^^MultiParamCandidate, int(int, int), false, + false>::call(erased, 2, 3); + EXPECT_EQ(result, 6); +} + +TEST(ReflectionThunkTest, IsNoexceptExactlyWhenRequested) { + using NoexceptThunk = xyz::reflection_detail::erased_call_thunk< + FakeImplementation, ^^FakeCandidate, int(int), false, true>; + static_assert(noexcept(NoexceptThunk::call(std::declval(), 0))); + + using ThrowingThunk = xyz::reflection_detail::erased_call_thunk< + FakeImplementation, ^^FakeCandidate, int(int), false, false>; + static_assert(!noexcept(ThrowingThunk::call(std::declval(), 0))); +} + +} // namespace diff --git a/protocol_reflection_detail/types.h b/protocol_reflection_detail/types.h new file mode 100644 index 0000000..342d873 --- /dev/null +++ b/protocol_reflection_detail/types.h @@ -0,0 +1,107 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ +// Type synthesis helpers for the C++26 reflection backend: building the +// R(Ps...) and R(*)(Erased, Ps...) types that thunks and call wrappers +// pattern-match against, from a reflected member's own return type and +// parameter list. Split out of protocol_reflection.h because this piece is +// self-contained (depends only on the standard library and , not on +// member enumeration, naming, or vtable machinery) and independently +// testable: types_test.cc includes this header directly rather than the +// whole backend. +#ifndef XYZ_PROTOCOL_REFLECTION_TYPES_H_ +#define XYZ_PROTOCOL_REFLECTION_TYPES_H_ + +#ifndef __cpp_impl_reflection +#error \ + "This header requires a compiler with C++26 reflection support (GCC 16+, -std=c++26 -freflection)." +#endif + +#include +#include +#include + +namespace xyz { +namespace reflection_detail { + +using std::meta::info; + +// --------------------------------------------------------------------------- +// Type synthesis helpers +// --------------------------------------------------------------------------- + +template +using function_type_for = ReturnType(ParameterTypes...); + +template +using function_pointer_type_for = ReturnType (*)(ParameterTypes...); + +template +using noexcept_function_pointer_type_for = + ReturnType (*)(ParameterTypes...) noexcept; + +consteval std::vector parameter_types_of(info member) { + std::vector result; + for (info parameter : std::meta::parameters_of(member)) { + result.push_back(std::meta::type_of(parameter)); + } + return result; +} + +// R(Ps...) for an interface member, used to pattern-match thunks and call +// wrappers so their signatures exactly mirror the interface declaration. +consteval info member_function_type(info member) { + std::vector arguments; + arguments.push_back(std::meta::return_type_of(member)); + for (info parameter_type : parameter_types_of(member)) { + arguments.push_back(parameter_type); + } + return std::meta::substitute(^^function_type_for, arguments); +} + +// R(*)(ErasedPointer, Ps...) [noexcept] — the type of one vtable entry. +consteval info vtable_entry_pointer_type(info member, + info erased_pointer_type) { + std::vector arguments; + arguments.push_back(std::meta::return_type_of(member)); + arguments.push_back(erased_pointer_type); + for (info parameter_type : parameter_types_of(member)) { + arguments.push_back(parameter_type); + } + return std::meta::substitute(std::meta::is_noexcept(member) + ? ^^noexcept_function_pointer_type_for + : ^^function_pointer_type_for, + arguments); +} + +consteval info data_member_named(info class_type, std::string_view name) { + for (info member : std::meta::nonstatic_data_members_of( + class_type, std::meta::access_context::current())) { + if (std::meta::has_identifier(member) && + std::meta::identifier_of(member) == name) { + return member; + } + } + return info{}; +} + +} // namespace reflection_detail +} // namespace xyz + +#endif // XYZ_PROTOCOL_REFLECTION_TYPES_H_ diff --git a/protocol_reflection_detail/types_test.cc b/protocol_reflection_detail/types_test.cc new file mode 100644 index 0000000..389e8d3 --- /dev/null +++ b/protocol_reflection_detail/types_test.cc @@ -0,0 +1,101 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ + +#include "types.h" + +#include + +#include +#include +#include +#include + +// Direct tests of reflection_detail's type-synthesis helpers (types.h): +// building the R(Ps...) and R(*)(Erased, Ps...) types that thunks and call +// wrappers pattern-match against. This file includes only the types header, +// not protocol.h, and none of the structs below are ever wrapped in +// xyz::protocol. + +namespace { + +struct SignatureProbe { + double compute(int, double) { return 0.0; } + + void notify() noexcept {} +}; + +consteval std::meta::info FindByName(std::string_view name) { + for (std::meta::info member : std::meta::members_of( + ^^SignatureProbe, std::meta::access_context::current())) { + if (std::meta::has_identifier(member) && + std::meta::identifier_of(member) == name) { + return member; + } + } + return std::meta::info{}; +} + +TEST(ReflectionTypesTest, MemberFunctionTypeMatchesReturnAndParameters) { + static_assert( + std::is_same_v); +} + +TEST(ReflectionTypesTest, VtableEntryPointerTypeAddsLeadingErasedPointer) { + static_assert(std::is_same_v< + typename[:xyz::reflection_detail::vtable_entry_pointer_type( + FindByName("compute"), + ^^void*):], double (*)(void*, int, double)>); +} + +TEST(ReflectionTypesTest, VtableEntryPointerTypeIsNoexceptWhenMemberIs) { + static_assert(std::is_same_v< + typename[:xyz::reflection_detail::vtable_entry_pointer_type( + FindByName("notify"), + ^^void*):], void (*)(void*) noexcept>); + static_assert( + !std::is_same_v< + typename[:xyz::reflection_detail::vtable_entry_pointer_type( + FindByName("compute"), + ^^void*):], double (*)(void*, int, double) noexcept>); +} + +TEST(ReflectionTypesTest, ParameterTypesOfReturnsTypesInOrder) { + static_assert( + xyz::reflection_detail::parameter_types_of(FindByName("compute")) == + std::vector{^^int, ^^double}); +} + +struct DataMemberProbe { + int found_member = 0; +}; + +TEST(ReflectionTypesTest, DataMemberNamedFindsExistingMember) { + static_assert(xyz::reflection_detail::data_member_named( + ^^DataMemberProbe, "found_member") != std::meta::info{}); +} + +TEST(ReflectionTypesTest, DataMemberNamedReturnsEmptyForMissingMember) { + static_assert(xyz::reflection_detail::data_member_named( + ^^DataMemberProbe, "no_such_member") == std::meta::info{}); +} + +} // namespace diff --git a/protocol_reflection_detail/vtable_layout.h b/protocol_reflection_detail/vtable_layout.h new file mode 100644 index 0000000..ec84cbe --- /dev/null +++ b/protocol_reflection_detail/vtable_layout.h @@ -0,0 +1,102 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ +// Vtable layout for the C++26 reflection backend: the shape of a vtable for +// a given interface, fixed by the interface alone before any implementation +// is chosen. Split out of protocol_reflection.h because this piece is +// self-contained (depends only on members.h, types.h, and naming.h, nothing +// from conformance-checking or the vtable-instance/dispatch machinery) and +// independently testable: vtable_layout_test.cc includes this header +// directly rather than the whole backend, checking that the generated +// aggregate has the right fields using synthetic probe structs alone. +#ifndef XYZ_PROTOCOL_REFLECTION_DETAIL_VTABLE_LAYOUT_H_ +#define XYZ_PROTOCOL_REFLECTION_DETAIL_VTABLE_LAYOUT_H_ + +#ifndef __cpp_impl_reflection +#error \ + "This header requires a compiler with C++26 reflection support (GCC 16+, -std=c++26 -freflection)." +#endif + +#include +#include + +#include "protocol_reflection_detail/members.h" +#include "protocol_reflection_detail/naming.h" +#include "protocol_reflection_detail/types.h" + +namespace xyz { +namespace reflection_detail { + +using std::meta::info; + +// --------------------------------------------------------------------------- +// Vtable layout per interface +// +// Each vtable is a handwritten shell (so it can carry typedefs and the fixed +// lifetime members, which define_aggregate cannot produce) containing a +// define_aggregate'd `entries` sub-aggregate with one function-pointer +// member per interface member, named by vtable_entry_name. +// --------------------------------------------------------------------------- + +consteval void define_vtable_entries(info incomplete_entries_type, + info interface_type) { + std::vector entry_specifications; + for (info member : interface_member_functions(interface_type, false)) { + info erased_pointer_type = + std::meta::is_const(member) ? ^^const void* : ^^void*; + entry_specifications.push_back(std::meta::data_member_spec( + vtable_entry_pointer_type(member, erased_pointer_type), + {.name = vtable_entry_name(member)})); + } + std::meta::define_aggregate(incomplete_entries_type, entry_specifications); +} + +template +struct view_vtable { + struct view_entries; + consteval { define_vtable_entries(^^view_entries, ^^Interface); } + + struct vtable { + using xyz_reflection_view_vtable_tag = void; + using protocol_type = Interface; + view_entries entries; + }; +}; + +template +struct owning_vtable { + struct owning_entries; + consteval { define_vtable_entries(^^owning_entries, ^^Interface); } + + struct vtable { + using xyz_reflection_owning_vtable_tag = void; + using protocol_type = Interface; + using allocator_type = Allocator; + void* (*xyz_protocol_clone)(void* erased, const Allocator& allocator); + void* (*xyz_protocol_move)(void* erased, const Allocator& allocator); + void (*xyz_protocol_destroy)(void* erased, const Allocator& allocator); + const typename view_vtable::vtable* view_vt; + owning_entries entries; + }; +}; + +} // namespace reflection_detail +} // namespace xyz + +#endif // XYZ_PROTOCOL_REFLECTION_DETAIL_VTABLE_LAYOUT_H_ diff --git a/protocol_reflection_detail/vtable_layout_test.cc b/protocol_reflection_detail/vtable_layout_test.cc new file mode 100644 index 0000000..b9f5061 --- /dev/null +++ b/protocol_reflection_detail/vtable_layout_test.cc @@ -0,0 +1,138 @@ +/* Copyright (c) 2026 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. +==============================================================================*/ + +#include "protocol_reflection_detail/vtable_layout.h" + +#include + +#include +#include +#include + +// Direct tests of vtable layout (vtable_layout.h): the shape of a vtable for +// a given interface. This file includes only the vtable-layout header, not +// protocol.h, and no implementation or xyz::protocol is ever plugged in. +// These checks are about the generated aggregate's fields. + +namespace { + +struct LayoutProbe { + int foo(int) const { return 0; } + + void bar() {} +}; + +consteval std::meta::info FindInterfaceMember(std::string_view name) { + for (std::meta::info member : + xyz::reflection_detail::interface_member_functions(^^LayoutProbe)) { + if (std::meta::identifier_of(member) == name) return member; + } + return std::meta::info{}; +} + +consteval std::meta::info FindDataMemberNamed(std::meta::info class_type, + std::string_view name) { + for (std::meta::info member : std::meta::nonstatic_data_members_of( + class_type, std::meta::access_context::current())) { + if (std::meta::identifier_of(member) == name) return member; + } + return std::meta::info{}; +} + +using ViewEntries = + xyz::reflection_detail::view_vtable::view_entries; + +consteval bool ViewEntryExistsFor(std::string_view member_name) { + std::meta::info member = FindInterfaceMember(member_name); + if (member == std::meta::info{}) return false; + std::string entry_name = xyz::reflection_detail::vtable_entry_name(member); + return FindDataMemberNamed(^^ViewEntries, entry_name) != std::meta::info{}; +} + +TEST(ReflectionVtableLayoutTest, ViewEntryExistsForConstMember) { + static_assert(ViewEntryExistsFor("foo")); +} + +TEST(ReflectionVtableLayoutTest, ViewEntryExistsForNonConstMember) { + static_assert(ViewEntryExistsFor("bar")); +} + +consteval std::size_t CountDataMembers(std::meta::info class_type) { + std::size_t count = 0; + for ([[maybe_unused]] std::meta::info member : + std::meta::nonstatic_data_members_of( + class_type, std::meta::access_context::current())) { + ++count; + } + return count; +} + +TEST(ReflectionVtableLayoutTest, ViewEntriesHasOneFieldPerInterfaceMember) { + static_assert( + CountDataMembers(^^ViewEntries) == + xyz::reflection_detail::interface_member_functions(^^LayoutProbe).size()); +} + +consteval bool EntryTypeMatchesExpected(std::string_view member_name) { + std::meta::info member = FindInterfaceMember(member_name); + std::meta::info erased_pointer_type = + std::meta::is_const(member) ? ^^const void* : ^^void*; + std::meta::info expected_type = + xyz::reflection_detail::vtable_entry_pointer_type(member, + erased_pointer_type); + std::string entry_name = xyz::reflection_detail::vtable_entry_name(member); + std::meta::info entry = FindDataMemberNamed(^^ViewEntries, entry_name); + return std::meta::dealias(std::meta::type_of(entry)) == + std::meta::dealias(expected_type); +} + +TEST(ReflectionVtableLayoutTest, ConstMemberEntryUsesConstErasedPointer) { + static_assert(EntryTypeMatchesExpected("foo")); +} + +TEST(ReflectionVtableLayoutTest, NonConstMemberEntryUsesMutableErasedPointer) { + static_assert(EntryTypeMatchesExpected("bar")); +} + +using OwningVtable = + xyz::reflection_detail::owning_vtable>::vtable; + +TEST(ReflectionVtableLayoutTest, OwningVtableHasLifetimeAndViewFields) { + static_assert(FindDataMemberNamed(^^OwningVtable, "xyz_protocol_clone") != + std::meta::info{}); + static_assert(FindDataMemberNamed(^^OwningVtable, "xyz_protocol_move") != + std::meta::info{}); + static_assert(FindDataMemberNamed(^^OwningVtable, "xyz_protocol_destroy") != + std::meta::info{}); + static_assert(FindDataMemberNamed(^^OwningVtable, "view_vt") != + std::meta::info{}); + static_assert(FindDataMemberNamed(^^OwningVtable, "entries") != + std::meta::info{}); +} + +TEST(ReflectionVtableLayoutTest, OwningVtableCarriesTagAndProtocolType) { + static_assert( + requires { typename OwningVtable::xyz_reflection_owning_vtable_tag; }); + static_assert( + std::is_same_v); +} + +} // namespace diff --git a/protocol_test.cc b/protocol_test.cc index 86eec78..f6646a2 100644 --- a/protocol_test.cc +++ b/protocol_test.cc @@ -29,11 +29,23 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include #include +#include "interface_A.h" +#include "interface_A_Subset.h" +#include "interface_B.h" +#include "interface_C.h" +#include "interface_D.h" + +// The reflection backend (enabled by protocol.h's own unconditional include +// of protocol_reflection.h) needs no per-interface generated header: it +// synthesizes the same machinery from the plain interface struct above at +// compile time. The Python backend still needs its generated headers. +#ifndef XYZ_PROTOCOL_ENABLE_REFLECTION #include "generated/protocol_A.h" #include "generated/protocol_A_Subset.h" #include "generated/protocol_B.h" #include "generated/protocol_C.h" #include "generated/protocol_D.h" +#endif #include "tracking_allocator.h" namespace { @@ -729,6 +741,11 @@ class DLike { void operator=(int x) { value_ = x; } + // A distinguishable marker (not a plausible int->double truncation of any + // value the int overload would also produce) proves which overload real + // overload resolution actually picked. + void operator=(double x) { value_ = static_cast(x) + 100; } + bool operator<(int x) const { return value_ < x; } bool operator>(int x) const { return value_ > x; } @@ -850,6 +867,15 @@ TEST(ProtocolTest, ProtocolDOperators) { EXPECT_EQ(d(), 42); EXPECT_EQ(d[5], 10); + + // D declares two operator= overloads of the same (non-const) constness, + // taking int and double respectively; real overload resolution over the + // merged set must pick the exact match for each rather than only one of + // them being reachable. + d = 7; + EXPECT_TRUE(d == 7); + d = 7.0; + EXPECT_TRUE(d == 107); } TEST(ProtocolViewTest, ProtocolViewDOperators) { @@ -917,6 +943,13 @@ TEST(ProtocolViewTest, ProtocolViewDOperators) { EXPECT_EQ(d(), 42); EXPECT_EQ(d[5], 10); + + // See ProtocolDOperators: same two-overload assignment coverage, through + // the mutable view instead of the owning protocol. + d = 7; + EXPECT_TRUE(d == 7); + d = 7.0; + EXPECT_TRUE(d == 107); } TEST(ProtocolViewTest, NarrowingConversion) { @@ -1278,3 +1311,168 @@ TEST(ProtocolTest, NarrowingConversionConcurrentStressing) { } } // namespace + +#ifdef XYZ_PROTOCOL_ENABLE_REFLECTION + +template +struct Function { + R operator()(Args... args) const; +}; + +template +struct RvalueParameterFunction { + R operator()(Args&&... args) const; +}; + +namespace { + +struct SquareAdder { + int operator()(int x, int y) const { return x * x + y * y; } +}; + +struct StringConcatenator { + std::string operator()(std::string_view a, std::string_view b) const { + return std::string(a) + std::string(b); + } +}; + +struct RvalueIntAdder { + int operator()(int&& x) const { return x + 10; } +}; + +struct NarrowingComputeInterface { + double compute(double x) const; +}; + +struct FloatOnlyImplementation { + float compute(float x) const { return x * 2.0f; } +}; + +struct FloatComputeInterface { + double compute(float x) const; +}; + +struct OverloadedComputeImplementation { + int compute(int x) const { return x + 1; } + + double compute(double x) const { return x * 2; } +}; + +TEST(ProtocolReflectionTest, ImplicitConversionConformance) { + // FloatOnlyImplementation::compute(float) does not exactly match + // NarrowingComputeInterface::compute(double); this now conforms (and + // dispatches through the implicit double->float/float->double + // conversions) the same way the Python backend's requires-expression + // already does, matching real C++ overload resolution. + static_assert( + xyz::reflection_protocol_const_concept); + xyz::protocol p( + std::in_place_type); + EXPECT_DOUBLE_EQ(p.compute(21.0), 42.0); +} + +TEST(ProtocolReflectionTest, ImplicitConversionPicksBestOverload) { + // OverloadedComputeImplementation offers compute(int) and compute(double); + // FloatComputeInterface's compute(float) must resolve to compute(double), + // since float->double is a better conversion than float->int -- proving + // real overload resolution over the merged candidate set, not just "any + // invocable candidate". + xyz::protocol p( + std::in_place_type); + EXPECT_DOUBLE_EQ(p.compute(3.0f), 6.0); +} + +struct NonConstMemberInterface { + int member(); +}; + +struct DualConstnessImplementation { + int member() { return 10; } + + int member() const { return 20; } +}; + +TEST(ProtocolReflectionTest, PrefersNonConstOverloadOnNonConstAccess) { + // DualConstnessImplementation declares both member() and member() const; + // NonConstMemberInterface::member() is non-const, so real overload + // resolution over the merged candidate set must prefer the non-const + // overload, exactly as calling member() directly on a non-const object + // would -- not treat the two as ambiguous. This is the case merging + // candidates with a single shared constness (rather than each candidate's + // own) would break. + xyz::protocol p( + std::in_place_type); + EXPECT_EQ(p.member(), 10); +} + +struct RvalueQualifiedImplementation { + // member() && can never be called through the pointer-based self this + // backend erases through, so it must never be treated as a candidate; + // only member() below is ever viable. + int member() && { return 99; } + + int member() { return 30; } +}; + +TEST(ProtocolReflectionTest, ExcludesRvalueQualifiedCandidate) { + xyz::protocol p( + std::in_place_type); + EXPECT_EQ(p.member(), 30); +} + +struct NoexceptRequiredInterface { + int compute() const noexcept; +}; + +struct ThrowingImplementation { + // Missing noexcept: the vtable thunk and every forwarder for a noexcept + // interface member are themselves generated noexcept(true), so accepting + // this as conforming would let an exception escape a noexcept function + // (terminating the program) instead of failing to compile. + int compute() const { return 1; } +}; + +TEST(ProtocolReflectionTest, ExcludesNonNoexceptCandidateForNoexceptMember) { + static_assert( + !xyz::reflection_protocol_const_concept); +} + +struct NoexceptMatchingImplementation { + int compute() const noexcept { return 7; } +}; + +TEST(ProtocolReflectionTest, DispatchesNoexceptMatchingCandidate) { + static_assert( + xyz::reflection_protocol_const_concept); + xyz::protocol p( + std::in_place_type); + EXPECT_EQ(p.compute(), 7); +} + +TEST(ProtocolReflectionTest, ClassTemplateInstantiationAsInterface) { + // Test Function without any opt-in variable template + xyz::protocol> integer_function(SquareAdder{}); + EXPECT_EQ(integer_function(3, 4), 25); + + xyz::protocol_view> integer_function_view( + integer_function); + EXPECT_EQ(integer_function_view(5, 12), 169); + + // Test Function + xyz::protocol> + string_function(StringConcatenator{}); + EXPECT_EQ(string_function("hello ", "world"), "hello world"); + + // Test RvalueParameterFunction with rvalue reference parameter + // (int&&) + xyz::protocol> reference_function( + RvalueIntAdder{}); + EXPECT_EQ(reference_function(5), 15); +} + +} // namespace + +#endif diff --git a/reference_interface_protocol.h b/reference_interface_protocol.h index b9bee36..741c4ff 100644 --- a/reference_interface_protocol.h +++ b/reference_interface_protocol.h @@ -141,8 +141,8 @@ struct protocol_owning_vtable_traits<::xyz::ReferenceInterface, Allocator> { }; template -inline void map_vtable_members(const From* from, - const_view_vtable_ReferenceInterface* to) { +inline void map_const_vtable_members(const From* from, + const_view_vtable_ReferenceInterface* to) { to->get_value_51992268 = from->get_value_51992268; to->overloaded_c1840915 = from->overloaded_c1840915; @@ -153,8 +153,8 @@ inline void map_vtable_members(const From* from, } template -inline void map_mutable_vtable_members(const From* from, - view_vtable_ReferenceInterface* to) { +inline void map_vtable_members(const From* from, + view_vtable_ReferenceInterface* to) { to->const_view.get_value_51992268 = from->const_view.get_value_51992268; to->const_view.overloaded_c1840915 = from->const_view.overloaded_c1840915; @@ -184,8 +184,9 @@ inline void map_owning_vtable_members( to->xyz_protocol_clone = from->xyz_protocol_clone; to->xyz_protocol_move = from->xyz_protocol_move; to->xyz_protocol_destroy = from->xyz_protocol_destroy; - to->view_vt = get_mutable_vtable(from->view_vt); + to->view_vt = + get_vtable( + from->view_vt); to->get_value_51992268 = from->get_value_51992268; @@ -715,13 +716,14 @@ class protocol_view { requires(!std::same_as) protocol_view(const protocol_view& other) noexcept : ptr_(other.ptr_), - vptr_(get_vtable(other.vptr_)) {} + vptr_(get_const_vtable(other.vptr_)) { + } template requires(!std::same_as) protocol_view(const protocol_view& other) noexcept : ptr_(other.ptr_), - vptr_(get_vtable( + vptr_(get_const_vtable( &other.vptr_->const_view)) {} template @@ -795,9 +797,7 @@ class protocol_view<::xyz::ReferenceInterface> { requires(!std::same_as) protocol_view(const protocol_view& other) noexcept : ptr_(other.ptr_), - vptr_( - get_mutable_vtable(other.vptr_)) { - } + vptr_(get_vtable(other.vptr_)) {} template requires(!std::same_as) diff --git a/scripts/cmake.py b/scripts/cmake.py index ecfca13..f84e480 100644 --- a/scripts/cmake.py +++ b/scripts/cmake.py @@ -37,6 +37,20 @@ def main() -> None: ) parser.add_argument("--tsan", action="store_true", help="Enable Thread Sanitizer") parser.add_argument("--msan", action="store_true", help="Enable Memory Sanitizer") + parser.add_argument( + "--implementation", + choices=["Python", "reflection"], + default="Python", + help=( + "Code-generation backend to build and test: 'Python' (default) " + "for the Python-generated backend, or 'reflection' for the " + "C++26-reflection backend (protocol_reflection.h), which " + "requires a compiler with P2996 reflection support (e.g. " + "CXX=g++-16 CC=gcc-16). If 'reflection' is requested and the " + "compiler does not support it, CMake configuration fails " + "outright: there is no fallback to the Python backend." + ), + ) parser.add_argument("-B", "--build-dir", help="Build directory") parser.add_argument( "--clean", action="store_true", help="Fresh configuration and clean-first build" @@ -74,6 +88,8 @@ def log(msg: Any) -> None: f"-DENABLE_UBSAN={'ON' if args.ubsan else 'OFF'}", f"-DENABLE_TSAN={'ON' if args.tsan else 'OFF'}", f"-DENABLE_MSAN={'ON' if args.msan else 'OFF'}", + f"-DXYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND=" + f"{'ON' if args.implementation == 'reflection' else 'OFF'}", ] if args.build_dir: configure_args.extend(["-B", args.build_dir]) diff --git a/scripts/docker-shell.sh b/scripts/docker-shell.sh new file mode 100755 index 0000000..e91b608 --- /dev/null +++ b/scripts/docker-shell.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -eu -o pipefail + +# Find workspace root +WORKSPACE_ROOT=$(git rev-parse --show-toplevel) +IMAGE_NAME="cc-protocol-sandbox" + +REBUILD=0 +for argument in "$@"; do + if [ "$argument" == "--rebuild-docker" ]; then + REBUILD=1 + fi +done + +# Check if image exists or rebuild requested +if [ "$REBUILD" -eq 1 ] || ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then + echo "--- Building Docker Image: ${IMAGE_NAME} ---" + docker build -t "$IMAGE_NAME" -f "${WORKSPACE_ROOT}/docker/Dockerfile" "$WORKSPACE_ROOT" +fi + +echo "--- Starting Interactive Docker Shell ---" +echo "Project root ${WORKSPACE_ROOT} is mounted at /workspace" +echo "" +echo "To build and test with the C++26 reflection backend (GCC 16):" +echo " CXX=g++-16 CC=gcc-16 ./scripts/cmake.sh --release --implementation=reflection -B build-reflection" +echo "" + +DOCKER_INTERACTIVE_FLAGS="-it" +# If not running in a terminal, drop -it +if [ ! -t 0 ]; then + DOCKER_INTERACTIVE_FLAGS="-i" +fi + +exec docker run $DOCKER_INTERACTIVE_FLAGS --rm \ + -v "${WORKSPACE_ROOT}:/workspace" \ + -w /workspace \ + ${TERM:+-e TERM="$TERM"} \ + ${COLORTERM:+-e COLORTERM="$COLORTERM"} \ + "$IMAGE_NAME" bash diff --git a/scripts/generate_operator_forwarders.py b/scripts/generate_operator_forwarders.py new file mode 100644 index 0000000..9045dff --- /dev/null +++ b/scripts/generate_operator_forwarders.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +""" +Generates protocol_reflection_detail/operator_forwarders.h. + +An interface operator's forwarder must declare a real `operator` +member, and the operator symbol has to appear as a literal token in that +declaration: there is currently no way to reflect a std::meta::operators +enumerator into the declarator-id of a new operator overload (the same +limitation https://github.com/RyanJK5/rjk-duck works around with its own +generate_operators.py, generating do_unary_op.hpp/do_binary_op.hpp/ +operator_friends.inl). This script performs the equivalent one-time, +offline generation for xyz::protocol's operator_forwarder/operator_join +specializations, instead of a C-preprocessor macro expanded on every build. + +Run with `uv run python3 scripts/generate_operator_forwarders.py` after +changing the OPERATORS table below, then let pre-commit's clang-format hook +reformat the output. +""" + +import os +import subprocess + +# (operator symbol, std::meta::operators enumerator, is_nullary). Nullary +# operators (-> ~ !) are declared with no parameter list at all; declaring +# them with an (always-empty) parameter pack is rejected. +OPERATORS = [ + ("=", "op_equals", False), + ("()", "op_parentheses", False), + ("[]", "op_square_brackets", False), + ("->", "op_arrow", True), + ("->*", "op_arrow_star", False), + ("~", "op_tilde", True), + ("!", "op_exclamation", True), + ("+", "op_plus", False), + ("-", "op_minus", False), + ("*", "op_star", False), + ("/", "op_slash", False), + ("%", "op_percent", False), + ("^", "op_caret", False), + ("&", "op_ampersand", False), + ("|", "op_pipe", False), + ("+=", "op_plus_equals", False), + ("-=", "op_minus_equals", False), + ("*=", "op_star_equals", False), + ("/=", "op_slash_equals", False), + ("%=", "op_percent_equals", False), + ("^=", "op_caret_equals", False), + ("&=", "op_ampersand_equals", False), + ("|=", "op_pipe_equals", False), + ("==", "op_equals_equals", False), + ("!=", "op_exclamation_equals", False), + ("<", "op_less", False), + (">", "op_greater", False), + ("<=", "op_less_equals", False), + (">=", "op_greater_equals", False), + ("<=>", "op_spaceship", False), + ("&&", "op_ampersand_ampersand", False), + ("||", "op_pipe_pipe", False), + ("<<", "op_less_less", False), + (">>", "op_greater_greater", False), + ("<<=", "op_less_less_equals", False), + (">>=", "op_greater_greater_equals", False), + ("++", "op_plus_plus", False), + ("--", "op_minus_minus", False), + (",", "op_comma", False), +] + +OUTPUT_PATH = "protocol_reflection_detail/operator_forwarders.h" + +HEADER = """/* Copyright (c) 2026 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. +==============================================================================*/ +// ============================================================================ +// AUTOMATICALLY GENERATED FILE - DO NOT MODIFY +// ============================================================================ +// This file was generated by scripts/generate_operator_forwarders.py. +// +// One operator_forwarder specialization pair (non-const, const) plus one +// operator_join specialization per operator kind, for the C++26 reflection +// backend: forwarding an interface operator requires a real `operator` +// declaration, and the symbol must appear as a literal token in source, so +// each kind is generated once here rather than reimplemented by hand. +// +// Any manual changes made to this file will be overwritten during the next +// generation run. +// ============================================================================ +#ifndef XYZ_PROTOCOL_REFLECTION_DETAIL_OPERATOR_FORWARDERS_H_ +#define XYZ_PROTOCOL_REFLECTION_DETAIL_OPERATOR_FORWARDERS_H_ + +#ifndef __cpp_impl_reflection +#error \\ + "This header requires a compiler with C++26 reflection support (GCC 16+, \ +-std=c++26 -freflection)." +#endif + +#include +#include + +namespace xyz { +namespace reflection_detail { + +using std::meta::info; + +template +struct operator_forwarder; + +// One join per operator kind gathers every overload of that kind into a +// single class: name lookup for e.g. `operator+` across multiple distinct +// bases would be ambiguous, so the overloads must be merged with +// using-declarations, which also require the operator symbol literally. +template +struct operator_join; + +""" + +FOOTER = """ +} // namespace reflection_detail +} // namespace xyz + +#endif // XYZ_PROTOCOL_REFLECTION_DETAIL_OPERATOR_FORWARDERS_H_ +""" + +NARY_TEMPLATE = """\ +template +struct operator_forwarder {{ + ReturnType operator{symbol}(ParameterTypes... parameters) noexcept( + IsNoexcept) {{ + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + }} +}}; + +template +struct operator_forwarder {{ + ReturnType operator{symbol}(ParameterTypes... parameters) const + noexcept(IsNoexcept) {{ + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member( + std::forward(parameters)...); + }} +}}; + +template +struct operator_join + : Forwarders... {{ + using Forwarders::operator{symbol}...; +}}; +""" + +NULLARY_TEMPLATE = """\ +template +struct operator_forwarder {{ + ReturnType operator{symbol}() noexcept(IsNoexcept) {{ + auto* owner = static_cast(this); + return owner->template dispatch_reflected_member(); + }} +}}; + +template +struct operator_forwarder {{ + ReturnType operator{symbol}() const noexcept(IsNoexcept) {{ + const auto* owner = static_cast(this); + return owner->template dispatch_reflected_member(); + }} +}}; + +template +struct operator_join + : Forwarders... {{ + using Forwarders::operator{symbol}...; +}}; +""" + + +def main() -> None: + """Generate operator_forwarders.h from the OPERATORS table.""" + script_dir = os.path.dirname(os.path.abspath(__file__)) + workspace_root = os.path.dirname(script_dir) + output_path = os.path.join(workspace_root, OUTPUT_PATH) + + blocks = [] + for symbol, kind, is_nullary in OPERATORS: + template = NULLARY_TEMPLATE if is_nullary else NARY_TEMPLATE + blocks.append(f"// {kind}\n" + template.format(symbol=symbol, kind=kind)) + + with open(output_path, "w") as f: + f.write(HEADER + "\n".join(blocks) + FOOTER) + + subprocess.run(["clang-format", "-i", output_path], check=True) + print(f"Generated {OUTPUT_PATH}") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_protocol.py b/scripts/generate_protocol.py index 96080cf..1591d25 100644 --- a/scripts/generate_protocol.py +++ b/scripts/generate_protocol.py @@ -25,6 +25,14 @@ pass +# Names that are always public members of the generated xyz::protocol / +# xyz::protocol_view classes (see scripts/protocol.j2). An interface method +# with one of these names would collide with the generated class's own +# member of the same name, so it is rejected at generation time rather than +# left to surface as a confusing redefinition error from the C++ compiler. +RESERVED_PROTOCOL_MEMBER_NAMES = {"swap", "valueless_after_move"} + + def get_method_signature(m: Any) -> str: """Generate a string signature for a method.""" args = ",".join(a.type.name for a in m.arguments) @@ -157,6 +165,23 @@ def main() -> None: print(f"Class {args.class_name} not found in {args.input}", file=sys.stderr) sys.exit(1) + reserved_names_used = sorted( + { + m.name + for m in target_class.methods + if m.name in RESERVED_PROTOCOL_MEMBER_NAMES + } + ) + if reserved_names_used: + print( + f"Error: interface {args.class_name!r} in {args.input} declares " + f"member function(s) named {', '.join(reserved_names_used)}, which " + "collide with xyz::protocol's own public members and cannot be " + "used as interface method names.", + file=sys.stderr, + ) + sys.exit(1) + template_dir = os.path.dirname(os.path.abspath(args.template)) template_name = os.path.basename(args.template) diff --git a/scripts/protocol.j2 b/scripts/protocol.j2 index e039de5..af71331 100644 --- a/scripts/protocol.j2 +++ b/scripts/protocol.j2 @@ -136,14 +136,14 @@ struct protocol_owning_vtable_traits<{{ full_class_name }}, Allocator> { }; template -inline void map_vtable_members(const From* from, const_view_vtable_{{ c.name }}* to) { +inline void map_const_vtable_members(const From* from, const_view_vtable_{{ c.name }}* to) { {% for m in c.methods %}{% if m.is_const %} to->{{ m.name | mangle }}_{{ method_guids[loop.index0] }} = from->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}; {% endif %}{% endfor %} } template -inline void map_mutable_vtable_members(const From* from, view_vtable_{{ c.name }}* to) { +inline void map_vtable_members(const From* from, view_vtable_{{ c.name }}* to) { {% for m in c.methods %}{% if m.is_const %} to->const_view.{{ m.name | mangle }}_{{ method_guids[loop.index0] }} = from->const_view.{{ m.name | mangle }}_{{ method_guids[loop.index0] }}; {% endif %}{% endfor %} @@ -157,7 +157,7 @@ inline void map_owning_vtable_members(const From* from, typename protocol_owning to->xyz_protocol_clone = from->xyz_protocol_clone; to->xyz_protocol_move = from->xyz_protocol_move; to->xyz_protocol_destroy = from->xyz_protocol_destroy; - to->view_vt = get_mutable_vtable(from->view_vt); + to->view_vt = get_vtable(from->view_vt); {% for m in c.methods %} to->{{ m.name | mangle }}_{{ method_guids[loop.index0] }} = from->{{ m.name | mangle }}_{{ method_guids[loop.index0] }}; {% endfor %} @@ -589,13 +589,13 @@ class protocol_view { requires(!std::same_as) protocol_view(const protocol_view& other) noexcept : ptr_(other.ptr_), - vptr_(get_vtable(other.vptr_)) {} + vptr_(get_const_vtable(other.vptr_)) {} template requires(!std::same_as) protocol_view(const protocol_view& other) noexcept : ptr_(other.ptr_), - vptr_(get_vtable(&other.vptr_->const_view)) {} + vptr_(get_const_vtable(&other.vptr_->const_view)) {} template requires(!std::same_as) @@ -669,7 +669,7 @@ class protocol_view<{{ full_class_name }}> { requires(!std::same_as) protocol_view(const protocol_view& other) noexcept : ptr_(other.ptr_), - vptr_(get_mutable_vtable(other.vptr_)) {} + vptr_(get_vtable(other.vptr_)) {} template requires(!std::same_as) diff --git a/scripts/test_concept_errors_reflection.py b/scripts/test_concept_errors_reflection.py new file mode 100644 index 0000000..d7937d6 --- /dev/null +++ b/scripts/test_concept_errors_reflection.py @@ -0,0 +1,299 @@ +""" +Tests for compiler error messages from the C++26 reflection backend. + +Unlike test_concept_errors.py, no per-interface generated header is needed: +the reflection backend synthesizes the same machinery from a plain struct at +compile time. Diagnostics also look different from the Python backend's: the +reflection backend's conformance concept is checked as one opaque boolean +(no per-member requires-clause), so failures are matched by the surrounding +constraint-failure text (reflection_protocol_concept, "no matching function +for call") rather than by the specific violating member call. + +These tests are run from CMake using CTest and require flags to be passed at +run-time, and only when XYZ_PROTOCOL_ENABLE_REFLECTION_BACKEND is enabled. +""" + +import os +import re +import subprocess +import tempfile +from typing import Any +from typing import Callable +from typing import List + +import pytest + + +def pytest_addoption(parser: Any) -> None: + """Add command-line options for the compiler and flags.""" + parser.addoption("--compiler", action="store", default="g++") + parser.addoption("--flags", action="append", default=[]) + + +@pytest.fixture +def compiler(request: Any) -> str: + """Fixture that provides the compiler path.""" + return str(request.config.getoption("--compiler")) + + +@pytest.fixture +def flags(request: Any) -> List[str]: + """Fixture that provides the compiler flags.""" + return list(request.config.getoption("--flags")) + + +def run_compiler( + compiler: str, flags: List[str], source_code: str +) -> subprocess.CompletedProcess[str]: + """Run the compiler on the given source code.""" + with tempfile.TemporaryDirectory() as tmpdir: + source_file = os.path.join(tmpdir, "test.cc") + obj_file = os.path.join(tmpdir, "test.o") + + with open(source_file, "w") as f: + f.write(source_code) + + cmd = [compiler] + flags + ["-c", source_file, "-o", obj_file] + return subprocess.run(cmd, capture_output=True, text=True) + + +@pytest.fixture +def compile_check(compiler: str, flags: List[str]) -> Callable[[str, List[str]], None]: + """Fixture that provides a function to check compiler errors.""" + + def check(source: str, expected_patterns: List[str]) -> None: + res = run_compiler(compiler, flags, source) + assert res.returncode != 0, "Compilation should have failed" + output = res.stderr + res.stdout + for pattern in expected_patterns: + assert re.search(pattern, output), ( + f"Expected pattern '{pattern}' not found in compiler output.\n" + "--- Compiler Output ---\n" + f"{output}\n" + "-----------------------" + ) + + return check + + +def test_missing_method(compile_check: Callable[[str, List[str]], None]) -> None: + """Test that a class missing a required method fails to conform.""" + source = """ + #include "protocol.h" + #include "interface_A.h" + #include + + class BadALike_MissingMethod { + public: + int count() { return 42; } + }; + + void test() { + xyz::protocol a(std::in_place_type); + } + """ + compile_check( + source, + [r"reflection_protocol_concept", r"no matching function for call"], + ) + + +def test_wrong_return_type(compile_check: Callable[[str, List[str]], None]) -> None: + """Test that a method whose return type isn't convertible fails.""" + source = """ + #include "protocol.h" + #include "interface_A.h" + #include + #include + + class BadALike_WrongReturnType { + public: + std::string_view name() const noexcept { return "name"; } + std::string count() { return "42"; } // not convertible to int + }; + + void test() { + xyz::protocol a(std::in_place_type); + } + """ + compile_check( + source, + [r"reflection_protocol_concept", r"no matching function for call"], + ) + + +def test_noexcept_violation(compile_check: Callable[[str, List[str]], None]) -> None: + """ + Test that a method missing noexcept fails to conform. + + The vtable thunk and every forwarder for a noexcept interface member are + themselves generated noexcept(true): accepting a throwing candidate would + let an exception escape a noexcept function (terminating the program) + instead of failing to compile. + """ + source = """ + #include "protocol.h" + #include "interface_A.h" + #include + + class BadALike_NoExceptViolation { + public: + std::string_view name() const { return "name"; } // Missing noexcept + int count() { return 42; } + }; + + void test() { + xyz::protocol a(std::in_place_type); + } + """ + compile_check( + source, + [r"reflection_protocol_concept", r"no matching function for call"], + ) + + +def test_reserved_member_name(compile_check: Callable[[str, List[str]], None]) -> None: + """ + Test that an interface declaring 'swap' or 'valueless_after_move' fails. + + Those names are reserved for protocol's own public members and would be + silently hidden by ordinary C++ name-hiding rules rather than reachable + through the generated forwarders, so this is rejected with a + static_assert naming the interface instead. + """ + source = """ + #include "protocol.h" + + namespace xyz { + struct ReservedNameInterface { + void swap(int); + }; + } + + void test() { + xyz::protocol p; + } + """ + compile_check( + source, + [ + r"static assertion failed", + r"must not declare a member function named 'swap' or " + r"'valueless_after_move'", + ], + ) + + +def test_rvalue_qualified_member( + compile_check: Callable[[str, List[str]], None], +) -> None: + """Test that an interface declaring an rvalue-qualified (&&) member fails.""" + source = """ + #include "protocol.h" + + namespace xyz { + struct RvalueMemberInterface { + void consume() &&; + }; + } + + void test() { + xyz::protocol p; + } + """ + compile_check( + source, + [ + r"static assertion failed", + r"must not declare an rvalue-qualified \(&&\) member function", + ], + ) + + +def test_invalid_view_widening(compile_check: Callable[[str, List[str]], None]) -> None: + """Verify that a protocol_view cannot be widened to one with more methods.""" + source = """ + #include "protocol.h" + + namespace xyz { + struct Subset { + int foo(int); + }; + struct Wide { + int foo(int); + int bar(int); + }; + } + + void test(xyz::protocol_view view_subset) { + xyz::protocol_view view_wide(view_subset); + } + """ + compile_check( + source, + [ + r"static assertion failed", + r"narrowing conversion requires every target interface member " + r"to exist in the source interface with an identical signature", + ], + ) + + +def test_invalid_protocol_widening( + compile_check: Callable[[str, List[str]], None], +) -> None: + """Verify that an owning protocol cannot be widened to one with more methods.""" + source = """ + #include "protocol.h" + + namespace xyz { + struct Subset { + int foo(int); + }; + struct Wide { + int foo(int); + int bar(int); + }; + } + + void test(xyz::protocol p_subset) { + xyz::protocol p(std::move(p_subset)); + } + """ + compile_check(source, [r"no matching function for call|no matching constructor"]) + + +def test_narrowing_signature_mismatch( + compile_check: Callable[[str, List[str]], None], +) -> None: + """ + Verify that narrowing requires an identical signature, not just a name. + + A same-named member with a different parameter type is not the "same" + interface member for narrowing purposes, even though it would be a + perfectly good implementation candidate on its own. + """ + source = """ + #include "protocol.h" + + namespace xyz { + struct WideInterface { + int foo(int); + }; + struct NarrowMismatchInterface { + int foo(double); + }; + } + + void test(xyz::protocol_view view_mismatch) { + xyz::protocol_view view(view_mismatch); + } + """ + compile_check( + source, + [ + r"static assertion failed", + r"narrowing conversion requires every target interface member " + r"to exist in the source interface with an identical signature", + ], + ) diff --git a/scripts/test_generate_protocol.py b/scripts/test_generate_protocol.py index 618aa1e..1194706 100644 --- a/scripts/test_generate_protocol.py +++ b/scripts/test_generate_protocol.py @@ -189,6 +189,30 @@ def test_class_not_found(temp_dir: str, compiler: str) -> None: assert "Class Missing not found" in res.stderr +def test_reserved_member_name(temp_dir: str, compiler: str) -> None: + """Test that the script rejects reserved interface method names.""" + input_header = os.path.join(temp_dir, "input.h") + output_header = os.path.join(temp_dir, "output.h") + + with open(input_header, "w") as f: + f.write( + """ + class Reserved { + public: + void swap(); + bool valueless_after_move() const; + }; + """ + ) + + res = run_generate_protocol( + input_header, output_header, "Reserved", "input.h", compiler=compiler + ) + assert res.returncode != 0 + assert "swap" in res.stderr + assert "valueless_after_move" in res.stderr + + def test_mangle_operators(temp_dir: str, compiler: str) -> None: """Test that C++ operators are correctly mangled in the generated code.""" input_header = os.path.join(temp_dir, "input.h")