Skip to content

Commit 14787a6

Browse files
authored
Merge pull request #360 from ManifoldFR/devel
Add example showing how to bind virtual classes, passing to overrides…
2 parents 2dbad07 + 9be4fa3 commit 14787a6

File tree

3 files changed

+236
-1
lines changed

3 files changed

+236
-1
lines changed

unittest/CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ if(CMAKE_CXX_STANDARD GREATER 14 AND CMAKE_CXX_STANDARD LESS 98)
6262
config_bind_optional(std "std::optional")
6363
endif()
6464

65+
add_lib_unit_test(bind_virtual_factory)
66+
6567
add_python_unit_test("py-matrix" "unittest/python/test_matrix.py" "unittest")
6668

6769
add_python_unit_test("py-tensor" "unittest/python/test_tensor.py" "unittest")
@@ -118,4 +120,8 @@ set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP})
118120

119121
add_python_unit_test("py-user-struct" "unittest/python/test_user_struct.py"
120122
"python;unittest")
121-
set_tests_properties("py-std-vector" PROPERTIES DEPENDS ${PYWRAP})
123+
set_tests_properties("py-user-struct" PROPERTIES DEPENDS ${PYWRAP})
124+
125+
add_python_unit_test("py-bind-virtual" "unittest/python/test_bind_virtual.py"
126+
"python;unittest")
127+
set_tests_properties("py-bind-virtual" PROPERTIES DEPENDS ${PYWRAP})

unittest/bind_virtual_factory.cpp

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/// Copyright 2023 LAAS-CNRS, INRIA
2+
#include <eigenpy/eigenpy.hpp>
3+
4+
using std::shared_ptr;
5+
namespace bp = boost::python;
6+
7+
// fwd declaration
8+
struct MyVirtualData;
9+
10+
/// A virtual class with two pure virtual functions taking different signatures,
11+
/// and a polymorphic factory function.
12+
struct MyVirtualClass {
13+
MyVirtualClass() {}
14+
virtual ~MyVirtualClass() {}
15+
16+
// polymorphic fn taking arg by shared_ptr
17+
virtual void doSomethingPtr(shared_ptr<MyVirtualData> const &data) const = 0;
18+
// polymorphic fn taking arg by reference
19+
virtual void doSomethingRef(MyVirtualData &data) const = 0;
20+
21+
virtual shared_ptr<MyVirtualData> createData() const {
22+
return std::make_shared<MyVirtualData>(*this);
23+
}
24+
};
25+
26+
struct MyVirtualData {
27+
MyVirtualData(MyVirtualClass const &) {}
28+
virtual ~MyVirtualData() {} // virtual dtor to mark class as polymorphic
29+
};
30+
31+
shared_ptr<MyVirtualData> callDoSomethingPtr(const MyVirtualClass &obj) {
32+
auto d = obj.createData();
33+
printf("Created MyVirtualData with address %p\n", (void *)d.get());
34+
obj.doSomethingPtr(d);
35+
return d;
36+
}
37+
38+
shared_ptr<MyVirtualData> callDoSomethingRef(const MyVirtualClass &obj) {
39+
auto d = obj.createData();
40+
printf("Created MyVirtualData with address %p\n", (void *)d.get());
41+
obj.doSomethingRef(*d);
42+
return d;
43+
}
44+
45+
void throw_virtual_not_implemented_error() {
46+
throw std::runtime_error("Called C++ virtual function.");
47+
}
48+
49+
/// Wrapper classes
50+
struct VirtualClassWrapper : MyVirtualClass, bp::wrapper<MyVirtualClass> {
51+
void doSomethingPtr(shared_ptr<MyVirtualData> const &data) const override {
52+
if (bp::override fo = this->get_override("doSomethingPtr")) {
53+
/// shared_ptr HAS to be passed by value.
54+
/// Boost.Python's argument converter has the wrong behaviour for
55+
/// reference_wrapper<shared_ptr<T>>, so boost::ref(data) does not work.
56+
fo(data);
57+
return;
58+
}
59+
throw_virtual_not_implemented_error();
60+
}
61+
62+
/// The data object is passed by mutable reference to this function,
63+
/// and wrapped in a @c boost::reference_wrapper when passed to the override.
64+
/// Otherwise, Boost.Python's argument converter will convert to Python by
65+
/// value and create a copy.
66+
void doSomethingRef(MyVirtualData &data) const override {
67+
if (bp::override fo = this->get_override("doSomethingRef")) {
68+
fo(boost::ref(data));
69+
return;
70+
}
71+
throw_virtual_not_implemented_error();
72+
}
73+
74+
shared_ptr<MyVirtualData> createData() const override {
75+
if (bp::override fo = this->get_override("createData")) return fo();
76+
return default_createData();
77+
}
78+
79+
shared_ptr<MyVirtualData> default_createData() const {
80+
return MyVirtualClass::createData();
81+
}
82+
};
83+
84+
/// This "trampoline class" does nothing but is ABSOLUTELY required to ensure
85+
/// downcasting works properly with non-smart ptr signatures. Otherwise,
86+
/// there is no handle to the original Python object ( @c PyObject *).
87+
/// Every single polymorphic type exposed to Python should be exposed through
88+
/// such a trampoline. Users can also create their own wrapper classes by taking
89+
/// inspiration from boost::python::wrapper<T>.
90+
struct DataWrapper : MyVirtualData, bp::wrapper<MyVirtualData> {
91+
/// we have to use-declare non-defaulted constructors
92+
/// (see https://en.cppreference.com/w/cpp/language/default_constructor)
93+
/// or define them manually.
94+
using MyVirtualData::MyVirtualData;
95+
};
96+
97+
/// Take and return a const reference
98+
const MyVirtualData &iden_ref(const MyVirtualData &d) {
99+
// try cast to holder
100+
return d;
101+
}
102+
103+
/// Take a shared_ptr (by const reference or value, doesn't matter), return by
104+
/// const reference
105+
const MyVirtualData &iden_shared(const shared_ptr<MyVirtualData> &d) {
106+
// get boost.python's custom deleter
107+
// boost.python hides the handle to the original object in there
108+
// dter being nonzero indicates shared_ptr was wrapped by Boost.Python
109+
auto *dter = std::get_deleter<bp::converter::shared_ptr_deleter>(d);
110+
if (dter != 0) printf("> input shared_ptr has a deleter\n");
111+
return *d;
112+
}
113+
114+
/// Take and return a shared_ptr
115+
shared_ptr<MyVirtualData> copy_shared(const shared_ptr<MyVirtualData> &d) {
116+
auto *dter = std::get_deleter<bp::converter::shared_ptr_deleter>(d);
117+
if (dter != 0) printf("> input shared_ptr has a deleter\n");
118+
return d;
119+
}
120+
121+
BOOST_PYTHON_MODULE(bind_virtual_factory) {
122+
assert(std::is_polymorphic<MyVirtualClass>::value &&
123+
"MyVirtualClass should be polymorphic!");
124+
assert(std::is_polymorphic<MyVirtualData>::value &&
125+
"MyVirtualData should be polymorphic!");
126+
127+
bp::class_<VirtualClassWrapper, boost::noncopyable>(
128+
"MyVirtualClass", bp::init<>(bp::args("self")))
129+
.def("doSomething", bp::pure_virtual(&MyVirtualClass::doSomethingPtr),
130+
bp::args("self", "data"))
131+
.def("doSomethingRef", bp::pure_virtual(&MyVirtualClass::doSomethingRef),
132+
bp::args("self", "data"))
133+
.def("createData", &MyVirtualClass::createData,
134+
&VirtualClassWrapper::default_createData, bp::args("self"));
135+
136+
bp::register_ptr_to_python<shared_ptr<MyVirtualData> >();
137+
/// Trampoline used as 1st argument
138+
/// otherwise if passed as "HeldType", we need to define
139+
/// the constructor and call initializer manually.
140+
bp::class_<DataWrapper, boost::noncopyable>("MyVirtualData", bp::no_init)
141+
.def(bp::init<MyVirtualClass const &>(bp::args("self", "model")));
142+
143+
bp::def("callDoSomethingPtr", callDoSomethingPtr, bp::args("obj"));
144+
bp::def("callDoSomethingRef", callDoSomethingRef, bp::args("obj"));
145+
146+
bp::def("iden_ref", iden_ref, bp::return_internal_reference<>());
147+
bp::def("iden_shared", iden_shared, bp::return_internal_reference<>());
148+
bp::def("copy_shared", copy_shared);
149+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import bind_virtual_factory as bvf
2+
3+
4+
class ImplClass(bvf.MyVirtualClass):
5+
def __init__(self):
6+
self.val = 42
7+
bvf.MyVirtualClass.__init__(self)
8+
9+
def createData(self):
10+
return ImplData(self)
11+
12+
# override MyVirtualClass::doSomethingPtr(shared_ptr data)
13+
def doSomethingPtr(self, data):
14+
print("Hello from doSomething!")
15+
assert isinstance(data, ImplData)
16+
print("Data value:", data.value)
17+
data.value += 1
18+
19+
# override MyVirtualClass::doSomethingPtr(data&)
20+
def doSomethingRef(self, data):
21+
print("Hello from doSomethingRef!")
22+
print(type(data))
23+
assert isinstance(data, ImplData)
24+
print("Data value:", data.value)
25+
data.value += 1
26+
27+
28+
class ImplData(bvf.MyVirtualData):
29+
def __init__(self, c):
30+
# parent virtual class requires arg
31+
bvf.MyVirtualData.__init__(self, c)
32+
self.value = c.val
33+
34+
35+
def test_instantiate_child():
36+
obj = ImplClass()
37+
data = obj.createData()
38+
print(data)
39+
40+
41+
def test_call_do_something_ptr():
42+
obj = ImplClass()
43+
print("Calling doSomething (by ptr)")
44+
d1 = bvf.callDoSomethingPtr(obj)
45+
print("Output data.value:", d1.value)
46+
47+
48+
def test_call_do_something_ref():
49+
obj = ImplClass()
50+
print("Ref variant:")
51+
d2 = bvf.callDoSomethingRef(obj)
52+
print(d2.value)
53+
print("-----")
54+
55+
56+
def test_iden_fns():
57+
obj = ImplClass()
58+
d = obj.createData()
59+
print(d, type(d))
60+
61+
# take and return const T&
62+
d1 = bvf.iden_ref(d)
63+
print(d1, type(d1))
64+
assert isinstance(d1, ImplData)
65+
66+
# take a shared_ptr, return const T&
67+
d2 = bvf.iden_shared(d)
68+
assert isinstance(d2, ImplData)
69+
print(d2, type(d2))
70+
71+
print("copy shared ptr -> py -> cpp")
72+
d3 = bvf.copy_shared(d)
73+
assert isinstance(d3, ImplData)
74+
print(d3, type(d3))
75+
76+
77+
test_instantiate_child()
78+
test_call_do_something_ptr()
79+
test_call_do_something_ref()
80+
test_iden_fns()

0 commit comments

Comments
 (0)