Skip to content

Commit 854cf72

Browse files
rwgkpre-commit-ci[bot]henryiiiSkylion007
authored
[v2.11] Backport: Enable type-safe interoperability between different independent Python/C++ bindings systems. (#5370)
* Enable type-safe interoperability between different independent Python/C++ bindings systems. (#5296) * `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 #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. * Fix sort order accident in tests/CMakeLists.txt * Apply suggestions from code review Co-authored-by: Aaron Gokaslan <[email protected]> * style: pre-commit fixes * refactor: rename to _pybind_conduit_v1_ Signed-off-by: Henry Schreiner <[email protected]> * Add test_home_planet_wrap_very_lonely_traveler(), test_exo_planet_pybind11_wrap_very_lonely_traveler() * Resolve clang-tidy errors: ``` /__w/pybind11/pybind11/tests/test_cpp_conduit_traveler_bindings.h:39:32: error: parameter 'm' is passed by value and only copied once; consider moving it to avoid unnecessary copies [performance-unnecessary-value-param,-warnings-as-errors] 10 | py::class_<LonelyTraveler>(m, "LonelyTraveler"); | ^ | std::move( ) /__w/pybind11/pybind11/tests/test_cpp_conduit_traveler_bindings.h:43:52: error: parameter 'm' is passed by value and only copied once; consider moving it to avoid unnecessary copies [performance-unnecessary-value-param,-warnings-as-errors] 43 | py::class_<VeryLonelyTraveler, LonelyTraveler>(m, "VeryLonelyTraveler"); | ^ | std::move( ) ``` --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Ralf W. Grosse-Kunstleve <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Henry Schreiner <[email protected]> Co-authored-by: Aaron Gokaslan <[email protected]> * Remove `from __future__ import annotations` --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Ralf W. Grosse-Kunstleve <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Henry Schreiner <[email protected]> Co-authored-by: Aaron Gokaslan <[email protected]>
1 parent 8a099e4 commit 854cf72

14 files changed

+522
-6
lines changed

Diff for: CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ cmake_dependent_option(PYBIND11_FINDPYTHON "Force new FindPython" OFF
113113
set(PYBIND11_HEADERS
114114
include/pybind11/detail/class.h
115115
include/pybind11/detail/common.h
116+
include/pybind11/detail/cpp_conduit.h
116117
include/pybind11/detail/descr.h
117118
include/pybind11/detail/init.h
118119
include/pybind11/detail/internals.h

Diff for: include/pybind11/detail/cpp_conduit.h

+77
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("_pybind11_conduit_v1_");
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 1
75+
76+
PYBIND11_NAMESPACE_END(detail)
77+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

Diff for: include/pybind11/detail/internals.h

+6-4
Original file line numberDiff line numberDiff line change
@@ -307,15 +307,17 @@ struct type_info {
307307
# endif
308308
#endif
309309

310+
#define PYBIND11_PLATFORM_ABI_ID \
311+
PYBIND11_INTERNALS_KIND PYBIND11_COMPILER_TYPE PYBIND11_STDLIB PYBIND11_BUILD_ABI \
312+
PYBIND11_BUILD_TYPE
313+
310314
#define PYBIND11_INTERNALS_ID \
311315
"__pybind11_internals_v" PYBIND11_TOSTRING(PYBIND11_INTERNALS_VERSION) \
312-
PYBIND11_INTERNALS_KIND PYBIND11_COMPILER_TYPE PYBIND11_STDLIB PYBIND11_BUILD_ABI \
313-
PYBIND11_BUILD_TYPE "__"
316+
PYBIND11_PLATFORM_ABI_ID "__"
314317

315318
#define PYBIND11_MODULE_LOCAL_ID \
316319
"__pybind11_module_local_v" PYBIND11_TOSTRING(PYBIND11_INTERNALS_VERSION) \
317-
PYBIND11_INTERNALS_KIND PYBIND11_COMPILER_TYPE PYBIND11_STDLIB PYBIND11_BUILD_ABI \
318-
PYBIND11_BUILD_TYPE "__"
320+
PYBIND11_PLATFORM_ABI_ID "__"
319321

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

Diff for: include/pybind11/detail/type_caster_base.h

+40
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111

1212
#include "../pytypes.h"
1313
#include "common.h"
14+
#include "cpp_conduit.h"
1415
#include "descr.h"
1516
#include "internals.h"
1617
#include "typeid.h"
1718

1819
#include <cstdint>
20+
#include <cstring>
1921
#include <iterator>
2022
#include <new>
23+
#include <stdexcept>
2124
#include <string>
2225
#include <type_traits>
2326
#include <typeindex>
@@ -637,6 +640,13 @@ class type_caster_generic {
637640
}
638641
return false;
639642
}
643+
bool try_cpp_conduit(handle src) {
644+
value = try_raw_pointer_ephemeral_from_cpp_conduit(src, cpptype);
645+
if (value != nullptr) {
646+
return true;
647+
}
648+
return false;
649+
}
640650
void check_holder_compat() {}
641651

642652
PYBIND11_NOINLINE static void *local_load(PyObject *src, const type_info *ti) {
@@ -768,6 +778,10 @@ class type_caster_generic {
768778
return true;
769779
}
770780

781+
if (convert && cpptype && this_.try_cpp_conduit(src)) {
782+
return true;
783+
}
784+
771785
return false;
772786
}
773787

@@ -795,6 +809,32 @@ class type_caster_generic {
795809
void *value = nullptr;
796810
};
797811

812+
inline object cpp_conduit_method(handle self,
813+
const bytes &pybind11_platform_abi_id,
814+
const capsule &cpp_type_info_capsule,
815+
const bytes &pointer_kind) {
816+
#ifdef PYBIND11_HAS_STRING_VIEW
817+
using cpp_str = std::string_view;
818+
#else
819+
using cpp_str = std::string;
820+
#endif
821+
if (cpp_str(pybind11_platform_abi_id) != PYBIND11_PLATFORM_ABI_ID) {
822+
return none();
823+
}
824+
if (std::strcmp(cpp_type_info_capsule.name(), typeid(std::type_info).name()) != 0) {
825+
return none();
826+
}
827+
if (cpp_str(pointer_kind) != "raw_pointer_ephemeral") {
828+
throw std::runtime_error("Invalid pointer_kind: \"" + std::string(pointer_kind) + "\"");
829+
}
830+
const auto *cpp_type_info = cpp_type_info_capsule.get_pointer<const std::type_info>();
831+
type_caster_generic caster(*cpp_type_info);
832+
if (!caster.load(self, false)) {
833+
return none();
834+
}
835+
return capsule(caster.value, cpp_type_info->name());
836+
}
837+
798838
/**
799839
* Determine suitable casting operator for pointer-or-lvalue-casting type casters. The type caster
800840
* needs to provide `operator T*()` and `operator T&()` operators.

Diff for: include/pybind11/pybind11.h

+5-2
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,8 @@ class cpp_function : public function {
569569
int index = 0;
570570
/* Create a nice pydoc rec including all signatures and
571571
docstrings of the functions in the overload chain */
572-
if (chain && options::show_function_signatures()) {
572+
if (chain && options::show_function_signatures()
573+
&& std::strcmp(rec->name, "_pybind11_conduit_v1_") != 0) {
573574
// First a generic signature
574575
signatures += rec->name;
575576
signatures += "(*args, **kwargs)\n";
@@ -578,7 +579,8 @@ class cpp_function : public function {
578579
// Then specific overload signatures
579580
bool first_user_def = true;
580581
for (auto *it = chain_start; it != nullptr; it = it->next) {
581-
if (options::show_function_signatures()) {
582+
if (options::show_function_signatures()
583+
&& std::strcmp(rec->name, "_pybind11_conduit_v1_") != 0) {
582584
if (index > 0) {
583585
signatures += '\n';
584586
}
@@ -1558,6 +1560,7 @@ class class_ : public detail::generic_type {
15581560
instances[std::type_index(typeid(type_alias))]
15591561
= instances[std::type_index(typeid(type))];
15601562
}
1563+
def("_pybind11_conduit_v1_", cpp_conduit_method);
15611564
}
15621565

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

Diff for: tests/CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ set(PYBIND11_TEST_FILES
122122
test_const_name
123123
test_constants_and_functions
124124
test_copy_move
125+
test_cpp_conduit
125126
test_custom_type_casters
126127
test_custom_type_setup
127128
test_docstring_options
@@ -219,6 +220,8 @@ tests_extra_targets("test_exceptions.py;test_local_bindings.py;test_stl.py;test_
219220
# And add additional targets for other tests.
220221
tests_extra_targets("test_exceptions.py" "cross_module_interleaved_error_already_set")
221222
tests_extra_targets("test_gil_scoped.py" "cross_module_gil_utils")
223+
tests_extra_targets("test_cpp_conduit.py"
224+
"exo_planet_pybind11;exo_planet_c_api;home_planet_very_lonely_traveler")
222225

223226
set(PYBIND11_EIGEN_REPO
224227
"https://gitlab.com/libeigen/eigen.git"

Diff for: tests/exo_planet_c_api.cpp

+103
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+
"_pybind11_conduit_v1_",
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+
}

Diff for: tests/exo_planet_pybind11.cpp

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
namespace pybind11_tests {
11+
namespace test_cpp_conduit {
12+
13+
PYBIND11_MODULE(exo_planet_pybind11, m) {
14+
wrap_traveler(m);
15+
m.def("wrap_very_lonely_traveler", [m]() { wrap_very_lonely_traveler(m); });
16+
}
17+
18+
} // namespace test_cpp_conduit
19+
} // namespace pybind11_tests

Diff for: tests/extra_python_package/test_files.py

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
detail_headers = {
5050
"include/pybind11/detail/class.h",
5151
"include/pybind11/detail/common.h",
52+
"include/pybind11/detail/cpp_conduit.h",
5253
"include/pybind11/detail/descr.h",
5354
"include/pybind11/detail/init.h",
5455
"include/pybind11/detail/internals.h",

Diff for: tests/home_planet_very_lonely_traveler.cpp

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) 2024 The pybind Community.
2+
3+
#include "test_cpp_conduit_traveler_bindings.h"
4+
5+
namespace pybind11_tests {
6+
namespace test_cpp_conduit {
7+
8+
PYBIND11_MODULE(home_planet_very_lonely_traveler, m) {
9+
m.def("wrap_very_lonely_traveler", [m]() { wrap_very_lonely_traveler(m); });
10+
}
11+
12+
} // namespace test_cpp_conduit
13+
} // namespace pybind11_tests

Diff for: tests/test_cpp_conduit.cpp

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) 2024 The pybind Community.
2+
3+
#include "pybind11_tests.h"
4+
#include "test_cpp_conduit_traveler_bindings.h"
5+
6+
#include <typeinfo>
7+
8+
namespace pybind11_tests {
9+
namespace test_cpp_conduit {
10+
11+
TEST_SUBMODULE(cpp_conduit, m) {
12+
m.attr("PYBIND11_PLATFORM_ABI_ID") = py::bytes(PYBIND11_PLATFORM_ABI_ID);
13+
m.attr("cpp_type_info_capsule_Traveler")
14+
= py::capsule(&typeid(Traveler), typeid(std::type_info).name());
15+
m.attr("cpp_type_info_capsule_int") = py::capsule(&typeid(int), typeid(std::type_info).name());
16+
17+
wrap_traveler(m);
18+
wrap_lonely_traveler(m);
19+
}
20+
21+
} // namespace test_cpp_conduit
22+
} // namespace pybind11_tests

0 commit comments

Comments
 (0)