Skip to content

Commit 75f61be

Browse files
committed
1 parent f365314 commit 75f61be

File tree

9 files changed

+293
-88
lines changed

9 files changed

+293
-88
lines changed

CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ set(PYBIND11_HEADERS
134134
include/pybind11/detail/cpp_conduit.h
135135
include/pybind11/detail/descr.h
136136
include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h
137+
include/pybind11/detail/function_record_pyobject.h
137138
include/pybind11/detail/init.h
138139
include/pybind11/detail/internals.h
139140
include/pybind11/detail/native_enum_data.h

include/pybind11/attr.h

+1
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ struct argument_record {
192192

193193
/// Internal data structure which holds metadata about a bound function (signature, overloads,
194194
/// etc.)
195+
#define PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID "v1" // PLEASE UPDATE if the struct is changed.
195196
struct function_record {
196197
function_record()
197198
: is_constructor(false), is_new_style_constructor(false), is_stateless(false),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright (c) 2024-2025 The Pybind Development Team.
2+
// All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// For background see the description of PR google/pybind11clif#30099.
6+
7+
#pragma once
8+
9+
#include <pybind11/attr.h>
10+
#include <pybind11/conduit/pybind11_platform_abi_id.h>
11+
#include <pybind11/pytypes.h>
12+
13+
#include "common.h"
14+
15+
#include <cstring>
16+
17+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
18+
PYBIND11_NAMESPACE_BEGIN(detail)
19+
20+
struct function_record_PyObject {
21+
PyObject_HEAD
22+
function_record *cpp_func_rec;
23+
};
24+
25+
PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods)
26+
27+
PyObject *tp_new_impl(PyTypeObject *type, PyObject *args, PyObject *kwds);
28+
PyObject *tp_alloc_impl(PyTypeObject *type, Py_ssize_t nitems);
29+
int tp_init_impl(PyObject *self, PyObject *args, PyObject *kwds);
30+
void tp_dealloc_impl(PyObject *self);
31+
void tp_free_impl(void *self);
32+
33+
static PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *);
34+
35+
PYBIND11_WARNING_PUSH
36+
#if defined(__GNUC__) && __GNUC__ >= 8
37+
PYBIND11_WARNING_DISABLE_GCC("-Wcast-function-type")
38+
#endif
39+
static PyMethodDef tp_methods_impl[]
40+
= {{"__reduce_ex__", (PyCFunction) reduce_ex_impl, METH_VARARGS | METH_KEYWORDS, nullptr},
41+
{nullptr, nullptr, 0, nullptr}};
42+
PYBIND11_WARNING_POP
43+
44+
// Note that this name is versioned.
45+
constexpr char tp_name_impl[]
46+
= "pybind11_detail_function_record_" PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID
47+
"_" PYBIND11_PLATFORM_ABI_ID;
48+
49+
PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods)
50+
51+
// Designated initializers are a C++20 feature:
52+
// https://en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers
53+
// MSVC rejects them unless /std:c++20 is used (error code C7555).
54+
PYBIND11_WARNING_PUSH
55+
PYBIND11_WARNING_DISABLE_CLANG("-Wmissing-field-initializers")
56+
#if defined(__GNUC__) && __GNUC__ >= 8
57+
PYBIND11_WARNING_DISABLE_GCC("-Wmissing-field-initializers")
58+
#endif
59+
static PyTypeObject function_record_PyTypeObject = {
60+
PyVarObject_HEAD_INIT(nullptr, 0)
61+
/* const char *tp_name */ function_record_PyTypeObject_methods::tp_name_impl,
62+
/* Py_ssize_t tp_basicsize */ sizeof(function_record_PyObject),
63+
/* Py_ssize_t tp_itemsize */ 0,
64+
/* destructor tp_dealloc */ function_record_PyTypeObject_methods::tp_dealloc_impl,
65+
/* Py_ssize_t tp_vectorcall_offset */ 0,
66+
/* getattrfunc tp_getattr */ nullptr,
67+
/* setattrfunc tp_setattr */ nullptr,
68+
/* PyAsyncMethods *tp_as_async */ nullptr,
69+
/* reprfunc tp_repr */ nullptr,
70+
/* PyNumberMethods *tp_as_number */ nullptr,
71+
/* PySequenceMethods *tp_as_sequence */ nullptr,
72+
/* PyMappingMethods *tp_as_mapping */ nullptr,
73+
/* hashfunc tp_hash */ nullptr,
74+
/* ternaryfunc tp_call */ nullptr,
75+
/* reprfunc tp_str */ nullptr,
76+
/* getattrofunc tp_getattro */ nullptr,
77+
/* setattrofunc tp_setattro */ nullptr,
78+
/* PyBufferProcs *tp_as_buffer */ nullptr,
79+
/* unsigned long tp_flags */ Py_TPFLAGS_DEFAULT,
80+
/* const char *tp_doc */ nullptr,
81+
/* traverseproc tp_traverse */ nullptr,
82+
/* inquiry tp_clear */ nullptr,
83+
/* richcmpfunc tp_richcompare */ nullptr,
84+
/* Py_ssize_t tp_weaklistoffset */ 0,
85+
/* getiterfunc tp_iter */ nullptr,
86+
/* iternextfunc tp_iternext */ nullptr,
87+
/* struct PyMethodDef *tp_methods */ function_record_PyTypeObject_methods::tp_methods_impl,
88+
/* struct PyMemberDef *tp_members */ nullptr,
89+
/* struct PyGetSetDef *tp_getset */ nullptr,
90+
/* struct _typeobject *tp_base */ nullptr,
91+
/* PyObject *tp_dict */ nullptr,
92+
/* descrgetfunc tp_descr_get */ nullptr,
93+
/* descrsetfunc tp_descr_set */ nullptr,
94+
/* Py_ssize_t tp_dictoffset */ 0,
95+
/* initproc tp_init */ function_record_PyTypeObject_methods::tp_init_impl,
96+
/* allocfunc tp_alloc */ function_record_PyTypeObject_methods::tp_alloc_impl,
97+
/* newfunc tp_new */ function_record_PyTypeObject_methods::tp_new_impl,
98+
/* freefunc tp_free */ function_record_PyTypeObject_methods::tp_free_impl,
99+
/* inquiry tp_is_gc */ nullptr,
100+
/* PyObject *tp_bases */ nullptr,
101+
/* PyObject *tp_mro */ nullptr,
102+
/* PyObject *tp_cache */ nullptr,
103+
/* PyObject *tp_subclasses */ nullptr,
104+
/* PyObject *tp_weaklist */ nullptr,
105+
/* destructor tp_del */ nullptr,
106+
/* unsigned int tp_version_tag */ 0,
107+
/* destructor tp_finalize */ nullptr,
108+
#if PY_VERSION_HEX >= 0x03080000
109+
/* vectorcallfunc tp_vectorcall */ nullptr,
110+
#endif
111+
};
112+
PYBIND11_WARNING_POP
113+
114+
static bool function_record_PyTypeObject_PyType_Ready_first_call = true;
115+
116+
inline void function_record_PyTypeObject_PyType_Ready() {
117+
if (function_record_PyTypeObject_PyType_Ready_first_call) {
118+
if (PyType_Ready(&function_record_PyTypeObject) < 0) {
119+
throw error_already_set();
120+
}
121+
function_record_PyTypeObject_PyType_Ready_first_call = false;
122+
}
123+
}
124+
125+
inline bool is_function_record_PyObject(PyObject *obj) {
126+
if (PyType_Check(obj) != 0) {
127+
return false;
128+
}
129+
PyTypeObject *obj_type = Py_TYPE(obj);
130+
// Fast path (pointer comparison).
131+
if (obj_type == &function_record_PyTypeObject) {
132+
return true;
133+
}
134+
// This works across extension modules. Note that tp_name is versioned.
135+
if (strcmp(obj_type->tp_name, function_record_PyTypeObject.tp_name) == 0) {
136+
return true;
137+
}
138+
return false;
139+
}
140+
141+
inline function_record *function_record_ptr_from_PyObject(PyObject *obj) {
142+
if (is_function_record_PyObject(obj)) {
143+
return ((detail::function_record_PyObject *) obj)->cpp_func_rec;
144+
}
145+
return nullptr;
146+
}
147+
148+
inline object function_record_PyObject_New() {
149+
auto *py_func_rec = PyObject_New(function_record_PyObject, &function_record_PyTypeObject);
150+
if (py_func_rec == nullptr) {
151+
throw error_already_set();
152+
}
153+
py_func_rec->cpp_func_rec = nullptr; // For clarity/purity. Redundant in practice.
154+
return reinterpret_steal<object>((PyObject *) py_func_rec);
155+
}
156+
157+
PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods)
158+
159+
// Guard against accidents & oversights, in particular when porting to future Python versions.
160+
inline PyObject *tp_new_impl(PyTypeObject *, PyObject *, PyObject *) {
161+
pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_new_impl");
162+
// return nullptr; // Unreachable.
163+
}
164+
165+
inline PyObject *tp_alloc_impl(PyTypeObject *, Py_ssize_t) {
166+
pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_alloc_impl");
167+
// return nullptr; // Unreachable.
168+
}
169+
170+
inline int tp_init_impl(PyObject *, PyObject *, PyObject *) {
171+
pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_init_impl");
172+
// return -1; // Unreachable.
173+
}
174+
175+
// The implementation needs the definition of `class cpp_function`.
176+
void tp_dealloc_impl(PyObject *self);
177+
178+
inline void tp_free_impl(void *) {
179+
pybind11_fail("UNEXPECTED CALL OF function_record_PyTypeObject_methods::tp_free_impl");
180+
}
181+
182+
inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) {
183+
// Deliberately ignoring the arguments for simplicity (expected is `protocol: int`).
184+
const function_record *rec = function_record_ptr_from_PyObject(self);
185+
if (rec == nullptr) {
186+
pybind11_fail(
187+
"FATAL: function_record_PyTypeObject reduce_ex_impl(): cannot obtain cpp_func_rec.");
188+
}
189+
if (rec->name != nullptr && rec->name[0] != '\0' && rec->scope
190+
&& PyModule_Check(rec->scope.ptr()) != 0) {
191+
object scope_module = get_scope_module(rec->scope);
192+
if (scope_module) {
193+
return make_tuple(reinterpret_borrow<object>(PyEval_GetBuiltins())["eval"],
194+
make_tuple(str("__import__('importlib').import_module('")
195+
+ scope_module + str("')")))
196+
.release()
197+
.ptr();
198+
}
199+
}
200+
set_error(PyExc_RuntimeError, repr(self) + str(" is not pickleable."));
201+
return nullptr;
202+
}
203+
204+
PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods)
205+
206+
PYBIND11_NAMESPACE_END(detail)
207+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

include/pybind11/detail/internals.h

+3-27
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@
3737
/// further ABI-incompatible changes may be made before the ABI is officially
3838
/// changed to the new version.
3939
#ifndef PYBIND11_INTERNALS_VERSION
40-
# define PYBIND11_INTERNALS_VERSION 8
40+
# define PYBIND11_INTERNALS_VERSION 9
4141
#endif
4242

43-
#if PYBIND11_INTERNALS_VERSION < 8
44-
# error "PYBIND11_INTERNALS_VERSION 8 is the minimum for all platforms for pybind11v3."
43+
#if PYBIND11_INTERNALS_VERSION < 9
44+
# error "PYBIND11_INTERNALS_VERSION 9 is the minimum for all platforms for pybind11v3."
4545
#endif
4646

4747
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
@@ -190,10 +190,6 @@ struct internals {
190190
// Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined:
191191
PyInterpreterState *istate = nullptr;
192192

193-
// Note that we have to use a std::string to allocate memory to ensure a unique address
194-
// We want unique addresses since we use pointer equality to compare function records
195-
std::string function_record_capsule_name = internals_function_record_capsule_name;
196-
197193
type_map<PyObject *> native_enum_type_map;
198194

199195
internals() = default;
@@ -612,26 +608,6 @@ const char *c_str(Args &&...args) {
612608
return strings.front().c_str();
613609
}
614610

615-
inline const char *get_function_record_capsule_name() {
616-
// On GraalPy, pointer equality of the names is currently not guaranteed
617-
#if !defined(GRAALVM_PYTHON)
618-
return get_internals().function_record_capsule_name.c_str();
619-
#else
620-
return nullptr;
621-
#endif
622-
}
623-
624-
// Determine whether or not the following capsule contains a pybind11 function record.
625-
// Note that we use `internals` to make sure that only ABI compatible records are touched.
626-
//
627-
// This check is currently used in two places:
628-
// - An important optimization in functional.h to avoid overhead in C++ -> Python -> C++
629-
// - The sibling feature of cpp_function to allow overloads
630-
inline bool is_function_record_capsule(const capsule &cap) {
631-
// Pointer equality as we rely on internals() to ensure unique pointers
632-
return cap.name() == get_function_record_capsule_name();
633-
}
634-
635611
PYBIND11_NAMESPACE_END(detail)
636612

637613
/// Returns a named pointer that is shared among all extension modules (using the same

include/pybind11/functional.h

+2-9
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,8 @@ struct type_caster<std::function<Return(Args...)>> {
9393
auto *cfunc_self = PyCFunction_GET_SELF(cfunc.ptr());
9494
if (cfunc_self == nullptr) {
9595
PyErr_Clear();
96-
} else if (isinstance<capsule>(cfunc_self)) {
97-
auto c = reinterpret_borrow<capsule>(cfunc_self);
98-
99-
function_record *rec = nullptr;
100-
// Check that we can safely reinterpret the capsule into a function_record
101-
if (detail::is_function_record_capsule(c)) {
102-
rec = c.get_pointer<function_record>();
103-
}
104-
96+
} else {
97+
function_record *rec = function_record_ptr_from_PyObject(cfunc_self);
10598
while (rec != nullptr) {
10699
if (rec->is_stateless
107100
&& same_type(typeid(function_type),

0 commit comments

Comments
 (0)