Skip to content

Commit 6bf25d1

Browse files
authored
feat: add semi-public API: pybind11::detail::is_holder_constructed (#5669)
* Add semi-public API: `pybind11::detail::is_holder_constructed` * Update docs for `pybind11::custom_type_setup` * Update tests in `test_custom_type_setup.cpp` * Apply suggestions from code review
1 parent 9afc9c4 commit 6bf25d1

File tree

3 files changed

+36
-8
lines changed

3 files changed

+36
-8
lines changed

docs/advanced/classes.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,13 +1384,21 @@ You can do that using ``py::custom_type_setup``:
13841384
auto *type = &heap_type->ht_type;
13851385
type->tp_flags |= Py_TPFLAGS_HAVE_GC;
13861386
type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) {
1387-
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
1388-
Py_VISIT(self.value.ptr());
1387+
// https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse
1388+
#if PY_VERSION_HEX >= 0x03090000
1389+
Py_VISIT(Py_TYPE(self_base));
1390+
#endif
1391+
if (py::detail::is_holder_constructed(self_base)) {
1392+
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
1393+
Py_VISIT(self.value.ptr());
1394+
}
13891395
return 0;
13901396
};
13911397
type->tp_clear = [](PyObject *self_base) {
1392-
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
1393-
self.value = py::none();
1398+
if (py::detail::is_holder_constructed(self_base)) {
1399+
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
1400+
self.value = py::none();
1401+
}
13941402
return 0;
13951403
};
13961404
}));

include/pybind11/detail/value_and_holder.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,17 @@ struct value_and_holder {
7474
}
7575
};
7676

77+
// This is a semi-public API to check if the corresponding instance has been constructed with a
78+
// holder. That is, if the instance has been constructed with a holder, the `__init__` method is
79+
// called and the C++ object is valid. Otherwise, the C++ object might only be allocated, but not
80+
// initialized. This will lead to **SEGMENTATION FAULTS** if the C++ object is used in any way.
81+
// Example usage: https://pybind11.readthedocs.io/en/stable/advanced/classes.html#custom-type-setup
82+
// for `tp_traverse` and `tp_clear` implementations.
83+
// WARNING: The caller is responsible for ensuring that the `reinterpret_cast` is valid.
84+
inline bool is_holder_constructed(PyObject *obj) {
85+
auto *const instance = reinterpret_cast<pybind11::detail::instance *>(obj);
86+
return instance->get_value_and_holder().holder_constructed();
87+
}
88+
7789
PYBIND11_NAMESPACE_END(detail)
7890
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

tests/test_custom_type_setup.cpp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,21 @@ TEST_SUBMODULE(custom_type_setup, m) {
2626
auto *type = &heap_type->ht_type;
2727
type->tp_flags |= Py_TPFLAGS_HAVE_GC;
2828
type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) {
29-
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
30-
Py_VISIT(self.value.ptr());
29+
// https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_traverse
30+
#if PY_VERSION_HEX >= 0x03090000
31+
Py_VISIT(Py_TYPE(self_base));
32+
#endif
33+
if (py::detail::is_holder_constructed(self_base)) {
34+
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
35+
Py_VISIT(self.value.ptr());
36+
}
3137
return 0;
3238
};
3339
type->tp_clear = [](PyObject *self_base) {
34-
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
35-
self.value = py::none();
40+
if (py::detail::is_holder_constructed(self_base)) {
41+
auto &self = py::cast<OwnsPythonObjects &>(py::handle(self_base));
42+
self.value = py::none();
43+
}
3644
return 0;
3745
};
3846
}));

0 commit comments

Comments
 (0)