From 3bb8cc4c6a37ca60badcd13519e1de005b4f83a7 Mon Sep 17 00:00:00 2001 From: Vadim Mishanin Date: Sun, 7 Jun 2026 20:05:17 +0800 Subject: [PATCH 1/6] Support constructors --- include/rebind/function_invoker.hpp | 20 +++++++++-- include/rebind/reflect.hpp | 54 +++++++++++++++++++++-------- tests/class_test.cpp | 20 +++++++++++ tests/test_class.py | 4 +++ 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/include/rebind/function_invoker.hpp b/include/rebind/function_invoker.hpp index 8d7ed76..c9173f6 100644 --- a/include/rebind/function_invoker.hpp +++ b/include/rebind/function_invoker.hpp @@ -62,10 +62,26 @@ struct MethodInvoker { static PyObject* invoke(PyObject* self, PyObject* args) noexcept { auto* wrapper = reinterpret_cast(self); - return invokePythonCallable(args, [wrapper](auto&&... converted_args) -> decltype(auto) { - return (wrapper->store.cpp_class.*Method)(std::forward(converted_args)...); + return invokePythonCallable< + args_tuple, + return_type>(args, [wrapper](auto&&... converted_args) -> decltype(auto) { + return (wrapper->store.cpp_class.value().*Method)(std::forward(converted_args)... + ); }); } }; +template +struct ConstructorInvoker { + using return_type = void; + + static int invoke(PyObject* self, PyObject* args, PyObject*) { + auto* wrapper = reinterpret_cast(self); + invokePythonCallable(args, [wrapper](auto&&... converted_args) -> void { + std::ignore = wrapper->store.cpp_class.emplace(std::forward(converted_args)...); + }); + return 0; + } +}; + } // namespace rebind diff --git a/include/rebind/reflect.hpp b/include/rebind/reflect.hpp index 909861d..24bb510 100644 --- a/include/rebind/reflect.hpp +++ b/include/rebind/reflect.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -105,22 +106,19 @@ struct PyClassWrapper { struct Storage; consteval { std::vector mems; - mems.push_back( - std::meta::data_member_spec( - ^^T, - { - .name = "cpp_class" - } - ) + constexpr std::meta::info optional = std::meta::substitute( + ^^std::optional, + { + ^^T + } ); + mems.push_back(std::meta::data_member_spec(optional, {.name = "cpp_class"})); std::meta::define_aggregate(^^Storage, mems); } PyObject_HEAD Storage store{}; static PyObject* create(PyTypeObject* o, PyObject* args, PyObject* kwds) { return o->tp_alloc(o, 0); } - static int init(PyObject* op, PyObject* args, PyObject* kwds) { return 0; } - static void dealloc(PyObject* o) { Py_TYPE(o)->tp_free(o); } }; @@ -135,7 +133,10 @@ inline consteval auto getMethodDef() noexcept { static constexpr auto ctx = std::meta::access_context::unprivileged(); constexpr auto members = std::define_static_array(std::meta::members_of(R, ctx)); - if constexpr (std::meta::is_function(members[I]) && !std::meta::is_special_member_function(members[I])) { + if constexpr (std::meta::is_function(members[I]) && !std::meta::is_special_member_function(members[I]) && + !std::meta::is_constructor(members[I])) + { + std::meta::parameters_of(members[I]); return std::make_tuple( PyMethodDef{ .ml_name = std::meta::identifier_of(members[I]).data(), @@ -159,6 +160,29 @@ inline consteval auto collectMethodDefs() noexcept { return collectMethodDefsImpl(std::make_index_sequence()>{}); } +template +consteval auto getTypesOfFunctionArgs() { + constexpr auto params = std::define_static_array(std::meta::parameters_of(R)); + + return [params](std::index_sequence) { + return std::tuple{}; + }(std::make_index_sequence()); +} + +template +inline consteval auto getConstructorInvoker() { + static constexpr auto ctx = std::meta::access_context::unprivileged(); + + template for (constexpr auto m : std::define_static_array(std::meta::members_of(C, ctx))) { + if constexpr (std::meta::is_public(m) && std::meta::is_constructor(m) && !is_copy_constructor(m) && + !is_move_constructor(m)) + { + auto argTypes = getTypesOfFunctionArgs(); + return &ConstructorInvoker::invoke; + } + } +} + template inline consteval auto reflect_class() noexcept { constexpr std::meta::info t = std::meta::substitute( @@ -169,8 +193,8 @@ inline consteval auto reflect_class() noexcept { ); const auto class_name = std::meta::identifier_of(C); using type_t = typename[:t:]; - constexpr auto method_defs = collectMethodDefs(); - ClassDescriptor> desc{}; + constexpr auto methodDefs = collectMethodDefs(); + ClassDescriptor> desc{}; desc.type = PyTypeObject{}; desc.type.ob_base.ob_base.ob_refcnt = 1; desc.type.ob_base.ob_base.ob_type = nullptr; @@ -181,7 +205,7 @@ inline consteval auto reflect_class() noexcept { desc.type.tp_dealloc = type_t::dealloc; desc.type.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; desc.type.tp_doc = PyDoc_STR(class_name.data()); - desc.type.tp_init = type_t::init; + desc.type.tp_init = getConstructorInvoker(); desc.type.tp_new = type_t::create; desc.type.tp_methods = nullptr; //< Note: Filled at runtime @@ -190,10 +214,10 @@ inline consteval auto reflect_class() noexcept { size_t method_id{}; ((desc.methods[method_id++] = method_def), ...); }, - method_defs + methodDefs ); // Fill sentinel method - desc.methods[std::tuple_size_v] = PyMethodDef{.ml_name = nullptr, .ml_meth = nullptr, .ml_flags = 0, .ml_doc = nullptr}; return desc; diff --git a/tests/class_test.cpp b/tests/class_test.cpp index e356afc..c1b56ec 100644 --- a/tests/class_test.cpp +++ b/tests/class_test.cpp @@ -22,6 +22,26 @@ struct TestClass { std::string describe(std::string_view name) const { return std::format("{} is {}", name, age); } }; +struct TestConstructor { + TestConstructor(int age) : m_age{age} {} + + int add_to_age(int value) const { return m_age + value; } + + int m_age{}; +}; + +/* +// TODO: Support =delete(std::meta::is_deleted). Now it doesn't compile +struct TestConstructorDeleted { + TestConstructorDeleted(int age) = delete; + + int add_to_age(int value) const { return m_age + value; } + + int m_age{}; +}; + +*/ + } // namespace class_test REFLB_MODULE(class_test, class_test) diff --git a/tests/test_class.py b/tests/test_class.py index 5cfc469..359156f 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -13,6 +13,10 @@ def test_init_class() -> None: assert obj.is_adult() is False +def test_init_ctor() -> None: + obj = class_test.TestConstructor(12) + assert obj.add_to_age(5) == 17 + def test_mutating_methods_update_instance_state() -> None: obj = class_test.TestClass() From 68fd282b6fe12959988f8e48dc6275a8e62044fd Mon Sep 17 00:00:00 2001 From: Vadim Mishanin Date: Sun, 7 Jun 2026 20:07:14 +0800 Subject: [PATCH 2/6] Rename files --- tests/CMakeLists.txt | 13 +++++++++++-- tests/{class_test.cpp => test_class.cpp} | 0 ...eturns.cpp => test_invoke_methods_with_args.cpp} | 0 3 files changed, 11 insertions(+), 2 deletions(-) rename tests/{class_test.cpp => test_class.cpp} (100%) rename tests/{args_and_returns.cpp => test_invoke_methods_with_args.cpp} (100%) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 79b021f..e9fdc34 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -25,8 +25,8 @@ function(add_test name src) set(test_targets ${test_targets};${name} PARENT_SCOPE) endfunction() -add_test(tests args_and_returns) -add_test(class_test class_test) +add_test(test_invoke_methods_with_args test_invoke_methods_with_args) +add_test(test_class test_class) message(STATUS "test_targets" ${test_targets}) @@ -47,3 +47,12 @@ add_custom_target( DEPENDS ${test_targets} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" USES_TERMINAL) + +# add_executable(class_refl class_refl.cpp) +# target_link_libraries(class_refl PRIVATE rebind) +# target_compile_options(class_refl PRIVATE +# -Wall +# -Wextra +# -std=c++26 +# -freflection +# ) \ No newline at end of file diff --git a/tests/class_test.cpp b/tests/test_class.cpp similarity index 100% rename from tests/class_test.cpp rename to tests/test_class.cpp diff --git a/tests/args_and_returns.cpp b/tests/test_invoke_methods_with_args.cpp similarity index 100% rename from tests/args_and_returns.cpp rename to tests/test_invoke_methods_with_args.cpp From 260879f5807d6baa373e28cee70c4ae90243aef9 Mon Sep 17 00:00:00 2001 From: Vadim Mishanin Date: Sun, 7 Jun 2026 20:22:45 +0800 Subject: [PATCH 3/6] Update readme --- README.md | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d76d9c5..2cf8254 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,21 @@ For example, there are C++ functions that we want to expose. ```cpp namespace test { - int a{}; - void foo() { - std::println("foo method"); - } - void bar() { - std::println("bar method"); + struct Age { + int age{}; + + Age(int age) : age{age} {} + + int get_age(int value) { return age; } + + void set_age(int value) { age = value; } + + int add_to_age(int value) const { return age + value; } + }; + + int add(int a, int b) { + return a + b; } } ``` @@ -24,14 +32,19 @@ All we need to do is to provide the `test` namespace: REFLB_MODULE(example, test) ``` -By using reflection, the rebind project can get `foo` and `bar` methods and makes them available for Python code. Exposing new methods does not require writing binding code. +By using reflection, the rebind can make entities(classes and functions) available for Python code. Exposing new methods does not require writing binding code. ```python -import test +import example -test.add(1, 2) -test.mul(3.4, 5.6) +age = example.Age(10) + +print("add to age", age.add_to_age(30)) + +print("get age", age.set_age(30)) + +print("sum 1 + 2", example.add(1, 2)) ``` @@ -101,14 +114,9 @@ cmake -S . -B build-ubsan -DENABLE_UNDEFINED_SANITIZER=ON ## TODO -Current limitations include: - - [ ] No overload resolution +- [ ] Support =delete and other specifiers +- [ ] Support access to public member variables - [ ] Handle types mismatch -- [ ] Only free functions in namespaces are supported -- [ ] Support classes, member functions, or variables - [ ] Handle exception translation (C++ → Python) -- [ ] Requires an experimental Clang fork (not standard C++) - -These limitations are intentional to keep the project focused on -demonstrating C++ reflection. +- [ ] And more From c4bc7b8e48c0e1206a04ca1195ff033132dd3744 Mon Sep 17 00:00:00 2001 From: Vadim Mishanin Date: Sun, 7 Jun 2026 20:45:26 +0800 Subject: [PATCH 4/6] upd --- tests/CMakeLists.txt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e9fdc34..f500bbf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -25,7 +25,7 @@ function(add_test name src) set(test_targets ${test_targets};${name} PARENT_SCOPE) endfunction() -add_test(test_invoke_methods_with_args test_invoke_methods_with_args) +add_test(tests test_invoke_methods_with_args) add_test(test_class test_class) @@ -46,13 +46,4 @@ add_custom_target( COMMAND ${CMAKE_COMMAND} -E env ${pytest_env} ${Python3_EXECUTABLE} -m pytest -s test_invoke_methods_with_args.py test_class.py DEPENDS ${test_targets} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" - USES_TERMINAL) - -# add_executable(class_refl class_refl.cpp) -# target_link_libraries(class_refl PRIVATE rebind) -# target_compile_options(class_refl PRIVATE -# -Wall -# -Wextra -# -std=c++26 -# -freflection -# ) \ No newline at end of file + USES_TERMINAL) \ No newline at end of file From 5fd1d90b89dcd948889ee289011edb98027365aa Mon Sep 17 00:00:00 2001 From: Vadim Mishanin Date: Sun, 7 Jun 2026 20:48:48 +0800 Subject: [PATCH 5/6] rename --- tests/test_class.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_class.cpp b/tests/test_class.cpp index c1b56ec..7cefc2f 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -4,7 +4,7 @@ #include #include -namespace class_test { +namespace test_class { struct TestClass { int age{}; @@ -42,6 +42,6 @@ struct TestConstructorDeleted { */ -} // namespace class_test +} // namespace test_class -REFLB_MODULE(class_test, class_test) +REFLB_MODULE(test_class, test_class) From 5fa091a8ad587491b6e1124782987937a200e042 Mon Sep 17 00:00:00 2001 From: Vadim Mishanin Date: Sun, 7 Jun 2026 21:01:33 +0800 Subject: [PATCH 6/6] fix --- tests/CMakeLists.txt | 2 +- tests/test_class.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f500bbf..958fa05 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,7 +26,7 @@ function(add_test name src) endfunction() add_test(tests test_invoke_methods_with_args) -add_test(test_class test_class) +add_test(class_test test_class) message(STATUS "test_targets" ${test_targets}) diff --git a/tests/test_class.cpp b/tests/test_class.cpp index 7cefc2f..89ce814 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -44,4 +44,4 @@ struct TestConstructorDeleted { } // namespace test_class -REFLB_MODULE(test_class, test_class) +REFLB_MODULE(class_test, test_class)