diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8713751..5d32094 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -214,7 +214,9 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT)
protocol_reflection_detail/naming.hxx
protocol_reflection_detail/naming_test.cc
protocol_reflection_detail/members.hxx
- protocol_reflection_detail/members_test.cc)
+ protocol_reflection_detail/members_test.cc
+ protocol_reflection_detail/types.hxx
+ protocol_reflection_detail/types_test.cc)
target_compile_options(protocol_reflection_detail_test
PRIVATE -freflection)
target_include_directories(protocol_reflection_detail_test
diff --git a/protocol_reflection_detail/types.hxx b/protocol_reflection_detail/types.hxx
new file mode 100644
index 0000000..08ddb06
--- /dev/null
+++ b/protocol_reflection_detail/types.hxx
@@ -0,0 +1,75 @@
+/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+==============================================================================*/
+// Synthesizing member and vtable-entry function types from a reflected
+// member function, via std::meta::substitute over a variadic alias
+// template.
+#ifndef XYZ_PROTOCOL_REFLECTION_DETAIL_TYPES_HXX_
+#define XYZ_PROTOCOL_REFLECTION_DETAIL_TYPES_HXX_
+
+#include
+#include
+
+namespace xyz::reflection_detail {
+
+// `member`'s parameter types, dealiased. std::meta::parameters_of never
+// yields the implicit object parameter, so none of these are it.
+consteval std::vector parameter_types_of(
+ std::meta::info member) {
+ std::vector types;
+ for (std::meta::info parameter : std::meta::parameters_of(member)) {
+ types.push_back(std::meta::dealias(std::meta::type_of(parameter)));
+ }
+ return types;
+}
+
+template
+using function_type_t = R(Ps...);
+
+// `member`'s own call signature as an ordinary function type, R(Ps...).
+consteval std::meta::info member_function_type(std::meta::info member) {
+ std::vector args{
+ std::meta::dealias(std::meta::return_type_of(member))};
+ for (std::meta::info parameter_type : parameter_types_of(member)) {
+ args.push_back(parameter_type);
+ }
+ return std::meta::substitute(^^function_type_t, args);
+}
+
+template
+using fn_ptr_t = R (*)(Ps...);
+
+// The vtable entry's pointer-to-function type for `member`:
+// R (*)(ErasedPointer, Ps...), with the member's implicit object parameter
+// replaced by `erased_pointer_type` (typically ^^void* for a non-const
+// member, ^^const void* for a const one).
+consteval std::meta::info vtable_entry_pointer_type(
+ std::meta::info member, std::meta::info erased_pointer_type) {
+ std::vector args{
+ std::meta::dealias(std::meta::return_type_of(member)),
+ erased_pointer_type};
+ for (std::meta::info parameter_type : parameter_types_of(member)) {
+ args.push_back(parameter_type);
+ }
+ return std::meta::substitute(^^fn_ptr_t, args);
+}
+
+} // namespace xyz::reflection_detail
+
+#endif // XYZ_PROTOCOL_REFLECTION_DETAIL_TYPES_HXX_
diff --git a/protocol_reflection_detail/types_test.cc b/protocol_reflection_detail/types_test.cc
new file mode 100644
index 0000000..78dde5e
--- /dev/null
+++ b/protocol_reflection_detail/types_test.cc
@@ -0,0 +1,81 @@
+/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+==============================================================================*/
+#include "protocol_reflection_detail/types.hxx"
+
+#include
+
+#include
+#include
+
+namespace {
+
+using xyz::reflection_detail::member_function_type;
+using xyz::reflection_detail::parameter_types_of;
+using xyz::reflection_detail::vtable_entry_pointer_type;
+
+struct Fixture {
+ int compute(double x, int y) const { return static_cast(x) + y; }
+
+ void set_value(int) {}
+
+ void no_args() {}
+};
+
+TEST(ParameterTypesOf, ListsEachParameterInOrder) {
+ constexpr auto types =
+ std::define_static_array(parameter_types_of(^^Fixture::compute));
+ static_assert(types.size() == 2);
+ static_assert(types[0] == ^^double);
+ static_assert(types[1] == ^^int);
+}
+
+TEST(ParameterTypesOf, EmptyForANoArgumentMember) {
+ constexpr auto types =
+ std::define_static_array(parameter_types_of(^^Fixture::no_args));
+ static_assert(types.size() == 0);
+}
+
+TEST(MemberFunctionType, BuildsTheMembersOwnCallSignature) {
+ static_assert(
+ std::is_same_v);
+ static_assert(
+ std::is_same_v<
+ typename[:member_function_type(^^Fixture::set_value):], void(int)>);
+ static_assert(std::is_same_v<
+ typename[:member_function_type(^^Fixture::no_args):], void()>);
+}
+
+TEST(VtableEntryPointerType,
+ ReplacesTheImplicitObjectParameterWithAnErasedPointer) {
+ static_assert(
+ std::is_same_v);
+ static_assert(std::is_same_v);
+ static_assert(std::is_same_v<
+ typename[:vtable_entry_pointer_type(
+ ^^Fixture::no_args, ^^void*):], void (*)(void*)>);
+}
+
+} // namespace