Skip to content

Experiments on top of PR #3643 (pybind11_select_caster) #3674

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a126d9b
Function pybind11_select_caster uses ADL to select the caster.
amauryfa Jan 25, 2022
5cfd67b
Add documentation.:
amauryfa Jan 25, 2022
dfd4bf0
Use correct function name in docs.
amauryfa Jan 25, 2022
43a065a
Attempt to fix compilation error on MSVC 2015
amauryfa Jan 25, 2022
28cef75
Address rst conformance checks.
amauryfa Jan 25, 2022
9100a05
Fix compilation.
amauryfa Jan 25, 2022
13a63ad
Undoing std::string, std::string_view caster changes.
rwgk Feb 1, 2022
222d867
Adding ReferenceSensitiveOptional copy/move/operator= as suggested by…
rwgk Feb 1, 2022
59fadb3
noexcept to make clang-tidy happy; explicit py::return_value_policy::…
rwgk Feb 1, 2022
74d9ee4
Disabling added copy/move/operator=
rwgk Feb 1, 2022
09c664a
Trying pybind11_select_caster(IntrinsicType *);
rwgk Feb 1, 2022
f4e9248
make_caster_impl experiment
rwgk Feb 1, 2022
1c73b00
test_make_caster_adl test_basic
rwgk Feb 1, 2022
62ca662
Undoing all changes in tests/test_stl.cpp
rwgk Feb 1, 2022
7d2e524
Adding test_minimal_real_caster. Keeping only one mock_caster.
rwgk Feb 2, 2022
733d8b1
Python 2 compatibility.
rwgk Feb 2, 2022
6a2ce0c
Make clang-tidy happy.
rwgk Feb 2, 2022
9caa190
Adding dummy returned caster pointer to pybind11_select_caster() over…
rwgk Feb 2, 2022
f6f2ee2
Undoing previous commit.
rwgk Feb 2, 2022
bc4a241
Using workaround only compilers that need it.
rwgk Feb 2, 2022
6f11212
Fixing silly oversight (missing parentheses).
rwgk Feb 2, 2022
9865877
Using enable_if_t to make the workaround code a little more compact.
rwgk Feb 2, 2022
4574aba
Additional mock_caster testing for global & unnamed namespaces.
rwgk Feb 2, 2022
dfd923d
Replacing non-portable warning-suppression pragma with a dummy implem…
rwgk Feb 2, 2022
59c8c77
Adding test_make_caster_adl_alt
rwgk Feb 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 70 additions & 50 deletions docs/advanced/cast/custom.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,63 +31,83 @@ The following Python snippet demonstrates the intended usage from the Python sid

print(A())

To register the necessary conversion routines, it is necessary to add an
instantiation of the ``pybind11::detail::type_caster<T>`` template.
Although this is an implementation detail, adding an instantiation of this
type is explicitly allowed.
To register the necessary conversion routines, it is necessary to define a
caster class, and register it with the other pybind11 casters:

.. code-block:: cpp

namespace pybind11 { namespace detail {
template <> struct type_caster<inty> {
public:
/**
* This macro establishes the name 'inty' in
* function signatures and declares a local variable
* 'value' of type inty
*/
PYBIND11_TYPE_CASTER(inty, const_name("inty"));

/**
* Conversion part 1 (Python->C++): convert a PyObject into a inty
* instance or return false upon failure. The second argument
* indicates whether implicit conversions should be applied.
*/
bool load(handle src, bool) {
/* Extract PyObject from handle */
PyObject *source = src.ptr();
/* Try converting into a Python integer value */
PyObject *tmp = PyNumber_Long(source);
if (!tmp)
return false;
/* Now try to convert into a C++ int */
value.long_value = PyLong_AsLong(tmp);
Py_DECREF(tmp);
/* Ensure return code was OK (to avoid out-of-range errors etc) */
return !(value.long_value == -1 && !PyErr_Occurred());
}

/**
* Conversion part 2 (C++ -> Python): convert an inty instance into
* a Python object. The second and third arguments are used to
* indicate the return value policy and parent object (for
* ``return_value_policy::reference_internal``) and are generally
* ignored by implicit casters.
*/
static handle cast(inty src, return_value_policy /* policy */, handle /* parent */) {
return PyLong_FromLong(src.long_value);
}
};
}} // namespace pybind11::detail
struct inty_type_caster {
public:
/**
* This macro establishes the name 'inty' in
* function signatures and declares a local variable
* 'value' of type inty
*/
PYBIND11_TYPE_CASTER(inty, const_name("inty"));

/**
* Conversion part 1 (Python->C++): convert a PyObject into a inty
* instance or return false upon failure. The second argument
* indicates whether implicit conversions should be applied.
*/
bool load(handle src, bool) {
/* Extract PyObject from handle */
PyObject *source = src.ptr();
/* Try converting into a Python integer value */
PyObject *tmp = PyNumber_Long(source);
if (!tmp)
return false;
/* Now try to convert into a C++ int */
value.long_value = PyLong_AsLong(tmp);
Py_DECREF(tmp);
/* Ensure return code was OK (to avoid out-of-range errors etc) */
return !(value.long_value == -1 && !PyErr_Occurred());
}

/**
* Conversion part 2 (C++ -> Python): convert an inty instance into
* a Python object. The second and third arguments are used to
* indicate the return value policy and parent object (for
* ``return_value_policy::reference_internal``) and are generally
* ignored by implicit casters.
*/
static handle cast(inty src, return_value_policy /* policy */, handle /* parent */) {
return PyLong_FromLong(src.long_value);
}
};

.. note::

A ``type_caster<T>`` defined with ``PYBIND11_TYPE_CASTER(T, ...)`` requires
A caster class defined with ``PYBIND11_TYPE_CASTER(T, ...)`` requires
that ``T`` is default-constructible (``value`` is first default constructed
and then ``load()`` assigns to it).

.. warning::
The caster defined above must be registered with pybind11.
There are two ways to do it:

When using custom type casters, it's important to declare them consistently
in every compilation unit of the Python extension module. Otherwise,
undefined behavior can ensue.
* As an instantiation of the ``pybind11::detail::type_caster<T>`` template.
Although this is an implementation detail, adding an instantiation of this
type is explicitly allowed:

.. code-block:: cpp

namespace pybind11 { namespace detail {
template <> struct type_caster<inty> : inty_type_caster {};
}} // namespace pybind11::detail

.. warning::

When using this method, it's important to declare them consistently
in every compilation unit of the Python extension module. Otherwise,
undefined behavior can ensue.

* The preferred method is to *declare* a function named
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"When you own the namespace where the type is defined, the preferred method..."

``pybind11_select_caster``, its only purpose is to associate the C++ type
with its caster class:

.. code-block:: cpp

inty_type_caster pybind11_select_caster(inty*);

The argument is a *pointer* to the C++ type, the return type is the
caster type. This function has no implementation!
55 changes: 45 additions & 10 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,34 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
PYBIND11_NAMESPACE_BEGIN(detail)

template <typename type, typename SFINAE = void> class type_caster : public type_caster_base<type> { };
template <typename type> using make_caster = type_caster<intrinsic_t<type>>;

template <typename IntrinsicType> type_caster<IntrinsicType> pybind11_select_caster(IntrinsicType *);

// MSVC 2015 generates an internal compiler error for the common code (in the #else branch below).
// MSVC 2017 in C++14 mode produces incorrect code, leading to a tests/test_stl runtime failure.
// Luckily, the workaround for MSVC 2015 also resolves the MSVC 2017 C++14 runtime failure.
#if defined(_MSC_VER) && (_MSC_VER < 1910 || (_MSC_VER < 1920 && !defined(PYBIND11_CPP17)))

template <typename IntrinsicType, typename SFINAE = void>
struct make_caster_impl;

template <typename IntrinsicType>
struct make_caster_impl<IntrinsicType, enable_if_t< std::is_arithmetic<IntrinsicType>::value>>
: type_caster<IntrinsicType> {};

template <typename IntrinsicType>
struct make_caster_impl<IntrinsicType, enable_if_t<!std::is_arithmetic<IntrinsicType>::value>>
: decltype(pybind11_select_caster(static_cast<IntrinsicType *>(nullptr))) {};

template <typename type>
using make_caster = make_caster_impl<intrinsic_t<type>>;

#else

template <typename type>
using make_caster = decltype(pybind11_select_caster(static_cast<intrinsic_t<type> *>(nullptr)));

#endif

// Shortcut for calling a caster's `cast_op_type` cast operator for casting a type_caster to a T
template <typename T> typename make_caster<T>::template cast_op_type<T> cast_op(make_caster<T> &caster) {
Expand Down Expand Up @@ -457,7 +484,7 @@ struct type_caster<std::basic_string_view<CharT, Traits>, enable_if_t<is_std_cha
// ability to use None as a nullptr char* (which the string caster doesn't allow).
template <typename CharT> struct type_caster<CharT, enable_if_t<is_std_char_type<CharT>::value>> {
using StringType = std::basic_string<CharT>;
using StringCaster = type_caster<StringType>;
using StringCaster = make_caster<StringType>;
StringCaster str_caster;
bool none = false;
CharT one_char = 0;
Expand Down Expand Up @@ -863,7 +890,7 @@ template <typename Return> struct return_value_policy_override<Return,
};

// Basic python -> C++ casting; throws if casting fails
template <typename T, typename SFINAE> type_caster<T, SFINAE> &load_type(type_caster<T, SFINAE> &conv, const handle &handle) {
template <typename T, typename caster> caster& load_type(caster& conv, const handle& handle) {
if (!conv.load(handle, true)) {
#if defined(NDEBUG)
throw cast_error("Unable to cast Python instance to C++ type (compile in debug mode for details)");
Expand All @@ -874,10 +901,13 @@ template <typename T, typename SFINAE> type_caster<T, SFINAE> &load_type(type_ca
}
return conv;
}
template <typename T, typename SFINAE> type_caster<T, SFINAE> &load_type(type_caster<T, SFINAE> &conv, const handle &handle) {
return load_type<T, type_caster<T, SFINAE>>(conv, handle);
}
// Wrapper around the above that also constructs and returns a type_caster
template <typename T> make_caster<T> load_type(const handle &handle) {
make_caster<T> conv;
load_type(conv, handle);
load_type<T>(conv, handle);
return conv;
}

Expand Down Expand Up @@ -964,19 +994,24 @@ template <typename ret_type> using override_caster_t = conditional_t<
// Trampoline use: for reference/pointer types to value-converted values, we do a value cast, then
// store the result in the given variable. For other types, this is a no-op.
template <typename T> enable_if_t<cast_is_temporary_value_reference<T>::value, T> cast_ref(object &&o, make_caster<T> &caster) {
return cast_op<T>(load_type(caster, o));
return cast_op<T>(load_type<T>(caster, o));
}
template <typename T> enable_if_t<!cast_is_temporary_value_reference<T>::value, T> cast_ref(object &&, override_unused &) {
pybind11_fail("Internal error: cast_ref fallback invoked"); }

// Trampoline use: Having a pybind11::cast with an invalid reference type is going to static_assert, even
// though if it's in dead code, so we provide a "trampoline" to pybind11::cast that only does anything in
// cases where pybind11::cast is valid.
template <typename T> enable_if_t<!cast_is_temporary_value_reference<T>::value, T> cast_safe(object &&o) {
return pybind11::cast<T>(std::move(o)); }
template <typename T> enable_if_t<cast_is_temporary_value_reference<T>::value, T> cast_safe(object &&) {
pybind11_fail("Internal error: cast_safe fallback invoked"); }
template <> inline void cast_safe<void>(object &&) {}
template <typename T>
enable_if_t<!std::is_same<void, intrinsic_t<T>>::value &&
!cast_is_temporary_value_reference<T>::value, T>
cast_safe(object &&o) { return pybind11::cast<T>(std::move(o)); }
template <typename T>
enable_if_t<cast_is_temporary_value_reference<T>::value, T>
cast_safe(object &&) { pybind11_fail("Internal error: cast_safe fallback invoked"); }
template <typename T>
enable_if_t<std::is_same<void, intrinsic_t<T>>::value, void>
cast_safe(object &&) {}

PYBIND11_NAMESPACE_END(detail)

Expand Down
2 changes: 2 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ set(PYBIND11_TEST_FILES
test_iostream
test_kwargs_and_defaults
test_local_bindings
test_make_caster_adl
test_make_caster_adl_alt
test_methods_and_attributes
test_modules
test_multiple_inheritance
Expand Down
89 changes: 89 additions & 0 deletions tests/test_make_caster_adl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// adl = Argument Dependent Lookup

#include "pybind11_tests.h"

namespace have_a_ns {
struct type_mock {};
struct mock_caster {
static int num() { return 101; }
};
mock_caster pybind11_select_caster(type_mock *);
} // namespace have_a_ns

// namespace global {
struct global_ns_type_mock {};
struct global_ns_mock_caster {
static int num() { return 202; }
};
global_ns_mock_caster pybind11_select_caster(global_ns_type_mock *);
// } // namespace global

namespace {
struct unnamed_ns_type_mock {};
struct unnamed_ns_mock_caster {
static int num() { return 303; }
};
unnamed_ns_mock_caster pybind11_select_caster(unnamed_ns_type_mock *);
#if !defined(_MSC_VER)
// Dummy implementation, purely to avoid compiler warnings (unused function).
// Easier than managing compiler-specific pragmas for warning suppression.
// MSVC happens to not generate any warnings. Leveraging that to prove that
// this test actually works without an implementation.
unnamed_ns_mock_caster pybind11_select_caster(unnamed_ns_type_mock *) {
return unnamed_ns_mock_caster{};
}
#endif
} // namespace

namespace mrc_ns { // minimal real caster

struct type_mrc {
int value = -9999;
};

struct minimal_real_caster {
static constexpr auto name = py::detail::const_name<type_mrc>();

static py::handle
cast(type_mrc const &src, py::return_value_policy /*policy*/, py::handle /*parent*/) {
return py::int_(src.value + 1000).release();
}

// Maximizing simplicity. This will go terribly wrong for other arg types.
template <typename>
using cast_op_type = const type_mrc &;

// NOLINTNEXTLINE(google-explicit-constructor)
operator type_mrc const &() {
static type_mrc obj;
obj.value = 404;
return obj;
}

bool load(py::handle src, bool /*convert*/) {
// Only accepts str, but the value is ignored.
return py::isinstance<py::str>(src);
}
};

minimal_real_caster pybind11_select_caster(type_mrc *);

} // namespace mrc_ns

TEST_SUBMODULE(make_caster_adl, m) {
m.def("have_a_ns_num", []() { return py::detail::make_caster<have_a_ns::type_mock>::num(); });
m.def("global_ns_num", []() { return py::detail::make_caster<global_ns_type_mock>::num(); });
m.def("unnamed_ns_num", []() { return py::detail::make_caster<unnamed_ns_type_mock>::num(); });

m.def("mrc_return", []() {
mrc_ns::type_mrc obj;
obj.value = 505;
return obj;
});
m.def("mrc_arg", [](mrc_ns::type_mrc const &obj) { return obj.value + 2000; });

#if !defined(_MSC_VER)
// Dummy call, purely to avoid compiler warnings (unused function).
(void) pybind11_select_caster(static_cast<unnamed_ns_type_mock *>(nullptr));
#endif
}
23 changes: 23 additions & 0 deletions tests/test_make_caster_adl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
import pytest

from pybind11_tests import make_caster_adl as m
from pybind11_tests import make_caster_adl_alt as m_alt


def test_mock_casters():
assert m.have_a_ns_num() == 101
assert m.global_ns_num() == 202
assert m.unnamed_ns_num() == 303


def test_mock_casters_alt():
assert m_alt.have_a_ns_num() == 121


def test_minimal_real_caster():
assert m.mrc_return() == 1505
assert m.mrc_arg(u"ignored") == 2404
with pytest.raises(TypeError) as excinfo:
m.mrc_arg(None)
assert "(arg0: mrc_ns::type_mrc) -> int" in str(excinfo.value)
13 changes: 13 additions & 0 deletions tests/test_make_caster_adl_alt.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include "pybind11_tests.h"

namespace have_a_ns {
struct type_mock {};
struct mock_caster_alt {
static int num() { return 121; }
};
mock_caster_alt pybind11_select_caster(type_mock *);
} // namespace have_a_ns

TEST_SUBMODULE(make_caster_adl_alt, m) {
m.def("have_a_ns_num", []() { return py::detail::make_caster<have_a_ns::type_mock>::num(); });
}
7 changes: 7 additions & 0 deletions tests/test_make_caster_adl_alt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-

from pybind11_tests import make_caster_adl_alt as m


def test_mock_casters():
assert m.have_a_ns_num() == 121