diff --git a/include/pybind11/detail/function_record_pyobject.h b/include/pybind11/detail/function_record_pyobject.h index fce08f3cf9..c7767eae62 100644 --- a/include/pybind11/detail/function_record_pyobject.h +++ b/include/pybind11/detail/function_record_pyobject.h @@ -31,6 +31,9 @@ void tp_dealloc_impl(PyObject *self); void tp_free_impl(void *self); static PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *); +static PyObject *get_capsule_for_scipy_LowLevelCallable_NO_ABI_OR_TYPE_SAFETY_impl(PyObject *self, + PyObject *, + PyObject *); PYBIND11_WARNING_PUSH #if defined(__GNUC__) && __GNUC__ >= 8 @@ -41,6 +44,10 @@ PYBIND11_WARNING_DISABLE_CLANG("-Wcast-function-type-mismatch") #endif static PyMethodDef tp_methods_impl[] = {{"__reduce_ex__", (PyCFunction) reduce_ex_impl, METH_VARARGS | METH_KEYWORDS, nullptr}, + {"get_capsule_for_scipy_LowLevelCallable_NO_ABI_OR_TYPE_SAFETY", + (PyCFunction) get_capsule_for_scipy_LowLevelCallable_NO_ABI_OR_TYPE_SAFETY_impl, + METH_VARARGS | METH_KEYWORDS, + "for use with scipy.LowLevelCallable()"}, {nullptr, nullptr, 0, nullptr}}; PYBIND11_WARNING_POP @@ -49,6 +56,23 @@ constexpr char tp_name_impl[] = "pybind11_detail_function_record_" PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID "_" PYBIND11_PLATFORM_ABI_ID; +inline PyObject *get_property_pybind11_detail_function_record_abi_id(PyObject *, void *) { + return PyUnicode_FromString(PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID); +} + +inline PyObject *get_property_pybind11_platform_abi_id(PyObject *, void *) { + return PyUnicode_FromString(PYBIND11_PLATFORM_ABI_ID); +} + +static PyGetSetDef tp_getset_impl[] = { + {"PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID", + get_property_pybind11_detail_function_record_abi_id, + nullptr, + nullptr, + nullptr}, + {"PYBIND11_PLATFORM_ABI_ID", get_property_pybind11_platform_abi_id, nullptr, nullptr, nullptr}, + {nullptr, nullptr, nullptr, nullptr, nullptr}}; + PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods) // Designated initializers are a C++20 feature: @@ -89,7 +113,7 @@ static PyTypeObject function_record_PyTypeObject = { /* 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 PyGetSetDef *tp_getset */ function_record_PyTypeObject_methods::tp_getset_impl, /* struct _typeobject *tp_base */ nullptr, /* PyObject *tp_dict */ nullptr, /* descrgetfunc tp_descr_get */ nullptr, @@ -202,6 +226,38 @@ inline PyObject *reduce_ex_impl(PyObject *self, PyObject *, PyObject *) { return nullptr; } +inline PyObject *get_capsule_for_scipy_LowLevelCallable_NO_ABI_OR_TYPE_SAFETY_impl( + PyObject *self, PyObject *args, PyObject *kwargs) { + static const char *kwlist[] = {"signature", nullptr}; + const char *signature = nullptr; + if (PyArg_ParseTupleAndKeywords(args, kwargs, "s", const_cast(kwlist), &signature) + == 0) { + return nullptr; + } + function_record *rec = function_record_ptr_from_PyObject(self); + if (rec == nullptr) { + pybind11_fail("FATAL: get_capsule_for_scipy_LowLevelCallable_impl(): cannot obtain C++ " + "function_record."); + } + if (!rec->is_stateless) { + set_error(PyExc_TypeError, repr(self) + str(" is not a stateless function.")); + return nullptr; + } + // This struct must match the layout of `struct capture` in pybind11/functional.h + struct type_erased_capture { + // Return (*)(Args...) in pybind11/functional.h + void (*void_func_ptr)(); // TYPE SAFETY IS LOST COMPLETELY HERE. + + static type_erased_capture *from_data(void **data) { + return PYBIND11_STD_LAUNDER(reinterpret_cast(data)); + } + }; + PYBIND11_ENSURE_PRECONDITION_FOR_FUNCTIONAL_H_PERFORMANCE_OPTIMIZATIONS( + std::is_standard_layout::value); + auto *tec = type_erased_capture::from_data(rec->data); + return capsule(reinterpret_cast(tec->void_func_ptr), signature).release().ptr(); +} + PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods) PYBIND11_NAMESPACE_END(detail) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1b65729dfd..a93e0479e4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -163,6 +163,7 @@ set(PYBIND11_TEST_FILES test_pickling test_python_multiple_inheritance test_pytypes + test_scipy_low_level_callable test_sequences_and_iterators test_smart_ptr test_stl diff --git a/tests/test_scipy_low_level_callable.cpp b/tests/test_scipy_low_level_callable.cpp new file mode 100644 index 0000000000..d41979352b --- /dev/null +++ b/tests/test_scipy_low_level_callable.cpp @@ -0,0 +1,15 @@ +#include "pybind11_tests.h" + +namespace pybind11_tests { +namespace scipy_low_level_callable { + +double square21(double x) { return x * x * 21; } + +} // namespace scipy_low_level_callable +} // namespace pybind11_tests + +TEST_SUBMODULE(scipy_low_level_callable, m) { + using namespace pybind11_tests::scipy_low_level_callable; + + m.def("square21", square21); +} diff --git a/tests/test_scipy_low_level_callable.py b/tests/test_scipy_low_level_callable.py new file mode 100644 index 0000000000..5a1173f382 --- /dev/null +++ b/tests/test_scipy_low_level_callable.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import pytest + +import pybind11_tests +from pybind11_tests import scipy_low_level_callable as m + + +def test_square21(): + assert m.square21(2.0) == 2.0 * 2.0 * 21 + + +def _m_square21_self(): + try: + return m.square21.__self__ + except AttributeError as e: + pytest.skip(f"{str(e)}") + + +def test_python_function_record_static_properties(): + func_rec = _m_square21_self() + assert func_rec.PYBIND11_DETAIL_FUNCTION_RECORD_ABI_ID == "v1" + assert func_rec.PYBIND11_PLATFORM_ABI_ID in pybind11_tests.PYBIND11_INTERNALS_ID + + +def test_get_capsule_for_scipy_LowLevelCallable(): + cap = ( + _m_square21_self().get_capsule_for_scipy_LowLevelCallable_NO_ABI_OR_TYPE_SAFETY( + signature="double (double)" + ) + ) + assert repr(cap).startswith("