Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 26 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
```
Expand All @@ -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))

```

Expand Down Expand Up @@ -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
20 changes: 18 additions & 2 deletions include/rebind/function_invoker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,26 @@ struct MethodInvoker {

static PyObject* invoke(PyObject* self, PyObject* args) noexcept {
auto* wrapper = reinterpret_cast<Wrapper*>(self);
return invokePythonCallable<args_tuple, return_type>(args, [wrapper](auto&&... converted_args) -> decltype(auto) {
return (wrapper->store.cpp_class.*Method)(std::forward<decltype(converted_args)>(converted_args)...);
return invokePythonCallable<
args_tuple,
return_type>(args, [wrapper](auto&&... converted_args) -> decltype(auto) {
return (wrapper->store.cpp_class.value().*Method)(std::forward<decltype(converted_args)>(converted_args)...
);
});
}
};

template <typename ArgsTuple, typename Wrapper>
struct ConstructorInvoker {
using return_type = void;

static int invoke(PyObject* self, PyObject* args, PyObject*) {
auto* wrapper = reinterpret_cast<Wrapper*>(self);
invokePythonCallable<ArgsTuple, return_type>(args, [wrapper](auto&&... converted_args) -> void {
std::ignore = wrapper->store.cpp_class.emplace(std::forward<decltype(converted_args)>(converted_args)...);
});
return 0;
}
};

} // namespace rebind
54 changes: 39 additions & 15 deletions include/rebind/reflect.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <array>
#include <functional>
#include <meta>
#include <optional>
#include <print>
#include <ranges>
#include <stdexcept>
Expand Down Expand Up @@ -105,22 +106,19 @@ struct PyClassWrapper {
struct Storage;
consteval {
std::vector<std::meta::info> 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); }
};

Expand All @@ -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(),
Expand All @@ -159,6 +160,29 @@ inline consteval auto collectMethodDefs() noexcept {
return collectMethodDefsImpl<C, Wrapper>(std::make_index_sequence<numOfMembers<C>()>{});
}

template <std::meta::info R>
consteval auto getTypesOfFunctionArgs() {
constexpr auto params = std::define_static_array(std::meta::parameters_of(R));

return [params]<size_t... I>(std::index_sequence<I...>) {
return std::tuple<typename[:std::meta::type_of(params[I]):]...>{};
}(std::make_index_sequence<params.size()>());
}

template <std::meta::info C, typename Wrapper>
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<m>();
return &ConstructorInvoker<decltype(argTypes), Wrapper>::invoke;
}
}
}

template <std::meta::info C>
inline consteval auto reflect_class() noexcept {
constexpr std::meta::info t = std::meta::substitute(
Expand All @@ -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<C, type_t>();
ClassDescriptor<std::tuple_size_v<decltype(method_defs)>> desc{};
constexpr auto methodDefs = collectMethodDefs<C, type_t>();
ClassDescriptor<std::tuple_size_v<decltype(methodDefs)>> desc{};
desc.type = PyTypeObject{};
desc.type.ob_base.ob_base.ob_refcnt = 1;
desc.type.ob_base.ob_base.ob_type = nullptr;
Expand All @@ -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<C, type_t>();
desc.type.tp_new = type_t::create;
desc.type.tp_methods = nullptr; //< Note: Filled at runtime

Expand All @@ -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<decltype(method_defs
desc.methods[std::tuple_size_v<decltype(methodDefs
)>] = PyMethodDef{.ml_name = nullptr, .ml_meth = nullptr, .ml_flags = 0, .ml_doc = nullptr};

return desc;
Expand Down
6 changes: 3 additions & 3 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand All @@ -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)
27 changes: 0 additions & 27 deletions tests/class_test.cpp

This file was deleted.

47 changes: 47 additions & 0 deletions tests/test_class.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#include "rebind/rebind.hpp"

#include <format>
#include <string>
#include <string_view>

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)
4 changes: 4 additions & 0 deletions tests/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
File renamed without changes.
Loading