Revert "Make wrapped C++ functions pickleable (#5580)"
This reverts commit e7e5d6e5bb0af543a2ded6d34163176c3e6ab745.
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ba5f665..d137565 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -186,7 +186,6 @@
include/pybind11/detail/descr.h
include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h
include/pybind11/detail/exception_translation.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/attr.h b/include/pybind11/attr.h
index 9b631fa..d6796f5 100644
--- a/include/pybind11/attr.h
+++ b/include/pybind11/attr.h
@@ -193,7 +193,6 @@
/// 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/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h
deleted file mode 100644
index 82a5742..0000000
--- a/include/pybind11/detail/function_record_pyobject.h
+++ /dev/null
@@ -1,208 +0,0 @@
-// Copyright (c) 2024-2025 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.
-
-// For background see the description of PR google/pybind11clif#30099.
-
-#pragma once
-
-#include <pybind11/attr.h>
-#include <pybind11/conduit/pybind11_platform_abi_id.h>
-#include <pybind11/pytypes.h>
-
-#include "common.h"
-
-#include <cstring>
-#include <utility>
-
-PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
-PYBIND11_NAMESPACE_BEGIN(detail)
-
-struct function_record_PyObject {
- PyObject_HEAD
- function_record *cpp_func_rec;
-};
-
-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);
-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 *);
-
-static PyMethodDef tp_methods_impl[]
- = {{"__reduce_ex__",
- // reduce_ex_impl is a PyCFunctionWithKeywords, but PyMethodDef
- // requires a PyCFunction. The cast through void* is safe and
- // idiomatic with METH_KEYWORDS, and it successfully sidesteps
- // unhelpful compiler warnings.
- // NOLINTNEXTLINE(bugprone-casting-through-void)
- reinterpret_cast<PyCFunction>(reinterpret_cast<void *>(reduce_ex_impl)),
- METH_VARARGS | METH_KEYWORDS,
- nullptr},
- {nullptr, nullptr, 0, nullptr}};
-
-// 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;
-
-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
-// 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 */ 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,
- /* 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,
- /* vectorcallfunc tp_vectorcall */ nullptr,
-};
-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<object>((PyObject *) py_func_rec);
-}
-
-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.
-}
-
-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 *, PyObject *, PyObject *) {
- pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_init_impl");
- // return -1; // Unreachable.
-}
-
-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 *) {
- // 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
- && PyModule_Check(rec->scope.ptr()) != 0) {
- object scope_module = get_scope_module(rec->scope);
- if (scope_module) {
- auto builtins = reinterpret_borrow<dict>(PyEval_GetBuiltins());
- auto builtins_eval = builtins["eval"];
- auto reconstruct_args = make_tuple(str("__import__('importlib').import_module('")
- + scope_module + str("')"));
- return make_tuple(std::move(builtins_eval), std::move(reconstruct_args))
- .release()
- .ptr();
- }
- }
- set_error(PyExc_RuntimeError, repr(self) + str(" is not pickleable."));
- return nullptr;
-}
-
-PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods)
-
-PYBIND11_NAMESPACE_END(detail)
-PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h
index ad2ff74..58e024b 100644
--- a/include/pybind11/detail/internals.h
+++ b/include/pybind11/detail/internals.h
@@ -37,11 +37,11 @@
/// further ABI-incompatible changes may be made before the ABI is officially
/// changed to the new version.
#ifndef PYBIND11_INTERNALS_VERSION
-# define PYBIND11_INTERNALS_VERSION 11
+# define PYBIND11_INTERNALS_VERSION 12
#endif
-#if PYBIND11_INTERNALS_VERSION < 11
-# error "PYBIND11_INTERNALS_VERSION 11 is the minimum for all platforms for pybind11v3."
+#if PYBIND11_INTERNALS_VERSION < 12
+# error "PYBIND11_INTERNALS_VERSION 12 is the minimum for all platforms for pybind11v3."
#endif
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
@@ -254,6 +254,10 @@
// Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined:
PyInterpreterState *istate = nullptr;
+ // 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
+ std::string function_record_capsule_name = internals_function_record_capsule_name;
+
type_map<PyObject *> native_enum_type_map;
internals()
@@ -747,6 +751,26 @@
return strings.front().c_str();
}
+inline const char *get_function_record_capsule_name() {
+ // On GraalPy, pointer equality of the names is currently not guaranteed
+#if !defined(GRAALVM_PYTHON)
+ return get_internals().function_record_capsule_name.c_str();
+#else
+ return nullptr;
+#endif
+}
+
+// Determine whether or not the following capsule contains a pybind11 function record.
+// Note that we use `internals` to make sure that only ABI compatible records are touched.
+//
+// This check is currently used in two places:
+// - An important optimization in functional.h to avoid overhead in C++ -> Python -> C++
+// - The sibling feature of cpp_function to allow overloads
+inline bool is_function_record_capsule(const capsule &cap) {
+ // Pointer equality as we rely on internals() to ensure unique pointers
+ return cap.name() == get_function_record_capsule_name();
+}
+
PYBIND11_NAMESPACE_END(detail)
/// Returns a named pointer that is shared among all extension modules (using the same
diff --git a/include/pybind11/functional.h b/include/pybind11/functional.h
index 8f59f5f..56b2e60 100644
--- a/include/pybind11/functional.h
+++ b/include/pybind11/functional.h
@@ -91,8 +91,15 @@
auto *cfunc_self = PyCFunction_GET_SELF(cfunc.ptr());
if (cfunc_self == nullptr) {
PyErr_Clear();
- } else {
- function_record *rec = function_record_ptr_from_PyObject(cfunc_self);
+ } else if (isinstance<capsule>(cfunc_self)) {
+ auto c = reinterpret_borrow<capsule>(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<function_record>();
+ }
+
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 06be7f1..7c330c8 100644
--- a/include/pybind11/pybind11.h
+++ b/include/pybind11/pybind11.h
@@ -12,7 +12,6 @@
#include "detail/class.h"
#include "detail/dynamic_raw_ptr_cast_if_possible.h"
#include "detail/exception_translation.h"
-#include "detail/function_record_pyobject.h"
#include "detail/init.h"
#include "detail/native_enum_data.h"
#include "detail/using_smart_holder.h"
@@ -23,7 +22,6 @@
#include "trampoline_self_life_support.h"
#include "typing.h"
-#include <cassert>
#include <cstdlib>
#include <cstring>
#include <memory>
@@ -603,11 +601,20 @@
if (rec->sibling) {
if (PyCFunction_Check(rec->sibling.ptr())) {
auto *self = PyCFunction_GET_SELF(rec->sibling.ptr());
- 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 */
+ if (!isinstance<capsule>(self)) {
chain = nullptr;
+ } else {
+ auto rec_capsule = reinterpret_borrow<capsule>(self);
+ if (detail::is_function_record_capsule(rec_capsule)) {
+ chain = rec_capsule.get_pointer<detail::function_record>();
+ /* 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
@@ -627,14 +634,21 @@
= reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)()>(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();
+ capsule rec_capsule(unique_rec.release(),
+ detail::get_function_record_capsule_name(),
+ [](void *ptr) { destruct((detail::function_record *) ptr); });
guarded_strdup.release();
- object scope_module = detail::get_scope_module(rec->scope);
- m_ptr = PyCFunction_NewEx(rec->def, py_func_rec.ptr(), scope_module.ptr());
+ 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__");
+ }
+ }
+
+ m_ptr = PyCFunction_NewEx(rec->def, rec_capsule.ptr(), scope_module.ptr());
if (!m_ptr) {
pybind11_fail("cpp_function::cpp_function(): Could not allocate function object");
}
@@ -663,9 +677,8 @@
// chain.
chain_start = rec;
rec->next = chain;
- auto *py_func_rec
- = (detail::function_record_PyObject *) PyCFunction_GET_SELF(m_ptr);
- py_func_rec->cpp_func_rec = unique_rec.release();
+ auto rec_capsule = reinterpret_borrow<capsule>(PyCFunction_GET_SELF(m_ptr));
+ rec_capsule.set_pointer(unique_rec.release());
guarded_strdup.release();
} else {
// Or end of chain (normal behavior)
@@ -740,8 +753,6 @@
}
}
- 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.
@@ -791,11 +802,13 @@
/// Main dispatch logic for calls to functions bound using pybind11
static PyObject *dispatcher(PyObject *self, PyObject *args_in, PyObject *kwargs_in) {
using namespace detail;
- const function_record *overloads = function_record_ptr_from_PyObject(self);
- assert(overloads != nullptr);
+ assert(isinstance<capsule>(self));
/* Iterator over the list of potentially admissible overloads */
- const function_record *current_overload = overloads;
+ const function_record *overloads = reinterpret_cast<function_record *>(
+ PyCapsule_GetPointer(self, get_function_record_capsule_name())),
+ *current_overload = overloads;
+ assert(overloads != nullptr);
/* Need to know how many arguments + keyword arguments there are to pick the right
overload */
@@ -1239,17 +1252,6 @@
PYBIND11_NAMESPACE_BEGIN(detail)
-PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods)
-
-// 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;
-}
-
-PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods)
-
template <>
struct handle_type_name<cpp_function> {
static constexpr auto name = const_name("collections.abc.Callable");
@@ -2516,7 +2518,14 @@
if (!func_self) {
throw error_already_set();
}
- return detail::function_record_ptr_from_PyObject(func_self.ptr());
+ if (!isinstance<capsule>(func_self)) {
+ return nullptr;
+ }
+ auto cap = reinterpret_borrow<capsule>(func_self);
+ if (!detail::is_function_record_capsule(cap)) {
+ return nullptr;
+ }
+ return cap.get_pointer<detail::function_record>();
}
};
diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h
index 9c60c94..4b13807 100644
--- a/include/pybind11/pytypes.h
+++ b/include/pybind11/pytypes.h
@@ -1399,18 +1399,6 @@
template <return_value_policy policy = return_value_policy::automatic_reference>
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
diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py
index 63e59f6..454062a 100644
--- a/tests/extra_python_package/test_files.py
+++ b/tests/extra_python_package/test_files.py
@@ -81,7 +81,6 @@
"include/pybind11/detail/cpp_conduit.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/tests/test_pickling.py b/tests/test_pickling.py
index 9febc3f..724c6b9 100644
--- a/tests/test_pickling.py
+++ b/tests/test_pickling.py
@@ -10,42 +10,18 @@
from pybind11_tests import pickling as m
-def all_pickle_protocols():
- assert pickle.HIGHEST_PROTOCOL >= 0
- return range(pickle.HIGHEST_PROTOCOL + 1)
-
-
-@pytest.mark.parametrize("protocol", all_pickle_protocols())
-def test_pickle_simple_callable(protocol):
+def test_pickle_simple_callable():
assert m.simple_callable() == 20220426
- 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
- assert deserialized is m.simple_callable
-
- # 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))
- 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.:
- # `<pybind11_detail_function_record_v1__gcc_libstdcpp_cxxabi1018 object at 0x...>`
- # 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.
+ if env.PYPY:
+ serialized = pickle.dumps(m.simple_callable)
+ deserialized = pickle.loads(serialized)
+ assert deserialized() == 20220426
+ else:
+ # To document broken behavior: currently it fails universally with
+ # all C Python versions.
+ with pytest.raises(TypeError) as excinfo:
+ pickle.dumps(m.simple_callable)
+ assert re.search("can.*t pickle .*[Cc]apsule.* object", str(excinfo.value))
@pytest.mark.parametrize("cls_name", ["Pickleable", "PickleableNew"])