From e4b1218e333ec84e189ee39a47d86f184c08cf62 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 7 Feb 2024 21:13:19 -0800 Subject: [PATCH 01/39] Add `test_*_repr()` to test behavior with different Python versions. --- tests/test_pickling.py | 12 ++++++++++++ tests/test_pytypes.py | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/tests/test_pickling.py b/tests/test_pickling.py index 859ac087..cbef450a 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -8,6 +8,18 @@ from pybind11_tests import pickling as m +def test_simple_callable_repr(): + assert repr(m.simple_callable).startswith( + " Date: Wed, 7 Feb 2024 22:45:33 -0800 Subject: [PATCH 02/39] Adjust expected repr for PyPy --- tests/test_pickling.py | 6 ++++-- tests/test_pytypes.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_pickling.py b/tests/test_pickling.py index cbef450a..45c8068f 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -15,9 +15,11 @@ def test_simple_callable_repr(): def test_Pickleable_value_repr(): - assert repr(m.Pickleable("").value).startswith( - " Date: Wed, 7 Feb 2024 23:01:39 -0800 Subject: [PATCH 03/39] Adjust another expected repr for PyPy --- tests/test_pickling.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_pickling.py b/tests/test_pickling.py index 45c8068f..ac78c38c 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -9,9 +9,11 @@ def test_simple_callable_repr(): - assert repr(m.simple_callable).startswith( - " Date: Wed, 7 Feb 2024 23:13:58 -0800 Subject: [PATCH 04/39] Try again: undo mistaken adjustment for PyPy --- tests/test_pytypes.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index b13a291d..220347ac 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -371,9 +371,7 @@ def test_capsule(capture): def test_capsule_with_name_repr(): cap = m.return_capsule_with_name_and_destructor() - c = "" if env.PYPY else "<" - expected = f'{c}capsule object "pointer type description" at 0x' - assert repr(cap).startswith(expected) + assert repr(cap).startswith(' Date: Wed, 7 Feb 2024 23:50:33 -0800 Subject: [PATCH 05/39] Give up on test_pytypes test_capsule_with_name_repr (not sufficiently important); PyPy still generates 2 different kinds of errors: test_print failure on macOS with Python 3.8; Python 3.9, 3.10 have no leading `<` --- tests/test_pytypes.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_pytypes.py b/tests/test_pytypes.py index 220347ac..2b202731 100644 --- a/tests/test_pytypes.py +++ b/tests/test_pytypes.py @@ -369,11 +369,6 @@ def test_capsule(capture): ) -def test_capsule_with_name_repr(): - cap = m.return_capsule_with_name_and_destructor() - assert repr(cap).startswith(' Date: Thu, 8 Feb 2024 18:53:47 -0800 Subject: [PATCH 06/39] `_wrapped_simple_callable` proof of concept --- tests/test_pickling.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/tests/test_pickling.py b/tests/test_pickling.py index ac78c38c..03267f36 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -1,41 +1,26 @@ import copy import pickle -import re import pytest import env from pybind11_tests import pickling as m +_orig_simple_callable = m.simple_callable -def test_simple_callable_repr(): - if env.PYPY: - expected = " Date: Fri, 9 Feb 2024 04:52:46 -0800 Subject: [PATCH 07/39] Add `module_::def_as_native()` --- include/pybind11/pybind11.h | 39 +++++++++++++++++++++++++++++++++++++ tests/test_pickling.cpp | 2 +- tests/test_pickling.py | 16 +++++++-------- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 169821f4..8a7a6be1 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1232,6 +1232,21 @@ extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *, return make_new_instance(type); } +inline object wrap_cpp_function_in_native_function(const char *name, object cpp_func) { + std::string py_code("def "); + py_code += name; + py_code += "(*args, **kwargs):\n"; + py_code += " return _cpp_func(*args, **kwargs)\n"; + dict globals; + globals["_cpp_func"] = cpp_func; + PyObject *run_result = PyRun_String(py_code.c_str(), Py_file_input, globals.ptr(), nullptr); + if (run_result == nullptr) { + throw error_already_set(); + } + Py_DECREF(run_result); + return globals[name]; +} + PYBIND11_NAMESPACE_END(detail) /// Wrapper for Python extension modules @@ -1264,6 +1279,30 @@ class module_ : public object { return *this; } + template + module_ &def_as_native(const char *name_, Func &&f, const Extra &...extra) { + if (!hasattr(*this, "_pybind11_cpp_function_registry")) { + add_object("_pybind11_cpp_function_registry", + module_::import("weakref").attr("WeakValueDictionary")()); + } + auto reg = getattr(*this, "_pybind11_cpp_function_registry"); + if (reg.contains(name_)) { + add_object(name_, reg[name_], true /* overwrite */); + } + cpp_function func(std::forward(f), + name(name_), + scope(*this), + sibling(getattr(*this, name_, none())), + extra...); + reg[name_] = func; + object wrapped_func = detail::wrap_cpp_function_in_native_function(name_, func); + if (hasattr(func, "__doc__")) { + wrapped_func.attr("__doc__") = getattr(func, "__doc__"); + } + add_object(name_, wrapped_func, true /* overwrite */); + return *this; + } + /** \rst Create and return a new Python submodule with the given name and docstring. This also works recursively, i.e. diff --git a/tests/test_pickling.cpp b/tests/test_pickling.cpp index fae2c115..a87e4199 100644 --- a/tests/test_pickling.cpp +++ b/tests/test_pickling.cpp @@ -111,7 +111,7 @@ void wrap(py::module m) { } // namespace exercise_getinitargs_getstate_setstate TEST_SUBMODULE(pickling, m) { - m.def("simple_callable", []() { return 20220426; }); + m.def_as_native("simple_callable", []() { return 20220426; }); // test_roundtrip class Pickleable { diff --git a/tests/test_pickling.py b/tests/test_pickling.py index 03267f36..c3455897 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -6,19 +6,17 @@ import env from pybind11_tests import pickling as m -_orig_simple_callable = m.simple_callable +def test_assumptions(): + assert pickle.HIGHEST_PROTOCOL >= 0 -def _wrapped_simple_callable(*args, **kwargs): - return _orig_simple_callable(*args, **kwargs) - -m.simple_callable = _wrapped_simple_callable - - -def test_pickle_simple_callable(): +@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1)) +def test_pickle_simple_callable(protocol): assert m.simple_callable() == 20220426 - serialized = pickle.dumps(m.simple_callable) + serialized = pickle.dumps(m.simple_callable, protocol=protocol) + assert b"pybind11_tests.pickling" in serialized + assert b"simple_callable" in serialized deserialized = pickle.loads(serialized) assert deserialized() == 20220426 From 424ace30583b7673c61515796f52d34cb45646f3 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 9 Feb 2024 11:31:17 -0800 Subject: [PATCH 08/39] Resolve PyPy `TypeError: cannot create weak reference to builtin_function_or_method object` --- include/pybind11/pybind11.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 8a7a6be1..88bdcac8 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1282,8 +1282,15 @@ class module_ : public object { template module_ &def_as_native(const char *name_, Func &&f, const Extra &...extra) { if (!hasattr(*this, "_pybind11_cpp_function_registry")) { +#if defined(PYPY_VERSION) + // Just to make the tests pass. This leaks (probably a very minor leak though). + // The error was: + // TypeError: cannot create weak reference to 'builtin_function_or_method' object + add_object("_pybind11_cpp_function_registry", dict()); +#else add_object("_pybind11_cpp_function_registry", module_::import("weakref").attr("WeakValueDictionary")()); +#endif } auto reg = getattr(*this, "_pybind11_cpp_function_registry"); if (reg.contains(name_)) { From f9fd9d0b52e1fe010e912dbaf6d3d4c10022520f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 10 Feb 2024 09:27:16 -0800 Subject: [PATCH 09/39] Replace `PyCapsule` with `function_record_PyObject`. --- include/pybind11/functional.h | 11 +-- include/pybind11/pybind11.h | 158 +++++++++++++++++++++++++++------- 2 files changed, 127 insertions(+), 42 deletions(-) diff --git a/include/pybind11/functional.h b/include/pybind11/functional.h index f5bc3f35..01c3ea8f 100644 --- a/include/pybind11/functional.h +++ b/include/pybind11/functional.h @@ -96,15 +96,8 @@ struct type_caster> { auto *cfunc_self = PyCFunction_GET_SELF(cfunc.ptr()); if (cfunc_self == nullptr) { PyErr_Clear(); - } else if (isinstance(cfunc_self)) { - auto c = reinterpret_borrow(cfunc_self); - - function_record *rec = nullptr; - // Check that we can safely reinterpret the capsule into a function_record - if (detail::is_function_record_capsule(c)) { - rec = c.get_pointer(); - } - + } else { + function_record *rec = function_record_ptr_from_PyObject(cfunc_self); while (rec != nullptr) { if (rec->is_stateless && same_type(typeid(function_type), diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 88bdcac8..38505476 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -121,6 +121,78 @@ inline bool apply_exception_translators(std::forward_list & # define PYBIND11_COMPAT_STRDUP strdup #endif +struct function_record_PyObject { + PyObject_HEAD + function_record *cpp_func_rec; +}; + +namespace function_record_PyTypeObject_methods { + +PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds); +PyObject *tp_alloc_impl(PyTypeObject *type, Py_ssize_t nitems); +int tp_init_impl(PyObject *self, PyObject *args, PyObject *kwds); +void tp_dealloc_impl(PyObject *self); +void tp_free_impl(void *self); + +} // namespace function_record_PyTypeObject_methods + +PYBIND11_WARNING_PUSH +PYBIND11_WARNING_DISABLE_GCC("-Wmissing-field-initializers") +static PyTypeObject function_record_PyTypeObject = { + .ob_base = PyVarObject_HEAD_INIT(nullptr, 0).tp_name = "pybind11_detail_function_record", + .tp_basicsize = sizeof(function_record_PyObject), + .tp_itemsize = 0, + .tp_dealloc = function_record_PyTypeObject_methods::tp_dealloc_impl, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR("pybind11::detail::function_record"), + .tp_init = function_record_PyTypeObject_methods::tp_init_impl, + .tp_alloc = function_record_PyTypeObject_methods::tp_alloc_impl, + .tp_new = function_record_PyTypeObject_methods::tp_new_impl, + .tp_free = function_record_PyTypeObject_methods::tp_free_impl, +}; +PYBIND11_WARNING_POP + +inline bool is_function_record_PyObject(PyObject *obj) { + if (PyType_Check(obj) != 0) { + return false; + } + PyTypeObject *obj_type = Py_TYPE(obj); + if (obj_type == &function_record_PyTypeObject) { + return true; + } + // TODO: MOVE function_record_PyTypeObject to internals. + if (strcmp(obj_type->tp_name, function_record_PyTypeObject.tp_name) == 0) { + return true; + } + return false; +} + +inline function_record *function_record_ptr_from_PyObject(PyObject *obj) { + if (is_function_record_PyObject(obj)) { + return ((detail::function_record_PyObject *) obj)->cpp_func_rec; + } + return nullptr; +} + +inline object function_record_PyObject_New() { + { + static std::once_flag pytype_ready_called_flag; + gil_scoped_release gil_rel; + std::call_once(pytype_ready_called_flag, [] { + gil_scoped_acquire gil_acq; + if (PyType_Ready(&function_record_PyTypeObject) < 0) { + throw error_already_set(); + } + }); + } + auto *py_func_rec = PyObject_New(function_record_PyObject, &function_record_PyTypeObject); + if (py_func_rec == nullptr) { + throw error_already_set(); + } + py_func_rec->cpp_func_rec = nullptr; // For clarity/purity. Redundant in practice. + return reinterpret_steal((PyObject *) py_func_rec); +} + PYBIND11_NAMESPACE_END(detail) /// Wraps an arbitrary C++ function/method/lambda function/.. into a callable Python object @@ -528,20 +600,11 @@ class cpp_function : public function { if (rec->sibling) { if (PyCFunction_Check(rec->sibling.ptr())) { auto *self = PyCFunction_GET_SELF(rec->sibling.ptr()); - if (!isinstance(self)) { + chain = detail::function_record_ptr_from_PyObject(self); + if (chain && !chain->scope.is(rec->scope)) { + /* Never append a method to an overload chain of a parent class; + instead, hide the parent's overloads in this case */ chain = nullptr; - } else { - auto rec_capsule = reinterpret_borrow(self); - if (detail::is_function_record_capsule(rec_capsule)) { - chain = rec_capsule.get_pointer(); - /* Never append a method to an overload chain of a parent class; - instead, hide the parent's overloads in this case */ - if (!chain->scope.is(rec->scope)) { - chain = nullptr; - } - } else { - chain = nullptr; - } } } // Don't trigger for things like the default __init__, which are wrapper_descriptors @@ -561,9 +624,9 @@ class cpp_function : public function { = reinterpret_cast(reinterpret_cast(dispatcher)); rec->def->ml_flags = METH_VARARGS | METH_KEYWORDS; - capsule rec_capsule(unique_rec.release(), - detail::get_function_record_capsule_name(), - [](void *ptr) { destruct((detail::function_record *) ptr); }); + object py_func_rec = detail::function_record_PyObject_New(); + ((detail::function_record_PyObject *) py_func_rec.ptr())->cpp_func_rec + = unique_rec.release(); guarded_strdup.release(); object scope_module; @@ -575,7 +638,7 @@ class cpp_function : public function { } } - m_ptr = PyCFunction_NewEx(rec->def, rec_capsule.ptr(), scope_module.ptr()); + m_ptr = PyCFunction_NewEx(rec->def, py_func_rec.ptr(), scope_module.ptr()); if (!m_ptr) { pybind11_fail("cpp_function::cpp_function(): Could not allocate function object"); } @@ -604,9 +667,9 @@ class cpp_function : public function { // chain. chain_start = rec; rec->next = chain; - auto rec_capsule - = reinterpret_borrow(((PyCFunctionObject *) m_ptr)->m_self); - rec_capsule.set_pointer(unique_rec.release()); + auto py_func_rec + = (detail::function_record_PyObject *) PyCFunction_GET_SELF(m_ptr); + py_func_rec->cpp_func_rec = unique_rec.release(); guarded_strdup.release(); } else { // Or end of chain (normal behavior) @@ -680,6 +743,8 @@ class cpp_function : public function { } } + friend void detail::function_record_PyTypeObject_methods::tp_dealloc_impl(PyObject *); + /// When a cpp_function is GCed, release any memory allocated by pybind11 static void destruct(detail::function_record *rec, bool free_strings = true) { // If on Python 3.9, check the interpreter "MICRO" (patch) version. @@ -729,13 +794,11 @@ class cpp_function : public function { /// Main dispatch logic for calls to functions bound using pybind11 static PyObject *dispatcher(PyObject *self, PyObject *args_in, PyObject *kwargs_in) { using namespace detail; - assert(isinstance(self)); + const function_record *overloads = function_record_ptr_from_PyObject(self); + assert(overloads != nullptr); /* Iterator over the list of potentially admissible overloads */ - const function_record *overloads = reinterpret_cast( - PyCapsule_GetPointer(self, get_function_record_capsule_name())), - *current_overload = overloads; - assert(overloads != nullptr); + const function_record *current_overload = overloads; /* Need to know how many arguments + keyword arguments there are to pick the right overload */ @@ -1209,6 +1272,42 @@ class cpp_function : public function { PYBIND11_NAMESPACE_BEGIN(detail) +namespace function_record_PyTypeObject_methods { + +inline PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds) { + // Create a new instance using the type's tp_alloc slot. + if (type) + pybind11_fail("INTENTIONAL BREAKAGE: tp_new_impl"); + return PyType_GenericNew(type, args, kwds); +} + +inline PyObject *tp_alloc_impl(PyTypeObject *type, Py_ssize_t nitems) { + // Use Python's default memory allocation mechanism to allocate a new instance + // and initialize all its contents to NULL. + if (type) + pybind11_fail("INTENTIONAL BREAKAGE: tp_alloc_impl"); + return PyType_GenericAlloc(type, nitems); +} + +inline int tp_init_impl(PyObject *self, PyObject *, PyObject *) { + if (self) + pybind11_fail("INTENTIONAL BREAKAGE: tp_init_impl"); + return -1; +} + +inline void tp_dealloc_impl(PyObject *self) { + auto py_func_rec = (function_record_PyObject *) self; + cpp_function::destruct(py_func_rec->cpp_func_rec); + py_func_rec->cpp_func_rec = nullptr; +} + +inline void tp_free_impl(void *self) { + if (self) + pybind11_fail("INTENTIONAL BREAKAGE: tp_free_impl"); +} + +} // namespace function_record_PyTypeObject_methods + /// Instance creation function for all pybind11 types. It only allocates space for the /// C++ object, but doesn't call the constructor -- an `__init__` function must do that. extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *, PyObject *) { @@ -2334,14 +2433,7 @@ class class_ : public detail::generic_type { if (!func_self) { throw error_already_set(); } - if (!isinstance(func_self)) { - return nullptr; - } - auto cap = reinterpret_borrow(func_self); - if (!detail::is_function_record_capsule(cap)) { - return nullptr; - } - return cap.get_pointer(); + return detail::function_record_ptr_from_PyObject(func_self.ptr()); } }; From 0723dda5229b396886c843b5d6dbea001c80b644 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 00:56:35 -0800 Subject: [PATCH 10/39] function_record_PyTypeObject: Replace C++20 designated initializers with full list of values. --- include/pybind11/pybind11.h | 65 +++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 38505476..df08ff03 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -136,21 +136,60 @@ void tp_free_impl(void *self); } // namespace function_record_PyTypeObject_methods -PYBIND11_WARNING_PUSH -PYBIND11_WARNING_DISABLE_GCC("-Wmissing-field-initializers") +// Designated initializers are a C++20 feature: +// https://en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers +// MSVC rejects them unless /std:c++20 is used (error code C7555). static PyTypeObject function_record_PyTypeObject = { - .ob_base = PyVarObject_HEAD_INIT(nullptr, 0).tp_name = "pybind11_detail_function_record", - .tp_basicsize = sizeof(function_record_PyObject), - .tp_itemsize = 0, - .tp_dealloc = function_record_PyTypeObject_methods::tp_dealloc_impl, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("pybind11::detail::function_record"), - .tp_init = function_record_PyTypeObject_methods::tp_init_impl, - .tp_alloc = function_record_PyTypeObject_methods::tp_alloc_impl, - .tp_new = function_record_PyTypeObject_methods::tp_new_impl, - .tp_free = function_record_PyTypeObject_methods::tp_free_impl, + PyVarObject_HEAD_INIT(nullptr, 0) + /* const char *tp_name */ "pybind11_detail_function_record", + /* Py_ssize_t tp_basicsize */ sizeof(function_record_PyObject), + /* Py_ssize_t tp_itemsize */ 0, + /* destructor tp_dealloc */ function_record_PyTypeObject_methods::tp_dealloc_impl, + /* Py_ssize_t tp_vectorcall_offset */ 0, + /* getattrfunc tp_getattr */ nullptr, + /* setattrfunc tp_setattr */ nullptr, + /* PyAsyncMethods *tp_as_async */ nullptr, + /* reprfunc tp_repr */ nullptr, + /* PyNumberMethods *tp_as_number */ nullptr, + /* PySequenceMethods *tp_as_sequence */ nullptr, + /* PyMappingMethods *tp_as_mapping */ nullptr, + /* hashfunc tp_hash */ nullptr, + /* ternaryfunc tp_call */ nullptr, + /* reprfunc tp_str */ nullptr, + /* getattrofunc tp_getattro */ nullptr, + /* setattrofunc tp_setattro */ nullptr, + /* PyBufferProcs *tp_as_buffer */ nullptr, + /* unsigned long tp_flags */ Py_TPFLAGS_DEFAULT, + /* const char *tp_doc */ "pybind11::detail::function_record", + /* traverseproc tp_traverse */ nullptr, + /* inquiry tp_clear */ nullptr, + /* richcmpfunc tp_richcompare */ nullptr, + /* Py_ssize_t tp_weaklistoffset */ 0, + /* getiterfunc tp_iter */ nullptr, + /* iternextfunc tp_iternext */ nullptr, + /* struct PyMethodDef *tp_methods */ nullptr, + /* struct PyMemberDef *tp_members */ nullptr, + /* struct PyGetSetDef *tp_getset */ nullptr, + /* struct _typeobject *tp_base */ nullptr, + /* PyObject *tp_dict */ nullptr, + /* descrgetfunc tp_descr_get */ nullptr, + /* descrsetfunc tp_descr_set */ nullptr, + /* Py_ssize_t tp_dictoffset */ 0, + /* initproc tp_init */ function_record_PyTypeObject_methods::tp_init_impl, + /* allocfunc tp_alloc */ function_record_PyTypeObject_methods::tp_alloc_impl, + /* newfunc tp_new */ function_record_PyTypeObject_methods::tp_new_impl, + /* freefunc tp_free */ function_record_PyTypeObject_methods::tp_free_impl, + /* inquiry tp_is_gc */ nullptr, + /* PyObject *tp_bases */ nullptr, + /* PyObject *tp_mro */ nullptr, + /* PyObject *tp_cache */ nullptr, + /* PyObject *tp_subclasses */ nullptr, + /* PyObject *tp_weaklist */ nullptr, + /* destructor tp_del */ nullptr, + /* unsigned int tp_version_tag */ 0, + /* destructor tp_finalize */ nullptr, + /* vectorcallfunc tp_vectorcall */ nullptr, }; -PYBIND11_WARNING_POP inline bool is_function_record_PyObject(PyObject *obj) { if (PyType_Check(obj) != 0) { From 694ebbde546a906dfc3754d343f47a898ddad175 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 01:25:18 -0800 Subject: [PATCH 11/39] Introduce `PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID` and use along with `PYBIND11_PLATFORM_ABI_ID_V4` to version `function_record_PyTypeObject` `tp_name` --- include/pybind11/attr.h | 1 + include/pybind11/pybind11.h | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index 0f38d26b..7d4a2216 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -201,6 +201,7 @@ struct argument_record { /// Internal data structure which holds metadata about a bound function (signature, overloads, /// etc.) +#define PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID "v1" // PLEASE UPDATE if the struct is changed. struct function_record { function_record() : is_constructor(false), is_new_style_constructor(false), is_stateless(false), diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index df08ff03..03cbb277 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -141,7 +141,9 @@ void tp_free_impl(void *self); // MSVC rejects them unless /std:c++20 is used (error code C7555). static PyTypeObject function_record_PyTypeObject = { PyVarObject_HEAD_INIT(nullptr, 0) - /* const char *tp_name */ "pybind11_detail_function_record", + /* const char *tp_name */ "pybind11_detail_function_" + "record_" PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID + "_" PYBIND11_PLATFORM_ABI_ID_V4, /* Py_ssize_t tp_basicsize */ sizeof(function_record_PyObject), /* Py_ssize_t tp_itemsize */ 0, /* destructor tp_dealloc */ function_record_PyTypeObject_methods::tp_dealloc_impl, @@ -160,7 +162,7 @@ static PyTypeObject function_record_PyTypeObject = { /* setattrofunc tp_setattro */ nullptr, /* PyBufferProcs *tp_as_buffer */ nullptr, /* unsigned long tp_flags */ Py_TPFLAGS_DEFAULT, - /* const char *tp_doc */ "pybind11::detail::function_record", + /* const char *tp_doc */ nullptr, /* traverseproc tp_traverse */ nullptr, /* inquiry tp_clear */ nullptr, /* richcmpfunc tp_richcompare */ nullptr, @@ -199,7 +201,6 @@ inline bool is_function_record_PyObject(PyObject *obj) { if (obj_type == &function_record_PyTypeObject) { return true; } - // TODO: MOVE function_record_PyTypeObject to internals. if (strcmp(obj_type->tp_name, function_record_PyTypeObject.tp_name) == 0) { return true; } From 16c19d4d3043ca99eeaac1392d673481a6dd02ec Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 01:49:38 -0800 Subject: [PATCH 12/39] Move `std::once_flag` out of `inline` function (in hopes that that fixes flaky behavior of test_gil_scoped.py). IncludeCleaner fixes. --- include/pybind11/pybind11.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 03cbb277..c9cdc26b 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -20,9 +20,11 @@ #include "options.h" #include "typing.h" +#include #include #include #include +#include #include #include #include @@ -214,11 +216,12 @@ inline function_record *function_record_ptr_from_PyObject(PyObject *obj) { return nullptr; } +static std::once_flag function_record_PyTypeObject_pytype_ready_call_once_flag; + inline object function_record_PyObject_New() { { - static std::once_flag pytype_ready_called_flag; gil_scoped_release gil_rel; - std::call_once(pytype_ready_called_flag, [] { + std::call_once(function_record_PyTypeObject_pytype_ready_call_once_flag, [] { gil_scoped_acquire gil_acq; if (PyType_Ready(&function_record_PyTypeObject) < 0) { throw error_already_set(); From ca31ae290a365c67e43fe812ae22393bf8b4d2c2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 02:11:31 -0800 Subject: [PATCH 13/39] `tp_vectorcall` was introduced only with Python 3.8 --- include/pybind11/pybind11.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index c9cdc26b..eb078bdb 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -192,7 +192,9 @@ static PyTypeObject function_record_PyTypeObject = { /* destructor tp_del */ nullptr, /* unsigned int tp_version_tag */ 0, /* destructor tp_finalize */ nullptr, +#if PY_VERSION_HEX >= 0x03080000 /* vectorcallfunc tp_vectorcall */ nullptr, +#endif }; inline bool is_function_record_PyObject(PyObject *obj) { From b5d3bf580fb82bb2f1798388064d3690f5a9b60a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 02:20:11 -0800 Subject: [PATCH 14/39] clang-tidy auto-fixes --- include/pybind11/pybind11.h | 16 ++++++++++------ tests/pybind11_tests.h | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index eb078bdb..eecf52bb 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -712,7 +712,7 @@ class cpp_function : public function { // chain. chain_start = rec; rec->next = chain; - auto py_func_rec + auto *py_func_rec = (detail::function_record_PyObject *) PyCFunction_GET_SELF(m_ptr); py_func_rec->cpp_func_rec = unique_rec.release(); guarded_strdup.release(); @@ -1321,34 +1321,38 @@ namespace function_record_PyTypeObject_methods { inline PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds) { // Create a new instance using the type's tp_alloc slot. - if (type) + if (type) { pybind11_fail("INTENTIONAL BREAKAGE: tp_new_impl"); + } return PyType_GenericNew(type, args, kwds); } inline PyObject *tp_alloc_impl(PyTypeObject *type, Py_ssize_t nitems) { // Use Python's default memory allocation mechanism to allocate a new instance // and initialize all its contents to NULL. - if (type) + if (type) { pybind11_fail("INTENTIONAL BREAKAGE: tp_alloc_impl"); + } return PyType_GenericAlloc(type, nitems); } inline int tp_init_impl(PyObject *self, PyObject *, PyObject *) { - if (self) + if (self) { pybind11_fail("INTENTIONAL BREAKAGE: tp_init_impl"); + } return -1; } inline void tp_dealloc_impl(PyObject *self) { - auto py_func_rec = (function_record_PyObject *) self; + auto *py_func_rec = (function_record_PyObject *) self; cpp_function::destruct(py_func_rec->cpp_func_rec); py_func_rec->cpp_func_rec = nullptr; } inline void tp_free_impl(void *self) { - if (self) + if (self) { pybind11_fail("INTENTIONAL BREAKAGE: tp_free_impl"); + } } } // namespace function_record_PyTypeObject_methods diff --git a/tests/pybind11_tests.h b/tests/pybind11_tests.h index 3924cf05..7be58feb 100644 --- a/tests/pybind11_tests.h +++ b/tests/pybind11_tests.h @@ -59,7 +59,7 @@ class UnusualOpRef { using NonTrivialType = std::shared_ptr; // Almost any non-trivial type will do. // Overriding operator& should not break pybind11. NonTrivialType operator&() { return non_trivial_member; } - const NonTrivialType operator&() const { return non_trivial_member; } + NonTrivialType operator&() const { return non_trivial_member; } private: NonTrivialType non_trivial_member; From 9e263c5a3a89f5d6e8457798057f990cec849d2e Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 03:12:06 -0800 Subject: [PATCH 15/39] Disable `-Wmissing-field-initializers`. Guard `PyType_Ready(&function_record_PyTypeObject)` also with a simple `static bool first_call` --- include/pybind11/pybind11.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index eecf52bb..a8f46625 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -141,6 +141,9 @@ void tp_free_impl(void *self); // Designated initializers are a C++20 feature: // https://en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers // MSVC rejects them unless /std:c++20 is used (error code C7555). +PYBIND11_WARNING_PUSH +PYBIND11_WARNING_DISABLE_CLANG("-Wmissing-field-initializers") +PYBIND11_WARNING_DISABLE_GCC("-Wmissing-field-initializers") static PyTypeObject function_record_PyTypeObject = { PyVarObject_HEAD_INIT(nullptr, 0) /* const char *tp_name */ "pybind11_detail_function_" @@ -196,6 +199,7 @@ static PyTypeObject function_record_PyTypeObject = { /* vectorcallfunc tp_vectorcall */ nullptr, #endif }; +PYBIND11_WARNING_POP inline bool is_function_record_PyObject(PyObject *obj) { if (PyType_Check(obj) != 0) { @@ -221,7 +225,8 @@ inline function_record *function_record_ptr_from_PyObject(PyObject *obj) { static std::once_flag function_record_PyTypeObject_pytype_ready_call_once_flag; inline object function_record_PyObject_New() { - { + static bool first_call = true; + if (first_call) { gil_scoped_release gil_rel; std::call_once(function_record_PyTypeObject_pytype_ready_call_once_flag, [] { gil_scoped_acquire gil_acq; @@ -229,6 +234,7 @@ inline object function_record_PyObject_New() { throw error_already_set(); } }); + first_call = false; } auto *py_func_rec = PyObject_New(function_record_PyObject, &function_record_PyTypeObject); if (py_func_rec == nullptr) { From 6b45525b89e14bb46ef8efaf362fff444399ea31 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 09:33:41 -0800 Subject: [PATCH 16/39] Give up on the `std::call_once` idea, for Python 3.6 compatibility (it works with all other Python versions). Instead call `function_record_PyTypeObject_PyType_Ready()` from `get_internals()`. --- include/pybind11/detail/internals.h | 5 +++++ include/pybind11/pybind11.h | 20 ++++++-------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index ab2d3f13..49a683e3 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -329,6 +329,8 @@ inline internals **get_internals_pp_from_capsule(handle obj) { return static_cast(raw_ptr); } +inline void function_record_PyTypeObject_PyType_Ready(); + /// Return a reference to the current `internals` data PYBIND11_NOINLINE internals &get_internals() { auto **&internals_pp = get_internals_pp(); @@ -383,6 +385,9 @@ PYBIND11_NOINLINE internals &get_internals() { internals_ptr->static_property_type = make_static_property_type(); internals_ptr->default_metaclass = make_default_metaclass(); internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass); + + // This is not directly related to the `internals`, but also needs to be called only once. + function_record_PyTypeObject_PyType_Ready(); } return **internals_pp; } diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index a8f46625..3c2ffe1e 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -201,6 +200,12 @@ static PyTypeObject function_record_PyTypeObject = { }; PYBIND11_WARNING_POP +inline void function_record_PyTypeObject_PyType_Ready() { + if (PyType_Ready(&function_record_PyTypeObject) < 0) { + throw error_already_set(); + } +} + inline bool is_function_record_PyObject(PyObject *obj) { if (PyType_Check(obj) != 0) { return false; @@ -222,20 +227,7 @@ inline function_record *function_record_ptr_from_PyObject(PyObject *obj) { return nullptr; } -static std::once_flag function_record_PyTypeObject_pytype_ready_call_once_flag; - inline object function_record_PyObject_New() { - static bool first_call = true; - if (first_call) { - gil_scoped_release gil_rel; - std::call_once(function_record_PyTypeObject_pytype_ready_call_once_flag, [] { - gil_scoped_acquire gil_acq; - if (PyType_Ready(&function_record_PyTypeObject) < 0) { - throw error_already_set(); - } - }); - first_call = false; - } auto *py_func_rec = PyObject_New(function_record_PyObject, &function_record_PyTypeObject); if (py_func_rec == nullptr) { throw error_already_set(); From 2c876d617941074b1395586d523126a780b3760b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 14:13:12 -0800 Subject: [PATCH 17/39] Add `__reduce_ex__` to `function_record_PyTypeObject`. Add `_pybind11_detail_function_record_import_helper` (proof of concept). --- include/pybind11/pybind11.h | 94 ++++++++++++++++++------------------- tests/test_pickling.cpp | 2 +- tests/test_pickling.py | 19 ++++++++ 3 files changed, 67 insertions(+), 48 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 3c2ffe1e..a35e0584 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -135,6 +135,15 @@ int tp_init_impl(PyObject *self, PyObject *args, PyObject *kwds); void tp_dealloc_impl(PyObject *self); void tp_free_impl(void *self); +static PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *); + +PYBIND11_WARNING_PUSH +PYBIND11_WARNING_DISABLE_GCC("-Wcast-function-type") +static PyMethodDef MethodsStaticAlloc[] + = {{"__reduce_ex__", (PyCFunction) reduce_ex_impl, METH_VARARGS | METH_KEYWORDS, nullptr}, + {nullptr, nullptr, 0, nullptr}}; +PYBIND11_WARNING_POP + } // namespace function_record_PyTypeObject_methods // Designated initializers are a C++20 feature: @@ -173,7 +182,7 @@ static PyTypeObject function_record_PyTypeObject = { /* Py_ssize_t tp_weaklistoffset */ 0, /* getiterfunc tp_iter */ nullptr, /* iternextfunc tp_iternext */ nullptr, - /* struct PyMethodDef *tp_methods */ nullptr, + /* struct PyMethodDef *tp_methods */ function_record_PyTypeObject_methods::MethodsStaticAlloc, /* struct PyMemberDef *tp_members */ nullptr, /* struct PyGetSetDef *tp_getset */ nullptr, /* struct _typeobject *tp_base */ nullptr, @@ -672,6 +681,7 @@ class cpp_function : public function { = unique_rec.release(); guarded_strdup.release(); + // TODO 30099: Move to helper function. object scope_module; if (rec->scope) { if (hasattr(rec->scope, "__module__")) { @@ -1378,21 +1388,6 @@ extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *, return make_new_instance(type); } -inline object wrap_cpp_function_in_native_function(const char *name, object cpp_func) { - std::string py_code("def "); - py_code += name; - py_code += "(*args, **kwargs):\n"; - py_code += " return _cpp_func(*args, **kwargs)\n"; - dict globals; - globals["_cpp_func"] = cpp_func; - PyObject *run_result = PyRun_String(py_code.c_str(), Py_file_input, globals.ptr(), nullptr); - if (run_result == nullptr) { - throw error_already_set(); - } - Py_DECREF(run_result); - return globals[name]; -} - PYBIND11_NAMESPACE_END(detail) /// Wrapper for Python extension modules @@ -1425,37 +1420,6 @@ class module_ : public object { return *this; } - template - module_ &def_as_native(const char *name_, Func &&f, const Extra &...extra) { - if (!hasattr(*this, "_pybind11_cpp_function_registry")) { -#if defined(PYPY_VERSION) - // Just to make the tests pass. This leaks (probably a very minor leak though). - // The error was: - // TypeError: cannot create weak reference to 'builtin_function_or_method' object - add_object("_pybind11_cpp_function_registry", dict()); -#else - add_object("_pybind11_cpp_function_registry", - module_::import("weakref").attr("WeakValueDictionary")()); -#endif - } - auto reg = getattr(*this, "_pybind11_cpp_function_registry"); - if (reg.contains(name_)) { - add_object(name_, reg[name_], true /* overwrite */); - } - cpp_function func(std::forward(f), - name(name_), - scope(*this), - sibling(getattr(*this, name_, none())), - extra...); - reg[name_] = func; - object wrapped_func = detail::wrap_cpp_function_in_native_function(name_, func); - if (hasattr(func, "__doc__")) { - wrapped_func.attr("__doc__") = getattr(func, "__doc__"); - } - add_object(name_, wrapped_func, true /* overwrite */); - return *this; - } - /** \rst Create and return a new Python submodule with the given name and docstring. This also works recursively, i.e. @@ -1558,6 +1522,42 @@ class module_ : public object { } }; +PYBIND11_NAMESPACE_BEGIN(detail) + +namespace function_record_PyTypeObject_methods { + +inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { + // Deliberately ignoring the arguments for simplicity (expected is `protocol: int`). + const function_record *rec = function_record_ptr_from_PyObject(self); + if (rec == nullptr) { + pybind11_fail( + "FATAL: function_record_PyTypeObject reduce_ex_impl(): cannot obtain cpp_func_rec."); + } + if (rec->name != nullptr && rec->name[0] != '\0' && rec->scope) { + // TODO 30099: Move to helper function. + object scope_module; + if (hasattr(rec->scope, "__module__")) { + scope_module = rec->scope.attr("__module__"); + } else if (hasattr(rec->scope, "__name__")) { + scope_module = rec->scope.attr("__name__"); + } + + if (scope_module) { + return make_tuple(module_::import("importlib") + .attr("_pybind11_detail_function_record_import_helper"), + make_tuple(scope_module, rec->name)) + .release() + .ptr(); + } + } + set_error(PyExc_RuntimeError, repr(self) + str(" is not pickleable.")); + return nullptr; +} + +} // namespace function_record_PyTypeObject_methods + +PYBIND11_NAMESPACE_END(detail) + // When inside a namespace (or anywhere as long as it's not the first item on a line), // C++20 allows "module" to be used. This is provided for backward compatibility, and for // simplicity, if someone wants to use py::module for example, that is perfectly safe. diff --git a/tests/test_pickling.cpp b/tests/test_pickling.cpp index a87e4199..fae2c115 100644 --- a/tests/test_pickling.cpp +++ b/tests/test_pickling.cpp @@ -111,7 +111,7 @@ void wrap(py::module m) { } // namespace exercise_getinitargs_getstate_setstate TEST_SUBMODULE(pickling, m) { - m.def_as_native("simple_callable", []() { return 20220426; }); + m.def("simple_callable", []() { return 20220426; }); // test_roundtrip class Pickleable { diff --git a/tests/test_pickling.py b/tests/test_pickling.py index c3455897..7e315bc7 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -1,4 +1,5 @@ import copy +import importlib import pickle import pytest @@ -7,6 +8,24 @@ from pybind11_tests import pickling as m +class _pybind11_detail_function_record_import_helper: + def __init__(self, fully_qualified_module_name, function_name): + self.fully_qualified_module_name = fully_qualified_module_name + self.function_name = function_name + + def __getattr__(self, name): + assert name == self.function_name + return getattr( + importlib.import_module(self.fully_qualified_module_name), + self.function_name, + ) + + +importlib._pybind11_detail_function_record_import_helper = ( + _pybind11_detail_function_record_import_helper +) + + def test_assumptions(): assert pickle.HIGHEST_PROTOCOL >= 0 From 3a5730ab6cfc7ca951866b43b5fb272657dfeefb Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 14:42:08 -0800 Subject: [PATCH 18/39] Move `function_record_PyTypeObject_PyType_Ready()` call in `get_internals()` so that it is always called when `get_internals()` is called the first time. --- include/pybind11/detail/internals.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 49a683e3..e2bf8d9e 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -385,10 +385,11 @@ PYBIND11_NOINLINE internals &get_internals() { internals_ptr->static_property_type = make_static_property_type(); internals_ptr->default_metaclass = make_default_metaclass(); internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass); - - // This is not directly related to the `internals`, but also needs to be called only once. - function_record_PyTypeObject_PyType_Ready(); } + + // This is not directly related to the `internals`, but also needs to be called only once. + function_record_PyTypeObject_PyType_Ready(); + return **internals_pp; } From fe1b7746ba0b1534362374e551b89cca560376ea Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 14:53:26 -0800 Subject: [PATCH 19/39] gcc 4.8.5 and 7.5.0 reject `PYBIND11_WARNING_DISABLE_GCC("-Wmissing-field-initializers")` --- include/pybind11/pybind11.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index a35e0584..d5c4c20d 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -151,7 +151,9 @@ PYBIND11_WARNING_POP // MSVC rejects them unless /std:c++20 is used (error code C7555). PYBIND11_WARNING_PUSH PYBIND11_WARNING_DISABLE_CLANG("-Wmissing-field-initializers") +#if defined(__GNUC__) && __GNUC__ >= 8 PYBIND11_WARNING_DISABLE_GCC("-Wmissing-field-initializers") +#endif static PyTypeObject function_record_PyTypeObject = { PyVarObject_HEAD_INIT(nullptr, 0) /* const char *tp_name */ "pybind11_detail_function_" From 1679fe0305542695617fde2ebe96f1caa468ebea Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 21:44:07 -0800 Subject: [PATCH 20/39] `function_record_PyTypeObject_PyType_Ready()`, `get_pybind11_detail_function_record_pickle_helper()` call-once initializations triggered from `cpp_function::initialize_generic()` --- include/pybind11/detail/internals.h | 6 -- include/pybind11/pybind11.h | 105 ++++++++++++++++++---------- tests/test_pickling.py | 19 ----- 3 files changed, 67 insertions(+), 63 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index e2bf8d9e..ab2d3f13 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -329,8 +329,6 @@ inline internals **get_internals_pp_from_capsule(handle obj) { return static_cast(raw_ptr); } -inline void function_record_PyTypeObject_PyType_Ready(); - /// Return a reference to the current `internals` data PYBIND11_NOINLINE internals &get_internals() { auto **&internals_pp = get_internals_pp(); @@ -386,10 +384,6 @@ PYBIND11_NOINLINE internals &get_internals() { internals_ptr->default_metaclass = make_default_metaclass(); internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass); } - - // This is not directly related to the `internals`, but also needs to be called only once. - function_record_PyTypeObject_PyType_Ready(); - return **internals_pp; } diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index d5c4c20d..ce62ed61 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -211,9 +211,14 @@ static PyTypeObject function_record_PyTypeObject = { }; PYBIND11_WARNING_POP +static bool function_record_PyTypeObject_PyType_Ready_first_call = true; + inline void function_record_PyTypeObject_PyType_Ready() { - if (PyType_Ready(&function_record_PyTypeObject) < 0) { - throw error_already_set(); + if (function_record_PyTypeObject_PyType_Ready_first_call) { + if (PyType_Ready(&function_record_PyTypeObject) < 0) { + throw error_already_set(); + } + function_record_PyTypeObject_PyType_Ready_first_call = false; } } @@ -247,6 +252,34 @@ inline object function_record_PyObject_New() { return reinterpret_steal((PyObject *) py_func_rec); } +inline object get_pybind11_detail_function_record_pickle_helper(handle rec_scope) { + constexpr char helper_name[] = "_pybind11_detail_function_record_pickle_helper_v1"; + if (hasattr(rec_scope, helper_name)) { + return rec_scope.attr(helper_name); + } + std::string py_code("class "); + py_code += helper_name; + py_code += ":\n"; + py_code += R"( + def __init__(self, function_name): + self.function_name = function_name + + def __getattr__(self, name): + assert name == self.function_name + return globals()[self.function_name] + )"; + // Passing the __dict__ directly does not work in some situations. + dict run_globals(rec_scope.attr("__dict__")); + PyObject *run_result + = PyRun_String(py_code.c_str(), Py_file_input, run_globals.ptr(), nullptr); + if (run_result == nullptr) { + throw error_already_set(); + } + Py_DECREF(run_result); + setattr(rec_scope, helper_name, run_globals[helper_name]); + return rec_scope.attr(helper_name); +} + PYBIND11_NAMESPACE_END(detail) /// Wraps an arbitrary C++ function/method/lambda function/.. into a callable Python object @@ -678,6 +711,7 @@ class cpp_function : public function { = reinterpret_cast(reinterpret_cast(dispatcher)); rec->def->ml_flags = METH_VARARGS | METH_KEYWORDS; + detail::function_record_PyTypeObject_PyType_Ready(); // Call-once initialization. object py_func_rec = detail::function_record_PyObject_New(); ((detail::function_record_PyObject *) py_func_rec.ptr())->cpp_func_rec = unique_rec.release(); @@ -693,6 +727,11 @@ class cpp_function : public function { } } + if (rec->name != nullptr && rec->name[0] != '\0' && scope_module + && hasattr(rec->scope, "__dict__")) { + // Call-once initialization. + detail::get_pybind11_detail_function_record_pickle_helper(rec->scope); + } m_ptr = PyCFunction_NewEx(rec->def, py_func_rec.ptr(), scope_module.ptr()); if (!m_ptr) { pybind11_fail("cpp_function::cpp_function(): Could not allocate function object"); @@ -1365,6 +1404,32 @@ inline void tp_free_impl(void *self) { } } +inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { + // Deliberately ignoring the arguments for simplicity (expected is `protocol: int`). + const function_record *rec = function_record_ptr_from_PyObject(self); + if (rec == nullptr) { + pybind11_fail( + "FATAL: function_record_PyTypeObject reduce_ex_impl(): cannot obtain cpp_func_rec."); + } + if (rec->name != nullptr && rec->name[0] != '\0' && rec->scope) { + // TODO 30099: Move to helper function. + object scope_module; + if (hasattr(rec->scope, "__module__")) { + scope_module = rec->scope.attr("__module__"); + } else if (hasattr(rec->scope, "__name__")) { + scope_module = rec->scope.attr("__name__"); + } + if (scope_module && hasattr(rec->scope, "__dict__")) { + return make_tuple(get_pybind11_detail_function_record_pickle_helper(rec->scope), + make_tuple(rec->name)) + .release() + .ptr(); + } + } + set_error(PyExc_RuntimeError, repr(self) + str(" is not pickleable.")); + return nullptr; +} + } // namespace function_record_PyTypeObject_methods /// Instance creation function for all pybind11 types. It only allocates space for the @@ -1524,42 +1589,6 @@ class module_ : public object { } }; -PYBIND11_NAMESPACE_BEGIN(detail) - -namespace function_record_PyTypeObject_methods { - -inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { - // Deliberately ignoring the arguments for simplicity (expected is `protocol: int`). - const function_record *rec = function_record_ptr_from_PyObject(self); - if (rec == nullptr) { - pybind11_fail( - "FATAL: function_record_PyTypeObject reduce_ex_impl(): cannot obtain cpp_func_rec."); - } - if (rec->name != nullptr && rec->name[0] != '\0' && rec->scope) { - // TODO 30099: Move to helper function. - object scope_module; - if (hasattr(rec->scope, "__module__")) { - scope_module = rec->scope.attr("__module__"); - } else if (hasattr(rec->scope, "__name__")) { - scope_module = rec->scope.attr("__name__"); - } - - if (scope_module) { - return make_tuple(module_::import("importlib") - .attr("_pybind11_detail_function_record_import_helper"), - make_tuple(scope_module, rec->name)) - .release() - .ptr(); - } - } - set_error(PyExc_RuntimeError, repr(self) + str(" is not pickleable.")); - return nullptr; -} - -} // namespace function_record_PyTypeObject_methods - -PYBIND11_NAMESPACE_END(detail) - // When inside a namespace (or anywhere as long as it's not the first item on a line), // C++20 allows "module" to be used. This is provided for backward compatibility, and for // simplicity, if someone wants to use py::module for example, that is perfectly safe. diff --git a/tests/test_pickling.py b/tests/test_pickling.py index 7e315bc7..c3455897 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -1,5 +1,4 @@ import copy -import importlib import pickle import pytest @@ -8,24 +7,6 @@ from pybind11_tests import pickling as m -class _pybind11_detail_function_record_import_helper: - def __init__(self, fully_qualified_module_name, function_name): - self.fully_qualified_module_name = fully_qualified_module_name - self.function_name = function_name - - def __getattr__(self, name): - assert name == self.function_name - return getattr( - importlib.import_module(self.fully_qualified_module_name), - self.function_name, - ) - - -importlib._pybind11_detail_function_record_import_helper = ( - _pybind11_detail_function_record_import_helper -) - - def test_assumptions(): assert pickle.HIGHEST_PROTOCOL >= 0 From 7359b5a02c53d487e3b2ffe2c694882743191cf5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 23:25:18 -0800 Subject: [PATCH 21/39] gcc 4.8.5 and 7.5.0 reject `PYBIND11_WARNING_DISABLE_GCC("-Wcast-function-type")` --- include/pybind11/pybind11.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index ce62ed61..217690a6 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -138,7 +138,9 @@ void tp_free_impl(void *self); static PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *); PYBIND11_WARNING_PUSH +#if defined(__GNUC__) && __GNUC__ >= 8 PYBIND11_WARNING_DISABLE_GCC("-Wcast-function-type") +#endif static PyMethodDef MethodsStaticAlloc[] = {{"__reduce_ex__", (PyCFunction) reduce_ex_impl, METH_VARARGS | METH_KEYWORDS, nullptr}, {nullptr, nullptr, 0, nullptr}}; From dc50802fb817287f11056d7d5d288aa9402fb925 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 12 Feb 2024 23:47:19 -0800 Subject: [PATCH 22/39] Python 3.6, 3.7: Skip `get_pybind11_detail_function_record_pickle_helper()` call-once initialization triggered from `cpp_function::initialize_generic()` --- include/pybind11/pybind11.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 217690a6..9b589741 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -729,11 +729,13 @@ class cpp_function : public function { } } +#if PY_VERSION_HEX >= 0x03080000 if (rec->name != nullptr && rec->name[0] != '\0' && scope_module && hasattr(rec->scope, "__dict__")) { // Call-once initialization. detail::get_pybind11_detail_function_record_pickle_helper(rec->scope); } +#endif m_ptr = PyCFunction_NewEx(rec->def, py_func_rec.ptr(), scope_module.ptr()); if (!m_ptr) { pybind11_fail("cpp_function::cpp_function(): Could not allocate function object"); From fe87588ee9d88eefcee63cf21bcde82c8f9c8010 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 13 Feb 2024 04:36:54 -0800 Subject: [PATCH 23/39] New version of `_function_record_pickle_helper`, using `collections.namedtuple` --- include/pybind11/pybind11.h | 82 ++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 9b589741..48b2d208 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -254,32 +254,31 @@ inline object function_record_PyObject_New() { return reinterpret_steal((PyObject *) py_func_rec); } -inline object get_pybind11_detail_function_record_pickle_helper(handle rec_scope) { - constexpr char helper_name[] = "_pybind11_detail_function_record_pickle_helper_v1"; - if (hasattr(rec_scope, helper_name)) { - return rec_scope.attr(helper_name); - } - std::string py_code("class "); - py_code += helper_name; - py_code += ":\n"; - py_code += R"( - def __init__(self, function_name): - self.function_name = function_name - - def __getattr__(self, name): - assert name == self.function_name - return globals()[self.function_name] - )"; - // Passing the __dict__ directly does not work in some situations. - dict run_globals(rec_scope.attr("__dict__")); - PyObject *run_result - = PyRun_String(py_code.c_str(), Py_file_input, run_globals.ptr(), nullptr); - if (run_result == nullptr) { - throw error_already_set(); +inline PyObject *function_record_pickle_helper(PyObject *, PyObject *tup_ptr); + +constexpr char function_record_pickle_helper_name[] = "_function_record_pickle_helper_v1"; + +static PyMethodDef function_record_pickle_helper_PyMethodDef + = {function_record_pickle_helper_name, + (PyCFunction) function_record_pickle_helper, + METH_O, + nullptr}; + +inline object get_function_record_pickle_helper(handle mod) { + if (!hasattr(mod, function_record_pickle_helper_name)) { + PyObject *pycfunc = PyCFunction_New(&function_record_pickle_helper_PyMethodDef, nullptr); + if (!pycfunc) { + pybind11_fail("FATAL: get_function_record_pickle_helper() PyCFunction_NewEx() FAILED"); + } + if (PyModule_AddObject( + mod.ptr(), function_record_pickle_helper_name, pycfunc /* steals a reference */) + < 0) { + Py_DECREF(pycfunc); + pybind11_fail( + "FATAL: get_function_record_pickle_helper() PyModule_AddObject() FAILED"); + } } - Py_DECREF(run_result); - setattr(rec_scope, helper_name, run_globals[helper_name]); - return rec_scope.attr(helper_name); + return getattr(mod, function_record_pickle_helper_name); } PYBIND11_NAMESPACE_END(detail) @@ -729,13 +728,11 @@ class cpp_function : public function { } } -#if PY_VERSION_HEX >= 0x03080000 if (rec->name != nullptr && rec->name[0] != '\0' && scope_module - && hasattr(rec->scope, "__dict__")) { + && PyModule_Check(rec->scope.ptr()) != 0) { // Call-once initialization. - detail::get_pybind11_detail_function_record_pickle_helper(rec->scope); + detail::get_function_record_pickle_helper(rec->scope); } -#endif m_ptr = PyCFunction_NewEx(rec->def, py_func_rec.ptr(), scope_module.ptr()); if (!m_ptr) { pybind11_fail("cpp_function::cpp_function(): Could not allocate function object"); @@ -1423,9 +1420,9 @@ inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { } else if (hasattr(rec->scope, "__name__")) { scope_module = rec->scope.attr("__name__"); } - if (scope_module && hasattr(rec->scope, "__dict__")) { - return make_tuple(get_pybind11_detail_function_record_pickle_helper(rec->scope), - make_tuple(rec->name)) + if (scope_module && PyModule_Check(rec->scope.ptr()) != 0) { + return make_tuple(get_function_record_pickle_helper(rec->scope), + make_tuple(make_tuple(scope_module, rec->name))) .release() .ptr(); } @@ -1598,6 +1595,27 @@ class module_ : public object { // simplicity, if someone wants to use py::module for example, that is perfectly safe. using module = module_; +PYBIND11_NAMESPACE_BEGIN(detail) + +inline PyObject *function_record_pickle_helper(PyObject *, PyObject *tup_ptr) { + auto tup_obj = tuple(reinterpret_borrow(tup_ptr)); + if (len(tup_obj) != 2) { + set_error(PyExc_ValueError, "Internal inconsistency: len(tup_obj) != 2"); + return nullptr; + } + auto import_module = module_::import("importlib").attr("import_module"); + auto mod = import_module(tup_obj[0]); + auto py_func = mod.attr(tup_obj[1]); + auto namedtuple = module_::import("collections").attr("namedtuple"); + auto proxy_type + = namedtuple(str(function_record_pickle_helper_name) + str("_proxy_") + tup_obj[1], + make_tuple(tup_obj[1])); + auto proxy_obj = proxy_type(py_func); + return proxy_obj.release().ptr(); +} + +PYBIND11_NAMESPACE_END(detail) + /// \ingroup python_builtins /// Return a dictionary representing the global variables in the current execution frame, /// or ``__main__.__dict__`` if there is no frame (usually when the interpreter is embedded). From 554d52913d341275657e49f8604da7a8e17375e8 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 13 Feb 2024 10:41:09 -0800 Subject: [PATCH 24/39] =?UTF-8?q?Explicit=20`str(tup=5Fobj[1])`=20to=20fix?= =?UTF-8?q?=20=F0=9F=90=8D=203=20=E2=80=A2=20centos:7=20=E2=80=A2=20x64=20?= =?UTF-8?q?segfault?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/pybind11/pybind11.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 48b2d208..814ae8b3 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1608,7 +1608,7 @@ inline PyObject *function_record_pickle_helper(PyObject *, PyObject *tup_ptr) { auto py_func = mod.attr(tup_obj[1]); auto namedtuple = module_::import("collections").attr("namedtuple"); auto proxy_type - = namedtuple(str(function_record_pickle_helper_name) + str("_proxy_") + tup_obj[1], + = namedtuple(str(function_record_pickle_helper_name) + str("_proxy_") + str(tup_obj[1]), make_tuple(tup_obj[1])); auto proxy_obj = proxy_type(py_func); return proxy_obj.release().ptr(); From 84f9c35313cb1f28582ed058f23610484655e7a5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 13 Feb 2024 12:12:45 -0800 Subject: [PATCH 25/39] Factor out detail/function_record_pyobject.h --- CMakeLists.txt | 1 + .../detail/function_record_pyobject.h | 240 ++++++++++++++++++ include/pybind11/pybind11.h | 219 +--------------- tests/extra_python_package/test_files.py | 1 + 4 files changed, 245 insertions(+), 216 deletions(-) create mode 100644 include/pybind11/detail/function_record_pyobject.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 47db07db..b05f2f95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,7 @@ set(PYBIND11_HEADERS include/pybind11/detail/cross_extension_shared_state.h include/pybind11/detail/descr.h include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h + include/pybind11/detail/function_record_pyobject.h include/pybind11/detail/init.h include/pybind11/detail/internals.h include/pybind11/detail/native_enum_data.h diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h new file mode 100644 index 00000000..53251652 --- /dev/null +++ b/include/pybind11/detail/function_record_pyobject.h @@ -0,0 +1,240 @@ +// Copyright (c) 2024 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#pragma once + +#include "../attr.h" +#include "../pytypes.h" +#include "common.h" + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) +PYBIND11_NAMESPACE_BEGIN(detail) + +struct function_record_PyObject { + PyObject_HEAD + function_record *cpp_func_rec; +}; + +namespace function_record_PyTypeObject_methods { + +PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds); +PyObject *tp_alloc_impl(PyTypeObject *type, Py_ssize_t nitems); +int tp_init_impl(PyObject *self, PyObject *args, PyObject *kwds); +void tp_dealloc_impl(PyObject *self); +void tp_free_impl(void *self); + +static PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *); + +PYBIND11_WARNING_PUSH +#if defined(__GNUC__) && __GNUC__ >= 8 +PYBIND11_WARNING_DISABLE_GCC("-Wcast-function-type") +#endif +static PyMethodDef tp_methods_impl[] + = {{"__reduce_ex__", (PyCFunction) reduce_ex_impl, METH_VARARGS | METH_KEYWORDS, nullptr}, + {nullptr, nullptr, 0, nullptr}}; +PYBIND11_WARNING_POP + +} // namespace function_record_PyTypeObject_methods + +// Designated initializers are a C++20 feature: +// https://en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers +// MSVC rejects them unless /std:c++20 is used (error code C7555). +PYBIND11_WARNING_PUSH +PYBIND11_WARNING_DISABLE_CLANG("-Wmissing-field-initializers") +#if defined(__GNUC__) && __GNUC__ >= 8 +PYBIND11_WARNING_DISABLE_GCC("-Wmissing-field-initializers") +#endif +static PyTypeObject function_record_PyTypeObject = { + PyVarObject_HEAD_INIT(nullptr, 0) + /* const char *tp_name */ "pybind11_detail_function_" + "record_" PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID + "_" PYBIND11_PLATFORM_ABI_ID_V4, + /* Py_ssize_t tp_basicsize */ sizeof(function_record_PyObject), + /* Py_ssize_t tp_itemsize */ 0, + /* destructor tp_dealloc */ function_record_PyTypeObject_methods::tp_dealloc_impl, + /* Py_ssize_t tp_vectorcall_offset */ 0, + /* getattrfunc tp_getattr */ nullptr, + /* setattrfunc tp_setattr */ nullptr, + /* PyAsyncMethods *tp_as_async */ nullptr, + /* reprfunc tp_repr */ nullptr, + /* PyNumberMethods *tp_as_number */ nullptr, + /* PySequenceMethods *tp_as_sequence */ nullptr, + /* PyMappingMethods *tp_as_mapping */ nullptr, + /* hashfunc tp_hash */ nullptr, + /* ternaryfunc tp_call */ nullptr, + /* reprfunc tp_str */ nullptr, + /* getattrofunc tp_getattro */ nullptr, + /* setattrofunc tp_setattro */ nullptr, + /* PyBufferProcs *tp_as_buffer */ nullptr, + /* unsigned long tp_flags */ Py_TPFLAGS_DEFAULT, + /* const char *tp_doc */ nullptr, + /* traverseproc tp_traverse */ nullptr, + /* inquiry tp_clear */ nullptr, + /* richcmpfunc tp_richcompare */ nullptr, + /* Py_ssize_t tp_weaklistoffset */ 0, + /* getiterfunc tp_iter */ nullptr, + /* iternextfunc tp_iternext */ nullptr, + /* struct PyMethodDef *tp_methods */ function_record_PyTypeObject_methods::tp_methods_impl, + /* struct PyMemberDef *tp_members */ nullptr, + /* struct PyGetSetDef *tp_getset */ nullptr, + /* struct _typeobject *tp_base */ nullptr, + /* PyObject *tp_dict */ nullptr, + /* descrgetfunc tp_descr_get */ nullptr, + /* descrsetfunc tp_descr_set */ nullptr, + /* Py_ssize_t tp_dictoffset */ 0, + /* initproc tp_init */ function_record_PyTypeObject_methods::tp_init_impl, + /* allocfunc tp_alloc */ function_record_PyTypeObject_methods::tp_alloc_impl, + /* newfunc tp_new */ function_record_PyTypeObject_methods::tp_new_impl, + /* freefunc tp_free */ function_record_PyTypeObject_methods::tp_free_impl, + /* inquiry tp_is_gc */ nullptr, + /* PyObject *tp_bases */ nullptr, + /* PyObject *tp_mro */ nullptr, + /* PyObject *tp_cache */ nullptr, + /* PyObject *tp_subclasses */ nullptr, + /* PyObject *tp_weaklist */ nullptr, + /* destructor tp_del */ nullptr, + /* unsigned int tp_version_tag */ 0, + /* destructor tp_finalize */ nullptr, +#if PY_VERSION_HEX >= 0x03080000 + /* vectorcallfunc tp_vectorcall */ nullptr, +#endif +}; +PYBIND11_WARNING_POP + +static bool function_record_PyTypeObject_PyType_Ready_first_call = true; + +inline void function_record_PyTypeObject_PyType_Ready() { + if (function_record_PyTypeObject_PyType_Ready_first_call) { + if (PyType_Ready(&function_record_PyTypeObject) < 0) { + throw error_already_set(); + } + function_record_PyTypeObject_PyType_Ready_first_call = false; + } +} + +inline bool is_function_record_PyObject(PyObject *obj) { + if (PyType_Check(obj) != 0) { + return false; + } + PyTypeObject *obj_type = Py_TYPE(obj); + // Fast path (pointer comparison). + if (obj_type == &function_record_PyTypeObject) { + return true; + } + // This works across extension modules. Note that tp_name is versioned. + if (strcmp(obj_type->tp_name, function_record_PyTypeObject.tp_name) == 0) { + return true; + } + return false; +} + +inline function_record *function_record_ptr_from_PyObject(PyObject *obj) { + if (is_function_record_PyObject(obj)) { + return ((detail::function_record_PyObject *) obj)->cpp_func_rec; + } + return nullptr; +} + +inline object function_record_PyObject_New() { + auto *py_func_rec = PyObject_New(function_record_PyObject, &function_record_PyTypeObject); + if (py_func_rec == nullptr) { + throw error_already_set(); + } + py_func_rec->cpp_func_rec = nullptr; // For clarity/purity. Redundant in practice. + return reinterpret_steal((PyObject *) py_func_rec); +} + +// The implementation needs the definition of `class module_`. +PyObject *function_record_pickle_helper(PyObject *, PyObject *tup_ptr); + +constexpr char function_record_pickle_helper_name[] = "_function_record_pickle_helper_v1"; + +static PyMethodDef function_record_pickle_helper_PyMethodDef + = {function_record_pickle_helper_name, + (PyCFunction) function_record_pickle_helper, + METH_O, + nullptr}; + +inline object get_function_record_pickle_helper(handle mod) { + if (!hasattr(mod, function_record_pickle_helper_name)) { + PyObject *pycfunc = PyCFunction_New(&function_record_pickle_helper_PyMethodDef, nullptr); + if (!pycfunc) { + pybind11_fail("FATAL: get_function_record_pickle_helper() PyCFunction_NewEx() FAILED"); + } + if (PyModule_AddObject( + mod.ptr(), function_record_pickle_helper_name, pycfunc /* steals a reference */) + < 0) { + Py_DECREF(pycfunc); + pybind11_fail( + "FATAL: get_function_record_pickle_helper() PyModule_AddObject() FAILED"); + } + } + return getattr(mod, function_record_pickle_helper_name); +} + +namespace function_record_PyTypeObject_methods { + +inline PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds) { + // Create a new instance using the type's tp_alloc slot. + if (type) { + pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_new_impl"); + } + return PyType_GenericNew(type, args, kwds); +} + +inline PyObject *tp_alloc_impl(PyTypeObject *type, Py_ssize_t nitems) { + // Use Python's default memory allocation mechanism to allocate a new instance + // and initialize all its contents to NULL. + if (type) { + pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_alloc_impl"); + } + return PyType_GenericAlloc(type, nitems); +} + +inline int tp_init_impl(PyObject *self, PyObject *, PyObject *) { + if (self) { + pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_init_impl"); + } + return -1; +} + +// The implementation needs the definition of `class cpp_function`. +void tp_dealloc_impl(PyObject *self); + +inline void tp_free_impl(void *self) { + if (self) { + pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_free_impl"); + } +} + +inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { + // Deliberately ignoring the arguments for simplicity (expected is `protocol: int`). + const function_record *rec = function_record_ptr_from_PyObject(self); + if (rec == nullptr) { + pybind11_fail( + "FATAL: function_record_PyTypeObject reduce_ex_impl(): cannot obtain cpp_func_rec."); + } + if (rec->name != nullptr && rec->name[0] != '\0' && rec->scope) { + // TODO 30099: Move to helper function. + object scope_module; + if (hasattr(rec->scope, "__module__")) { + scope_module = rec->scope.attr("__module__"); + } else if (hasattr(rec->scope, "__name__")) { + scope_module = rec->scope.attr("__name__"); + } + if (scope_module && PyModule_Check(rec->scope.ptr()) != 0) { + return make_tuple(get_function_record_pickle_helper(rec->scope), + make_tuple(make_tuple(scope_module, rec->name))) + .release() + .ptr(); + } + } + set_error(PyExc_RuntimeError, repr(self) + str(" is not pickleable.")); + return nullptr; +} + +} // namespace function_record_PyTypeObject_methods + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 814ae8b3..eb31aac1 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -11,6 +11,7 @@ #pragma once #include "detail/class.h" +#include "detail/function_record_pyobject.h" #include "detail/init.h" #include "detail/native_enum_data.h" #include "detail/smart_holder_sfinae_hooks_only.h" @@ -20,7 +21,6 @@ #include "options.h" #include "typing.h" -#include #include #include #include @@ -122,165 +122,6 @@ inline bool apply_exception_translators(std::forward_list & # define PYBIND11_COMPAT_STRDUP strdup #endif -struct function_record_PyObject { - PyObject_HEAD - function_record *cpp_func_rec; -}; - -namespace function_record_PyTypeObject_methods { - -PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject *tp_alloc_impl(PyTypeObject *type, Py_ssize_t nitems); -int tp_init_impl(PyObject *self, PyObject *args, PyObject *kwds); -void tp_dealloc_impl(PyObject *self); -void tp_free_impl(void *self); - -static PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *); - -PYBIND11_WARNING_PUSH -#if defined(__GNUC__) && __GNUC__ >= 8 -PYBIND11_WARNING_DISABLE_GCC("-Wcast-function-type") -#endif -static PyMethodDef MethodsStaticAlloc[] - = {{"__reduce_ex__", (PyCFunction) reduce_ex_impl, METH_VARARGS | METH_KEYWORDS, nullptr}, - {nullptr, nullptr, 0, nullptr}}; -PYBIND11_WARNING_POP - -} // namespace function_record_PyTypeObject_methods - -// Designated initializers are a C++20 feature: -// https://en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers -// MSVC rejects them unless /std:c++20 is used (error code C7555). -PYBIND11_WARNING_PUSH -PYBIND11_WARNING_DISABLE_CLANG("-Wmissing-field-initializers") -#if defined(__GNUC__) && __GNUC__ >= 8 -PYBIND11_WARNING_DISABLE_GCC("-Wmissing-field-initializers") -#endif -static PyTypeObject function_record_PyTypeObject = { - PyVarObject_HEAD_INIT(nullptr, 0) - /* const char *tp_name */ "pybind11_detail_function_" - "record_" PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID - "_" PYBIND11_PLATFORM_ABI_ID_V4, - /* Py_ssize_t tp_basicsize */ sizeof(function_record_PyObject), - /* Py_ssize_t tp_itemsize */ 0, - /* destructor tp_dealloc */ function_record_PyTypeObject_methods::tp_dealloc_impl, - /* Py_ssize_t tp_vectorcall_offset */ 0, - /* getattrfunc tp_getattr */ nullptr, - /* setattrfunc tp_setattr */ nullptr, - /* PyAsyncMethods *tp_as_async */ nullptr, - /* reprfunc tp_repr */ nullptr, - /* PyNumberMethods *tp_as_number */ nullptr, - /* PySequenceMethods *tp_as_sequence */ nullptr, - /* PyMappingMethods *tp_as_mapping */ nullptr, - /* hashfunc tp_hash */ nullptr, - /* ternaryfunc tp_call */ nullptr, - /* reprfunc tp_str */ nullptr, - /* getattrofunc tp_getattro */ nullptr, - /* setattrofunc tp_setattro */ nullptr, - /* PyBufferProcs *tp_as_buffer */ nullptr, - /* unsigned long tp_flags */ Py_TPFLAGS_DEFAULT, - /* const char *tp_doc */ nullptr, - /* traverseproc tp_traverse */ nullptr, - /* inquiry tp_clear */ nullptr, - /* richcmpfunc tp_richcompare */ nullptr, - /* Py_ssize_t tp_weaklistoffset */ 0, - /* getiterfunc tp_iter */ nullptr, - /* iternextfunc tp_iternext */ nullptr, - /* struct PyMethodDef *tp_methods */ function_record_PyTypeObject_methods::MethodsStaticAlloc, - /* struct PyMemberDef *tp_members */ nullptr, - /* struct PyGetSetDef *tp_getset */ nullptr, - /* struct _typeobject *tp_base */ nullptr, - /* PyObject *tp_dict */ nullptr, - /* descrgetfunc tp_descr_get */ nullptr, - /* descrsetfunc tp_descr_set */ nullptr, - /* Py_ssize_t tp_dictoffset */ 0, - /* initproc tp_init */ function_record_PyTypeObject_methods::tp_init_impl, - /* allocfunc tp_alloc */ function_record_PyTypeObject_methods::tp_alloc_impl, - /* newfunc tp_new */ function_record_PyTypeObject_methods::tp_new_impl, - /* freefunc tp_free */ function_record_PyTypeObject_methods::tp_free_impl, - /* inquiry tp_is_gc */ nullptr, - /* PyObject *tp_bases */ nullptr, - /* PyObject *tp_mro */ nullptr, - /* PyObject *tp_cache */ nullptr, - /* PyObject *tp_subclasses */ nullptr, - /* PyObject *tp_weaklist */ nullptr, - /* destructor tp_del */ nullptr, - /* unsigned int tp_version_tag */ 0, - /* destructor tp_finalize */ nullptr, -#if PY_VERSION_HEX >= 0x03080000 - /* vectorcallfunc tp_vectorcall */ nullptr, -#endif -}; -PYBIND11_WARNING_POP - -static bool function_record_PyTypeObject_PyType_Ready_first_call = true; - -inline void function_record_PyTypeObject_PyType_Ready() { - if (function_record_PyTypeObject_PyType_Ready_first_call) { - if (PyType_Ready(&function_record_PyTypeObject) < 0) { - throw error_already_set(); - } - function_record_PyTypeObject_PyType_Ready_first_call = false; - } -} - -inline bool is_function_record_PyObject(PyObject *obj) { - if (PyType_Check(obj) != 0) { - return false; - } - PyTypeObject *obj_type = Py_TYPE(obj); - if (obj_type == &function_record_PyTypeObject) { - return true; - } - if (strcmp(obj_type->tp_name, function_record_PyTypeObject.tp_name) == 0) { - return true; - } - return false; -} - -inline function_record *function_record_ptr_from_PyObject(PyObject *obj) { - if (is_function_record_PyObject(obj)) { - return ((detail::function_record_PyObject *) obj)->cpp_func_rec; - } - return nullptr; -} - -inline object function_record_PyObject_New() { - auto *py_func_rec = PyObject_New(function_record_PyObject, &function_record_PyTypeObject); - if (py_func_rec == nullptr) { - throw error_already_set(); - } - py_func_rec->cpp_func_rec = nullptr; // For clarity/purity. Redundant in practice. - return reinterpret_steal((PyObject *) py_func_rec); -} - -inline PyObject *function_record_pickle_helper(PyObject *, PyObject *tup_ptr); - -constexpr char function_record_pickle_helper_name[] = "_function_record_pickle_helper_v1"; - -static PyMethodDef function_record_pickle_helper_PyMethodDef - = {function_record_pickle_helper_name, - (PyCFunction) function_record_pickle_helper, - METH_O, - nullptr}; - -inline object get_function_record_pickle_helper(handle mod) { - if (!hasattr(mod, function_record_pickle_helper_name)) { - PyObject *pycfunc = PyCFunction_New(&function_record_pickle_helper_PyMethodDef, nullptr); - if (!pycfunc) { - pybind11_fail("FATAL: get_function_record_pickle_helper() PyCFunction_NewEx() FAILED"); - } - if (PyModule_AddObject( - mod.ptr(), function_record_pickle_helper_name, pycfunc /* steals a reference */) - < 0) { - Py_DECREF(pycfunc); - pybind11_fail( - "FATAL: get_function_record_pickle_helper() PyModule_AddObject() FAILED"); - } - } - return getattr(mod, function_record_pickle_helper_name); -} - PYBIND11_NAMESPACE_END(detail) /// Wraps an arbitrary C++ function/method/lambda function/.. into a callable Python object @@ -1369,68 +1210,13 @@ PYBIND11_NAMESPACE_BEGIN(detail) namespace function_record_PyTypeObject_methods { -inline PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds) { - // Create a new instance using the type's tp_alloc slot. - if (type) { - pybind11_fail("INTENTIONAL BREAKAGE: tp_new_impl"); - } - return PyType_GenericNew(type, args, kwds); -} - -inline PyObject *tp_alloc_impl(PyTypeObject *type, Py_ssize_t nitems) { - // Use Python's default memory allocation mechanism to allocate a new instance - // and initialize all its contents to NULL. - if (type) { - pybind11_fail("INTENTIONAL BREAKAGE: tp_alloc_impl"); - } - return PyType_GenericAlloc(type, nitems); -} - -inline int tp_init_impl(PyObject *self, PyObject *, PyObject *) { - if (self) { - pybind11_fail("INTENTIONAL BREAKAGE: tp_init_impl"); - } - return -1; -} - +// This implementation needs the definition of `class cpp_function`. inline void tp_dealloc_impl(PyObject *self) { auto *py_func_rec = (function_record_PyObject *) self; cpp_function::destruct(py_func_rec->cpp_func_rec); py_func_rec->cpp_func_rec = nullptr; } -inline void tp_free_impl(void *self) { - if (self) { - pybind11_fail("INTENTIONAL BREAKAGE: tp_free_impl"); - } -} - -inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { - // Deliberately ignoring the arguments for simplicity (expected is `protocol: int`). - const function_record *rec = function_record_ptr_from_PyObject(self); - if (rec == nullptr) { - pybind11_fail( - "FATAL: function_record_PyTypeObject reduce_ex_impl(): cannot obtain cpp_func_rec."); - } - if (rec->name != nullptr && rec->name[0] != '\0' && rec->scope) { - // TODO 30099: Move to helper function. - object scope_module; - if (hasattr(rec->scope, "__module__")) { - scope_module = rec->scope.attr("__module__"); - } else if (hasattr(rec->scope, "__name__")) { - scope_module = rec->scope.attr("__name__"); - } - if (scope_module && PyModule_Check(rec->scope.ptr()) != 0) { - return make_tuple(get_function_record_pickle_helper(rec->scope), - make_tuple(make_tuple(scope_module, rec->name))) - .release() - .ptr(); - } - } - set_error(PyExc_RuntimeError, repr(self) + str(" is not pickleable.")); - return nullptr; -} - } // namespace function_record_PyTypeObject_methods /// Instance creation function for all pybind11 types. It only allocates space for the @@ -1597,6 +1383,7 @@ using module = module_; PYBIND11_NAMESPACE_BEGIN(detail) +// This implementation needs the definition of `class module_`. inline PyObject *function_record_pickle_helper(PyObject *, PyObject *tup_ptr) { auto tup_obj = tuple(reinterpret_borrow(tup_ptr)); if (len(tup_obj) != 2) { diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 02f23359..16418163 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -58,6 +58,7 @@ "include/pybind11/detail/cross_extension_shared_state.h", "include/pybind11/detail/descr.h", "include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h", + "include/pybind11/detail/function_record_pyobject.h", "include/pybind11/detail/init.h", "include/pybind11/detail/internals.h", "include/pybind11/detail/native_enum_data.h", From 808cbbfe167be5d727c32feeda2ef92fc4297e6a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 13 Feb 2024 12:17:30 -0800 Subject: [PATCH 26/39] Use PYBIND11_NAMESPACE_BEGIN/END for function_record_PyTypeObject_methods --- include/pybind11/detail/function_record_pyobject.h | 8 ++++---- include/pybind11/pybind11.h | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index 53251652..abafada0 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -16,7 +16,7 @@ struct function_record_PyObject { function_record *cpp_func_rec; }; -namespace function_record_PyTypeObject_methods { +PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods) PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject *tp_alloc_impl(PyTypeObject *type, Py_ssize_t nitems); @@ -35,7 +35,7 @@ static PyMethodDef tp_methods_impl[] {nullptr, nullptr, 0, nullptr}}; PYBIND11_WARNING_POP -} // namespace function_record_PyTypeObject_methods +PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods) // Designated initializers are a C++20 feature: // https://en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers @@ -173,7 +173,7 @@ inline object get_function_record_pickle_helper(handle mod) { return getattr(mod, function_record_pickle_helper_name); } -namespace function_record_PyTypeObject_methods { +PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods) inline PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds) { // Create a new instance using the type's tp_alloc slot. @@ -234,7 +234,7 @@ inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { return nullptr; } -} // namespace function_record_PyTypeObject_methods +PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods) PYBIND11_NAMESPACE_END(detail) PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index eb31aac1..6620c33e 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1208,7 +1208,7 @@ class cpp_function : public function { PYBIND11_NAMESPACE_BEGIN(detail) -namespace function_record_PyTypeObject_methods { +PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods) // This implementation needs the definition of `class cpp_function`. inline void tp_dealloc_impl(PyObject *self) { @@ -1217,7 +1217,7 @@ inline void tp_dealloc_impl(PyObject *self) { py_func_rec->cpp_func_rec = nullptr; } -} // namespace function_record_PyTypeObject_methods +PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods) /// Instance creation function for all pybind11 types. It only allocates space for the /// C++ object, but doesn't call the constructor -- an `__init__` function must do that. From 61973dfc0d499eee19e4c9de08e2dc541904498b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 13 Feb 2024 12:25:51 -0800 Subject: [PATCH 27/39] Factor out function_record_PyTypeObject_methods::tp_name_impl, mainly to stop clang-format from breaking up a string literal. --- include/pybind11/detail/function_record_pyobject.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index abafada0..8ef363c1 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -35,6 +35,11 @@ static PyMethodDef tp_methods_impl[] {nullptr, nullptr, 0, nullptr}}; PYBIND11_WARNING_POP +// Note that this name is versioned. +constexpr char tp_name_impl[] + = "pybind11_detail_function_record_" PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID + "_" PYBIND11_PLATFORM_ABI_ID_V4; + PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods) // Designated initializers are a C++20 feature: @@ -47,9 +52,7 @@ PYBIND11_WARNING_DISABLE_GCC("-Wmissing-field-initializers") #endif static PyTypeObject function_record_PyTypeObject = { PyVarObject_HEAD_INIT(nullptr, 0) - /* const char *tp_name */ "pybind11_detail_function_" - "record_" PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID - "_" PYBIND11_PLATFORM_ABI_ID_V4, + /* const char *tp_name */ function_record_PyTypeObject_methods::tp_name_impl, /* Py_ssize_t tp_basicsize */ sizeof(function_record_PyObject), /* Py_ssize_t tp_itemsize */ 0, /* destructor tp_dealloc */ function_record_PyTypeObject_methods::tp_dealloc_impl, From 9e22ddd6f52bb738c1fa412cee0bdae90d0b2613 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 13 Feb 2024 12:34:28 -0800 Subject: [PATCH 28/39] Simplify implementation of UNEXPECTED CALL functions. --- .../detail/function_record_pyobject.h | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index 8ef363c1..587ec8a2 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -178,37 +178,27 @@ inline object get_function_record_pickle_helper(handle mod) { PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods) -inline PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds) { - // Create a new instance using the type's tp_alloc slot. - if (type) { - pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_new_impl"); - } - return PyType_GenericNew(type, args, kwds); +// Guard against accidents & oversights, in particular when porting to future Python versions. +inline PyObject *tp_new_impl(PyTypeObject *, PyObject *, PyObject *) { + pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_new_impl"); + return nullptr; // Unreachable. } -inline PyObject *tp_alloc_impl(PyTypeObject *type, Py_ssize_t nitems) { - // Use Python's default memory allocation mechanism to allocate a new instance - // and initialize all its contents to NULL. - if (type) { - pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_alloc_impl"); - } - return PyType_GenericAlloc(type, nitems); +inline PyObject *tp_alloc_impl(PyTypeObject *, Py_ssize_t) { + pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_alloc_impl"); + return nullptr; // Unreachable. } -inline int tp_init_impl(PyObject *self, PyObject *, PyObject *) { - if (self) { - pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_init_impl"); - } - return -1; +inline int tp_init_impl(PyObject *, PyObject *, PyObject *) { + pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_init_impl"); + return -1; // Unreachable. } // The implementation needs the definition of `class cpp_function`. void tp_dealloc_impl(PyObject *self); -inline void tp_free_impl(void *self) { - if (self) { - pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_free_impl"); - } +inline void tp_free_impl(void *) { + pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_free_impl"); } inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { From d701abbda011e116ad4f30c44713c836d4ce94fb Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 13 Feb 2024 12:54:43 -0800 Subject: [PATCH 29/39] Factor out `detail::get_scope_module()` --- .../pybind11/detail/function_record_pyobject.h | 13 ++++--------- include/pybind11/pybind11.h | 15 +++------------ include/pybind11/pytypes.h | 12 ++++++++++++ 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index 587ec8a2..c3a1d4c7 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -208,15 +208,10 @@ inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { pybind11_fail( "FATAL: function_record_PyTypeObject reduce_ex_impl(): cannot obtain cpp_func_rec."); } - if (rec->name != nullptr && rec->name[0] != '\0' && rec->scope) { - // TODO 30099: Move to helper function. - object scope_module; - if (hasattr(rec->scope, "__module__")) { - scope_module = rec->scope.attr("__module__"); - } else if (hasattr(rec->scope, "__name__")) { - scope_module = rec->scope.attr("__name__"); - } - if (scope_module && PyModule_Check(rec->scope.ptr()) != 0) { + if (rec->name != nullptr && rec->name[0] != '\0' && rec->scope + && PyModule_Check(rec->scope.ptr()) != 0) { + object scope_module = get_scope_module(rec->scope); + if (scope_module) { return make_tuple(get_function_record_pickle_helper(rec->scope), make_tuple(make_tuple(scope_module, rec->name))) .release() diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 6620c33e..48166875 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -559,18 +559,9 @@ class cpp_function : public function { = unique_rec.release(); guarded_strdup.release(); - // TODO 30099: Move to helper function. - object scope_module; - if (rec->scope) { - if (hasattr(rec->scope, "__module__")) { - scope_module = rec->scope.attr("__module__"); - } else if (hasattr(rec->scope, "__name__")) { - scope_module = rec->scope.attr("__name__"); - } - } - - if (rec->name != nullptr && rec->name[0] != '\0' && scope_module - && PyModule_Check(rec->scope.ptr()) != 0) { + object scope_module = detail::get_scope_module(rec->scope); + if (rec->name != nullptr && rec->name[0] != '\0' && rec->scope + && PyModule_Check(rec->scope.ptr()) != 0 && scope_module) { // Call-once initialization. detail::get_function_record_pickle_helper(rec->scope); } diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index baa0c242..2492b774 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -1367,6 +1367,18 @@ class simple_collector; template class unpacking_collector; +inline object get_scope_module(handle scope) { + if (scope) { + if (hasattr(scope, "__module__")) { + return scope.attr("__module__"); + } + if (hasattr(scope, "__name__")) { + return scope.attr("__name__"); + } + } + return object(); +} + PYBIND11_NAMESPACE_END(detail) // TODO: After the deprecated constructors are removed, this macro can be simplified by From d148a7432a7a7d36a5709d299f28028966a060b5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 13 Feb 2024 13:06:37 -0800 Subject: [PATCH 30/39] IncludeCleaner fixes (Google toolchain). --- include/pybind11/detail/function_record_pyobject.h | 2 ++ include/pybind11/pybind11.h | 1 + 2 files changed, 3 insertions(+) diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index c3a1d4c7..c72de5c3 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -8,6 +8,8 @@ #include "../pytypes.h" #include "common.h" +#include + PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) PYBIND11_NAMESPACE_BEGIN(detail) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 48166875..0dddc0e5 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -21,6 +21,7 @@ #include "options.h" #include "typing.h" +#include #include #include #include From 0d210a54fd9637f1deafa7f6e58b20749dd89d1c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 13 Feb 2024 17:19:10 -0800 Subject: [PATCH 31/39] Comment out unreachable code (to resolve MSVC Werrors). --- include/pybind11/detail/function_record_pyobject.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index c72de5c3..40942f45 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -183,17 +183,17 @@ PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods) // Guard against accidents & oversights, in particular when porting to future Python versions. inline PyObject *tp_new_impl(PyTypeObject *, PyObject *, PyObject *) { pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_new_impl"); - return nullptr; // Unreachable. + // return nullptr; // Unreachable. } inline PyObject *tp_alloc_impl(PyTypeObject *, Py_ssize_t) { pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_alloc_impl"); - return nullptr; // Unreachable. + // return nullptr; // Unreachable. } inline int tp_init_impl(PyObject *, PyObject *, PyObject *) { pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_init_impl"); - return -1; // Unreachable. + // return -1; // Unreachable. } // The implementation needs the definition of `class cpp_function`. From 0e7bb1034e3acfbde139594ebcb84d37b4dfc023 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 14 Feb 2024 07:48:24 -0800 Subject: [PATCH 32/39] Use built-in `eval()` instead of `function_record_pickle_helper()` Much simpler! (Note that the `function_record_pickle_helper()` code is NOT removed in this commit.) This approach was discovered in an attempt to solve the problem that stubgen picks up `_function_record_pickle_helper_v1`. For example (tensorflow_text/core/pybinds/tflite_registrar.pyi): ```diff +from typing import Any + +def _function_record_pickle_helper_v1(*args, **kwargs) -> Any: ... ``` --- include/pybind11/detail/function_record_pyobject.h | 5 +++-- include/pybind11/pybind11.h | 5 ----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index 40942f45..cff8a245 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -214,8 +214,9 @@ inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { && PyModule_Check(rec->scope.ptr()) != 0) { object scope_module = get_scope_module(rec->scope); if (scope_module) { - return make_tuple(get_function_record_pickle_helper(rec->scope), - make_tuple(make_tuple(scope_module, rec->name))) + return make_tuple(reinterpret_borrow(PyEval_GetBuiltins())["eval"], + make_tuple(str("__import__('importlib').import_module('") + + scope_module + str("')"))) .release() .ptr(); } diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 0dddc0e5..4fe30dd8 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -561,11 +561,6 @@ class cpp_function : public function { guarded_strdup.release(); object scope_module = detail::get_scope_module(rec->scope); - if (rec->name != nullptr && rec->name[0] != '\0' && rec->scope - && PyModule_Check(rec->scope.ptr()) != 0 && scope_module) { - // Call-once initialization. - detail::get_function_record_pickle_helper(rec->scope); - } m_ptr = PyCFunction_NewEx(rec->def, py_func_rec.ptr(), scope_module.ptr()); if (!m_ptr) { pybind11_fail("cpp_function::cpp_function(): Could not allocate function object"); From 1607d5458ba4b7b106f0461fec82ba57bf6253f8 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 14 Feb 2024 08:11:12 -0800 Subject: [PATCH 33/39] Remove `function_record_pickle_helper()` --- .../detail/function_record_pyobject.h | 28 ------------------- include/pybind11/pybind11.h | 22 --------------- 2 files changed, 50 deletions(-) diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index cff8a245..80874dfa 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -150,34 +150,6 @@ inline object function_record_PyObject_New() { return reinterpret_steal((PyObject *) py_func_rec); } -// The implementation needs the definition of `class module_`. -PyObject *function_record_pickle_helper(PyObject *, PyObject *tup_ptr); - -constexpr char function_record_pickle_helper_name[] = "_function_record_pickle_helper_v1"; - -static PyMethodDef function_record_pickle_helper_PyMethodDef - = {function_record_pickle_helper_name, - (PyCFunction) function_record_pickle_helper, - METH_O, - nullptr}; - -inline object get_function_record_pickle_helper(handle mod) { - if (!hasattr(mod, function_record_pickle_helper_name)) { - PyObject *pycfunc = PyCFunction_New(&function_record_pickle_helper_PyMethodDef, nullptr); - if (!pycfunc) { - pybind11_fail("FATAL: get_function_record_pickle_helper() PyCFunction_NewEx() FAILED"); - } - if (PyModule_AddObject( - mod.ptr(), function_record_pickle_helper_name, pycfunc /* steals a reference */) - < 0) { - Py_DECREF(pycfunc); - pybind11_fail( - "FATAL: get_function_record_pickle_helper() PyModule_AddObject() FAILED"); - } - } - return getattr(mod, function_record_pickle_helper_name); -} - PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods) // Guard against accidents & oversights, in particular when porting to future Python versions. diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 4fe30dd8..c20ff766 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1368,28 +1368,6 @@ class module_ : public object { // simplicity, if someone wants to use py::module for example, that is perfectly safe. using module = module_; -PYBIND11_NAMESPACE_BEGIN(detail) - -// This implementation needs the definition of `class module_`. -inline PyObject *function_record_pickle_helper(PyObject *, PyObject *tup_ptr) { - auto tup_obj = tuple(reinterpret_borrow(tup_ptr)); - if (len(tup_obj) != 2) { - set_error(PyExc_ValueError, "Internal inconsistency: len(tup_obj) != 2"); - return nullptr; - } - auto import_module = module_::import("importlib").attr("import_module"); - auto mod = import_module(tup_obj[0]); - auto py_func = mod.attr(tup_obj[1]); - auto namedtuple = module_::import("collections").attr("namedtuple"); - auto proxy_type - = namedtuple(str(function_record_pickle_helper_name) + str("_proxy_") + str(tup_obj[1]), - make_tuple(tup_obj[1])); - auto proxy_obj = proxy_type(py_func); - return proxy_obj.release().ptr(); -} - -PYBIND11_NAMESPACE_END(detail) - /// \ingroup python_builtins /// Return a dictionary representing the global variables in the current execution frame, /// or ``__main__.__dict__`` if there is no frame (usually when the interpreter is embedded). From 8b77252efbe33c4d3c46c68f77402dc09199250c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 14 Feb 2024 08:34:32 -0800 Subject: [PATCH 34/39] Mark `internals::function_record_capsule_name` as OBSOLETE. --- include/pybind11/detail/internals.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index ab2d3f13..5d330f88 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -143,6 +143,7 @@ struct internals { # if PYBIND11_INTERNALS_VERSION > 4 // Note that we have to use a std::string to allocate memory to ensure a unique address // We want unique addresses since we use pointer equality to compare function records + // OBSOLETE: google/pywrapcc#30099 std::string function_record_capsule_name = internals_function_record_capsule_name; # endif From 0731b7198931500574fd0ef9a6bb1dcf5755ea7d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 14 Feb 2024 11:02:37 -0800 Subject: [PATCH 35/39] Add comment pointing to google/pywrapcc#30099 --- include/pybind11/detail/function_record_pyobject.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index 80874dfa..9f59772f 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -2,6 +2,8 @@ // All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// For background see the description of PR google/pywrapcc#30099. + #pragma once #include "../attr.h" From 02b31b1b75979ff36985127056937cd3c3061f2d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 16 Feb 2024 12:46:22 -0800 Subject: [PATCH 36/39] Archive experimental code from video meet with @rainwoodman 2024-02-15 --- tests/test_pickling.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_pickling.py b/tests/test_pickling.py index c3455897..2a86fe5f 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -19,6 +19,33 @@ def test_pickle_simple_callable(protocol): assert b"simple_callable" in serialized deserialized = pickle.loads(serialized) assert deserialized() == 20220426 + assert deserialized is m.simple_callable + orig_red = m.simple_callable.__reduce_ex__(protocol) + desz_red = deserialized.__reduce_ex__(protocol) + orig_fr = orig_red[1][0] + desz_fr = desz_red[1][0] + assert orig_fr is desz_fr + seri_orig_fr = pickle.dumps(orig_fr) + desz_orig_fr = pickle.loads(seri_orig_fr) + print(f"\nLOOOK {dir(m.simple_callable)=}") + print(f"\nLOOOK {repr(orig_fr)=}") + print(f"\nLOOOK {repr(m.simple_callable.__self__)=}") + print(f"\nLOOOK {dir(type(m.simple_callable))=}") + print(f"\nLOOOK {repr(desz_orig_fr)=}", flush=True) + + """ + +(, ("__import__('importlib').import_module('pybind11_tests.pickling').simple_callable.__self__",)) + +LOOOK repr(orig_fr)='' + +LOOOK repr(m.simple_callable.__self__)='' + +LOOOK dir(type(m.simple_callable))=['__call__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__text_signature__'] + +LOOOK repr(desz_orig_fr)="" + + """ @pytest.mark.parametrize("cls_name", ["Pickleable", "PickleableNew"]) From bcdfc6980169dabb7848122602158aeffe41f423 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 16 Feb 2024 13:32:35 -0800 Subject: [PATCH 37/39] Add a pickle roundtrip test starting with `m.simple_callable.__self__` and a long comment to explain the unusual behavior. --- tests/test_pickling.py | 45 +++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/tests/test_pickling.py b/tests/test_pickling.py index 2a86fe5f..fbacb86b 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -20,32 +20,27 @@ def test_pickle_simple_callable(protocol): deserialized = pickle.loads(serialized) assert deserialized() == 20220426 assert deserialized is m.simple_callable - orig_red = m.simple_callable.__reduce_ex__(protocol) - desz_red = deserialized.__reduce_ex__(protocol) - orig_fr = orig_red[1][0] - desz_fr = desz_red[1][0] - assert orig_fr is desz_fr - seri_orig_fr = pickle.dumps(orig_fr) - desz_orig_fr = pickle.loads(seri_orig_fr) - print(f"\nLOOOK {dir(m.simple_callable)=}") - print(f"\nLOOOK {repr(orig_fr)=}") - print(f"\nLOOOK {repr(m.simple_callable.__self__)=}") - print(f"\nLOOOK {dir(type(m.simple_callable))=}") - print(f"\nLOOOK {repr(desz_orig_fr)=}", flush=True) - """ - -(, ("__import__('importlib').import_module('pybind11_tests.pickling').simple_callable.__self__",)) - -LOOOK repr(orig_fr)='' - -LOOOK repr(m.simple_callable.__self__)='' - -LOOOK dir(type(m.simple_callable))=['__call__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__text_signature__'] - -LOOOK repr(desz_orig_fr)="" - - """ + # UNUSUAL: A pickle roundtrip starting with `m.simple_callable.__self__` yields `m`: + assert ( + pickle.loads(pickle.dumps(m.simple_callable.__self__, protocol=protocol)) is m + ) + + # This is not expected to create issues because the only purpose of + # `m.simple_callable.__self__` is to enable pickling: the only method it has is + # `__reduce_ex__`. Direct access for any other purpose is not supported. + # Note that `repr(m.simple_callable.__self__)` shows, e.g.: + # `` + # It is considered to be as much an implementation detail as the + # `pybind11::detail::function_record` C++ type is. + + # @rainwoodman suggested that the unusual pickle roundtrip behavior could be + # avoided by changing `reduce_ex_impl()` to produce, e.g.: + # `"__import__('importlib').import_module('pybind11_tests.pickling').simple_callable.__self__"` + # as the argument for the `eval()` function, and adding a getter to the + # `function_record_PyTypeObject` that returns `self`. However, the additional code complexity + # for this is deemed warranted only if the unusual pickle roundtrip behavior actually + # creates issues. @pytest.mark.parametrize("cls_name", ["Pickleable", "PickleableNew"]) From 5793372bfcc1e610ae8393116866d6f6f3902864 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 16 Feb 2024 13:47:46 -0800 Subject: [PATCH 38/39] PyPy does not have `m.simple_callable.__self__` --- tests/test_pickling.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_pickling.py b/tests/test_pickling.py index fbacb86b..1c9dfec4 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -22,10 +22,11 @@ def test_pickle_simple_callable(protocol): assert deserialized is m.simple_callable # UNUSUAL: A pickle roundtrip starting with `m.simple_callable.__self__` yields `m`: - assert ( - pickle.loads(pickle.dumps(m.simple_callable.__self__, protocol=protocol)) is m - ) - + if not env.PYPY: + assert ( + pickle.loads(pickle.dumps(m.simple_callable.__self__, protocol=protocol)) + is m + ) # This is not expected to create issues because the only purpose of # `m.simple_callable.__self__` is to enable pickling: the only method it has is # `__reduce_ex__`. Direct access for any other purpose is not supported. From 1c24995d75c2b54443191d8bcba2f5d927022cdd Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 20 Feb 2024 09:21:13 -0800 Subject: [PATCH 39/39] Change "UNUSUAL" comment as suggested by @rainwoodman (only very slightly differently as suggested). --- tests/test_pickling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pickling.py b/tests/test_pickling.py index 1c9dfec4..2d84ac9f 100644 --- a/tests/test_pickling.py +++ b/tests/test_pickling.py @@ -21,7 +21,7 @@ def test_pickle_simple_callable(protocol): assert deserialized() == 20220426 assert deserialized is m.simple_callable - # UNUSUAL: A pickle roundtrip starting with `m.simple_callable.__self__` yields `m`: + # UNUSUAL: function record pickle roundtrip returns a module, not a function record object: if not env.PYPY: assert ( pickle.loads(pickle.dumps(m.simple_callable.__self__, protocol=protocol))