Skip to content

Commit 5237df4

Browse files
authored
Merge pull request #220 from robotpy/safe-object
Add gilsafe_object to simplify storing python objects in containers
2 parents b11aac5 + c235056 commit 5237df4

File tree

5 files changed

+180
-0
lines changed

5 files changed

+180
-0
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
2+
#pragma once
3+
4+
#include <pybind11/pybind11.h>
5+
6+
namespace py = pybind11;
7+
8+
// Py_IsFinalizing is public API in 3.13
9+
#if PY_VERSION_HEX < 0x03130000
10+
#define Py_IsFinalizing _Py_IsFinalizing
11+
#endif
12+
13+
namespace rpy {
14+
15+
/*
16+
This object holds a python object, and can be stored in C++ containers that
17+
aren't pybind11 aware.
18+
19+
It is very inefficient -- it will acquire and release the GIL each time
20+
a move/copy operation occurs! You should only use this object type as a
21+
last resort.
22+
23+
Assigning, moves, copies, and destruction acquire the GIL; only converting
24+
this back into a python object requires holding the GIL.
25+
*/
26+
template <typename T>
27+
class gilsafe_t final {
28+
py::object o;
29+
30+
public:
31+
32+
//
33+
// These operations require the caller to hold the GIL
34+
//
35+
36+
// copy conversion
37+
operator py::object() const & {
38+
return o;
39+
}
40+
41+
// move conversion
42+
operator py::object() const && {
43+
return std::move(o);
44+
}
45+
46+
//
47+
// These operations do not require the caller to hold the GIL
48+
//
49+
50+
gilsafe_t() = default;
51+
52+
~gilsafe_t() {
53+
if (o) {
54+
// If the interpreter is alive, acquire the GIL, otherwise just leak
55+
// the object to avoid a crash
56+
if (!Py_IsFinalizing()) {
57+
py::gil_scoped_acquire lock;
58+
o.dec_ref();
59+
}
60+
61+
o.release();
62+
}
63+
}
64+
65+
// Copy constructor; always increases the reference count
66+
gilsafe_t(const gilsafe_t &other) {
67+
py::gil_scoped_acquire lock;
68+
o = other.o;
69+
}
70+
71+
// Copy constructor; always increases the reference count
72+
gilsafe_t(const py::object &other) {
73+
py::gil_scoped_acquire lock;
74+
o = other;
75+
}
76+
77+
gilsafe_t(const py::handle &other) {
78+
py::gil_scoped_acquire lock;
79+
o = py::reinterpret_borrow<py::object>(other);
80+
}
81+
82+
// Move constructor; steals object from ``other`` and preserves its reference count
83+
gilsafe_t(gilsafe_t &&other) noexcept : o(std::move(other.o)) {}
84+
85+
// Move constructor; steals object from ``other`` and preserves its reference count
86+
gilsafe_t(py::object &&other) noexcept : o(std::move(other)) {}
87+
88+
// copy assignment
89+
gilsafe_t &operator=(const gilsafe_t& other) {
90+
if (!o.is(other.o)) {
91+
py::gil_scoped_acquire lock;
92+
o = other.o;
93+
}
94+
return *this;
95+
}
96+
97+
// move assignment
98+
gilsafe_t &operator=(gilsafe_t&& other) noexcept {
99+
if (this != &other) {
100+
py::gil_scoped_acquire lock;
101+
o = std::move(other.o);
102+
}
103+
return *this;
104+
}
105+
106+
explicit operator bool() const {
107+
return (bool)o;
108+
}
109+
};
110+
111+
// convenience alias
112+
using gilsafe_object = gilsafe_t<py::object>;
113+
114+
} // namespace rpy
115+
116+
117+
118+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
119+
PYBIND11_NAMESPACE_BEGIN(detail)
120+
121+
template <typename T>
122+
struct type_caster<rpy::gilsafe_t<T>> {
123+
bool load(handle src, bool convert) {
124+
value = src;
125+
return true;
126+
}
127+
128+
static handle cast(const handle &src, return_value_policy /* policy */, handle /* parent */) {
129+
return src.inc_ref();
130+
}
131+
132+
PYBIND11_TYPE_CASTER(rpy::gilsafe_t<T>, handle_type_name<T>::name);
133+
};
134+
135+
template <typename T>
136+
struct handle_type_name<rpy::gilsafe_t<T>> {
137+
static constexpr auto name = handle_type_name<T>::name;
138+
};
139+
140+
PYBIND11_NAMESPACE_END(detail)
141+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

tests/cpp/pyproject.toml.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ generate = [
5353
{ enums = "enums.h" },
5454
{ factory = "factory.h" },
5555
{ fields = "fields.h" },
56+
{ gilsafe_container = "gilsafe_container.h" },
5657
{ keepalive = "keepalive.h" },
5758
{ ignore = "ignore.h" },
5859
{ ignored_by_default = "ignored_by_default.h" },

tests/cpp/rpytest/ft/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
GCEnum,
2121
GEnum,
2222
GEnumMath,
23+
GilsafeContainer,
2324
HasFactory,
2425
HasOperator,
2526
HasOperatorNoDefault,
@@ -135,6 +136,7 @@
135136
"GCEnum",
136137
"GEnum",
137138
"GEnumMath",
139+
"GilsafeContainer",
138140
"HasFactory",
139141
"HasOperator",
140142
"HasOperatorNoDefault",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
#pragma once
3+
4+
#include <gilsafe_object.h>
5+
6+
class GilsafeContainer {
7+
rpy::gilsafe_object m_o;
8+
public:
9+
void assign(rpy::gilsafe_object o) {
10+
m_o = o;
11+
}
12+
13+
static void check() {
14+
auto c = std::make_unique<GilsafeContainer>();
15+
16+
py::gil_scoped_acquire a;
17+
18+
py::object v = py::none();
19+
20+
{
21+
py::gil_scoped_release r;
22+
c->assign(v);
23+
c.reset();
24+
}
25+
}
26+
27+
};

tests/test_ft_misc.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ def test_factory():
130130
assert o.m_x == 5
131131

132132

133+
#
134+
# gilsafe_container.h
135+
#
136+
137+
138+
def test_gilsafe_container():
139+
ft.GilsafeContainer.check()
140+
141+
133142
#
134143
# inline_code.h
135144
#

0 commit comments

Comments
 (0)