Skip to content

Commit 4841661

Browse files
Import current pybind/pybind11#5296: Enable type-safe interoperability between different independent Python/C++ bindings systems. (#30158)
* `self.__cpp_transporter__()` proof of concept: Enable passing C++ pointers across extensions even if the `PYBIND11_INTERNALS_VERSION`s do not match. * Include cleanup (mainly to resolve PyPy build failures). * Fix clang-tidy errors. * Resolve `error: extra * factor out platform_abi_id.h from internals.h (no functional changes) * factor out internals_version.h from internals.h (no functional changes) * Update CMakeLists.txt, tests/extra_python_package/test_files.py * Revert "factor out internals_version.h from internals.h (no functional changes)" This reverts commit 3ccea8c. * Remove internals_version.h from CMakeLists.txt, tests/extra_python_package/test_files.py * `.__cpp_transporter__()` implementation: compare `pybind11_platform_abi_id`, `cpp_typeid_name` * Add PremiumTraveler * Rename test_cpp_transporter_traveler_type.h -> test_cpp_transporter_traveler_types.h * Expand tests: `PremiumTraveler`, `get_points()` * Shuffle order of tests (no real changes). * Move `__cpp_transporter__` lambda to `py::cpp_transporter()` regular function. * Use `type_caster_generic::load(self)` instead of `cast<T *>(self)` * Pass `const std::type_info *` via `py::capsule` (instead of `cpp_typeid_name`). * Make platform_abi_id.h completely stand-alone. * rename exo_planet.cpp -> exo_planet_pybind11.cpp * Add exo_planet_c_api.cpp (incomplete). * Fix silly oversight (wrong filename in `#include`). * Resolve clang-tidy errors: ``` /__w/pybind11/pybind11/tests/exo_planet_c_api.cpp:10:18: error: 'wrapGetLuggage' is a static definition in anonymous namespace; static is redundant here [readability-static-definition-in-anonymous-namespace,-warnings-as-errors] 10 | static PyObject *wrapGetLuggage(PyObject *, PyObject *) { return PyUnicode_FromString("TODO"); } | ~~~~~~ ^ /__w/pybind11/pybind11/tests/exo_planet_c_api.cpp:14:20: error: 'ThisMethodDef' is a static definition in anonymous namespace; static is redundant here [readability-static-definition-in-anonymous-namespace,-warnings-as-errors] 14 | static PyMethodDef ThisMethodDef[] | ~~~~~~ ^ /__w/pybind11/pybind11/tests/exo_planet_c_api.cpp:17:27: error: 'ThisModuleDef' is a static definition in anonymous namespace; static is redundant here [readability-static-definition-in-anonymous-namespace,-warnings-as-errors] 17 | static struct PyModuleDef ThisModuleDef = { | ~~~~~~ ^ ``` * Implement exo_planet_c_api GetLuggage(), GetPoints() * Move new code from test_cpp_transporter_traveler_bindings.h to pybind11/detail/type_caster_base.h, under the name `class_dunder_cpp_transporter()` * Fix oversight. * Unconditionally add `__cpp_transporter__` method to all `py::class_` objects, but do not include that magic method in docstring signatures. * Back out pybind11/detail/platform_abi_id.h for now. Maximizing reusability can be handled separately, later. * Small cleanup. * Restore and add to `test_call_cpp_transporter_*()` * Ensure pybind/pybind11#3788 does not bite again. * `class_dunder_cpp_transporter()`: replace `obj.cast<std::string>()` with `std::string(obj)` * Add (simple) copyright notices in all newly added files. * Globally replace cpp_transporter with cpp_conduit * style: pre-commit fixes * IWYU fixes * Rename `class_dunder_cpp_conduit()` -> `cpp_conduit_method()` * Change `pybind11_platform_abi_id`, `pointer_kind` argument types from `str` to `bytes`. This avoids the unicode decode/encode roundtrips: * More robust (no decode/encode errors). * Minor runtime optimization. * Systematically rename `cap_cpp_type_info` -> `cpp_type_info_capsule` (no functional changes). * Systematically replace `cpp_type_info_capsule` `name`: `"const std::type_info *"` -> `typeid(std::type_info).name()` (this IS a functional change). This provides an extra layer of protection against C++ ABI mismatches: * The first and most important layer is that the `PYBIND11_PLATFORM_ABI_ID`s must match between extensions. * The second layer is that the `typeid(std::type_info).name()`s must match between extensions. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 7d51202 commit 4841661

14 files changed

+462
-10
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ endif()
129129
set(PYBIND11_HEADERS
130130
include/pybind11/detail/class.h
131131
include/pybind11/detail/common.h
132+
include/pybind11/detail/cpp_conduit.h
132133
include/pybind11/detail/descr.h
133134
include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h
134135
include/pybind11/detail/function_record_pyobject.h

include/pybind11/detail/cpp_conduit.h

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) 2024 The pybind Community.
2+
3+
#pragma once
4+
5+
#include <pybind11/pytypes.h>
6+
7+
#include "common.h"
8+
#include "internals.h"
9+
10+
#include <typeinfo>
11+
12+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
13+
PYBIND11_NAMESPACE_BEGIN(detail)
14+
15+
// Forward declaration needed here: Refactoring opportunity.
16+
extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *, PyObject *);
17+
18+
inline bool type_is_managed_by_our_internals(PyTypeObject *type_obj) {
19+
#if defined(PYPY_VERSION)
20+
auto &internals = get_internals();
21+
return bool(internals.registered_types_py.find(type_obj)
22+
!= internals.registered_types_py.end());
23+
#else
24+
return bool(type_obj->tp_new == pybind11_object_new);
25+
#endif
26+
}
27+
28+
inline bool is_instance_method_of_type(PyTypeObject *type_obj, PyObject *attr_name) {
29+
PyObject *descr = _PyType_Lookup(type_obj, attr_name);
30+
return bool((descr != nullptr) && PyInstanceMethod_Check(descr));
31+
}
32+
33+
inline object try_get_cpp_conduit_method(PyObject *obj) {
34+
if (PyType_Check(obj)) {
35+
return object();
36+
}
37+
PyTypeObject *type_obj = Py_TYPE(obj);
38+
str attr_name("__cpp_conduit__");
39+
bool assumed_to_be_callable = false;
40+
if (type_is_managed_by_our_internals(type_obj)) {
41+
if (!is_instance_method_of_type(type_obj, attr_name.ptr())) {
42+
return object();
43+
}
44+
assumed_to_be_callable = true;
45+
}
46+
PyObject *method = PyObject_GetAttr(obj, attr_name.ptr());
47+
if (method == nullptr) {
48+
PyErr_Clear();
49+
return object();
50+
}
51+
if (!assumed_to_be_callable && PyCallable_Check(method) == 0) {
52+
Py_DECREF(method);
53+
return object();
54+
}
55+
return reinterpret_steal<object>(method);
56+
}
57+
58+
inline void *try_raw_pointer_ephemeral_from_cpp_conduit(handle src,
59+
const std::type_info *cpp_type_info) {
60+
object method = try_get_cpp_conduit_method(src.ptr());
61+
if (method) {
62+
capsule cpp_type_info_capsule(const_cast<void *>(static_cast<const void *>(cpp_type_info)),
63+
typeid(std::type_info).name());
64+
object cpp_conduit = method(bytes(PYBIND11_PLATFORM_ABI_ID),
65+
cpp_type_info_capsule,
66+
bytes("raw_pointer_ephemeral"));
67+
if (isinstance<capsule>(cpp_conduit)) {
68+
return reinterpret_borrow<capsule>(cpp_conduit).get_pointer();
69+
}
70+
}
71+
return nullptr;
72+
}
73+
74+
#define PYBIND11_HAS_CPP_CONDUIT
75+
76+
PYBIND11_NAMESPACE_END(detail)
77+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

include/pybind11/detail/internals.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,15 +353,17 @@ struct type_info {
353353
# define PYBIND11_INTERNALS_KIND ""
354354
#endif
355355

356+
#define PYBIND11_PLATFORM_ABI_ID \
357+
PYBIND11_INTERNALS_KIND PYBIND11_COMPILER_TYPE PYBIND11_STDLIB PYBIND11_BUILD_ABI \
358+
PYBIND11_BUILD_TYPE
359+
356360
#define PYBIND11_INTERNALS_ID \
357361
"__pybind11_internals_v" PYBIND11_TOSTRING(PYBIND11_INTERNALS_VERSION) \
358-
PYBIND11_INTERNALS_KIND PYBIND11_COMPILER_TYPE PYBIND11_STDLIB \
359-
PYBIND11_BUILD_ABI PYBIND11_BUILD_TYPE "__"
362+
PYBIND11_PLATFORM_ABI_ID "__"
360363

361364
#define PYBIND11_MODULE_LOCAL_ID \
362365
"__pybind11_module_local_v" PYBIND11_TOSTRING(PYBIND11_INTERNALS_VERSION) \
363-
PYBIND11_INTERNALS_KIND PYBIND11_COMPILER_TYPE PYBIND11_STDLIB \
364-
PYBIND11_BUILD_ABI PYBIND11_BUILD_TYPE "__"
366+
PYBIND11_PLATFORM_ABI_ID "__"
365367

366368
/// Each module locally stores a pointer to the `internals` data. The data
367369
/// itself is shared among modules with the same `PYBIND11_INTERNALS_ID`.

include/pybind11/detail/try_as_void_ptr_capsule_get_pointer.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
1717
PYBIND11_NAMESPACE_BEGIN(detail)
1818

19+
// Forward declaration needed here: Refactoring opportunity.
20+
extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *, PyObject *);
21+
22+
// To avoid conflicts with PR pybind/pybind11#5296 before it is merged.
23+
PYBIND11_NAMESPACE_BEGIN(pybind11clif)
24+
1925
// Replace all occurrences of substrings in a string.
2026
inline void replace_all(std::string &str, const std::string &from, const std::string &to) {
2127
if (str.empty()) {
@@ -28,9 +34,6 @@ inline void replace_all(std::string &str, const std::string &from, const std::st
2834
}
2935
}
3036

31-
// Forward declaration needed here: Refactoring opportunity.
32-
extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *, PyObject *);
33-
3437
inline bool type_is_pybind11_class_(PyTypeObject *type_obj) {
3538
#if defined(PYPY_VERSION)
3639
auto &internals = get_internals();
@@ -86,5 +89,6 @@ inline void *try_as_void_ptr_capsule_get_pointer(handle src, const char *typeid_
8689

8790
#define PYBIND11_HAS_TRY_AS_VOID_PTR_CAPSULE_GET_POINTER
8891

92+
PYBIND11_NAMESPACE_END(pybind11clif)
8993
PYBIND11_NAMESPACE_END(detail)
9094
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

include/pybind11/detail/type_caster_base.h

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <pybind11/trampoline_self_life_support.h>
1515

1616
#include "common.h"
17+
#include "cpp_conduit.h"
1718
#include "descr.h"
1819
#include "dynamic_raw_ptr_cast_if_possible.h"
1920
#include "internals.h"
@@ -23,8 +24,10 @@
2324
#include "value_and_holder.h"
2425

2526
#include <cstdint>
27+
#include <cstring>
2628
#include <iterator>
2729
#include <new>
30+
#include <stdexcept>
2831
#include <string>
2932
#include <type_traits>
3033
#include <typeindex>
@@ -993,14 +996,22 @@ class type_caster_generic {
993996
}
994997
return false;
995998
}
999+
bool try_cpp_conduit(handle src) {
1000+
value = try_raw_pointer_ephemeral_from_cpp_conduit(src, cpptype);
1001+
if (value != nullptr) {
1002+
return true;
1003+
}
1004+
return false;
1005+
}
1006+
9961007
bool try_as_void_ptr_capsule(handle src) {
9971008
#ifdef PYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT
9981009
// The `as_void_ptr_capsule` feature is needed for PyCLIF-SWIG interoperability
9991010
// in the Google-internal environment, but the current implementation is lacking
10001011
// any safety checks. To lower the risk potential, the feature is activated
10011012
// only if the smart_holder is used (PyCLIF-pybind11 uses `classh`).
10021013
if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) {
1003-
value = try_as_void_ptr_capsule_get_pointer(src, cpptype->name());
1014+
value = pybind11clif::try_as_void_ptr_capsule_get_pointer(src, cpptype->name());
10041015
if (value != nullptr) {
10051016
return true;
10061017
}
@@ -1139,6 +1150,10 @@ class type_caster_generic {
11391150
return true;
11401151
}
11411152

1153+
if (convert && cpptype && this_.try_cpp_conduit(src)) {
1154+
return true;
1155+
}
1156+
11421157
if (convert && cpptype && this_.try_as_void_ptr_capsule(src)) {
11431158
return true;
11441159
}
@@ -1170,6 +1185,32 @@ class type_caster_generic {
11701185
void *value = nullptr;
11711186
};
11721187

1188+
inline object cpp_conduit_method(handle self,
1189+
const bytes &pybind11_platform_abi_id,
1190+
const capsule &cpp_type_info_capsule,
1191+
const bytes &pointer_kind) {
1192+
#ifdef PYBIND11_HAS_STRING_VIEW
1193+
using cpp_str = std::string_view;
1194+
#else
1195+
using cpp_str = std::string;
1196+
#endif
1197+
if (cpp_str(pybind11_platform_abi_id) != PYBIND11_PLATFORM_ABI_ID) {
1198+
return none();
1199+
}
1200+
if (std::strcmp(cpp_type_info_capsule.name(), typeid(std::type_info).name()) != 0) {
1201+
return none();
1202+
}
1203+
if (cpp_str(pointer_kind) != "raw_pointer_ephemeral") {
1204+
throw std::runtime_error("Invalid pointer_kind: \"" + std::string(pointer_kind) + "\"");
1205+
}
1206+
const auto *cpp_type_info = cpp_type_info_capsule.get_pointer<const std::type_info>();
1207+
type_caster_generic caster(*cpp_type_info);
1208+
if (!caster.load(self, false)) {
1209+
return none();
1210+
}
1211+
return capsule(caster.value, cpp_type_info->name());
1212+
}
1213+
11731214
/**
11741215
* Determine suitable casting operator for pointer-or-lvalue-casting type casters. The type caster
11751216
* needs to provide `operator T*()` and `operator T&()` operators.

include/pybind11/pybind11.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,8 @@ class cpp_function : public function {
608608
int index = 0;
609609
/* Create a nice pydoc rec including all signatures and
610610
docstrings of the functions in the overload chain */
611-
if (chain && options::show_function_signatures()) {
611+
if (chain && options::show_function_signatures()
612+
&& strcmp(rec->name, "__cpp_conduit__") != 0) {
612613
// First a generic signature
613614
signatures += rec->name;
614615
signatures += "(*args, **kwargs)\n";
@@ -617,7 +618,7 @@ class cpp_function : public function {
617618
// Then specific overload signatures
618619
bool first_user_def = true;
619620
for (auto *it = chain_start; it != nullptr; it = it->next) {
620-
if (options::show_function_signatures()) {
621+
if (options::show_function_signatures() && strcmp(rec->name, "__cpp_conduit__") != 0) {
621622
if (index > 0) {
622623
signatures += '\n';
623624
}
@@ -1955,6 +1956,7 @@ class class_ : public detail::generic_type {
19551956
= instances[std::type_index(typeid(type))];
19561957
});
19571958
}
1959+
def("__cpp_conduit__", cpp_conduit_method);
19581960
}
19591961

19601962
template <typename Base, detail::enable_if_t<is_base<Base>::value, int> = 0>

tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ set(PYBIND11_TEST_FILES
138138
test_const_name
139139
test_constants_and_functions
140140
test_copy_move
141+
test_cpp_conduit
141142
test_custom_type_casters
142143
test_custom_type_setup
143144
test_descr_src_loc
@@ -245,6 +246,7 @@ tests_extra_targets("test_exceptions.py;test_local_bindings.py;test_stl.py;test_
245246
# And add additional targets for other tests.
246247
tests_extra_targets("test_exceptions.py" "cross_module_interleaved_error_already_set")
247248
tests_extra_targets("test_gil_scoped.py" "cross_module_gil_utils")
249+
tests_extra_targets("test_cpp_conduit.py" "exo_planet_pybind11;exo_planet_c_api")
248250
tests_extra_targets("test_exc_namespace_visibility.py"
249251
"namespace_visibility_1;namespace_visibility_2")
250252

tests/exo_planet_c_api.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) 2024 The pybind Community.
2+
3+
// THIS MUST STAY AT THE TOP!
4+
#include <pybind11/pybind11.h> // EXCLUSIVELY for PYBIND11_PLATFORM_ABI_ID
5+
// Potential future direction to maximize reusability:
6+
// (e.g. for use from SWIG, Cython, PyCLIF, nanobind):
7+
// #include <pybind11/compat/platform_abi_id.h>
8+
// This would only depend on:
9+
// 1. A C++ compiler, WITHOUT requiring -fexceptions.
10+
// 2. Python.h
11+
12+
#include "test_cpp_conduit_traveler_types.h"
13+
14+
#include <Python.h>
15+
#include <typeinfo>
16+
17+
namespace {
18+
19+
void *get_cpp_conduit_void_ptr(PyObject *py_obj, const std::type_info *cpp_type_info) {
20+
PyObject *cpp_type_info_capsule
21+
= PyCapsule_New(const_cast<void *>(static_cast<const void *>(cpp_type_info)),
22+
typeid(std::type_info).name(),
23+
nullptr);
24+
if (cpp_type_info_capsule == nullptr) {
25+
return nullptr;
26+
}
27+
PyObject *cpp_conduit = PyObject_CallMethod(py_obj,
28+
"__cpp_conduit__",
29+
"yOy",
30+
PYBIND11_PLATFORM_ABI_ID,
31+
cpp_type_info_capsule,
32+
"raw_pointer_ephemeral");
33+
Py_DECREF(cpp_type_info_capsule);
34+
if (cpp_conduit == nullptr) {
35+
return nullptr;
36+
}
37+
void *void_ptr = PyCapsule_GetPointer(cpp_conduit, cpp_type_info->name());
38+
Py_DECREF(cpp_conduit);
39+
if (PyErr_Occurred()) {
40+
return nullptr;
41+
}
42+
return void_ptr;
43+
}
44+
45+
template <typename T>
46+
T *get_cpp_conduit_type_ptr(PyObject *py_obj) {
47+
void *void_ptr = get_cpp_conduit_void_ptr(py_obj, &typeid(T));
48+
if (void_ptr == nullptr) {
49+
return nullptr;
50+
}
51+
return static_cast<T *>(void_ptr);
52+
}
53+
54+
extern "C" PyObject *wrapGetLuggage(PyObject * /*self*/, PyObject *traveler) {
55+
const auto *cpp_traveler
56+
= get_cpp_conduit_type_ptr<pybind11_tests::test_cpp_conduit::Traveler>(traveler);
57+
if (cpp_traveler == nullptr) {
58+
return nullptr;
59+
}
60+
return PyUnicode_FromString(cpp_traveler->luggage.c_str());
61+
}
62+
63+
extern "C" PyObject *wrapGetPoints(PyObject * /*self*/, PyObject *premium_traveler) {
64+
const auto *cpp_premium_traveler
65+
= get_cpp_conduit_type_ptr<pybind11_tests::test_cpp_conduit::PremiumTraveler>(
66+
premium_traveler);
67+
if (cpp_premium_traveler == nullptr) {
68+
return nullptr;
69+
}
70+
return PyLong_FromLong(static_cast<long>(cpp_premium_traveler->points));
71+
}
72+
73+
PyMethodDef ThisMethodDef[] = {{"GetLuggage", wrapGetLuggage, METH_O, nullptr},
74+
{"GetPoints", wrapGetPoints, METH_O, nullptr},
75+
{nullptr, nullptr, 0, nullptr}};
76+
77+
struct PyModuleDef ThisModuleDef = {
78+
PyModuleDef_HEAD_INIT, // m_base
79+
"exo_planet_c_api", // m_name
80+
nullptr, // m_doc
81+
-1, // m_size
82+
ThisMethodDef, // m_methods
83+
nullptr, // m_slots
84+
nullptr, // m_traverse
85+
nullptr, // m_clear
86+
nullptr // m_free
87+
};
88+
89+
} // namespace
90+
91+
#if defined(WIN32) || defined(_WIN32)
92+
# define EXO_PLANET_C_API_EXPORT __declspec(dllexport)
93+
#else
94+
# define EXO_PLANET_C_API_EXPORT __attribute__((visibility("default")))
95+
#endif
96+
97+
extern "C" EXO_PLANET_C_API_EXPORT PyObject *PyInit_exo_planet_c_api() {
98+
PyObject *m = PyModule_Create(&ThisModuleDef);
99+
if (m == nullptr) {
100+
return nullptr;
101+
}
102+
return m;
103+
}

tests/exo_planet_pybind11.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) 2024 The pybind Community.
2+
3+
#if defined(PYBIND11_INTERNALS_VERSION)
4+
# undef PYBIND11_INTERNALS_VERSION
5+
#endif
6+
#define PYBIND11_INTERNALS_VERSION 900000001
7+
8+
#include "test_cpp_conduit_traveler_bindings.h"
9+
10+
PYBIND11_MODULE(exo_planet_pybind11, m) { pybind11_tests::test_cpp_conduit::wrap_traveler(m); }

tests/extra_python_package/test_files.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
detail_headers = {
5757
"include/pybind11/detail/class.h",
5858
"include/pybind11/detail/common.h",
59+
"include/pybind11/detail/cpp_conduit.h",
5960
"include/pybind11/detail/descr.h",
6061
"include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h",
6162
"include/pybind11/detail/function_record_pyobject.h",

0 commit comments

Comments
 (0)