Skip to content

Commit ef33713

Browse files
Add smart pointer classes for managing pyobject pointers.
In particular, there are 4 possible sets of smart pointer semantics defined here corresponding to the cases where a PyObject pointer does or does not own a reference and where a pointer is or is not allowed to be null valued.
1 parent b9f91bc commit ef33713

1 file changed

Lines changed: 255 additions & 67 deletions

File tree

dynd/include/utility_functions.hpp

Lines changed: 255 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -22,86 +22,280 @@ struct callable_type_data;
2222

2323
namespace pydynd {
2424

25-
/**
26-
* A container class for managing the local lifetime of
27-
* PyObject *.
28-
*
29-
* Throws an exception if the object passed into the constructor
30-
* is NULL.
31-
*/
32-
class pyobject_ownref {
33-
PyObject *m_obj;
25+
template <bool not_null = false>
26+
inline void incref(PyObject *) noexcept;
27+
28+
template <>
29+
inline void incref<false>(PyObject *obj) noexcept
30+
{
31+
Py_XINCREF(obj);
32+
}
33+
34+
template <>
35+
inline void incref<true>(PyObject *obj) noexcept
36+
{
37+
Py_INCREF(obj);
38+
}
39+
40+
template <bool not_null>
41+
inline void decref(PyObject *) noexcept;
42+
43+
template <>
44+
inline void decref<false>(PyObject *obj) noexcept
45+
{
46+
Py_XDECREF(obj);
47+
}
48+
49+
template <>
50+
inline void decref<true>(PyObject *obj) noexcept
51+
{
52+
Py_DECREF(obj);
53+
}
54+
55+
template <bool owns_ref, bool not_null>
56+
inline void incref_if_owned(PyObject *obj) noexcept;
57+
58+
template <>
59+
inline void incref_if_owned<true, true>(PyObject *obj) noexcept
60+
{
61+
Py_INCREF(obj);
62+
}
63+
64+
template <>
65+
inline void incref_if_owned<true, false>(PyObject *obj) noexcept
66+
{
67+
Py_XINCREF(obj);
68+
}
69+
70+
template <>
71+
inline void incref_if_owned<false, true>(PyObject *obj) noexcept
72+
{
73+
}
74+
75+
template <>
76+
inline void incref_if_owned<false, false>(PyObject *obj) noexcept
77+
{
78+
}
79+
80+
template <bool owns_ref, bool not_null>
81+
inline void decref_if_owned(PyObject *obj) noexcept;
82+
83+
template <>
84+
inline void decref_if_owned<true, true>(PyObject *obj) noexcept
85+
{
86+
Py_DECREF(obj);
87+
}
88+
89+
template <>
90+
inline void decref_if_owned<true, false>(PyObject *obj) noexcept
91+
{
92+
Py_XDECREF(obj);
93+
}
94+
95+
template <>
96+
inline void decref_if_owned<false, true>(PyObject *obj) noexcept
97+
{
98+
}
99+
100+
template <>
101+
inline void decref_if_owned<false, false>(PyObject *obj) noexcept
102+
{
103+
}
34104

35-
// Non-copyable
36-
pyobject_ownref(const pyobject_ownref &);
37-
pyobject_ownref &operator=(const pyobject_ownref &);
105+
template <bool owns_ref = true, bool not_null = true>
106+
class py_ref {
107+
PyObject *o;
38108

39109
public:
40-
inline pyobject_ownref() : m_obj(NULL) {}
41-
inline explicit pyobject_ownref(PyObject *obj) : m_obj(obj)
110+
// Explicit specializations for default constructor
111+
// are defined after the class declaration.
112+
// If the class is allowed to be null, it default-initializes to null.
113+
// If the class is not allowed to be null, it default-initializes to None.
114+
py_ref() noexcept;
115+
116+
// First define an accessor to get the PyObject pointer from
117+
// the wrapper class.
118+
PyObject *get() noexcept { return o; }
119+
120+
// All copy and move constructors are defined as noexcept.
121+
// If the conversion or move operation would move from a type
122+
// that can be null to one that can not, the corresponding
123+
// constructor is declared explicit.
124+
125+
/* If:
126+
* this type allows null,
127+
* Or:
128+
* this type doesn't allow null
129+
* and the input type doesn't allow null,
130+
* Then:
131+
* Allow implicit conversions from the other py_ref type to this type.
132+
*/
133+
template <bool other_owns, bool other_not_null,
134+
typename std::enable_if_t<!not_null || (not_null && other_not_null)> * = nullptr>
135+
py_ref(const py_ref<other_owns, other_not_null> &other) noexcept
42136
{
43-
if (obj == NULL) {
44-
throw std::runtime_error("propagating a Python exception...");
45-
}
137+
o = other.o;
138+
incref_if_owned<owns_ref, not_null>(o);
46139
}
47140

48-
inline pyobject_ownref(PyObject *obj, bool inc_ref) : m_obj(obj)
141+
/* If:
142+
* this type doesn't allow null,
143+
* and the input type does,
144+
* Then:
145+
* Require that conversions from the other py_ref type to this type be explcit.
146+
*/
147+
template <bool other_owns, bool other_not_null, typename std::enable_if_t<not_null && !other_not_null> * = nullptr>
148+
explicit py_ref(const py_ref<other_owns, other_not_null> &other) noexcept
49149
{
50-
if (obj == NULL) {
51-
throw std::runtime_error("propagating a Python exception...");
52-
}
53-
if (inc_ref) {
54-
Py_INCREF(m_obj);
55-
}
150+
o = other.o;
151+
incref_if_owned<owns_ref, not_null>(o);
56152
}
57153

58-
inline ~pyobject_ownref() { Py_XDECREF(m_obj); }
59-
60-
inline PyObject **obj_addr() { return &m_obj; }
154+
// Move constructors are really only useful for moving
155+
// between types that own their references.
156+
// Only define them for those cases.
61157

62-
/**
63-
* Resets the reference owned by this object to the one provided.
64-
* This steals a reference to the input parameter, 'obj'.
65-
*
66-
* \param obj The reference to replace the current one in this object.
158+
/* If:
159+
* both this type and the input type own their reference,
160+
* and we are not moving from a type that allows null values to one that does not,
161+
* Then:
162+
* a move operation can be implicitly performed.
67163
*/
68-
inline void reset(PyObject *obj)
164+
template <bool other_not_null, typename std::enable_if_t<owns_ref && (other_not_null || !not_null)> * = nullptr>
165+
py_ref(py_ref<true, other_not_null> &&other) noexcept
69166
{
70-
if (obj == NULL) {
71-
throw std::runtime_error("propagating a Python exception...");
72-
}
73-
Py_XDECREF(m_obj);
74-
m_obj = obj;
167+
o = other.o;
75168
}
76169

77-
/**
78-
* Clears the owned reference to NULL.
170+
/* If:
171+
* both this type and the input type own their reference,
172+
* and we are moving from a type that allows null values to one that does not,
173+
* Then:
174+
* only an explicit move operation can be performed.
79175
*/
80-
inline void clear()
176+
template <bool other_not_null, typename std::enable_if_t<owns_ref && !other_not_null && not_null> * = nullptr>
177+
explicit py_ref(py_ref<true, other_not_null> &&other) noexcept
81178
{
82-
Py_XDECREF(m_obj);
83-
m_obj = NULL;
179+
o = other.o;
84180
}
85181

86-
/** Returns a borrowed reference. */
87-
inline PyObject *get() const { return m_obj; }
88-
89-
/** Returns a borrowed reference. */
90-
inline operator PyObject *() { return m_obj; }
182+
/* When constructing from a PyObject*, the boolean value `consume_ref` must
183+
* be passed to the constructor so it is clear whether or not to
184+
* increment, decrement, or do nothing the reference when storing it in the
185+
* desired smart pointer type. If you do not intend for the smart
186+
* pointer to capture the reference that you own, you should
187+
* specify `consume_ref` as false regardless of whether or not you
188+
* own the reference represented in the PyObject* you pass in.
189+
*/
190+
explicit py_ref(PyObject *obj, bool consume_ref) noexcept
191+
{
192+
o = obj;
193+
if (consume_ref) {
194+
decref_if_owned<!owns_ref, not_null>(o);
195+
}
196+
else {
197+
incref_if_owned<owns_ref, not_null>(o);
198+
}
199+
}
91200

92-
/**
93-
* Returns the reference owned by this object,
94-
* use it like "return obj.release()". After the
95-
* call, this object contains NULL.
201+
~py_ref() { decref_if_owned<owns_ref, not_null>(o); }
202+
203+
// For assignment operators, only allow assignment
204+
// in cases where implicit conversions are also allowed.
205+
// This forces explicit handling of cases that could
206+
// need to have an exception raised.
207+
// For that reason, these are all marked as noexcept.
208+
// Assignment never comsumes a reference.
209+
210+
/* If:
211+
* this type allows null,
212+
* Or:
213+
* this type doesn't allow null
214+
* and the input type doesn't allow null,
215+
* Then:
216+
* Allow assignment from the other py_ref type to this type.
96217
*/
97-
inline PyObject *release()
218+
template <bool other_owns, bool other_not_null,
219+
typename std::enable_if_t<!not_null || (not_null && other_not_null)> * = nullptr>
220+
py_ref<owns_ref, not_null> operator=(const py_ref<other_owns, other_not_null> &other) noexcept
98221
{
99-
PyObject *result = m_obj;
100-
m_obj = NULL;
101-
return result;
222+
decref_if_owned<owns_ref, not_null>(o);
223+
o = other.o;
224+
incref_if_owned<owns_ref, not_null>(o);
225+
}
226+
227+
// Check if the wrapped pointer is null.
228+
// Always return true if it is not null by definition.
229+
bool is_null() noexcept
230+
{
231+
// This function will be a no-op returning true
232+
// when not_null is false.
233+
if (not_null || (o != nullptr)) {
234+
return false;
235+
}
236+
return true;
102237
}
238+
239+
// A debug version of is_null with a purely dynamic check.
240+
bool is_null_dbg() noexcept { return o != nullptr; }
103241
};
104242

243+
// Default constructors for various cases.
244+
template <>
245+
inline py_ref<true, true>::py_ref() noexcept
246+
{
247+
o = Py_None;
248+
incref<true>(o);
249+
}
250+
251+
template <>
252+
inline py_ref<true, false>::py_ref() noexcept
253+
{
254+
o = nullptr;
255+
}
256+
257+
template <>
258+
inline py_ref<false, true>::py_ref() noexcept
259+
{
260+
o = Py_None;
261+
}
262+
263+
template <>
264+
inline py_ref<false, false>::py_ref() noexcept
265+
{
266+
o = nullptr;
267+
}
268+
269+
// Convenience aliases for the templated smart pointer classes.
270+
// All the template arguments to py_ref have defaults,
271+
// so py_ref can already be used for pointers that
272+
// own their reference and cannot be null.
273+
// The following aliases fill in the other cases.
274+
275+
using py_ref_with_null = py_ref<true, false>;
276+
277+
using py_borref = py_ref<false, true>;
278+
279+
using py_borref_with_null = py_ref<false, false>;
280+
281+
// To help with the transition to the new classes.
282+
using pyobject_ownref = py_ref<true, false>;
283+
284+
/* Check if a wrapped pointer is null.
285+
* If it is not, return the pointer
286+
* wrapped in the corresponding not_null wrapper type.
287+
* If it is, raise an exception.
288+
* This can be used to forward exceptions from Python.
289+
*/
290+
template <bool owns_ref, bool not_null>
291+
py_ref<owns_ref, true> check_null(py_ref<owns_ref, not_null> &o)
292+
{
293+
if (o.is_null()) {
294+
throw std::runtime_error("Unexpected null pointer.");
295+
}
296+
return reinterpret_cast<py_ref_tmpl<owns_ref, true>>(o);
297+
}
298+
105299
class PyGILState_RAII {
106300
PyGILState_STATE m_gstate;
107301

@@ -149,8 +343,7 @@ inline intptr_t pyobject_as_index(PyObject *index)
149343
#endif
150344
}
151345
else {
152-
throw std::runtime_error(
153-
"Value returned from PyNumber_Index is not an int or long");
346+
throw std::runtime_error("Value returned from PyNumber_Index is not an int or long");
154347
}
155348
if (result == -1 && PyErr_Occurred()) {
156349
throw std::exception();
@@ -170,8 +363,7 @@ inline int pyobject_as_int_index(PyObject *index)
170363
throw std::exception();
171364
}
172365
if (((unsigned long)result & 0xffffffffu) != (unsigned long)result) {
173-
throw std::overflow_error(
174-
"overflow converting Python integer to 32-bit int");
366+
throw std::overflow_error("overflow converting Python integer to 32-bit int");
175367
}
176368
return (int)result;
177369
}
@@ -248,8 +440,7 @@ inline std::string pyobject_repr(PyObject *obj)
248440
return pystring_as_string(src_repr.get());
249441
}
250442

251-
inline void pyobject_as_vector_string(PyObject *list_string,
252-
std::vector<std::string> &vector_string)
443+
inline void pyobject_as_vector_string(PyObject *list_string, std::vector<std::string> &vector_string)
253444
{
254445
Py_ssize_t size = PySequence_Size(list_string);
255446
vector_string.resize(size);
@@ -259,9 +450,7 @@ inline void pyobject_as_vector_string(PyObject *list_string,
259450
}
260451
}
261452

262-
inline void pyobject_as_vector_intp(PyObject *list_index,
263-
std::vector<intptr_t> &vector_intp,
264-
bool allow_int)
453+
inline void pyobject_as_vector_intp(PyObject *list_index, std::vector<intptr_t> &vector_intp, bool allow_int)
265454
{
266455
if (allow_int) {
267456
// If permitted, convert an int into a size-1 list
@@ -389,8 +578,7 @@ inline void mark_axis(PyObject *int_axis, int ndim, dynd::bool1 *reduce_axes)
389578
*
390579
* Returns the number of axes which were set.
391580
*/
392-
inline int pyarg_axis_argument(PyObject *axis, int ndim,
393-
dynd::bool1 *reduce_axes)
581+
inline int pyarg_axis_argument(PyObject *axis, int ndim, dynd::bool1 *reduce_axes)
394582
{
395583
int axis_count = 0;
396584

0 commit comments

Comments
 (0)