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 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/CMakeLists.txt b/tests/CMakeLists.txt index 79b021f..958fa05 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(tests test_invoke_methods_with_args) +add_test(class_test test_class) message(STATUS "test_targets" ${test_targets}) @@ -46,4 +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) + USES_TERMINAL) \ No newline at end of file diff --git a/tests/class_test.cpp b/tests/class_test.cpp deleted file mode 100644 index e356afc..0000000 --- a/tests/class_test.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "rebind/rebind.hpp" - -#include -#include -#include - -namespace class_test { - -struct TestClass { - int age{}; - - int get_age() const { return age; } - - void set_age(int value) { age = value; } - - void birthday() { ++age; } - - int add_to_age(int value) const { return age + value; } - - bool is_adult() const { return age >= 18; } - - std::string describe(std::string_view name) const { return std::format("{} is {}", name, age); } -}; - -} // namespace class_test - -REFLB_MODULE(class_test, class_test) diff --git a/tests/test_class.cpp b/tests/test_class.cpp new file mode 100644 index 0000000..89ce814 --- /dev/null +++ b/tests/test_class.cpp @@ -0,0 +1,47 @@ +#include "rebind/rebind.hpp" + +#include +#include +#include + +namespace test_class { + +struct TestClass { + int age{}; + + int get_age() const { return age; } + + void set_age(int value) { age = value; } + + void birthday() { ++age; } + + int add_to_age(int value) const { return age + value; } + + bool is_adult() const { return age >= 18; } + + 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 test_class + +REFLB_MODULE(class_test, test_class) 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() 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