Skip to content

Commit c5e8fec

Browse files
b-passpre-commit-ci[bot]rwgk
authored
Make function record subinterpreter safe (#5771)
* Make function_record type subinterpreter safe * Get rid of static state in implicit conversion * style: pre-commit fixes * Fix lambda * Bump ABI because we added an internals member * Set __module__ on the type instance to get rid of DepricationWarning * Work around internal compiler error in CUDA by not using typedef hopefully * Make clang-tidy happy * Use the same __module__ as pybind11_static_property * style: pre-commit fixes * Oops, find-replace error * style: pre-commit fixes * Move the once initialization to happen more behind the scenes * Oops, need those casts... * Undo implicit conversion change, will do a separate PR * Use local_internals for function_record pointer to avoid ABI bump * style: pre-commit fixes * Get rid of this auto for readability * Change back to using unqualified tp_name, set __module__ attribute, explicitly add Py_TPFLAGS_HEAPTYPE → does not resolve DeprecationWarning :-( * Revert "Change back to using unqualified tp_name, set __module__ attribute, explicitly add Py_TPFLAGS_HEAPTYPE → does not resolve DeprecationWarning :-(" This reverts commit 9ccd6de. * Add Py_TPFLAGS_HEAPTYPE to be explicit (more readable). * Remove obsolete PYBIND11_WARNING_DISABLE_... * Make tp_plainname_impl, tp_qualname_impl more DRY * Change PYBIND11_INTERNAL_MODULE_NAME → PYBIND11_DUMMY_MODULE_NAME * Add a long comment to explain the tp_qualname_impl workaround. * Rename local_internals::function_record → function_record_py_type --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ralf W. Grosse-Kunstleve <[email protected]> Co-authored-by: Ralf W. Grosse-Kunstleve <[email protected]>
1 parent 0db3d59 commit c5e8fec

File tree

5 files changed

+62
-84
lines changed

5 files changed

+62
-84
lines changed

include/pybind11/detail/class.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ inline PyTypeObject *make_static_property_type() {
9898
pybind11_fail("make_static_property_type(): failure in PyType_Ready()!");
9999
}
100100

101-
setattr((PyObject *) type, "__module__", str("pybind11_builtins"));
101+
setattr((PyObject *) type, "__module__", str(PYBIND11_DUMMY_MODULE_NAME));
102102
PYBIND11_SET_OLDPY_QUALNAME(type, name_obj);
103103

104104
return type;
@@ -282,7 +282,7 @@ inline PyTypeObject *make_default_metaclass() {
282282
pybind11_fail("make_default_metaclass(): failure in PyType_Ready()!");
283283
}
284284

285-
setattr((PyObject *) type, "__module__", str("pybind11_builtins"));
285+
setattr((PyObject *) type, "__module__", str(PYBIND11_DUMMY_MODULE_NAME));
286286
PYBIND11_SET_OLDPY_QUALNAME(type, name_obj);
287287

288288
return type;
@@ -544,7 +544,7 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) {
544544
pybind11_fail("PyType_Ready failed in make_object_base_type(): " + error_string());
545545
}
546546

547-
setattr((PyObject *) type, "__module__", str("pybind11_builtins"));
547+
setattr((PyObject *) type, "__module__", str(PYBIND11_DUMMY_MODULE_NAME));
548548
PYBIND11_SET_OLDPY_QUALNAME(type, name_obj);
549549

550550
assert(!PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));

include/pybind11/detail/function_record_pyobject.h

Lines changed: 56 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -45,96 +45,79 @@ static PyMethodDef tp_methods_impl[]
4545
nullptr},
4646
{nullptr, nullptr, 0, nullptr}};
4747

48+
// Python 3.12+ emits a DeprecationWarning for heap types whose tp_name does
49+
// not contain a dot ('.') and that lack a __module__ attribute. For pybind11's
50+
// internal function_record type, we do not have an actual module object to
51+
// attach, so we cannot use PyType_FromModuleAndSpec (introduced in Python 3.9)
52+
// to set __module__ automatically.
53+
//
54+
// As a workaround, we define a "qualified" type name that includes a dummy
55+
// module name (PYBIND11_DUMMY_MODULE_NAME). This is non‑idiomatic but avoids
56+
// the deprecation warning, and results in reprs like
57+
//
58+
// <class 'pybind11_builtins.pybind11_detail_function_record_...'>
59+
//
60+
// even though no real pybind11_builtins module exists. If pybind11 gains an
61+
// actual module object in the future, this code should switch to
62+
// PyType_FromModuleAndSpec for Python 3.9+ and drop the dummy module
63+
// workaround.
64+
//
4865
// Note that this name is versioned.
49-
constexpr char tp_name_impl[]
50-
= "pybind11_detail_function_record_" PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID
51-
"_" PYBIND11_PLATFORM_ABI_ID;
66+
#define PYBIND11_DETAIL_FUNCTION_RECORD_TP_PLAINNAME \
67+
"pybind11_detail_function_record_" PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID \
68+
"_" PYBIND11_PLATFORM_ABI_ID
69+
constexpr char tp_plainname_impl[] = PYBIND11_DETAIL_FUNCTION_RECORD_TP_PLAINNAME;
70+
constexpr char tp_qualname_impl[]
71+
= PYBIND11_DUMMY_MODULE_NAME "." PYBIND11_DETAIL_FUNCTION_RECORD_TP_PLAINNAME;
5272

5373
PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods)
5474

55-
// Designated initializers are a C++20 feature:
56-
// https://en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers
57-
// MSVC rejects them unless /std:c++20 is used (error code C7555).
58-
PYBIND11_WARNING_PUSH
59-
PYBIND11_WARNING_DISABLE_CLANG("-Wmissing-field-initializers")
60-
#if defined(__GNUC__) && __GNUC__ >= 8
61-
PYBIND11_WARNING_DISABLE_GCC("-Wmissing-field-initializers")
62-
#endif
63-
static PyTypeObject function_record_PyTypeObject = {
64-
PyVarObject_HEAD_INIT(nullptr, 0)
65-
/* const char *tp_name */ function_record_PyTypeObject_methods::tp_name_impl,
66-
/* Py_ssize_t tp_basicsize */ sizeof(function_record_PyObject),
67-
/* Py_ssize_t tp_itemsize */ 0,
68-
/* destructor tp_dealloc */ function_record_PyTypeObject_methods::tp_dealloc_impl,
69-
/* Py_ssize_t tp_vectorcall_offset */ 0,
70-
/* getattrfunc tp_getattr */ nullptr,
71-
/* setattrfunc tp_setattr */ nullptr,
72-
/* PyAsyncMethods *tp_as_async */ nullptr,
73-
/* reprfunc tp_repr */ nullptr,
74-
/* PyNumberMethods *tp_as_number */ nullptr,
75-
/* PySequenceMethods *tp_as_sequence */ nullptr,
76-
/* PyMappingMethods *tp_as_mapping */ nullptr,
77-
/* hashfunc tp_hash */ nullptr,
78-
/* ternaryfunc tp_call */ nullptr,
79-
/* reprfunc tp_str */ nullptr,
80-
/* getattrofunc tp_getattro */ nullptr,
81-
/* setattrofunc tp_setattro */ nullptr,
82-
/* PyBufferProcs *tp_as_buffer */ nullptr,
83-
/* unsigned long tp_flags */ Py_TPFLAGS_DEFAULT,
84-
/* const char *tp_doc */ nullptr,
85-
/* traverseproc tp_traverse */ nullptr,
86-
/* inquiry tp_clear */ nullptr,
87-
/* richcmpfunc tp_richcompare */ nullptr,
88-
/* Py_ssize_t tp_weaklistoffset */ 0,
89-
/* getiterfunc tp_iter */ nullptr,
90-
/* iternextfunc tp_iternext */ nullptr,
91-
/* struct PyMethodDef *tp_methods */ function_record_PyTypeObject_methods::tp_methods_impl,
92-
/* struct PyMemberDef *tp_members */ nullptr,
93-
/* struct PyGetSetDef *tp_getset */ nullptr,
94-
/* struct _typeobject *tp_base */ nullptr,
95-
/* PyObject *tp_dict */ nullptr,
96-
/* descrgetfunc tp_descr_get */ nullptr,
97-
/* descrsetfunc tp_descr_set */ nullptr,
98-
/* Py_ssize_t tp_dictoffset */ 0,
99-
/* initproc tp_init */ function_record_PyTypeObject_methods::tp_init_impl,
100-
/* allocfunc tp_alloc */ function_record_PyTypeObject_methods::tp_alloc_impl,
101-
/* newfunc tp_new */ function_record_PyTypeObject_methods::tp_new_impl,
102-
/* freefunc tp_free */ function_record_PyTypeObject_methods::tp_free_impl,
103-
/* inquiry tp_is_gc */ nullptr,
104-
/* PyObject *tp_bases */ nullptr,
105-
/* PyObject *tp_mro */ nullptr,
106-
/* PyObject *tp_cache */ nullptr,
107-
/* PyObject *tp_subclasses */ nullptr,
108-
/* PyObject *tp_weaklist */ nullptr,
109-
/* destructor tp_del */ nullptr,
110-
/* unsigned int tp_version_tag */ 0,
111-
/* destructor tp_finalize */ nullptr,
112-
/* vectorcallfunc tp_vectorcall */ nullptr,
113-
};
114-
PYBIND11_WARNING_POP
115-
116-
static bool function_record_PyTypeObject_PyType_Ready_first_call = true;
117-
118-
inline void function_record_PyTypeObject_PyType_Ready() {
119-
if (function_record_PyTypeObject_PyType_Ready_first_call) {
120-
if (PyType_Ready(&function_record_PyTypeObject) < 0) {
75+
static PyType_Slot function_record_PyType_Slots[] = {
76+
{Py_tp_dealloc,
77+
reinterpret_cast<void *>(function_record_PyTypeObject_methods::tp_dealloc_impl)},
78+
{Py_tp_methods,
79+
reinterpret_cast<void *>(function_record_PyTypeObject_methods::tp_methods_impl)},
80+
{Py_tp_init, reinterpret_cast<void *>(function_record_PyTypeObject_methods::tp_init_impl)},
81+
{Py_tp_alloc, reinterpret_cast<void *>(function_record_PyTypeObject_methods::tp_alloc_impl)},
82+
{Py_tp_new, reinterpret_cast<void *>(function_record_PyTypeObject_methods::tp_new_impl)},
83+
{Py_tp_free, reinterpret_cast<void *>(function_record_PyTypeObject_methods::tp_free_impl)},
84+
{0, nullptr}};
85+
86+
static PyType_Spec function_record_PyType_Spec
87+
= {function_record_PyTypeObject_methods::tp_qualname_impl,
88+
sizeof(function_record_PyObject),
89+
0,
90+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HEAPTYPE,
91+
function_record_PyType_Slots};
92+
93+
inline PyTypeObject *get_function_record_PyTypeObject() {
94+
PyTypeObject *&py_type_obj = detail::get_local_internals().function_record_py_type;
95+
if (!py_type_obj) {
96+
PyObject *py_obj = PyType_FromSpec(&function_record_PyType_Spec);
97+
if (py_obj == nullptr) {
12198
throw error_already_set();
12299
}
123-
function_record_PyTypeObject_PyType_Ready_first_call = false;
100+
py_type_obj = reinterpret_cast<PyTypeObject *>(py_obj);
124101
}
102+
return py_type_obj;
125103
}
126104

127105
inline bool is_function_record_PyObject(PyObject *obj) {
128106
if (PyType_Check(obj) != 0) {
129107
return false;
130108
}
131109
PyTypeObject *obj_type = Py_TYPE(obj);
110+
111+
PyTypeObject *frtype = get_function_record_PyTypeObject();
112+
132113
// Fast path (pointer comparison).
133-
if (obj_type == &function_record_PyTypeObject) {
114+
if (obj_type == frtype) {
134115
return true;
135116
}
136117
// This works across extension modules. Note that tp_name is versioned.
137-
if (strcmp(obj_type->tp_name, function_record_PyTypeObject.tp_name) == 0) {
118+
if (strcmp(obj_type->tp_name, function_record_PyTypeObject_methods::tp_qualname_impl) == 0
119+
|| strcmp(obj_type->tp_name, function_record_PyTypeObject_methods::tp_plainname_impl)
120+
== 0) {
138121
return true;
139122
}
140123
return false;
@@ -148,7 +131,7 @@ inline function_record *function_record_ptr_from_PyObject(PyObject *obj) {
148131
}
149132

150133
inline object function_record_PyObject_New() {
151-
auto *py_func_rec = PyObject_New(function_record_PyObject, &function_record_PyTypeObject);
134+
auto *py_func_rec = PyObject_New(function_record_PyObject, get_function_record_PyTypeObject());
152135
if (py_func_rec == nullptr) {
153136
throw error_already_set();
154137
}

include/pybind11/detail/internals.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ class thread_specific_storage {
132132

133133
PYBIND11_NAMESPACE_BEGIN(detail)
134134

135-
constexpr const char *internals_function_record_capsule_name = "pybind11_function_record_capsule";
135+
// This does NOT actually exist as a module.
136+
#define PYBIND11_DUMMY_MODULE_NAME "pybind11_builtins"
136137

137138
// Forward declarations
138139
inline PyTypeObject *make_static_property_type();
@@ -298,6 +299,7 @@ struct internals {
298299
struct local_internals {
299300
type_map<type_info *> registered_types_cpp;
300301
std::forward_list<ExceptionTranslator> registered_exception_translators;
302+
PyTypeObject *function_record_py_type = nullptr;
301303
};
302304

303305
enum class holder_enum_t : uint8_t {

include/pybind11/pybind11.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,6 @@ class cpp_function : public function {
631631
= reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)()>(dispatcher));
632632
rec->def->ml_flags = METH_VARARGS | METH_KEYWORDS;
633633

634-
detail::function_record_PyTypeObject_PyType_Ready(); // Call-once initialization.
635634
object py_func_rec = detail::function_record_PyObject_New();
636635
((detail::function_record_PyObject *) py_func_rec.ptr())->cpp_func_rec
637636
= unique_rec.release();

tests/test_callbacks.cpp

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,7 @@ TEST_SUBMODULE(callbacks, m) {
286286
return &def;
287287
}();
288288

289-
// rec_capsule with name that has the same value (but not pointer) as our internal one
290-
// This capsule should be detected by our code as foreign and not inspected as the pointers
291-
// shouldn't match
292-
constexpr const char *rec_capsule_name
293-
= pybind11::detail::internals_function_record_capsule_name;
294289
py::capsule rec_capsule(std::malloc(1), [](void *data) { std::free(data); });
295-
rec_capsule.set_name(rec_capsule_name);
296290
m.add_object("custom_function", PyCFunction_New(custom_def, rec_capsule.ptr()));
297291

298292
// rec_capsule with nullptr name

0 commit comments

Comments
 (0)