Skip to content

Commit ee2b522

Browse files
EthanSteinbergpre-commit-ci[bot]Skylion007
authored
Fix functional.h bug + introduce test to verify that it is fixed (#4254)
* Illustrate bug in functional.h * style: pre-commit fixes * Make functional casting more robust / add workaround * Make function_record* casting even more robust * See if this fixes PyPy issue * It still fails on PyPy sadly * Do not make new CTOR just yet * Fix test * Add name to ensure correctness * style: pre-commit fixes * Clean up tests + remove ifdef guards * Add comments * Improve comments, error handling, and safety * Fix compile error * Fix magic logic * Extract helper function * Fix func signature * move to local internals * style: pre-commit fixes * Switch to simpler design * style: pre-commit fixes * Move to function_record * style: pre-commit fixes * Switch to internals, update tests and docs * Fix lint * Oops, forgot to resolve last comment * Fix typo * Update in response to comments * Implement suggestion to improve test * Update comment * Simple fixes Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Aaron Gokaslan <[email protected]>
1 parent 0176632 commit ee2b522

File tree

5 files changed

+124
-12
lines changed

5 files changed

+124
-12
lines changed

include/pybind11/detail/internals.h

+31
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ using ExceptionTranslator = void (*)(std::exception_ptr);
4343

4444
PYBIND11_NAMESPACE_BEGIN(detail)
4545

46+
constexpr const char *internals_function_record_capsule_name = "pybind11_function_record_capsule";
47+
4648
// Forward declarations
4749
inline PyTypeObject *make_static_property_type();
4850
inline PyTypeObject *make_default_metaclass();
@@ -182,6 +184,16 @@ struct internals {
182184
# endif // PYBIND11_INTERNALS_VERSION > 4
183185
// Unused if PYBIND11_SIMPLE_GIL_MANAGEMENT is defined:
184186
PyInterpreterState *istate = nullptr;
187+
188+
# if PYBIND11_INTERNALS_VERSION > 4
189+
// Note that we have to use a std::string to allocate memory to ensure a unique address
190+
// We want unique addresses since we use pointer equality to compare function records
191+
std::string function_record_capsule_name = internals_function_record_capsule_name;
192+
# endif
193+
194+
internals() = default;
195+
internals(const internals &other) = delete;
196+
internals &operator=(const internals &other) = delete;
185197
~internals() {
186198
# if PYBIND11_INTERNALS_VERSION > 4
187199
PYBIND11_TLS_FREE(loader_life_support_tls_key);
@@ -548,6 +560,25 @@ const char *c_str(Args &&...args) {
548560
return strings.front().c_str();
549561
}
550562

563+
inline const char *get_function_record_capsule_name() {
564+
#if PYBIND11_INTERNALS_VERSION > 4
565+
return get_internals().function_record_capsule_name.c_str();
566+
#else
567+
return nullptr;
568+
#endif
569+
}
570+
571+
// Determine whether or not the following capsule contains a pybind11 function record.
572+
// Note that we use `internals` to make sure that only ABI compatible records are touched.
573+
//
574+
// This check is currently used in two places:
575+
// - An important optimization in functional.h to avoid overhead in C++ -> Python -> C++
576+
// - The sibling feature of cpp_function to allow overloads
577+
inline bool is_function_record_capsule(const capsule &cap) {
578+
// Pointer equality as we rely on internals() to ensure unique pointers
579+
return cap.name() == get_function_record_capsule_name();
580+
}
581+
551582
PYBIND11_NAMESPACE_END(detail)
552583

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

include/pybind11/functional.h

+9-2
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,16 @@ struct type_caster<std::function<Return(Args...)>> {
4848
*/
4949
if (auto cfunc = func.cpp_function()) {
5050
auto *cfunc_self = PyCFunction_GET_SELF(cfunc.ptr());
51-
if (isinstance<capsule>(cfunc_self)) {
51+
if (cfunc_self == nullptr) {
52+
PyErr_Clear();
53+
} else if (isinstance<capsule>(cfunc_self)) {
5254
auto c = reinterpret_borrow<capsule>(cfunc_self);
53-
auto *rec = (function_record *) c;
55+
56+
function_record *rec = nullptr;
57+
// Check that we can safely reinterpret the capsule into a function_record
58+
if (detail::is_function_record_capsule(c)) {
59+
rec = c.get_pointer<function_record>();
60+
}
5461

5562
while (rec != nullptr) {
5663
if (rec->is_stateless

include/pybind11/pybind11.h

+34-10
Original file line numberDiff line numberDiff line change
@@ -468,13 +468,20 @@ class cpp_function : public function {
468468
if (rec->sibling) {
469469
if (PyCFunction_Check(rec->sibling.ptr())) {
470470
auto *self = PyCFunction_GET_SELF(rec->sibling.ptr());
471-
capsule rec_capsule = isinstance<capsule>(self) ? reinterpret_borrow<capsule>(self)
472-
: capsule(self);
473-
chain = (detail::function_record *) rec_capsule;
474-
/* Never append a method to an overload chain of a parent class;
475-
instead, hide the parent's overloads in this case */
476-
if (!chain->scope.is(rec->scope)) {
471+
if (!isinstance<capsule>(self)) {
477472
chain = nullptr;
473+
} else {
474+
auto rec_capsule = reinterpret_borrow<capsule>(self);
475+
if (detail::is_function_record_capsule(rec_capsule)) {
476+
chain = rec_capsule.get_pointer<detail::function_record>();
477+
/* Never append a method to an overload chain of a parent class;
478+
instead, hide the parent's overloads in this case */
479+
if (!chain->scope.is(rec->scope)) {
480+
chain = nullptr;
481+
}
482+
} else {
483+
chain = nullptr;
484+
}
478485
}
479486
}
480487
// Don't trigger for things like the default __init__, which are wrapper_descriptors
@@ -496,6 +503,7 @@ class cpp_function : public function {
496503

497504
capsule rec_capsule(unique_rec.release(),
498505
[](void *ptr) { destruct((detail::function_record *) ptr); });
506+
rec_capsule.set_name(detail::get_function_record_capsule_name());
499507
guarded_strdup.release();
500508

501509
object scope_module;
@@ -661,10 +669,13 @@ class cpp_function : public function {
661669
/// Main dispatch logic for calls to functions bound using pybind11
662670
static PyObject *dispatcher(PyObject *self, PyObject *args_in, PyObject *kwargs_in) {
663671
using namespace detail;
672+
assert(isinstance<capsule>(self));
664673

665674
/* Iterator over the list of potentially admissible overloads */
666-
const function_record *overloads = (function_record *) PyCapsule_GetPointer(self, nullptr),
675+
const function_record *overloads = reinterpret_cast<function_record *>(
676+
PyCapsule_GetPointer(self, get_function_record_capsule_name())),
667677
*it = overloads;
678+
assert(overloads != nullptr);
668679

669680
/* Need to know how many arguments + keyword arguments there are to pick the right
670681
overload */
@@ -1871,9 +1882,22 @@ class class_ : public detail::generic_type {
18711882

18721883
static detail::function_record *get_function_record(handle h) {
18731884
h = detail::get_function(h);
1874-
return h ? (detail::function_record *) reinterpret_borrow<capsule>(
1875-
PyCFunction_GET_SELF(h.ptr()))
1876-
: nullptr;
1885+
if (!h) {
1886+
return nullptr;
1887+
}
1888+
1889+
handle func_self = PyCFunction_GET_SELF(h.ptr());
1890+
if (!func_self) {
1891+
throw error_already_set();
1892+
}
1893+
if (!isinstance<capsule>(func_self)) {
1894+
return nullptr;
1895+
}
1896+
auto cap = reinterpret_borrow<capsule>(func_self);
1897+
if (!detail::is_function_record_capsule(cap)) {
1898+
return nullptr;
1899+
}
1900+
return cap.get_pointer<detail::function_record>();
18771901
}
18781902
};
18791903

tests/test_callbacks.cpp

+37
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,41 @@ TEST_SUBMODULE(callbacks, m) {
240240
f();
241241
}
242242
});
243+
244+
auto *custom_def = []() {
245+
static PyMethodDef def;
246+
def.ml_name = "example_name";
247+
def.ml_doc = "Example doc";
248+
def.ml_meth = [](PyObject *, PyObject *args) -> PyObject * {
249+
if (PyTuple_Size(args) != 1) {
250+
throw std::runtime_error("Invalid number of arguments for example_name");
251+
}
252+
PyObject *first = PyTuple_GetItem(args, 0);
253+
if (!PyLong_Check(first)) {
254+
throw std::runtime_error("Invalid argument to example_name");
255+
}
256+
auto result = py::cast(PyLong_AsLong(first) * 9);
257+
return result.release().ptr();
258+
};
259+
def.ml_flags = METH_VARARGS;
260+
return &def;
261+
}();
262+
263+
// rec_capsule with name that has the same value (but not pointer) as our internal one
264+
// This capsule should be detected by our code as foreign and not inspected as the pointers
265+
// shouldn't match
266+
constexpr const char *rec_capsule_name
267+
= pybind11::detail::internals_function_record_capsule_name;
268+
py::capsule rec_capsule(std::malloc(1), [](void *data) { std::free(data); });
269+
rec_capsule.set_name(rec_capsule_name);
270+
m.add_object("custom_function", PyCFunction_New(custom_def, rec_capsule.ptr()));
271+
272+
// This test requires a new ABI version to pass
273+
#if PYBIND11_INTERNALS_VERSION > 4
274+
// rec_capsule with nullptr name
275+
py::capsule rec_capsule2(std::malloc(1), [](void *data) { std::free(data); });
276+
m.add_object("custom_function2", PyCFunction_New(custom_def, rec_capsule2.ptr()));
277+
#else
278+
m.add_object("custom_function2", py::none());
279+
#endif
243280
}

tests/test_callbacks.py

+13
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,16 @@ def test_callback_num_times():
193193
if len(rates) > 1:
194194
print("Min Mean Max")
195195
print(f"{min(rates):6.3f} {sum(rates) / len(rates):6.3f} {max(rates):6.3f}")
196+
197+
198+
def test_custom_func():
199+
assert m.custom_function(4) == 36
200+
assert m.roundtrip(m.custom_function)(4) == 36
201+
202+
203+
@pytest.mark.skipif(
204+
m.custom_function2 is None, reason="Current PYBIND11_INTERNALS_VERSION too low"
205+
)
206+
def test_custom_func2():
207+
assert m.custom_function2(3) == 27
208+
assert m.roundtrip(m.custom_function2)(3) == 27

0 commit comments

Comments
 (0)