From 1bafd5db5fec75bafd6986af8157a61035aced60 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 23 Feb 2021 21:50:42 -0800 Subject: [PATCH] Adding py::smart_holder (for smart-pointer interoperability). (#2672) * Adding test_unique_ptr_member (for desired PyCLIF behavior). See also: https://github.com/pybind/pybind11/issues/2583 Does not build with upstream master or https://github.com/pybind/pybind11/pull/2047, but builds with https://github.com/RobotLocomotion/pybind11 and almost runs: ``` Running tests in directory "/usr/local/google/home/rwgk/forked/EricCousineau-TRI/pybind11/tests": ================================================================================= test session starts ================================================================================= platform linux -- Python 3.8.5, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 rootdir: /usr/local/google/home/rwgk/forked/EricCousineau-TRI/pybind11/tests, inifile: pytest.ini collected 2 items test_unique_ptr_member.py .F [100%] ====================================================================================== FAILURES ======================================================================================= _____________________________________________________________________________ test_pointee_and_ptr_owner ______________________________________________________________________________ def test_pointee_and_ptr_owner(): obj = m.pointee() assert obj.get_int() == 213 m.ptr_owner(obj) with pytest.raises(ValueError) as exc_info: > obj.get_int() E Failed: DID NOT RAISE test_unique_ptr_member.py:17: Failed ============================================================================= 1 failed, 1 passed in 0.06s ============================================================================= ``` * unique_ptr or shared_ptr return * new test_variant_unique_shared with vptr_holder prototype * moving prototype code to pybind11/vptr_holder.h, adding type_caster specialization to make the bindings involving unique_ptr passing compile, but load and cast implementations are missing * disabling GitHub Actions on pull_request (for this PR) * disabling AppVeyor (for this PR) * TRIGGER_SEGSEV macro, annotations for GET_STACK (vptr::get), GET_INT_STACK (pointee) * adding test_promotion_of_disowned_to_shared * Copying tests as-is from xxx_value_ptr_xxx_holder branch. https://github.com/rwgk/pybind11/tree/xxx_value_ptr_xxx_holder Systematically exercising returning and passing unique_ptr, shared_ptr with unique_ptr, shared_ptr holder. Observations: test_holder_unique_ptr: make_unique_pointee OK pass_unique_pointee BUILD_FAIL (as documented) make_shared_pointee Abort free(): double free detected pass_shared_pointee RuntimeError: Unable to load a custom holder type from a default-holder instance test_holder_shared_ptr: make_unique_pointee Segmentation fault (#1138) pass_unique_pointee BUILD_FAIL (as documented) make_shared_pointee OK pass_shared_pointee OK * Copying tests as-is from xxx_value_ptr_xxx_holder branch. https://github.com/rwgk/pybind11/tree/xxx_value_ptr_xxx_holder Systematically exercising casting between shared_ptr, shared_ptr. * Demonstration of Undefined Behavior in handling of shared_ptr holder. Based on https://godbolt.org/z/4fdjaW by jorgbrown@ (thanks Jorg!). * Additional demonstration of Undefined Behavior in handling of shared_ptr holder. * fixing up-down mixup in comment * Demonstration of Undefined Behavior in handling of polymorphic pointers. (This demo does NOT involve smart pointers at all, unlike the otherwise similar test_smart_ptr_private_first_base.) * minor test_private_first_base.cpp simplification (after discovering that this can be wrapped with Boost.Python, using boost::noncopyable) * pybind11 equivalent of Boost.Python test similar to reproducer under #1333 * Snapshot of WIP, TODO: shared_ptr deleter with on/off switch * Adding vptr_deleter. * Adding from/as unique_ptr and unique_ptr. * Adding from_shared_ptr. Some polishing. * New tests/core/smart_holder_poc_test.cpp, using Catch2. * Adding in vptr_deleter_guard_flag. * Improved labeling of TEST_CASEs. * Shuffling existing TEST_CASEs into systematic matrix. * Implementing all [S]uccess tests. * Implementing all [E]xception tests. * Testing of exceptions not covered by the from-as matrix. * Adding top-level comment. * Converting from methods to factory functions (no functional change). * Removing obsolete and very incomplete test (replaced by Catch2-based test). * Removing stray file. * Adding type_caster_bare_interface_demo. * Adding shared_ptr, shared_ptr casters. * Adding unique_ptr, unique_ptr casters. * Pure copy of `class class_` implementation in pybind11.h (master commit 98f1bbb8004f654ba9e26717bdf5912fb899b05a). * classh.h: renaming of class_ to classh + namespace; forking test_classh_wip from test_type_caster_bare_interface_demo. * Hard-coding smart_holder into classh. * Adding mpty::mtxt string member. * Adding isinstance in type_caster::load functions. * Adding rvalue_ref, renaming const_value_ref to lvalue_ref & removing const. * Retrieving smart_holder pointer in type_caster::load, and using it cast_op operators. * Factoring out smart_holder_type_caster_load. * Retrieving smart_holder pointer in type_caster>::load, and using it cast_op operators. * Improved error messaging: Cannot disown nullptr (as_unique_ptr). * Retrieving smart_holder pointer in type_caster>::load, and using it cast_op operators. * Pure `clang-format --style=file -i` change. * Pure `clang-format --style=file -i` change, with two `clang-format off` directives. * Fixing oversight (discovered by flake8). * flake8 cleanup * Systematically setting mtxt for all rtrn_mpty_* functions (preparation, the values are not actually used yet). * static cast handle for rtrn_cptr works by simply dropping in code from type_caster_base (marked with comments). * static cast handle for rtrn_cref works by simply dropping in code from type_caster_base (marked with comments). rtrn_mref and rtrn_mptr work via const_cast (to add const). * static cast handle for rtrn_valu works by simply dropping in code from type_caster_base (marked with comments). rtrn_rref raises a RuntimeError, to be investigated. * Copying type_caster_generic::cast into type_caster as-is (preparation for handling smart pointers). * Pure clang-format change (applied to original type_caster_generic::cast). * Adding comment re potential use_count data race. * static handle cast implementations for rtrn_shmp, rtrn_shcp. * Adding MISSING comments in operator std::unique_ptr. * static handle cast implementations for rtrn_uqmp, rtrn_uqcp. * Bug fix: vptr_deleter_armed_flag_ptr has to live on the heap. See new bullet point in comment section near the top. The variable was also renamed to reflect its function more accurately. * Fixing bugs discovered by ASAN. The code is now ASAN, MSAN, UBSAN clean. * Making test_type_caster_bare_interface_demo.cpp slightly more realistic, ASAN, MSAN, UBSAN clean. * Calling deregister_instance after disowning via unique_ptr. * Removing enable_shared_from_this stub, simplifying existing code, clang-format. Open question, with respect to the original code: https://github.com/pybind/pybind11/blob/76a160070b369f8d82b945c97924227e8b835c94/include/pybind11/pybind11.h#L1510 To me it looks like the exact situation marked as `std::shared_ptr gp1 = not_so_good.getptr();` here: https://en.cppreference.com/w/cpp/memory/enable_shared_from_this The comment there is: `// undefined behavior (until C++17) and std::bad_weak_ptr thrown (since C++17)` Does the existing code have UB pre C++17? I'll leave handling of enable_shared_from_this for later, as the need arises. * Cosmetical change around helper functions. * Using type_caster_base::src_and_type directly, removing copy. Also renaming one cast to cast_const_raw_ptr, for clarity. * Fixing clang-format oversight. * Using factored-out make_constructor (PR #2798), removing duplicate code. * Inserting additional assert to ensure a returned unique_ptr is always a new Python instance. * Adding minor comment (change to internals needed to distinguish uninitialized/disowned in error message). * Factoring out find_existing_python_instance(). * Moving factored-out make_constructor to test_classh_wip.cpp, restoring previous version of cast.h. This is currently the most practical approach. See PR #2798 for background. * Copying classh type_casters from test_classh_wip.cpp UNMODIFIED, as a baseline for generalizing the code. * Using pybind11/detail/classh_type_casters.h from test_classh_wip.cpp. * Adding & using PYBIND11_CLASSH_TYPE_CASTERS define. * Adding test_classh_inheritance, currently failing (passes with class_). * Removing .clang-format before git rebase master (where the file was added). * Bringing back .clang-format, the previous rm was a bad idea. * Folding in modified_type_caster_generic_load_impl, just enough to pass test_class_wip. test_classh_inheritance is still failing, but with a different error: [RuntimeError: Incompatible type (as_raw_ptr_unowned).] * Minimal changes needed to pass test_classh_inheritance. * First pass adjusting try_implicit_casts and try_load_foreign_module_local to capture loaded_v_h, but untested and guarded with pybind11_failure("Untested"). This was done mainly to determine general feasibility. Note the TODO in pybind11.h, where type_caster_generic::local_load is currently hard-coded. test_classh_wip and test_classh_inheritance still pass, as before. * Decoupling generic_type from type_caster_generic. * Changes and tests covering classh_type_casters try_implicit_casts. * Minimal test covering classh_type_casters load_impl Case 2b. * Removing stray isinstance(src): it interferes with the py::module_local feature. Adding missing #includes. * Tests for classh py::module_local() feature. * Pure renaming of function names in test_classh_inheritance, similar to the systematic approach used in test_class_wip. NO functional changes. * Pure renaming of function and variable names, for better generalization when convoluting with inheritance. NO functional changes. * Adopting systematic naming scheme from test_classh_wip. NO functional changes. * Moving const after type name, for functions that cover a systematic scheme. NO functional changes. * Adding smart_holder_type_caster_load::loaded_as_shared_ptr, currently bypassing smart_holder shared_ptr tracking completely, but the tests pass and are sanitizer clean. * Removing rtti_held from smart_holder. See updated comment. * Cleaning up loaded_as_raw_ptr_unowned, loaded_as_shared_ptr. * Factoring out convert_type and folding into loaded_as_unique_ptr. * Folding convert_type into lvalue_ref and rvalue_ref paths. Some smart_holder_type_caster_load cleanup. * Using unique_ptr in local_load to replace static variable. Also adding local_load_safety_guard. * Converting test_unique_ptr_member to using classh: fully working, ASAN, MSAN, UBSAN clean. * Removing debugging comments (GET_STACK, GET_INT_STACK). cast.h is identical to current master again, pybind11.h only has the generic_type::initialize(..., &type_caster_generic::local_load) change. * Purging obsolete pybind11/vptr_holder.h and associated test. * Moving several tests to github.com/rwgk/rwgk_tbx/tree/main/pybind11_tests https://github.com/rwgk/rwgk_tbx/commit/a2c2f88174a30f5de80d7d26e0f77c7b60f5fb85 These tests are from experimenting, and for demonstrating UB in pybind11 multiple inheritance handling ("first_base"), to be fixed later. * Adding py::smart_holder support to py::class_, purging py::classh completely. * Renaming files in include directory, creating pybind11/smart_holder.h. * Renaming all "classh" to "smart_holder" in pybind11/detail/smart_holder_type_casters.h. The user-facing macro is now PYBIND11_SMART_HOLDER_TYPE_CASTERS. * Systematically renaming tests to use "class_sh" in the name. * Renaming test_type_caster_bare_interface_demo to test_type_caster_bare_interface. * Renaming new tests/core subdirectory to tests/pure_cpp. * Adding new tests to CMake config, resetting CI config. * Changing CMake file so that test_class_sh_module_local.py actually runs. * clang-tidy fixes. * 32-bit compatibility. * Reusing type_caster_base make_copy_constructor, make_move_constructor with a trick. * CMake COMPARE NATURAL is not available with older versions. * Adding copyright notices to new header files. * Explicitly define copy/move constructors/assignments. * Adding new header files to tests/extra_python_package/test_files.py. * Adding tests/pure_cpp/CMakeLists.txt. * Making use of the new find_existing_python_instance() function factored out with PR #2822. * Moving define PYBIND11_SMART_HOLDER_TYPE_CASTERS(T) down in the file. NO functional changes. Preparation for follow-up work (to keep that diff smaller). * Reintroducing py::classh, this time as a simple alias for py::class_. * Replacing detail::is_smart_holder in cast.h with detail::is_smart_holder_type_caster. Moving get_local_load_function_ptr, init_instance_for_type to smart_holder_type_caster_class_hooks. Expanding static_assert in py::type::handle_of<> to accommodate smart_holder_type_casters. * Fixing oversight. * Adding classu alias for class_>. * Giving up on idea to use legacy init_instance only if is_base_of. There are use cases in the wild that define both a custom type_caster and class_. * Removing test_type_caster_bare_interface, which was moved to the separate PR #2834. * Moving up is_smart_holder_type_caster, to also use in cast_is_temporary_value_reference. * Adding smart_holder_type_casters for unique_ptr with custom deleter. SEVERE CODE DUPLICATION. This commit is to establish a baseline for consolidating the unique_ptr code. * Unification of unique_ptr, unique_ptr_with_deleter code in smart_holder_poc.h. Leads to more fitting error messages. Enables use of unique_ptr smart_holder_type_casters also for unique_ptr. * Copying files as-is from branch test_unique_ptr_member (PR #2672). * Adding comment, simplifying naming, cmake addition. * Introducing PYBIND11_USE_SMART_HOLDER_AS_DEFAULT macro (tested only undefined; there are many errors with the macro defined). * Removing test_type_caster_bare_interface, which was moved to the separate PR #2834. * Fixing oversight introduced with commit 95425f13d6c14fcb6ee479b62b602dc8a605ec49. * Setting record.default_holder correctly for PYBIND11_USE_SMART_HOLDER_AS_DEFAULT. With this test_class.cpp builds and even mostly runs, except `test_multiple_instances_with_same_pointer`, which segfaults because it is using a `unique_ptr` holder but `smart_holder` `type_caster`. Also adding `static_assert`s to generate build errors for such situations, but guarding with `#if 0` to first pivot to test_factory_constructors.cpp. * Fixing up cast.h and smart_holder.h after rebase. * Removing detail/smart_holder_type_casters.h in separate commit. * Commenting out const in def_buffer(... const). With this, test_buffers builds and runs with PYBIND11_USE_SMART_HOLDER_AS_DEFAULT. Explanation why the const needs to be removed, or fix elsewhere, is still needed, but left for later. * Adding test_class_sh_factory_constructors, reproducing test_factory_constructors failure. Using py::class_ in this commit, to be changed to py::classh for debugging. * Removing include/pybind11/detail/smart_holder_type_casters.h from CMakeLists.txt, test_files.py (since it does not exist in this branch). * Adding // DANGER ZONE reminders. * Converting as many py::class_ to py::classh as possible, not breaking tests. * Adding initimpl::construct() overloads, resulting in test_class_sh_factory_constructors feature parity for py::class_ and py::classh. * Adding enable_if !is_smart_holder_type_caster to existing initimpl::construct(). With this test_factory_constructors.cpp builds with PYBIND11_USE_SMART_HOLDER_AS_DEFAULT. * Disabling shared_ptr&, shared_ptr* tests when building with PYBIND11_USE_SMART_HOLDER_AS_DEFAULT for now, pending work on smart_holder_type_caster. * Factoring out struct and class definitions into anonymous namespace. Preparation for building with PYBIND11_USE_SMART_HOLDER_AS_DEFAULT. * Simplifying from_unique_ptr(): typename D = std::default_delete is not needed. Factoring out is_std_default_delete() for consistentcy between ensure_compatible_rtti_uqp_del() and from_unique_ptr(). * Introducing PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS. Using it in test_smart_ptr.cpp. With this test_smart_ptr builds with PYBIND11_USE_SMART_HOLDER_AS_DEFAULT and all but one test run successfully. * Introducing 1. type_caster_for_class_, used in PYBIND11_MAKE_OPAQUE, and 2. default_holder_type, used in stl_bind.h. * Using __VA_ARGS__ in PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS. * Replacing condense_for_macro with much simpler approach. * Softening static_assert, to only check specifically that smart_holder is not mixed with type_caster_base, and unique_ptr/shared_ptr holders are not mixed with smart_holder_type_casters. * Adding PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS in test_class.cpp (with this all but one test succeed with PYBIND11_USE_SMART_HOLDER_AS_DEFAULT). * Adding remaining PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS. static_assert for "necessary conditions" for both types of default holder, static_assert for "strict conditions" guarded by new PYBIND11_STRICT_ASSERTS_CLASS_HOLDER_VS_TYPE_CASTER_MIX. All tests build & run as before with unique_ptr as the default holder, all tests build for smart_holder as the default holder, even with the strict static_assert. * Introducing check_is_smart_holder_type_caster() function for runtime check, and reinterpreting record.default_holder as "uses_unique_ptr_holder". With this test_smart_ptr succeeds. (All 42 tests build, 35 tests succeed, 5 run but have some failures, 2 segfault.) * Bug fix: Adding have_value() to smart_holder_type_caster_load. With this test_builtin_casters succeeds. (All 42 tests build, 36 tests succeed, 5 run but have some failures, 1 segfault.) * Adding unowned_void_ptr_from_direct_conversion to modified_type_caster_generic_load_impl. This fixes the last remaining segfault (test_numpy_dtypes). New stats for all tests combined: 12 failed, 458 passed. * Adding "Lazy allocation for unallocated values" (for old-style __init__) into load_value_and_holder. Deferring destruction of disowned holder until clear_instance, to remain inspectable for "uninitialized" or "disowned" detection. New stats for all tests combined: 5 failed, 465 passed. * Changing std::shared_ptr pointer/reference to const pointer/reference. New stats for all tests combined: 4 failed, 466 passed. * Adding return_value_policy::move to permissible policies for unique_ptr returns. New stats for all tests combined: 3 failed, 467 passed. * Overlooked flake8 fixes. * Manipulating failing ConstructorStats test to pass, to be able to run all tests with ASAN. This version of the code is ASAN clean with unique_ptr or smart_holder as the default. This change needs to be reverted after adopting the existing move-only-if-refcount-is-1 logic used by type_caster_base. * Adding copy constructor and move constructor tracking to atyp. Preparation for a follow-up change in smart_holder_type_caster, to make this test sensitive to the changing behavior. [skip ci] * Removing `operator T&&() &&` from smart_holder_type_caster, for compatibility with the behavior of type_caster_base. Enables reverting 2 of 3 test manipulations applied under commit 249df7cbdb09817fed0ddf80f01ba5af12466820. The manipulation in test_factory_constructors.py is NOT reverted in this commit. [skip ci] * Fixing unfortunate editing mishap. This reverts the last remaining test manipulation in commit 249df7cbdb09817fed0ddf80f01ba5af12466820 and makes all existing unit tests pass with smart_holder as default holder. * GitHub CI clang-tidy fixes. * Adding messages to terse `static_assert`s, for pre-C++17 compatibility. * Using @pytest.mark.parametrize to run each assert separately (to see all errors, not just the first). * Systematically removing _atyp from function names, to make the test code simpler. * Using re.match to accommodate variable number of intermediate MvCtor. * Also removing `operator T()` from smart_holder_type_caster, to fix gcc compilation errors. The only loss is pass_rref in test_class_sh_basic. * Systematically replacing `detail::enable_if_t<...smart_holder...>` with `typename std::enable_if<...smart_holder...>::type`. Attempt to work around MSVC 2015 issues, to be tested via GitHub CI. The idea for this change originates from this comment: https://github.com/pybind/pybind11/issues/1616#issuecomment-444536813 * Importing re before pytest after observing a PyPy CI flake when importing pytest first. * Copying MSVC 2015 compatibility change from branch pr2672_use_smart_holder_as_default. * Introducing is_smart_holder_type_caster_base_tag, to keep smart_holder code more disconnected. * Working around MSVC 2015 bug. * Expanding comment for MSVC 2015 workaround. * Systematically changing std::enable_if back to detail::enable_if_t, effectively reverting commit 5d4b6890a337ae1bbaec4091f4195606f89a3b06. * Removing unused smart_holder_type_caster_load::loaded_as_rvalue_ref (it was an oversight that it was not removed with commit 23036a45eb4731a06b488ec1fdf83bca677b7f67). * Removing py::classu, because it does not seem useful enough. * Reverting commit 63495313066119dcf7510c2ae8b468b46c12ef8f by un-commenting `const` in `def_buffer(...)`. To make this possible, `operator T const&` and `operator T const*` in `smart_holder_type_caster` need to be marked as `const` member functions. * Adding construct() overloads for constructing smart_holder from alias unique_ptr, shared_ptr returns. * Adding test_class_sh_factory_constructors.cpp to tests/CMakeLists.txt (fixes oversight, this should have been added long before). * Compatibility with old clang versions (clang 3.6, 3.7 C++11). * Cleaning up changes to existing unit tests. * Systematically adding SMART_HOLDER_WIP tag. Removing minor UNTESTED tags (only the throw are not actually exercised, investing time there has a high cost but very little benefit). * Splitting out smart_holder_type_casters again, into new detail/smart_holder_type_casters_inline_include.h. * Splitting out smart_holder_init_inline_include.h. * Adding additional new include files to CMakeLists.txt, tests/extra_python_package/test_files.py. * clang-format cleanup of most smart_holder code. * Adding source code comments in response to review. * Simple micro-benchmark ("ubench") comparing runtime performance for several holders. Tested using github.com/rwgk/pybind11_scons and Google-internal build system. Sorry, no cmake support at the moment. First results: https://docs.google.com/spreadsheets/d/1InapCYws2Gt-stmFf_Bwl33eOMo3aLE_gc9adveY7RU/edit#gid=0 * Breaking out number_bucket.h, adding hook for also collecting performance data for PyCLIF. * Accounting for ubench in MANIFEST.in (simply prune, for now). * Smarter determination of call_repetitions. [skip ci] * Also scaling performance data to PyCLIF. [skip ci] * Adding ubench/python/number_bucket.clif here for general visibility. * Fix after rebase * Merging detail/smart_holder_init_inline_include.h into detail/init.h. * Renaming detail/is_smart_holder_type_caster.h -> detail/smart_holder_sfinae_hooks_only.h. * Renaming is_smart_holder_type_caster -> type_uses_smart_holder_type_caster for clarity. * Renaming type_caster_type_is_smart_holder_type_caster -> wrapped_type_uses_smart_holder_type_caster for clarity. * Renaming is_smart_holder_type_caster_base_tag -> smart_holder_type_caster_base_tag for simplicity. * Adding copyright notices and minor colateral cleanup. * iwyu cleanup (comprehensive only for cast.h and smart_holder*.h files). * Fixing `git rebase master` accident. * Moving large `pragma warning` block from pybind11.h to detail/common.h. * Fixing another `git rebase master` accident. --- CMakeLists.txt | 4 + MANIFEST.in | 1 + include/pybind11/cast.h | 41 +- include/pybind11/detail/class.h | 2 + include/pybind11/detail/common.h | 32 + include/pybind11/detail/init.h | 69 +- include/pybind11/detail/smart_holder_poc.h | 266 +++++++ .../detail/smart_holder_sfinae_hooks_only.h | 33 + .../detail/smart_holder_type_casters.h | 747 ++++++++++++++++++ include/pybind11/pybind11.h | 139 +++- include/pybind11/smart_holder.h | 23 + include/pybind11/stl_bind.h | 4 +- tests/CMakeLists.txt | 21 + tests/class_sh_module_local_0.cpp | 27 + tests/class_sh_module_local_1.cpp | 33 + tests/class_sh_module_local_2.cpp | 33 + tests/extra_python_package/test_files.py | 4 + tests/pure_cpp/CMakeLists.txt | 20 + tests/pure_cpp/smart_holder_poc_test.cpp | 281 +++++++ tests/test_class.cpp | 40 +- tests/test_class_sh_basic.cpp | 120 +++ tests/test_class_sh_basic.py | 103 +++ tests/test_class_sh_factory_constructors.cpp | 180 +++++ tests/test_class_sh_factory_constructors.py | 52 ++ tests/test_class_sh_inheritance.cpp | 105 +++ tests/test_class_sh_inheritance.py | 63 ++ tests/test_class_sh_module_local.py | 27 + tests/test_class_sh_unique_ptr_member.cpp | 61 ++ tests/test_class_sh_unique_ptr_member.py | 29 + tests/test_factory_constructors.cpp | 5 + tests/test_methods_and_attributes.cpp | 6 +- tests/test_multiple_inheritance.cpp | 51 +- tests/test_smart_ptr.cpp | 434 +++++----- tests/test_smart_ptr.py | 6 +- ubench/holder_comparison.cpp | 54 ++ ubench/holder_comparison.py | 140 ++++ .../holder_comparison_extract_sheet_data.py | 66 ++ ubench/number_bucket.h | 55 ++ ubench/python/number_bucket.clif | 6 + 39 files changed, 3111 insertions(+), 272 deletions(-) create mode 100644 include/pybind11/detail/smart_holder_poc.h create mode 100644 include/pybind11/detail/smart_holder_sfinae_hooks_only.h create mode 100644 include/pybind11/detail/smart_holder_type_casters.h create mode 100644 include/pybind11/smart_holder.h create mode 100644 tests/class_sh_module_local_0.cpp create mode 100644 tests/class_sh_module_local_1.cpp create mode 100644 tests/class_sh_module_local_2.cpp create mode 100644 tests/pure_cpp/CMakeLists.txt create mode 100644 tests/pure_cpp/smart_holder_poc_test.cpp create mode 100644 tests/test_class_sh_basic.cpp create mode 100644 tests/test_class_sh_basic.py create mode 100644 tests/test_class_sh_factory_constructors.cpp create mode 100644 tests/test_class_sh_factory_constructors.py create mode 100644 tests/test_class_sh_inheritance.cpp create mode 100644 tests/test_class_sh_inheritance.py create mode 100644 tests/test_class_sh_module_local.py create mode 100644 tests/test_class_sh_unique_ptr_member.cpp create mode 100644 tests/test_class_sh_unique_ptr_member.py create mode 100644 ubench/holder_comparison.cpp create mode 100644 ubench/holder_comparison.py create mode 100644 ubench/holder_comparison_extract_sheet_data.py create mode 100644 ubench/number_bucket.h create mode 100644 ubench/python/number_bucket.clif diff --git a/CMakeLists.txt b/CMakeLists.txt index 152763eb42..39800c3e9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,9 @@ set(PYBIND11_HEADERS include/pybind11/detail/descr.h include/pybind11/detail/init.h include/pybind11/detail/internals.h + include/pybind11/detail/smart_holder_poc.h + include/pybind11/detail/smart_holder_sfinae_hooks_only.h + include/pybind11/detail/smart_holder_type_casters.h include/pybind11/detail/type_caster_base.h include/pybind11/detail/typeid.h include/pybind11/attr.h @@ -124,6 +127,7 @@ set(PYBIND11_HEADERS include/pybind11/operators.h include/pybind11/pybind11.h include/pybind11/pytypes.h + include/pybind11/smart_holder.h include/pybind11/stl.h include/pybind11/stl_bind.h) diff --git a/MANIFEST.in b/MANIFEST.in index aed183e874..dc39f97229 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,3 +4,4 @@ recursive-include pybind11 py.typed recursive-include pybind11 *.pyi include pybind11/share/cmake/pybind11/*.cmake include LICENSE README.rst pyproject.toml setup.py setup.cfg +prune ubench diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 0ad10e9a93..fd8f9a6668 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1,3 +1,4 @@ +// clang-format off /* pybind11/cast.h: Partial template specializations to cast between C++ and Python types @@ -13,6 +14,7 @@ #include "pytypes.h" #include "detail/common.h" #include "detail/descr.h" +#include "detail/smart_holder_sfinae_hooks_only.h" #include "detail/type_caster_base.h" #include "detail/typeid.h" #include @@ -27,6 +29,10 @@ #include #include +#ifdef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT +#include "detail/smart_holder_type_casters.h" +#endif + #if defined(PYBIND11_CPP17) # if defined(__has_include) # if __has_include() @@ -47,8 +53,24 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) PYBIND11_NAMESPACE_BEGIN(detail) -template class type_caster : public type_caster_base { }; -template using make_caster = type_caster>; +// clang-format on +#ifndef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT +template +class type_caster_for_class_ : public type_caster_base {}; +#endif + +template +class type_caster : public type_caster_for_class_ {}; + +template +using make_caster = type_caster>; + +template +struct type_uses_smart_holder_type_caster { + static constexpr bool value + = std::is_base_of>::value; +}; +// clang-format off // Shortcut for calling a caster's `cast_op_type` cast operator for casting a type_caster to a T template typename make_caster::template cast_op_type cast_op(make_caster &caster) { @@ -696,9 +718,11 @@ struct copyable_holder_caster : public type_caster_base { holder_type holder; }; +#ifndef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT /// Specialize for the common std::shared_ptr, so users don't need to template class type_caster> : public copyable_holder_caster> { }; +#endif /// Type caster for holder types like std::unique_ptr. /// Please consider the SFINAE hook an implementation detail, as explained @@ -715,9 +739,11 @@ struct move_only_holder_caster { static constexpr auto name = type_caster_base::name; }; +#ifndef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT template class type_caster> : public move_only_holder_caster> { }; +#endif template using type_caster_holder = conditional_t::value, @@ -820,6 +846,7 @@ template using move_never = none_of, move_if_unrefer template using cast_is_temporary_value_reference = bool_constant< (std::is_reference::value || std::is_pointer::value) && !std::is_base_of>::value && + !type_uses_smart_holder_type_caster>::value && !std::is_same, void>::value >; @@ -1362,10 +1389,10 @@ PYBIND11_NAMESPACE_END(detail) template handle type::handle_of() { - static_assert( - std::is_base_of>::value, - "py::type::of only supports the case where T is a registered C++ types." - ); + static_assert( + detail::any_of>, + detail::type_uses_smart_holder_type_caster>::value, + "py::type::of only supports the case where T is a registered C++ types."); return detail::get_type_handle(typeid(T), true); } @@ -1373,7 +1400,7 @@ handle type::handle_of() { #define PYBIND11_MAKE_OPAQUE(...) \ namespace pybind11 { namespace detail { \ - template<> class type_caster<__VA_ARGS__> : public type_caster_base<__VA_ARGS__> { }; \ + template<> class type_caster<__VA_ARGS__> : public type_caster_for_class_<__VA_ARGS__> { }; \ }} /// Lets you pass a type containing a `,` through a macro parameter without needing a separate diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 2f414e5c7c..f5be75e1a7 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -398,6 +398,8 @@ inline void clear_instance(PyObject *self) { if (instance->owned || v_h.holder_constructed()) v_h.type->dealloc(v_h); + } else if (v_h.holder_constructed()) { + v_h.type->dealloc(v_h); // Disowned instance. } } // Deallocate the value/holder layout internals: diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 5560198a05..17021978a1 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -13,6 +13,38 @@ #define PYBIND11_VERSION_MINOR 6 #define PYBIND11_VERSION_PATCH 3.dev1 +#if defined(__INTEL_COMPILER) +# pragma warning push +# pragma warning disable 68 // integer conversion resulted in a change of sign +# pragma warning disable 186 // pointless comparison of unsigned integer with zero +# pragma warning disable 878 // incompatible exception specifications +# pragma warning disable 1334 // the "template" keyword used for syntactic disambiguation may only be used within a template +# pragma warning disable 1682 // implicit conversion of a 64-bit integral type to a smaller integral type (potential portability problem) +# pragma warning disable 1786 // function "strdup" was declared deprecated +# pragma warning disable 1875 // offsetof applied to non-POD (Plain Old Data) types is nonstandard +# pragma warning disable 2196 // warning #2196: routine is both "inline" and "noinline" +#elif defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable: 4100) // warning C4100: Unreferenced formal parameter +# pragma warning(disable: 4127) // warning C4127: Conditional expression is constant +# pragma warning(disable: 4512) // warning C4512: Assignment operator was implicitly defined as deleted +# pragma warning(disable: 4800) // warning C4800: 'int': forcing value to bool 'true' or 'false' (performance warning) +# pragma warning(disable: 4996) // warning C4996: The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conformant name +# pragma warning(disable: 4702) // warning C4702: unreachable code +# pragma warning(disable: 4522) // warning C4522: multiple assignment operators specified +# pragma warning(disable: 4505) // warning C4505: 'PySlice_GetIndicesEx': unreferenced local function has been removed (PyPy only) +#elif defined(__GNUG__) && !defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-but-set-parameter" +# pragma GCC diagnostic ignored "-Wunused-but-set-variable" +# pragma GCC diagnostic ignored "-Wmissing-field-initializers" +# pragma GCC diagnostic ignored "-Wstrict-aliasing" +# pragma GCC diagnostic ignored "-Wattributes" +# if __GNUC__ >= 7 +# pragma GCC diagnostic ignored "-Wnoexcept-type" +# endif +#endif + #define PYBIND11_NAMESPACE_BEGIN(name) namespace name { #define PYBIND11_NAMESPACE_END(name) } diff --git a/include/pybind11/detail/init.h b/include/pybind11/detail/init.h index 3ef78c1179..2c1c77ae95 100644 --- a/include/pybind11/detail/init.h +++ b/include/pybind11/detail/init.h @@ -1,3 +1,4 @@ +// clang-format off /* pybind11/detail/init.h: init factory function implementation and support code. @@ -10,6 +11,7 @@ #pragma once #include "class.h" +#include "smart_holder_sfinae_hooks_only.h" PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) PYBIND11_NAMESPACE_BEGIN(detail) @@ -105,11 +107,13 @@ void construct(value_and_holder &v_h, Cpp *ptr, bool need_alias) { // the holder and destruction happens when we leave the C++ scope, and the holder // class gets to handle the destruction however it likes. v_h.value_ptr() = ptr; - v_h.set_instance_registered(true); // To prevent init_instance from registering it + v_h.set_instance_registered(true); // SHORTCUT To prevent init_instance from registering it + // DANGER ZONE BEGIN: exceptions will leave v_h in an invalid state. v_h.type->init_instance(v_h.inst, nullptr); // Set up the holder Holder temp_holder(std::move(v_h.holder>())); // Steal the holder v_h.type->dealloc(v_h); // Destroys the moved-out holder remains, resets value ptr to null v_h.set_instance_registered(false); + // DANGER ZONE END. construct_alias_from_cpp(is_alias_constructible{}, v_h, std::move(*ptr)); } else { @@ -129,7 +133,8 @@ void construct(value_and_holder &v_h, Alias *alias_ptr, bool) { // Holder return: copy its pointer, and move or copy the returned holder into the new instance's // holder. This also handles types like std::shared_ptr and std::unique_ptr where T is a // derived type (through those holder's implicit conversion from derived class holder constructors). -template +template >::value, int> = 0> void construct(value_and_holder &v_h, Holder holder, bool need_alias) { auto *ptr = holder_helper>::get(holder); no_nullptr(ptr); @@ -166,6 +171,66 @@ void construct(value_and_holder &v_h, Alias &&result, bool) { v_h.value_ptr() = new Alias(std::move(result)); } +// clang-format on +template < + typename Class, + typename D = std::default_delete>, + detail::enable_if_t>::value, int> = 0> +void construct(value_and_holder &v_h, std::unique_ptr, D> &&unq_ptr, bool need_alias) { + auto *ptr = unq_ptr.get(); + no_nullptr(ptr); + if (Class::has_alias && need_alias) + throw type_error("pybind11::init(): construction failed: returned std::unique_ptr pointee " + "is not an alias instance"); + auto smhldr + = type_caster>::template smart_holder_from_unique_ptr(std::move(unq_ptr)); + v_h.value_ptr() = ptr; + v_h.type->init_instance(v_h.inst, &smhldr); +} + +template < + typename Class, + typename D = std::default_delete>, + detail::enable_if_t>::value, int> = 0> +void construct(value_and_holder &v_h, + std::unique_ptr, D> &&unq_ptr, + bool /*need_alias*/) { + auto *ptr = unq_ptr.get(); + no_nullptr(ptr); + auto smhldr + = type_caster>::template smart_holder_from_unique_ptr(std::move(unq_ptr)); + v_h.value_ptr() = ptr; + v_h.type->init_instance(v_h.inst, &smhldr); +} + +template < + typename Class, + detail::enable_if_t>::value, int> = 0> +void construct(value_and_holder &v_h, std::shared_ptr> &&shd_ptr, bool need_alias) { + auto *ptr = shd_ptr.get(); + no_nullptr(ptr); + if (Class::has_alias && need_alias) + throw type_error("pybind11::init(): construction failed: returned std::shared_ptr pointee " + "is not an alias instance"); + auto smhldr = type_caster>::template smart_holder_from_shared_ptr(shd_ptr); + v_h.value_ptr() = ptr; + v_h.type->init_instance(v_h.inst, &smhldr); +} + +template < + typename Class, + detail::enable_if_t>::value, int> = 0> +void construct(value_and_holder &v_h, + std::shared_ptr> &&shd_ptr, + bool /*need_alias*/) { + auto *ptr = shd_ptr.get(); + no_nullptr(ptr); + auto smhldr = type_caster>::template smart_holder_from_shared_ptr(shd_ptr); + v_h.value_ptr() = ptr; + v_h.type->init_instance(v_h.inst, &smhldr); +} +// clang-format off + // Implementing class for py::init<...>() template struct constructor { diff --git a/include/pybind11/detail/smart_holder_poc.h b/include/pybind11/detail/smart_holder_poc.h new file mode 100644 index 0000000000..f6aaae2d0a --- /dev/null +++ b/include/pybind11/detail/smart_holder_poc.h @@ -0,0 +1,266 @@ +// Copyright (c) 2020-2021 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/* Proof-of-Concept for smart pointer interoperability. + +High-level aspects: + +* Support all `unique_ptr`, `shared_ptr` interops that are feasible. + +* Cleanly and clearly report all interops that are infeasible. + +* Meant to fit into a `PyObject`, as a holder for C++ objects. + +* Support a system design that makes it impossible to trigger + C++ Undefined Behavior, especially from Python. + +* Support a system design with clean runtime inheritance casting. From this + it follows that the `smart_holder` needs to be type-erased (`void*`). + +* Handling of RTTI for the type-erased held pointer is NOT implemented here. + It is the responsibility of the caller to ensure that `static_cast` + is well-formed when calling `as_*` member functions. Inheritance casting + needs to be handled in a different layer (similar to the code organization + in boost/python/object/inheritance.hpp). + +Details: + +* The "root holder" chosen here is a `shared_ptr` (named `vptr` in this + implementation). This choice is practically inevitable because `shared_ptr` + has only very limited support for inspecting and accessing its deleter. + +* If created from a raw pointer, or a `unique_ptr` without a custom deleter, + `vptr` always uses a custom deleter, to support `unique_ptr`-like disowning. + The custom deleters can be extended to included life-time managment for + external objects (e.g. `PyObject`). + +* If created from an external `shared_ptr`, or a `unique_ptr` with a custom + deleter, including life-time management for external objects is infeasible. + +* The smart_holder is movable but not copyable, as a consequence of using + unique_ptr for the vptr_deleter_armed_flag_ptr. Note that the bool for + the flag has to live on the heap, for the smart_holder to be movable. + unique_ptr is a great fit for this situation. +*/ + +#pragma once + +#include +#include +#include +#include + +// pybindit = Python Bindings Innovation Track. +// Currently not in pybind11 namespace to signal that this POC does not depend +// on any existing pybind11 functionality. +namespace pybindit { +namespace memory { + +template +struct guarded_builtin_delete { + bool *flag_ptr; + explicit guarded_builtin_delete(bool *armed_flag_ptr) : flag_ptr{armed_flag_ptr} {} + void operator()(T *raw_ptr) { + if (*flag_ptr) + delete raw_ptr; + } +}; + +template +struct guarded_custom_deleter { + bool *flag_ptr; + explicit guarded_custom_deleter(bool *armed_flag_ptr) : flag_ptr{armed_flag_ptr} {} + void operator()(T *raw_ptr) { + if (*flag_ptr) + D()(raw_ptr); + } +}; + +template +inline bool is_std_default_delete(const std::type_info &rtti_deleter) { + return rtti_deleter == typeid(std::default_delete) + || rtti_deleter == typeid(std::default_delete); +} + +struct smart_holder { + const std::type_info *rtti_uqp_del; + std::unique_ptr vptr_deleter_armed_flag_ptr; + std::shared_ptr vptr; + bool vptr_is_using_noop_deleter : 1; + bool vptr_is_using_builtin_delete : 1; + bool vptr_is_external_shared_ptr : 1; + bool is_populated : 1; + + smart_holder() + : rtti_uqp_del{nullptr}, vptr_is_using_noop_deleter{false}, + vptr_is_using_builtin_delete{false}, vptr_is_external_shared_ptr{false}, is_populated{ + false} {} + + explicit smart_holder(bool vptr_deleter_armed_flag) + : rtti_uqp_del{nullptr}, vptr_deleter_armed_flag_ptr{new bool{vptr_deleter_armed_flag}}, + vptr_is_using_noop_deleter{false}, vptr_is_using_builtin_delete{false}, + vptr_is_external_shared_ptr{false}, is_populated{false} {} + + bool has_pointee() const { return vptr.get() != nullptr; } + + void ensure_is_populated(const char *context) const { + if (!is_populated) { + throw std::runtime_error(std::string("Unpopulated holder (") + context + ")."); + } + } + + void ensure_vptr_is_using_builtin_delete(const char *context) const { + if (vptr_is_external_shared_ptr) { + throw std::runtime_error(std::string("Cannot disown external shared_ptr (") + context + + ")."); + } + if (vptr_is_using_noop_deleter) { + throw std::runtime_error(std::string("Cannot disown non-owning holder (") + context + + ")."); + } + if (!vptr_is_using_builtin_delete) { + throw std::runtime_error(std::string("Cannot disown custom deleter (") + context + + ")."); + } + } + + template + void ensure_compatible_rtti_uqp_del(const char *context) const { + const std::type_info *rtti_requested = &typeid(D); + if (!rtti_uqp_del) { + if (!is_std_default_delete(*rtti_requested)) { + throw std::runtime_error(std::string("Missing unique_ptr deleter (") + context + + ")."); + } + ensure_vptr_is_using_builtin_delete(context); + } else if (!(*rtti_requested == *rtti_uqp_del)) { + throw std::runtime_error(std::string("Incompatible unique_ptr deleter (") + context + + ")."); + } + } + + void ensure_has_pointee(const char *context) const { + if (!has_pointee()) { + throw std::runtime_error(std::string("Disowned holder (") + context + ")."); + } + } + + void ensure_use_count_1(const char *context) const { + if (vptr.get() == nullptr) { + throw std::runtime_error(std::string("Cannot disown nullptr (") + context + ")."); + } + // In multithreaded environments accessing use_count can lead to + // race conditions, but in the context of Python it is a bug (elsewhere) + // if the Global Interpreter Lock (GIL) is not being held when this code + // is reached. + // SMART_HOLDER_WIP: IMPROVABLE: assert(GIL is held). + if (vptr.use_count() != 1) { + throw std::runtime_error(std::string("Cannot disown use_count != 1 (") + context + + ")."); + } + } + + template + static smart_holder from_raw_ptr_unowned(T *raw_ptr) { + smart_holder hld(false); + hld.vptr.reset(raw_ptr, guarded_builtin_delete(hld.vptr_deleter_armed_flag_ptr.get())); + hld.vptr_is_using_noop_deleter = true; + hld.is_populated = true; + return hld; + } + + template + T *as_raw_ptr_unowned() const { + return static_cast(vptr.get()); + } + + template + T &as_lvalue_ref() const { + static const char *context = "as_lvalue_ref"; + ensure_is_populated(context); + ensure_has_pointee(context); + return *as_raw_ptr_unowned(); + } + + template + T &&as_rvalue_ref() const { + static const char *context = "as_rvalue_ref"; + ensure_is_populated(context); + ensure_has_pointee(context); + return std::move(*as_raw_ptr_unowned()); + } + + template + static smart_holder from_raw_ptr_take_ownership(T *raw_ptr) { + smart_holder hld(true); + hld.vptr.reset(raw_ptr, guarded_builtin_delete(hld.vptr_deleter_armed_flag_ptr.get())); + hld.vptr_is_using_builtin_delete = true; + hld.is_populated = true; + return hld; + } + + void ensure_can_release_ownership(const char *context = "ensure_can_release_ownership") { + ensure_vptr_is_using_builtin_delete(context); + ensure_use_count_1(context); + } + + // Caller is responsible for calling ensure_can_release_ownership(). + void release_ownership() { + *vptr_deleter_armed_flag_ptr = false; + vptr.reset(); + vptr_deleter_armed_flag_ptr.reset(); + } + + template + T *as_raw_ptr_release_ownership(const char *context = "as_raw_ptr_release_ownership") { + ensure_can_release_ownership(context); + T *raw_ptr = as_raw_ptr_unowned(); + release_ownership(); + return raw_ptr; + } + + template + static smart_holder from_unique_ptr(std::unique_ptr &&unq_ptr) { + smart_holder hld(true); + hld.rtti_uqp_del = &typeid(D); + hld.vptr_is_using_builtin_delete = is_std_default_delete(*hld.rtti_uqp_del); + if (hld.vptr_is_using_builtin_delete) { + hld.vptr.reset(unq_ptr.get(), + guarded_builtin_delete(hld.vptr_deleter_armed_flag_ptr.get())); + } else { + hld.vptr.reset(unq_ptr.get(), + guarded_custom_deleter(hld.vptr_deleter_armed_flag_ptr.get())); + } + unq_ptr.release(); + hld.is_populated = true; + return hld; + } + + template > + std::unique_ptr as_unique_ptr() { + static const char *context = "as_unique_ptr"; + ensure_compatible_rtti_uqp_del(context); + ensure_use_count_1(context); + T *raw_ptr = as_raw_ptr_unowned(); + release_ownership(); + return std::unique_ptr(raw_ptr); + } + + template + static smart_holder from_shared_ptr(std::shared_ptr shd_ptr) { + smart_holder hld; + hld.vptr = std::static_pointer_cast(shd_ptr); + hld.vptr_is_external_shared_ptr = true; + hld.is_populated = true; + return hld; + } + + template + std::shared_ptr as_shared_ptr() const { + return std::static_pointer_cast(vptr); + } +}; + +} // namespace memory +} // namespace pybindit diff --git a/include/pybind11/detail/smart_holder_sfinae_hooks_only.h b/include/pybind11/detail/smart_holder_sfinae_hooks_only.h new file mode 100644 index 0000000000..f324854751 --- /dev/null +++ b/include/pybind11/detail/smart_holder_sfinae_hooks_only.h @@ -0,0 +1,33 @@ +// Copyright (c) 2021 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#pragma once + +#include "common.h" + +#include + +#ifndef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT +// #define PYBIND11_USE_SMART_HOLDER_AS_DEFAULT +// Currently the main purpose of this switch is to enable non-intrusive comprehensive testing. If +// and when `smart_holder` will actually become the released default is currently open. In the +// meantime, the full functionality is easily available by using `py::classh`, which is just a +// handy shortcut for `py::class_` (see `pybind11/smart_holder.h`). Classes +// wrapped in this way are fully compatible with everything existing. +#endif + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) +PYBIND11_NAMESPACE_BEGIN(detail) + +template +struct is_smart_holder_type : std::false_type {}; + +// Tag to be used as base class, inspected by type_uses_smart_holder_type_caster test. +struct smart_holder_type_caster_base_tag {}; + +template +struct type_uses_smart_holder_type_caster; + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/detail/smart_holder_type_casters.h b/include/pybind11/detail/smart_holder_type_casters.h new file mode 100644 index 0000000000..3865ece255 --- /dev/null +++ b/include/pybind11/detail/smart_holder_type_casters.h @@ -0,0 +1,747 @@ +// Copyright (c) 2021 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#pragma once + +#include "../pytypes.h" +#include "common.h" +#include "descr.h" +#include "internals.h" +#include "smart_holder_poc.h" +#include "smart_holder_sfinae_hooks_only.h" +#include "type_caster_base.h" +#include "typeid.h" + +#include +#include +#include +#include +#include +#include +#include + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +using pybindit::memory::smart_holder; + +PYBIND11_NAMESPACE_BEGIN(detail) + +template <> +struct is_smart_holder_type : std::true_type {}; + +// SMART_HOLDER_WIP: Needs refactoring of existing pybind11 code. +inline void register_instance(instance *self, void *valptr, const type_info *tinfo); +inline bool deregister_instance(instance *self, void *valptr, const type_info *tinfo); + +// The modified_type_caster_generic_load_impl could replace type_caster_generic::load_impl but not +// vice versa. The main difference is that the original code only propagates a reference to the +// held value, while the modified implementation propagates value_and_holder. +// clang-format off +class modified_type_caster_generic_load_impl { +public: + PYBIND11_NOINLINE modified_type_caster_generic_load_impl(const std::type_info &type_info) + : typeinfo(get_type_info(type_info)), cpptype(&type_info) { } + + explicit modified_type_caster_generic_load_impl(const type_info *typeinfo = nullptr) + : typeinfo(typeinfo), cpptype(typeinfo ? typeinfo->cpptype : nullptr) { } + + bool load(handle src, bool convert) { + return load_impl(src, convert); + } + + // Base methods for generic caster; there are overridden in copyable_holder_caster + void load_value_and_holder(value_and_holder &&v_h) { + if (!v_h.holder_constructed()) { + // This is needed for old-style __init__. + // type_caster_generic::load_value BEGIN + auto *&vptr = v_h.value_ptr(); + // Lazy allocation for unallocated values: + if (vptr == nullptr) { + // Lazy allocation for unallocated values: + auto *type = v_h.type ? v_h.type : typeinfo; + if (type->operator_new) { + vptr = type->operator_new(type->type_size); + } else { + #if defined(__cpp_aligned_new) && (!defined(_MSC_VER) || _MSC_VER >= 1912) + if (type->type_align > __STDCPP_DEFAULT_NEW_ALIGNMENT__) + vptr = ::operator new(type->type_size, + std::align_val_t(type->type_align)); + else + #endif + vptr = ::operator new(type->type_size); + } + } + // type_caster_generic::load_value END + } + loaded_v_h = std::move(v_h); + loaded_v_h.type = typeinfo; + } + + bool try_implicit_casts(handle src, bool convert) { + for (auto &cast : typeinfo->implicit_casts) { + modified_type_caster_generic_load_impl sub_caster(*cast.first); + if (sub_caster.load(src, convert)) { + if (loaded_v_h_cpptype != nullptr) { + pybind11_fail("smart_holder_type_casters: try_implicit_casts failure."); + } + loaded_v_h = sub_caster.loaded_v_h; + loaded_v_h_cpptype = cast.first; + implicit_cast = cast.second; + return true; + } + } + return false; + } + + bool try_direct_conversions(handle src) { + for (auto &converter : *typeinfo->direct_conversions) { + if (converter(src.ptr(), unowned_void_ptr_from_direct_conversion)) { + return true; + } + } + return false; + } + + PYBIND11_NOINLINE static void *local_load(PyObject *src, const type_info *ti) { + std::unique_ptr loader( + new modified_type_caster_generic_load_impl(ti)); + if (loader->load(src, false)) { + // Trick to work with the existing pybind11 internals. + // The void pointer is immediately captured in a new unique_ptr in + // try_load_foreign_module_local. If this assumption is violated sanitizers + // will most likely flag a leak (verified to be the case with ASAN). + return static_cast(loader.release()); + } + return nullptr; + } + + /// Try to load with foreign typeinfo, if available. Used when there is no + /// native typeinfo, or when the native one wasn't able to produce a value. + PYBIND11_NOINLINE bool try_load_foreign_module_local(handle src) { + constexpr auto *local_key = PYBIND11_MODULE_LOCAL_ID; + const auto pytype = type::handle_of(src); + if (!hasattr(pytype, local_key)) + return false; + + type_info *foreign_typeinfo = reinterpret_borrow(getattr(pytype, local_key)); + // Only consider this foreign loader if actually foreign and is a loader of the correct cpp type + if (foreign_typeinfo->module_local_load == &local_load + || (cpptype && !same_type(*cpptype, *foreign_typeinfo->cpptype))) + return false; + + void* foreign_loader_void_ptr = + foreign_typeinfo->module_local_load(src.ptr(), foreign_typeinfo); + if (foreign_loader_void_ptr != nullptr) { + auto foreign_loader = std::unique_ptr( + static_cast(foreign_loader_void_ptr)); + // Magic number intentionally hard-coded for simplicity and maximum robustness. + if (foreign_loader->local_load_safety_guard != 1887406645) { + pybind11_fail( + "smart_holder_type_casters: Unexpected local_load_safety_guard," + " possibly due to py::class_ holder mixup."); + } + if (loaded_v_h_cpptype != nullptr) { + pybind11_fail("smart_holder_type_casters: try_load_foreign_module_local failure."); + } + loaded_v_h = foreign_loader->loaded_v_h; + loaded_v_h_cpptype = foreign_loader->loaded_v_h_cpptype; + implicit_cast = foreign_loader->implicit_cast; + return true; + } + return false; + } + + // Implementation of `load`; this takes the type of `this` so that it can dispatch the relevant + // bits of code between here and copyable_holder_caster where the two classes need different + // logic (without having to resort to virtual inheritance). + template + PYBIND11_NOINLINE bool load_impl(handle src, bool convert) { + if (!src) return false; + if (!typeinfo) return try_load_foreign_module_local(src); + if (src.is_none()) { + // Defer accepting None to other overloads (if we aren't in convert mode): + if (!convert) return false; + loaded_v_h = value_and_holder(); + return true; + } + + auto &this_ = static_cast(*this); + + PyTypeObject *srctype = Py_TYPE(src.ptr()); + + // Case 1: If src is an exact type match for the target type then we can reinterpret_cast + // the instance's value pointer to the target type: + if (srctype == typeinfo->type) { + this_.load_value_and_holder(reinterpret_cast(src.ptr())->get_value_and_holder()); + return true; + } + // Case 2: We have a derived class + else if (PyType_IsSubtype(srctype, typeinfo->type)) { + auto &bases = all_type_info(srctype); // subtype bases + bool no_cpp_mi = typeinfo->simple_type; + + // Case 2a: the python type is a Python-inherited derived class that inherits from just + // one simple (no MI) pybind11 class, or is an exact match, so the C++ instance is of + // the right type and we can use reinterpret_cast. + // (This is essentially the same as case 2b, but because not using multiple inheritance + // is extremely common, we handle it specially to avoid the loop iterator and type + // pointer lookup overhead) + if (bases.size() == 1 && (no_cpp_mi || bases.front()->type == typeinfo->type)) { + this_.load_value_and_holder(reinterpret_cast(src.ptr())->get_value_and_holder()); + loaded_v_h_cpptype = bases.front()->cpptype; + reinterpret_cast_deemed_ok = true; + return true; + } + // Case 2b: the python type inherits from multiple C++ bases. Check the bases to see if + // we can find an exact match (or, for a simple C++ type, an inherited match); if so, we + // can safely reinterpret_cast to the relevant pointer. + else if (bases.size() > 1) { + for (auto base : bases) { + if (no_cpp_mi ? PyType_IsSubtype(base->type, typeinfo->type) : base->type == typeinfo->type) { + this_.load_value_and_holder(reinterpret_cast(src.ptr())->get_value_and_holder(base)); + loaded_v_h_cpptype = base->cpptype; + reinterpret_cast_deemed_ok = true; + return true; + } + } + } + + // Case 2c: C++ multiple inheritance is involved and we couldn't find an exact type match + // in the registered bases, above, so try implicit casting (needed for proper C++ casting + // when MI is involved). + if (this_.try_implicit_casts(src, convert)) { + return true; + } + } + + // Perform an implicit conversion + if (convert) { + for (auto &converter : typeinfo->implicit_conversions) { + auto temp = reinterpret_steal(converter(src.ptr(), typeinfo->type)); + if (load_impl(temp, false)) { + loader_life_support::add_patient(temp); + return true; + } + } + if (this_.try_direct_conversions(src)) + return true; + } + + // Failed to match local typeinfo. Try again with global. + if (typeinfo->module_local) { + if (auto gtype = get_global_type_info(*typeinfo->cpptype)) { + typeinfo = gtype; + return load(src, false); + } + } + + // Global typeinfo has precedence over foreign module_local + return try_load_foreign_module_local(src); + } + + const type_info *typeinfo = nullptr; + const std::type_info *cpptype = nullptr; + void *unowned_void_ptr_from_direct_conversion = nullptr; + const std::type_info *loaded_v_h_cpptype = nullptr; + void *(*implicit_cast)(void *) = nullptr; + value_and_holder loaded_v_h; + bool reinterpret_cast_deemed_ok = false; + // Magic number intentionally hard-coded, to guard against class_ holder mixups. + // Ideally type_caster_generic would have a similar guard, but this requires a change there. + // SMART_HOLDER_WIP: If it is decided that this guard is useful long term, potentially + // set/reset this value in ctor/dtor, mark volatile. + std::size_t local_load_safety_guard = 1887406645; // 32-bit compatible value for portability. +}; +// clang-format on + +struct smart_holder_type_caster_class_hooks : smart_holder_type_caster_base_tag { + static decltype(&modified_type_caster_generic_load_impl::local_load) + get_local_load_function_ptr() { + return &modified_type_caster_generic_load_impl::local_load; + } + + template + static void init_instance_for_type(detail::instance *inst, const void *holder_const_void_ptr) { + // Need for const_cast is a consequence of the type_info::init_instance type: + // void (*init_instance)(instance *, const void *); + auto holder_void_ptr = const_cast(holder_const_void_ptr); + + auto v_h = inst->get_value_and_holder(detail::get_type_info(typeid(T))); + if (!v_h.instance_registered()) { + register_instance(inst, v_h.value_ptr(), v_h.type); + v_h.set_instance_registered(); + } + using holder_type = pybindit::memory::smart_holder; + if (holder_void_ptr) { + // Note: inst->owned ignored. + auto holder_ptr = static_cast(holder_void_ptr); + new (std::addressof(v_h.holder())) holder_type(std::move(*holder_ptr)); + } else if (inst->owned) { + new (std::addressof(v_h.holder())) + holder_type(holder_type::from_raw_ptr_take_ownership(v_h.value_ptr())); + } else { + new (std::addressof(v_h.holder())) + holder_type(holder_type::from_raw_ptr_unowned(v_h.value_ptr())); + } + v_h.set_holder_constructed(); + } + + template + static smart_holder smart_holder_from_unique_ptr(std::unique_ptr &&unq_ptr) { + return pybindit::memory::smart_holder::from_unique_ptr(std::move(unq_ptr)); + } + + template + static smart_holder smart_holder_from_shared_ptr(std::shared_ptr shd_ptr) { + return pybindit::memory::smart_holder::from_shared_ptr(shd_ptr); + } +}; + +template +struct smart_holder_type_caster_load { + using holder_type = pybindit::memory::smart_holder; + + bool load(handle src, bool convert) { + static_assert(type_uses_smart_holder_type_caster::value, "Internal consistency error."); + load_impl = modified_type_caster_generic_load_impl(typeid(T)); + if (!load_impl.load(src, convert)) + return false; + return true; + } + + T *loaded_as_raw_ptr_unowned() const { + void *void_ptr = load_impl.unowned_void_ptr_from_direct_conversion; + if (void_ptr == nullptr) { + if (have_holder()) { + throw_if_uninitialized_or_disowned_holder(); + void_ptr = holder().template as_raw_ptr_unowned(); + } else if (load_impl.loaded_v_h.vh != nullptr) + void_ptr = load_impl.loaded_v_h.value_ptr(); + if (void_ptr == nullptr) + return nullptr; + } + return convert_type(void_ptr); + } + + T &loaded_as_lvalue_ref() const { + T *raw_ptr = loaded_as_raw_ptr_unowned(); + if (raw_ptr == nullptr) + throw reference_cast_error(); + return *raw_ptr; + } + + std::shared_ptr loaded_as_shared_ptr() const { + if (load_impl.unowned_void_ptr_from_direct_conversion != nullptr) + throw cast_error("Unowned pointer from direct conversion cannot be converted to a" + " std::shared_ptr."); + if (!have_holder()) + return nullptr; + throw_if_uninitialized_or_disowned_holder(); + std::shared_ptr void_ptr = holder().template as_shared_ptr(); + return std::shared_ptr(void_ptr, convert_type(void_ptr.get())); + } + + template + std::unique_ptr loaded_as_unique_ptr(const char *context = "loaded_as_unique_ptr") { + if (load_impl.unowned_void_ptr_from_direct_conversion != nullptr) + throw cast_error("Unowned pointer from direct conversion cannot be converted to a" + " std::unique_ptr."); + if (!have_holder()) + return nullptr; + throw_if_uninitialized_or_disowned_holder(); + holder().template ensure_compatible_rtti_uqp_del(context); + holder().ensure_use_count_1(context); + auto raw_void_ptr = holder().template as_raw_ptr_unowned(); + // SMART_HOLDER_WIP: MISSING: Safety checks for type conversions + // (T must be polymorphic or meet certain other conditions). + T *raw_type_ptr = convert_type(raw_void_ptr); + + // Critical transfer-of-ownership section. This must stay together. + holder().release_ownership(); + auto result = std::unique_ptr(raw_type_ptr); + + void *value_void_ptr = load_impl.loaded_v_h.value_ptr(); + if (value_void_ptr != raw_void_ptr) { + pybind11_fail("smart_holder_type_casters: loaded_as_unique_ptr failure:" + " value_void_ptr != raw_void_ptr"); + } + load_impl.loaded_v_h.value_ptr() = nullptr; + deregister_instance(load_impl.loaded_v_h.inst, value_void_ptr, load_impl.loaded_v_h.type); + + return result; + } + +private: + modified_type_caster_generic_load_impl load_impl; + + bool have_holder() const { + return load_impl.loaded_v_h.vh != nullptr && load_impl.loaded_v_h.holder_constructed(); + } + + holder_type &holder() const { return load_impl.loaded_v_h.holder(); } + + // have_holder() must be true or this function will fail. + void throw_if_uninitialized_or_disowned_holder() const { + if (!holder().is_populated) { + pybind11_fail("Missing value for wrapped C++ type:" + " Python instance is uninitialized."); + } + if (!holder().has_pointee()) { + throw cast_error("Missing value for wrapped C++ type:" + " Python instance was disowned."); + } + } + + T *convert_type(void *void_ptr) const { + if (void_ptr != nullptr && load_impl.loaded_v_h_cpptype != nullptr + && !load_impl.reinterpret_cast_deemed_ok && load_impl.implicit_cast != nullptr) { + void_ptr = load_impl.implicit_cast(void_ptr); + } + return static_cast(void_ptr); + } +}; + +// SMART_HOLDER_WIP: Needs refactoring of existing pybind11 code. +struct make_constructor : private type_caster_base { // Any type, nothing special about int. + using type_caster_base::Constructor; + using type_caster_base::make_copy_constructor; + using type_caster_base::make_move_constructor; +}; + +template +struct smart_holder_type_caster : smart_holder_type_caster_load, + smart_holder_type_caster_class_hooks { + static constexpr auto name = _(); + + // static handle cast(T, ...) + // is redundant (leads to ambiguous overloads). + + static handle cast(T &&src, return_value_policy /*policy*/, handle parent) { + // type_caster_base BEGIN + // clang-format off + return cast(&src, return_value_policy::move, parent); + // clang-format on + // type_caster_base END + } + + static handle cast(T const &src, return_value_policy policy, handle parent) { + // type_caster_base BEGIN + // clang-format off + if (policy == return_value_policy::automatic || policy == return_value_policy::automatic_reference) + policy = return_value_policy::copy; + return cast(&src, policy, parent); + // clang-format on + // type_caster_base END + } + + static handle cast(T &src, return_value_policy policy, handle parent) { + return cast(const_cast(src), policy, parent); // Mutbl2Const + } + + static handle cast(T const *src, return_value_policy policy, handle parent) { + auto st = type_caster_base::src_and_type(src); + return cast_const_raw_ptr( // Originally type_caster_generic::cast. + st.first, + policy, + parent, + st.second, + make_constructor::make_copy_constructor(src), + make_constructor::make_move_constructor(src)); + } + + static handle cast(T *src, return_value_policy policy, handle parent) { + return cast(const_cast(src), policy, parent); // Mutbl2Const + } + +#if defined(_MSC_VER) && _MSC_VER < 1910 + // Working around MSVC 2015 bug. const-correctness is lost. + // SMART_HOLDER_WIP: IMPROVABLE: make common code work with MSVC 2015. + template + using cast_op_type = detail::cast_op_type; +#else + template + using cast_op_type = conditional_t< + std::is_same, T const *>::value, + T const *, + conditional_t, T *>::value, + T *, + conditional_t::value, T const &, T &>>>; +#endif + + // The const operators here prove that the existing type_caster mechanism already supports + // const-correctness. However, fully implementing const-correctness inside this type_caster + // is still a major project. + operator T const &() const { + return const_cast(this)->loaded_as_lvalue_ref(); + } + operator T const *() const { + return const_cast(this)->loaded_as_raw_ptr_unowned(); + } + operator T &() { return this->loaded_as_lvalue_ref(); } + operator T *() { return this->loaded_as_raw_ptr_unowned(); } + + // Originally type_caster_generic::cast. + PYBIND11_NOINLINE static handle cast_const_raw_ptr(const void *_src, + return_value_policy policy, + handle parent, + const detail::type_info *tinfo, + void *(*copy_constructor)(const void *), + void *(*move_constructor)(const void *), + const void *existing_holder = nullptr) { + if (!tinfo) // no type info: error will be set already + return handle(); + + void *src = const_cast(_src); + if (src == nullptr) + return none().release(); + + if (handle existing_inst = find_registered_python_instance(src, tinfo)) + return existing_inst; + + auto inst = reinterpret_steal(make_new_instance(tinfo->type)); + auto wrapper = reinterpret_cast(inst.ptr()); + wrapper->owned = false; + void *&valueptr = values_and_holders(wrapper).begin()->value_ptr(); + + switch (policy) { + case return_value_policy::automatic: + case return_value_policy::take_ownership: + valueptr = src; + wrapper->owned = true; + break; + + case return_value_policy::automatic_reference: + case return_value_policy::reference: + valueptr = src; + wrapper->owned = false; + break; + + case return_value_policy::copy: + if (copy_constructor) + valueptr = copy_constructor(src); + else { +#if defined(NDEBUG) + throw cast_error("return_value_policy = copy, but type is " + "non-copyable! (compile in debug mode for details)"); +#else + std::string type_name(tinfo->cpptype->name()); + detail::clean_type_id(type_name); + throw cast_error("return_value_policy = copy, but type " + type_name + + " is non-copyable!"); +#endif + } + wrapper->owned = true; + break; + + case return_value_policy::move: + if (move_constructor) + valueptr = move_constructor(src); + else if (copy_constructor) + valueptr = copy_constructor(src); + else { +#if defined(NDEBUG) + throw cast_error("return_value_policy = move, but type is neither " + "movable nor copyable! " + "(compile in debug mode for details)"); +#else + std::string type_name(tinfo->cpptype->name()); + detail::clean_type_id(type_name); + throw cast_error("return_value_policy = move, but type " + type_name + + " is neither movable nor copyable!"); +#endif + } + wrapper->owned = true; + break; + + case return_value_policy::reference_internal: + valueptr = src; + wrapper->owned = false; + keep_alive_impl(inst, parent); + break; + + default: + throw cast_error("unhandled return_value_policy: should not happen!"); + } + + tinfo->init_instance(wrapper, existing_holder); + + return inst.release(); + } +}; + +template +struct smart_holder_type_caster> : smart_holder_type_caster_load, + smart_holder_type_caster_class_hooks { + static constexpr auto name = _>(); + + static handle cast(const std::shared_ptr &src, return_value_policy policy, handle parent) { + if (policy != return_value_policy::automatic + && policy != return_value_policy::reference_internal) { + // SMART_HOLDER_WIP: IMPROVABLE: Error message. + throw cast_error("Invalid return_value_policy for shared_ptr."); + } + + auto src_raw_ptr = src.get(); + auto st = type_caster_base::src_and_type(src_raw_ptr); + if (st.first == nullptr) + return none().release(); // PyErr was set already. + + void *src_raw_void_ptr = static_cast(src_raw_ptr); + const detail::type_info *tinfo = st.second; + if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) + // SMART_HOLDER_WIP: MISSING: Enforcement of consistency with existing smart_holder. + // SMART_HOLDER_WIP: MISSING: keep_alive. + return existing_inst; + + auto inst = reinterpret_steal(make_new_instance(tinfo->type)); + auto *inst_raw_ptr = reinterpret_cast(inst.ptr()); + inst_raw_ptr->owned = true; + void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr(); + valueptr = src_raw_void_ptr; + + auto smhldr = pybindit::memory::smart_holder::from_shared_ptr(src); + tinfo->init_instance(inst_raw_ptr, static_cast(&smhldr)); + + if (policy == return_value_policy::reference_internal) + keep_alive_impl(inst, parent); + + return inst.release(); + } + + template + using cast_op_type = std::shared_ptr; + + operator std::shared_ptr() { return this->loaded_as_shared_ptr(); } +}; + +template +struct smart_holder_type_caster> : smart_holder_type_caster_load, + smart_holder_type_caster_class_hooks { + static constexpr auto name = _>(); + + static handle + cast(const std::shared_ptr &src, return_value_policy policy, handle parent) { + return smart_holder_type_caster>::cast( + std::const_pointer_cast(src), // Const2Mutbl + policy, + parent); + } + + template + using cast_op_type = std::shared_ptr; + + operator std::shared_ptr() { return this->loaded_as_shared_ptr(); } // Mutbl2Const +}; + +template +struct smart_holder_type_caster> : smart_holder_type_caster_load, + smart_holder_type_caster_class_hooks { + static constexpr auto name = _>(); + + static handle cast(std::unique_ptr &&src, return_value_policy policy, handle parent) { + if (policy != return_value_policy::automatic + && policy != return_value_policy::reference_internal + && policy != return_value_policy::move) { + // SMART_HOLDER_WIP: IMPROVABLE: Error message. + throw cast_error("Invalid return_value_policy for unique_ptr."); + } + + auto src_raw_ptr = src.get(); + auto st = type_caster_base::src_and_type(src_raw_ptr); + if (st.first == nullptr) + return none().release(); // PyErr was set already. + + void *src_raw_void_ptr = static_cast(src_raw_ptr); + const detail::type_info *tinfo = st.second; + if (find_registered_python_instance(src_raw_void_ptr, tinfo)) + throw cast_error("Invalid unique_ptr: another instance owns this pointer already."); + + auto inst = reinterpret_steal(make_new_instance(tinfo->type)); + auto *inst_raw_ptr = reinterpret_cast(inst.ptr()); + inst_raw_ptr->owned = true; + void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr(); + valueptr = src_raw_void_ptr; + + auto smhldr = pybindit::memory::smart_holder::from_unique_ptr(std::move(src)); + tinfo->init_instance(inst_raw_ptr, static_cast(&smhldr)); + + if (policy == return_value_policy::reference_internal) + keep_alive_impl(inst, parent); + + return inst.release(); + } + + template + using cast_op_type = std::unique_ptr; + + operator std::unique_ptr() { return this->template loaded_as_unique_ptr(); } +}; + +template +struct smart_holder_type_caster> + : smart_holder_type_caster_load, smart_holder_type_caster_class_hooks { + static constexpr auto name = _>(); + + static handle + cast(std::unique_ptr &&src, return_value_policy policy, handle parent) { + return smart_holder_type_caster>::cast( + std::unique_ptr(const_cast(src.release())), // Const2Mutbl + policy, + parent); + } + + template + using cast_op_type = std::unique_ptr; + + operator std::unique_ptr() { return this->template loaded_as_unique_ptr(); } +}; + +#ifndef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT + +# define PYBIND11_SMART_HOLDER_TYPE_CASTERS(T) \ + namespace pybind11 { \ + namespace detail { \ + template <> \ + class type_caster : public smart_holder_type_caster {}; \ + template <> \ + class type_caster> \ + : public smart_holder_type_caster> {}; \ + template <> \ + class type_caster> \ + : public smart_holder_type_caster> {}; \ + template \ + class type_caster> \ + : public smart_holder_type_caster> {}; \ + template \ + class type_caster> \ + : public smart_holder_type_caster> {}; \ + } \ + } +#else + +# define PYBIND11_SMART_HOLDER_TYPE_CASTERS(T) + +template +class type_caster_for_class_ : public smart_holder_type_caster {}; + +template +class type_caster_for_class_> + : public smart_holder_type_caster> {}; + +template +class type_caster_for_class_> + : public smart_holder_type_caster> {}; + +template +class type_caster_for_class_> + : public smart_holder_type_caster> {}; + +template +class type_caster_for_class_> + : public smart_holder_type_caster> {}; + +#endif + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 648300eef2..e3dd4c8226 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1,3 +1,4 @@ +// clang-format off /* pybind11/pybind11.h: Main header file of the C++11 python binding generator library @@ -10,43 +11,12 @@ #pragma once -#if defined(__INTEL_COMPILER) -# pragma warning push -# pragma warning disable 68 // integer conversion resulted in a change of sign -# pragma warning disable 186 // pointless comparison of unsigned integer with zero -# pragma warning disable 878 // incompatible exception specifications -# pragma warning disable 1334 // the "template" keyword used for syntactic disambiguation may only be used within a template -# pragma warning disable 1682 // implicit conversion of a 64-bit integral type to a smaller integral type (potential portability problem) -# pragma warning disable 1786 // function "strdup" was declared deprecated -# pragma warning disable 1875 // offsetof applied to non-POD (Plain Old Data) types is nonstandard -# pragma warning disable 2196 // warning #2196: routine is both "inline" and "noinline" -#elif defined(_MSC_VER) -# pragma warning(push) -# pragma warning(disable: 4100) // warning C4100: Unreferenced formal parameter -# pragma warning(disable: 4127) // warning C4127: Conditional expression is constant -# pragma warning(disable: 4512) // warning C4512: Assignment operator was implicitly defined as deleted -# pragma warning(disable: 4800) // warning C4800: 'int': forcing value to bool 'true' or 'false' (performance warning) -# pragma warning(disable: 4996) // warning C4996: The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conformant name -# pragma warning(disable: 4702) // warning C4702: unreachable code -# pragma warning(disable: 4522) // warning C4522: multiple assignment operators specified -# pragma warning(disable: 4505) // warning C4505: 'PySlice_GetIndicesEx': unreferenced local function has been removed (PyPy only) -#elif defined(__GNUG__) && !defined(__clang__) -# pragma GCC diagnostic push -# pragma GCC diagnostic ignored "-Wunused-but-set-parameter" -# pragma GCC diagnostic ignored "-Wunused-but-set-variable" -# pragma GCC diagnostic ignored "-Wmissing-field-initializers" -# pragma GCC diagnostic ignored "-Wstrict-aliasing" -# pragma GCC diagnostic ignored "-Wattributes" -# if __GNUC__ >= 7 -# pragma GCC diagnostic ignored "-Wnoexcept-type" -# endif -#endif - #include "attr.h" #include "gil.h" #include "options.h" #include "detail/class.h" #include "detail/init.h" +#include "detail/smart_holder_sfinae_hooks_only.h" #include #include @@ -1081,7 +1051,8 @@ class generic_type : public object { public: PYBIND11_OBJECT_DEFAULT(generic_type, object, PyType_Check) protected: - void initialize(const type_record &rec) { + void initialize(const type_record &rec, + void *(*type_caster_module_local_load)(PyObject *, const type_info *)) { if (rec.scope && hasattr(rec.scope, "__dict__") && rec.scope.attr("__dict__").contains(rec.name)) pybind11_fail("generic_type: cannot initialize type \"" + std::string(rec.name) + "\": an object with that name is already defined"); @@ -1127,7 +1098,7 @@ class generic_type : public object { if (rec.module_local) { // Stash the local typeinfo and loader so that external modules can access it. - tinfo->module_local_load = &type_caster_generic::local_load; + tinfo->module_local_load = type_caster_module_local_load; setattr(m_ptr, PYBIND11_MODULE_LOCAL_ID, capsule(tinfo)); } } @@ -1240,11 +1211,45 @@ auto method_adaptor(Return (Class::*pmf)(Args...) const) -> Return (Derived::*)( return pmf; } +// clang-format on +template +#ifndef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT + +using default_holder_type = std::unique_ptr; + +# define PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(T, ...) + +#else + +using default_holder_type = smart_holder; + +// This define could be hidden away inside detail/smart_holder_type_casters.h, but is kept here +// for clarity. +# define PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(T, ...) \ + namespace pybind11 { \ + namespace detail { \ + template <> \ + class type_caster : public type_caster_base {}; \ + template <> \ + class type_caster<__VA_ARGS__> : public type_caster_holder {}; \ + } \ + } + +#endif +// clang-format off + template class class_ : public detail::generic_type { - template using is_holder = detail::is_holder_type; template using is_subtype = detail::is_strict_base_of; template using is_base = detail::is_strict_base_of; + template + // clang-format on + using is_holder + = detail::any_of, + detail::all_of>, + detail::negation>, + detail::type_uses_smart_holder_type_caster>>; + // clang-format off // struct instead of using here to help MSVC: template struct is_valid_class_option : detail::any_of, is_subtype, is_base> {}; @@ -1253,7 +1258,7 @@ class class_ : public detail::generic_type { using type = type_; using type_alias = detail::exactly_one_t; constexpr static bool has_alias = !std::is_void::value; - using holder_type = detail::exactly_one_t, options...>; + using holder_type = detail::exactly_one_t, options...>; static_assert(detail::all_of...>::value, "Unknown/invalid class_ template parameters provided"); @@ -1275,6 +1280,39 @@ class class_ : public detail::generic_type { none_of...>::value), // no multiple_inheritance attr "Error: multiple inheritance bases must be specified via class_ template options"); + // clang-format on + static constexpr bool holder_is_smart_holder + = detail::is_smart_holder_type::value; + static constexpr bool wrapped_type_uses_smart_holder_type_caster + = detail::type_uses_smart_holder_type_caster::value; + static constexpr bool type_caster_type_is_type_caster_base_subtype + = std::is_base_of, detail::type_caster>::value; + // Necessary conditions, but not strict. + static_assert( + !(detail::is_instantiation::value + && wrapped_type_uses_smart_holder_type_caster), + "py::class_ holder vs type_caster mismatch:" + " missing PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(T, std::unique_ptr)?"); + static_assert( + !(detail::is_instantiation::value + && wrapped_type_uses_smart_holder_type_caster), + "py::class_ holder vs type_caster mismatch:" + " missing PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(T, std::shared_ptr)?"); + static_assert(!(holder_is_smart_holder && type_caster_type_is_type_caster_base_subtype), + "py::class_ holder vs type_caster mismatch:" + " missing PYBIND11_SMART_HOLDER_TYPE_CASTERS(T)?"); +#ifdef PYBIND11_STRICT_ASSERTS_CLASS_HOLDER_VS_TYPE_CASTER_MIX + // Strict conditions cannot be enforced universally at the moment (PR #2836). + static_assert(holder_is_smart_holder == wrapped_type_uses_smart_holder_type_caster, + "py::class_ holder vs type_caster mismatch:" + " missing PYBIND11_SMART_HOLDER_TYPE_CASTERS(T)" + " or collision with custom py::detail::type_caster?"); + static_assert(!holder_is_smart_holder == type_caster_type_is_type_caster_base_subtype, + "py::class_ holder vs type_caster mismatch:" + " missing PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(T, ...)" + " or collision with custom py::detail::type_caster?"); +#endif + // clang-format off type_record record; record.scope = scope; record.name = name; @@ -1284,6 +1322,8 @@ class class_ : public detail::generic_type { record.holder_size = sizeof(holder_type); record.init_instance = init_instance; record.dealloc = dealloc; + + // A more fitting name would be uses_unique_ptr_holder. record.default_holder = detail::is_instantiation::value; set_operator_new(&record); @@ -1294,7 +1334,7 @@ class class_ : public detail::generic_type { /* Process optional arguments, if any */ process_attributes::init(extra..., &record); - generic_type::initialize(record); + generic_type_initialize(record); if (has_alias) { auto &instances = record.module_local ? registered_local_types_cpp() : get_internals().registered_types_cpp; @@ -1502,6 +1542,20 @@ class class_ : public detail::generic_type { } private: + // clang-format on + template ::value, int> = 0> + void generic_type_initialize(const detail::type_record &record) { + generic_type::initialize(record, &detail::type_caster_generic::local_load); + } + + template ::value, int> = 0> + void generic_type_initialize(const detail::type_record &record) { + generic_type::initialize(record, detail::type_caster::get_local_load_function_ptr()); + } + // clang-format off + /// Initialize holder object, variant 1: object derives from enable_shared_from_this template static void init_holder(detail::instance *inst, detail::value_and_holder &v_h, @@ -1546,6 +1600,9 @@ class class_ : public detail::generic_type { /// instance. Should be called as soon as the `type` value_ptr is set for an instance. Takes an /// optional pointer to an existing holder to use; if not specified and the instance is /// `.owned`, a new holder will be constructed to manage the value pointer. + template < + typename T = type, + detail::enable_if_t::value, int> = 0> static void init_instance(detail::instance *inst, const void *holder_ptr) { auto v_h = inst->get_value_and_holder(detail::get_type_info(typeid(type))); if (!v_h.instance_registered()) { @@ -1555,6 +1612,14 @@ class class_ : public detail::generic_type { init_holder(inst, v_h, (const holder_type *) holder_ptr, v_h.value_ptr()); } + // clang-format on + template ::value, int> = 0> + static void init_instance(detail::instance *inst, const void *holder_ptr) { + detail::type_caster::template init_instance_for_type(inst, holder_ptr); + } + // clang-format off + /// Deallocates an instance; via holder, if constructed; otherwise via operator delete. static void dealloc(detail::value_and_holder &v_h) { // We could be deallocating because we are cleaning up after a Python exception. diff --git a/include/pybind11/smart_holder.h b/include/pybind11/smart_holder.h new file mode 100644 index 0000000000..7a185f7692 --- /dev/null +++ b/include/pybind11/smart_holder.h @@ -0,0 +1,23 @@ +// Copyright (c) 2021 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#pragma once + +#include "detail/common.h" +#include "detail/smart_holder_type_casters.h" +#include "pybind11.h" + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +// Supports easier switching between py::class_ and py::class_: +// users can simply replace the `_` in `class_` with `h` or vice versa. +// Note though that the PYBIND11_SMART_HOLDER_TYPE_CASTERS(U) macro also needs to be +// added (for `classh`) or commented out (for `class_`). +template +class classh : public class_ { +public: + using class_::class_; +}; + +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/stl_bind.h b/include/pybind11/stl_bind.h index 83195ee496..39499f3491 100644 --- a/include/pybind11/stl_bind.h +++ b/include/pybind11/stl_bind.h @@ -438,7 +438,7 @@ PYBIND11_NAMESPACE_END(detail) // // std::vector // -template , typename... Args> +template , typename... Args> class_ bind_vector(handle scope, std::string const &name, Args&&... args) { using Class_ = class_; @@ -599,7 +599,7 @@ template auto map_if_insertion_operator(Class_ & PYBIND11_NAMESPACE_END(detail) -template , typename... Args> +template , typename... Args> class_ bind_map(handle scope, const std::string &name, Args&&... args) { using KeyType = typename Map::key_type; using MappedType = typename Map::mapped_type; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9ff6c0dd9a..ef6fc28f11 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -101,6 +101,10 @@ set(PYBIND11_TEST_FILES test_callbacks.cpp test_chrono.cpp test_class.cpp + test_class_sh_basic.cpp + test_class_sh_factory_constructors.cpp + test_class_sh_inheritance.cpp + test_class_sh_unique_ptr_member.cpp test_constants_and_functions.cpp test_copy_move.cpp test_custom_type_casters.cpp @@ -160,6 +164,8 @@ if(PYBIND11_CUDA_TESTS) endif() string(REPLACE ".cpp" ".py" PYBIND11_PYTEST_FILES "${PYBIND11_TEST_FILES}") +list(APPEND PYBIND11_PYTEST_FILES test_class_sh_module_local.py) +list(SORT PYBIND11_PYTEST_FILES) # Contains the set of test files that require pybind11_cross_module_tests to be # built; if none of these are built (i.e. because TEST_OVERRIDE is used and @@ -169,6 +175,8 @@ set(PYBIND11_CROSS_MODULE_TESTS test_exceptions.py test_local_bindings.py test_s set(PYBIND11_CROSS_MODULE_GIL_TESTS test_gil_scoped.py) +set(PYBIND11_CLASS_SH_MODULE_LOCAL_TESTS test_class_sh_module_local.py) + # Check if Eigen is available; if not, remove from PYBIND11_TEST_FILES (but # keep it in PYBIND11_PYTEST_FILES, so that we get the "eigen is not installed" # skip message). @@ -304,6 +312,16 @@ foreach(t ${PYBIND11_CROSS_MODULE_GIL_TESTS}) endif() endforeach() +foreach(t ${PYBIND11_CLASS_SH_MODULE_LOCAL_TESTS}) + list(FIND PYBIND11_PYTEST_FILES ${t} i) + if(i GREATER -1) + list(APPEND test_targets class_sh_module_local_0) + list(APPEND test_targets class_sh_module_local_1) + list(APPEND test_targets class_sh_module_local_2) + break() + endif() +endforeach() + # Support CUDA testing by forcing the target file to compile with NVCC if(PYBIND11_CUDA_TESTS) set_property(SOURCE ${PYBIND11_TEST_FILES} PROPERTY LANGUAGE CUDA) @@ -443,6 +461,9 @@ add_custom_command( ${CMAKE_CURRENT_BINARY_DIR}/sosize-$.txt) if(NOT PYBIND11_CUDA_TESTS) + # Test pure C++ code (not depending on Python). Provides the `test_pure_cpp` target. + add_subdirectory(pure_cpp) + # Test embedding the interpreter. Provides the `cpptest` target. add_subdirectory(test_embed) diff --git a/tests/class_sh_module_local_0.cpp b/tests/class_sh_module_local_0.cpp new file mode 100644 index 0000000000..bb1f46d5cd --- /dev/null +++ b/tests/class_sh_module_local_0.cpp @@ -0,0 +1,27 @@ +#include + +#include + +namespace pybind11_tests { +namespace class_sh_module_local { + +struct atyp { // Short for "any type". + std::string mtxt; +}; + +std::string get_mtxt(const atyp &obj) { return obj.mtxt; } + +atyp rtrn_valu_atyp() { return atyp(); } + +} // namespace class_sh_module_local +} // namespace pybind11_tests + +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_module_local::atyp) + +PYBIND11_MODULE(class_sh_module_local_0, m) { + using namespace pybind11_tests::class_sh_module_local; + + m.def("get_mtxt", get_mtxt); + + m.def("rtrn_valu_atyp", rtrn_valu_atyp); +} diff --git a/tests/class_sh_module_local_1.cpp b/tests/class_sh_module_local_1.cpp new file mode 100644 index 0000000000..66bc955163 --- /dev/null +++ b/tests/class_sh_module_local_1.cpp @@ -0,0 +1,33 @@ +// Identical to class_sh_module_local_2.cpp, except 2 replaced with 1. +#include + +#include + +namespace pybind11_tests { +namespace class_sh_module_local { + +struct atyp { // Short for "any type". + std::string mtxt; +}; + +std::string get_mtxt(const atyp &obj) { return obj.mtxt; } + +} // namespace class_sh_module_local +} // namespace pybind11_tests + +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_module_local::atyp) + +PYBIND11_MODULE(class_sh_module_local_1, m) { + namespace py = pybind11; + using namespace pybind11_tests::class_sh_module_local; + + py::classh(m, "atyp", py::module_local()) + .def(py::init([](const std::string &mtxt) { + atyp obj; + obj.mtxt = mtxt; + return obj; + })) + .def("tag", [](const atyp &) { return 1; }); + + m.def("get_mtxt", get_mtxt); +} diff --git a/tests/class_sh_module_local_2.cpp b/tests/class_sh_module_local_2.cpp new file mode 100644 index 0000000000..bef105aade --- /dev/null +++ b/tests/class_sh_module_local_2.cpp @@ -0,0 +1,33 @@ +// Identical to class_sh_module_local_1.cpp, except 1 replaced with 2. +#include + +#include + +namespace pybind11_tests { +namespace class_sh_module_local { + +struct atyp { // Short for "any type". + std::string mtxt; +}; + +std::string get_mtxt(const atyp &obj) { return obj.mtxt; } + +} // namespace class_sh_module_local +} // namespace pybind11_tests + +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_module_local::atyp) + +PYBIND11_MODULE(class_sh_module_local_2, m) { + namespace py = pybind11; + using namespace pybind11_tests::class_sh_module_local; + + py::classh(m, "atyp", py::module_local()) + .def(py::init([](const std::string &mtxt) { + atyp obj; + obj.mtxt = mtxt; + return obj; + })) + .def("tag", [](const atyp &) { return 2; }); + + m.def("get_mtxt", get_mtxt); +} diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 064a3e12fe..12d14e45ad 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -32,6 +32,7 @@ "include/pybind11/options.h", "include/pybind11/pybind11.h", "include/pybind11/pytypes.h", + "include/pybind11/smart_holder.h", "include/pybind11/stl.h", "include/pybind11/stl_bind.h", } @@ -42,6 +43,9 @@ "include/pybind11/detail/descr.h", "include/pybind11/detail/init.h", "include/pybind11/detail/internals.h", + "include/pybind11/detail/smart_holder_poc.h", + "include/pybind11/detail/smart_holder_sfinae_hooks_only.h", + "include/pybind11/detail/smart_holder_type_casters.h", "include/pybind11/detail/type_caster_base.h", "include/pybind11/detail/typeid.h", } diff --git a/tests/pure_cpp/CMakeLists.txt b/tests/pure_cpp/CMakeLists.txt new file mode 100644 index 0000000000..17be74b7f0 --- /dev/null +++ b/tests/pure_cpp/CMakeLists.txt @@ -0,0 +1,20 @@ +find_package(Catch 2.13.2) + +if(CATCH_FOUND) + message(STATUS "Building pure C++ tests (not depending on Python) using Catch v${CATCH_VERSION}") +else() + message(STATUS "Catch not detected. Interpreter tests will be skipped. Install Catch headers" + " manually or use `cmake -DDOWNLOAD_CATCH=ON` to fetch them automatically.") + return() +endif() + +add_executable(smart_holder_poc_test smart_holder_poc_test.cpp) +pybind11_enable_warnings(smart_holder_poc_test) +target_link_libraries(smart_holder_poc_test PRIVATE pybind11::headers Catch2::Catch2) + +add_custom_target( + test_pure_cpp + COMMAND "$" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + +add_dependencies(check test_pure_cpp) diff --git a/tests/pure_cpp/smart_holder_poc_test.cpp b/tests/pure_cpp/smart_holder_poc_test.cpp new file mode 100644 index 0000000000..4d05147a17 --- /dev/null +++ b/tests/pure_cpp/smart_holder_poc_test.cpp @@ -0,0 +1,281 @@ +#include "pybind11/detail/smart_holder_poc.h" + +#define CATCH_CONFIG_MAIN +#include "catch.hpp" + +using pybindit::memory::smart_holder; + +namespace helpers { + +struct movable_int { + int valu; + movable_int(int v) : valu{v} {} + movable_int(movable_int &&other) { + valu = other.valu; + other.valu = 91; + } +}; + +template +struct functor_builtin_delete { + void operator()(T *ptr) { delete ptr; } +}; + +template +struct functor_other_delete : functor_builtin_delete {}; + +} // namespace helpers + +TEST_CASE("from_raw_ptr_unowned+as_raw_ptr_unowned", "[S]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE(*hld.as_raw_ptr_unowned() == 19); +} + +TEST_CASE("from_raw_ptr_unowned+as_lvalue_ref", "[S]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE(hld.as_lvalue_ref() == 19); +} + +TEST_CASE("from_raw_ptr_unowned+as_rvalue_ref", "[S]") { + helpers::movable_int orig(19); + { + auto hld = smart_holder::from_raw_ptr_unowned(&orig); + helpers::movable_int othr(hld.as_rvalue_ref()); + REQUIRE(othr.valu == 19); + REQUIRE(orig.valu == 91); + } +} + +TEST_CASE("from_raw_ptr_unowned+as_raw_ptr_release_ownership", "[E]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE_THROWS_WITH(hld.as_raw_ptr_release_ownership(), + "Cannot disown non-owning holder (as_raw_ptr_release_ownership)."); +} + +TEST_CASE("from_raw_ptr_unowned+as_unique_ptr", "[E]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE_THROWS_WITH(hld.as_unique_ptr(), + "Cannot disown non-owning holder (as_unique_ptr)."); +} + +TEST_CASE("from_raw_ptr_unowned+as_unique_ptr_with_deleter", "[E]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE_THROWS_WITH((hld.as_unique_ptr>()), + "Missing unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_raw_ptr_unowned+as_shared_ptr", "[S]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE(*hld.as_shared_ptr() == 19); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_lvalue_ref", "[S]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + REQUIRE(hld.has_pointee()); + REQUIRE(hld.as_lvalue_ref() == 19); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_raw_ptr_release_ownership1", "[S]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + auto new_owner = std::unique_ptr(hld.as_raw_ptr_release_ownership()); + REQUIRE(!hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_raw_ptr_release_ownership2", "[E]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + auto shd_ptr = hld.as_shared_ptr(); + REQUIRE_THROWS_WITH(hld.as_raw_ptr_release_ownership(), + "Cannot disown use_count != 1 (as_raw_ptr_release_ownership)."); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_unique_ptr1", "[S]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + std::unique_ptr new_owner = hld.as_unique_ptr(); + REQUIRE(!hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_unique_ptr2", "[E]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + auto shd_ptr = hld.as_shared_ptr(); + REQUIRE_THROWS_WITH(hld.as_unique_ptr(), "Cannot disown use_count != 1 (as_unique_ptr)."); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_unique_ptr_with_deleter", "[E]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + REQUIRE_THROWS_WITH((hld.as_unique_ptr>()), + "Missing unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_shared_ptr", "[S]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + std::shared_ptr new_owner = hld.as_shared_ptr(); + REQUIRE(hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_unique_ptr+as_lvalue_ref", "[S]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE(hld.as_lvalue_ref() == 19); +} + +TEST_CASE("from_unique_ptr+as_raw_ptr_release_ownership1", "[S]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + auto new_owner = std::unique_ptr(hld.as_raw_ptr_release_ownership()); + REQUIRE(!hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_unique_ptr+as_raw_ptr_release_ownership2", "[E]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + auto shd_ptr = hld.as_shared_ptr(); + REQUIRE_THROWS_WITH(hld.as_raw_ptr_release_ownership(), + "Cannot disown use_count != 1 (as_raw_ptr_release_ownership)."); +} + +TEST_CASE("from_unique_ptr+as_unique_ptr1", "[S]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + std::unique_ptr new_owner = hld.as_unique_ptr(); + REQUIRE(!hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_unique_ptr+as_unique_ptr2", "[E]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + auto shd_ptr = hld.as_shared_ptr(); + REQUIRE_THROWS_WITH(hld.as_unique_ptr(), "Cannot disown use_count != 1 (as_unique_ptr)."); +} + +TEST_CASE("from_unique_ptr+as_unique_ptr_with_deleter", "[E]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE_THROWS_WITH((hld.as_unique_ptr>()), + "Incompatible unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_unique_ptr+as_shared_ptr", "[S]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + std::shared_ptr new_owner = hld.as_shared_ptr(); + REQUIRE(hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_lvalue_ref", "[S]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE(hld.as_lvalue_ref() == 19); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_raw_ptr_release_ownership", "[E]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE_THROWS_WITH(hld.as_raw_ptr_release_ownership(), + "Cannot disown custom deleter (as_raw_ptr_release_ownership)."); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_unique_ptr", "[E]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE_THROWS_WITH(hld.as_unique_ptr(), + "Incompatible unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_unique_ptr_with_deleter1", "[S]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + std::unique_ptr> new_owner + = hld.as_unique_ptr>(); + REQUIRE(!hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_unique_ptr_with_deleter2", "[E]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE_THROWS_WITH((hld.as_unique_ptr>()), + "Incompatible unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_shared_ptr", "[S]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + std::shared_ptr new_owner = hld.as_shared_ptr(); + REQUIRE(hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_shared_ptr+as_lvalue_ref", "[S]") { + std::shared_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_shared_ptr(orig_owner); + REQUIRE(hld.as_lvalue_ref() == 19); +} + +TEST_CASE("from_shared_ptr+as_raw_ptr_release_ownership", "[E]") { + std::shared_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_shared_ptr(orig_owner); + REQUIRE_THROWS_WITH(hld.as_raw_ptr_release_ownership(), + "Cannot disown external shared_ptr (as_raw_ptr_release_ownership)."); +} + +TEST_CASE("from_shared_ptr+as_unique_ptr", "[E]") { + std::shared_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_shared_ptr(orig_owner); + REQUIRE_THROWS_WITH(hld.as_unique_ptr(), + "Cannot disown external shared_ptr (as_unique_ptr)."); +} + +TEST_CASE("from_shared_ptr+as_unique_ptr_with_deleter", "[E]") { + std::shared_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_shared_ptr(orig_owner); + REQUIRE_THROWS_WITH((hld.as_unique_ptr>()), + "Missing unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_shared_ptr+as_shared_ptr", "[S]") { + std::shared_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_shared_ptr(orig_owner); + REQUIRE(*hld.as_shared_ptr() == 19); +} + +TEST_CASE("error_unpopulated_holder", "[E]") { + smart_holder hld; + REQUIRE_THROWS_WITH(hld.as_lvalue_ref(), "Unpopulated holder (as_lvalue_ref)."); +} + +TEST_CASE("error_disowned_holder", "[E]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + hld.as_unique_ptr(); + REQUIRE_THROWS_WITH(hld.as_lvalue_ref(), "Disowned holder (as_lvalue_ref)."); +} + +TEST_CASE("error_cannot_disown_nullptr", "[E]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + hld.as_unique_ptr(); + REQUIRE_THROWS_WITH(hld.as_unique_ptr(), "Cannot disown nullptr (as_unique_ptr)."); +} diff --git a/tests/test_class.cpp b/tests/test_class.cpp index bd545e8c68..4b1857e85b 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -23,6 +23,8 @@ # pragma warning(disable: 4324) // warning C4324: structure was padded due to alignment specifier #endif +namespace { + // test_brace_initialization struct NoBraceInitialization { NoBraceInitialization(std::vector v) : vec{std::move(v)} {} @@ -32,6 +34,23 @@ struct NoBraceInitialization { std::vector vec; }; +// test_mismatched_holder +struct MismatchBase1 { }; +struct MismatchDerived1 : MismatchBase1 { }; +struct MismatchBase2 { }; +struct MismatchDerived2 : MismatchBase2 { }; + +// test_multiple_instances_with_same_pointer +struct SamePointer {}; + +} // namespace + +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MismatchBase1, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MismatchDerived1, std::unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MismatchBase2, std::unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MismatchDerived2, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(SamePointer, std::unique_ptr) + TEST_SUBMODULE(class_, m) { // test_instance struct NoConstructor { @@ -168,20 +187,15 @@ TEST_SUBMODULE(class_, m) { }); // test_mismatched_holder - struct MismatchBase1 { }; - struct MismatchDerived1 : MismatchBase1 { }; - - struct MismatchBase2 { }; - struct MismatchDerived2 : MismatchBase2 { }; - m.def("mismatched_holder_1", []() { auto mod = py::module_::import("__main__"); py::class_>(mod, "MismatchBase1"); - py::class_(mod, "MismatchDerived1"); + py::class_, + MismatchBase1>(mod, "MismatchDerived1"); }); m.def("mismatched_holder_2", []() { auto mod = py::module_::import("__main__"); - py::class_(mod, "MismatchBase2"); + py::class_>(mod, "MismatchBase2"); py::class_, MismatchBase2>(mod, "MismatchDerived2"); }); @@ -431,7 +445,6 @@ TEST_SUBMODULE(class_, m) { .def("throw_something", &PyPrintDestructor::throw_something); // test_multiple_instances_with_same_pointer - struct SamePointer {}; static SamePointer samePointer; py::class_>(m, "SamePointer") .def(py::init([]() { return &samePointer; })); @@ -504,7 +517,14 @@ CHECK_BASE(1); CHECK_BASE(2); CHECK_BASE(3); CHECK_BASE(4); CHECK_BASE(5); CHECK CHECK_ALIAS(1); CHECK_ALIAS(2); CHECK_NOALIAS(3); CHECK_ALIAS(4); CHECK_NOALIAS(5); CHECK_ALIAS(6); CHECK_ALIAS(7); CHECK_NOALIAS(8); #define CHECK_HOLDER(N, TYPE) static_assert(std::is_same>>::value, \ "DoesntBreak" #N " has wrong holder_type!") -CHECK_HOLDER(1, unique); CHECK_HOLDER(2, unique); CHECK_HOLDER(3, unique); CHECK_HOLDER(4, unique); CHECK_HOLDER(5, unique); +#define CHECK_SMART_HOLDER(N) static_assert(std::is_same::value, \ + "DoesntBreak" #N " has wrong holder_type!") +CHECK_HOLDER(1, unique); CHECK_HOLDER(2, unique); CHECK_HOLDER(3, unique); +#ifndef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT +CHECK_HOLDER(4, unique); CHECK_HOLDER(5, unique); +#else +CHECK_SMART_HOLDER(4); CHECK_SMART_HOLDER(5); +#endif CHECK_HOLDER(6, shared); CHECK_HOLDER(7, shared); CHECK_HOLDER(8, shared); // There's no nice way to test that these fail because they fail to compile; leave them here, diff --git a/tests/test_class_sh_basic.cpp b/tests/test_class_sh_basic.cpp new file mode 100644 index 0000000000..f66b5623d3 --- /dev/null +++ b/tests/test_class_sh_basic.cpp @@ -0,0 +1,120 @@ +#include "pybind11_tests.h" + +#include + +#include +#include + +namespace pybind11_tests { +namespace class_sh_basic { + +struct atyp { // Short for "any type". + std::string mtxt; + atyp() : mtxt("DefaultConstructor") {} + atyp(const std::string &mtxt_) : mtxt(mtxt_) {} + atyp(const atyp &other) { mtxt = other.mtxt + "_CpCtor"; } + atyp(atyp &&other) { mtxt = other.mtxt + "_MvCtor"; } +}; + +// clang-format off + +atyp rtrn_valu() { atyp obj{"rtrn_valu"}; return obj; } +atyp&& rtrn_rref() { static atyp obj; obj.mtxt = "rtrn_rref"; return std::move(obj); } +atyp const& rtrn_cref() { static atyp obj; obj.mtxt = "rtrn_cref"; return obj; } +atyp& rtrn_mref() { static atyp obj; obj.mtxt = "rtrn_mref"; return obj; } +atyp const* rtrn_cptr() { return new atyp{"rtrn_cptr"}; } +atyp* rtrn_mptr() { return new atyp{"rtrn_mptr"}; } + +std::string pass_valu(atyp obj) { return "pass_valu:" + obj.mtxt; } +std::string pass_cref(atyp const& obj) { return "pass_cref:" + obj.mtxt; } +std::string pass_mref(atyp& obj) { return "pass_mref:" + obj.mtxt; } +std::string pass_cptr(atyp const* obj) { return "pass_cptr:" + obj->mtxt; } +std::string pass_mptr(atyp* obj) { return "pass_mptr:" + obj->mtxt; } + +std::shared_ptr rtrn_shmp() { return std::shared_ptr(new atyp{"rtrn_shmp"}); } +std::shared_ptr rtrn_shcp() { return std::shared_ptr(new atyp{"rtrn_shcp"}); } + +std::string pass_shmp(std::shared_ptr obj) { return "pass_shmp:" + obj->mtxt; } +std::string pass_shcp(std::shared_ptr obj) { return "pass_shcp:" + obj->mtxt; } + +std::unique_ptr rtrn_uqmp() { return std::unique_ptr(new atyp{"rtrn_uqmp"}); } +std::unique_ptr rtrn_uqcp() { return std::unique_ptr(new atyp{"rtrn_uqcp"}); } + +std::string pass_uqmp(std::unique_ptr obj) { return "pass_uqmp:" + obj->mtxt; } +std::string pass_uqcp(std::unique_ptr obj) { return "pass_uqcp:" + obj->mtxt; } + +struct sddm : std::default_delete {}; +struct sddc : std::default_delete {}; + +std::unique_ptr rtrn_udmp() { return std::unique_ptr(new atyp{"rtrn_udmp"}); } +std::unique_ptr rtrn_udcp() { return std::unique_ptr(new atyp{"rtrn_udcp"}); } + +std::string pass_udmp(std::unique_ptr obj) { return "pass_udmp:" + obj->mtxt; } +std::string pass_udcp(std::unique_ptr obj) { return "pass_udcp:" + obj->mtxt; } + +// clang-format on + +// Helpers for testing. +std::string get_mtxt(atyp const &obj) { return obj.mtxt; } +std::unique_ptr unique_ptr_roundtrip(std::unique_ptr obj) { return obj; } + +} // namespace class_sh_basic +} // namespace pybind11_tests + +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_basic::atyp) + +namespace pybind11_tests { +namespace class_sh_basic { + +TEST_SUBMODULE(class_sh_basic, m) { + namespace py = pybind11; + + py::classh(m, "atyp").def(py::init<>()).def(py::init([](const std::string &mtxt) { + atyp obj; + obj.mtxt = mtxt; + return obj; + })); + + m.def("rtrn_valu", rtrn_valu); + m.def("rtrn_rref", rtrn_rref); + m.def("rtrn_cref", rtrn_cref); + m.def("rtrn_mref", rtrn_mref); + m.def("rtrn_cptr", rtrn_cptr); + m.def("rtrn_mptr", rtrn_mptr); + + m.def("pass_valu", pass_valu); + m.def("pass_cref", pass_cref); + m.def("pass_mref", pass_mref); + m.def("pass_cptr", pass_cptr); + m.def("pass_mptr", pass_mptr); + + m.def("rtrn_shmp", rtrn_shmp); + m.def("rtrn_shcp", rtrn_shcp); + + m.def("pass_shmp", pass_shmp); + m.def("pass_shcp", pass_shcp); + + m.def("rtrn_uqmp", rtrn_uqmp); + m.def("rtrn_uqcp", rtrn_uqcp); + + m.def("pass_uqmp", pass_uqmp); + m.def("pass_uqcp", pass_uqcp); + + m.def("rtrn_udmp", rtrn_udmp); + m.def("rtrn_udcp", rtrn_udcp); + + m.def("pass_udmp", pass_udmp); + m.def("pass_udcp", pass_udcp); + + // Helpers for testing. + // These require selected functions above to work first, as indicated: + m.def("get_mtxt", get_mtxt); // pass_cref + m.def("unique_ptr_roundtrip", unique_ptr_roundtrip); // pass_uqmp, rtrn_uqmp + + m.def("py_type_handle_of_atyp", []() { + return py::type::handle_of(); // Exercises static_cast in this function. + }); +} + +} // namespace class_sh_basic +} // namespace pybind11_tests diff --git a/tests/test_class_sh_basic.py b/tests/test_class_sh_basic.py new file mode 100644 index 0000000000..4bd793f15b --- /dev/null +++ b/tests/test_class_sh_basic.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Importing re before pytest after observing a PyPy CI flake when importing pytest first. +import re + +import pytest + +from pybind11_tests import class_sh_basic as m + + +def test_atyp_constructors(): + obj = m.atyp() + assert obj.__class__.__name__ == "atyp" + obj = m.atyp("") + assert obj.__class__.__name__ == "atyp" + obj = m.atyp("txtm") + assert obj.__class__.__name__ == "atyp" + + +@pytest.mark.parametrize( + "rtrn_f, expected", + [ + (m.rtrn_valu, "rtrn_valu(_MvCtor)*_MvCtor"), + (m.rtrn_rref, "rtrn_rref(_MvCtor)*_MvCtor"), + (m.rtrn_cref, "rtrn_cref(_MvCtor)*_CpCtor"), + (m.rtrn_mref, "rtrn_mref(_MvCtor)*_CpCtor"), + (m.rtrn_cptr, "rtrn_cptr"), + (m.rtrn_mptr, "rtrn_mptr"), + (m.rtrn_shmp, "rtrn_shmp"), + (m.rtrn_shcp, "rtrn_shcp"), + (m.rtrn_uqmp, "rtrn_uqmp"), + (m.rtrn_uqcp, "rtrn_uqcp"), + (m.rtrn_udmp, "rtrn_udmp"), + (m.rtrn_udcp, "rtrn_udcp"), + ], +) +def test_cast(rtrn_f, expected): + assert re.match(expected, m.get_mtxt(rtrn_f())) + + +@pytest.mark.parametrize( + "pass_f, mtxt, expected", + [ + (m.pass_valu, "Valu", "pass_valu:Valu(_MvCtor)*_CpCtor"), + (m.pass_cref, "Cref", "pass_cref:Cref(_MvCtor)*_MvCtor"), + (m.pass_mref, "Mref", "pass_mref:Mref(_MvCtor)*_MvCtor"), + (m.pass_cptr, "Cptr", "pass_cptr:Cptr(_MvCtor)*_MvCtor"), + (m.pass_mptr, "Mptr", "pass_mptr:Mptr(_MvCtor)*_MvCtor"), + (m.pass_shmp, "Shmp", "pass_shmp:Shmp(_MvCtor)*_MvCtor"), + (m.pass_shcp, "Shcp", "pass_shcp:Shcp(_MvCtor)*_MvCtor"), + (m.pass_uqmp, "Uqmp", "pass_uqmp:Uqmp(_MvCtor)*_MvCtor"), + (m.pass_uqcp, "Uqcp", "pass_uqcp:Uqcp(_MvCtor)*_MvCtor"), + ], +) +def test_load_with_mtxt(pass_f, mtxt, expected): + assert re.match(expected, pass_f(m.atyp(mtxt))) + + +@pytest.mark.parametrize( + "pass_f, rtrn_f, expected", + [ + (m.pass_udmp, m.rtrn_udmp, "pass_udmp:rtrn_udmp"), + (m.pass_udcp, m.rtrn_udcp, "pass_udcp:rtrn_udcp"), + ], +) +def test_load_with_rtrn_f(pass_f, rtrn_f, expected): + assert pass_f(rtrn_f()) == expected + + +@pytest.mark.parametrize( + "pass_f, rtrn_f, expected", + [ + (m.pass_uqmp, m.rtrn_uqmp, "pass_uqmp:rtrn_uqmp"), + (m.pass_uqcp, m.rtrn_uqcp, "pass_uqcp:rtrn_uqcp"), + (m.pass_udmp, m.rtrn_udmp, "pass_udmp:rtrn_udmp"), + (m.pass_udcp, m.rtrn_udcp, "pass_udcp:rtrn_udcp"), + ], +) +def test_pass_unique_ptr_disowns(pass_f, rtrn_f, expected): + obj = rtrn_f() + assert pass_f(obj) == expected + with pytest.raises(RuntimeError) as exc_info: + pass_f(obj) + assert str(exc_info.value) == ( + "Missing value for wrapped C++ type: Python instance was disowned." + ) + + +def test_unique_ptr_roundtrip(num_round_trips=1000): + # Multiple roundtrips to stress-test instance registration/deregistration. + recycled = m.atyp("passenger") + for _ in range(num_round_trips): + id_orig = id(recycled) + recycled = m.unique_ptr_roundtrip(recycled) + assert re.match("passenger(_MvCtor)*_MvCtor", m.get_mtxt(recycled)) + id_rtrn = id(recycled) + # Ensure the returned object is a different Python instance. + assert id_rtrn != id_orig + id_orig = id_rtrn + + +def test_py_type_handle_of_atyp(): + obj = m.py_type_handle_of_atyp() + assert obj.__class__.__name__ == "pybind11_type" diff --git a/tests/test_class_sh_factory_constructors.cpp b/tests/test_class_sh_factory_constructors.cpp new file mode 100644 index 0000000000..9a0e5f18d9 --- /dev/null +++ b/tests/test_class_sh_factory_constructors.cpp @@ -0,0 +1,180 @@ +#include "pybind11_tests.h" + +#include + +#include +#include + +namespace pybind11_tests { +namespace test_class_sh_factory_constructors { + +template // Using int as a trick to easily generate a series of types. +struct atyp { // Short for "any type". + std::string mtxt; +}; + +template +std::string get_mtxt(const T &obj) { + return obj.mtxt; +} + +using atyp_valu = atyp<0x0>; +using atyp_rref = atyp<0x1>; +using atyp_cref = atyp<0x2>; +using atyp_mref = atyp<0x3>; +using atyp_cptr = atyp<0x4>; +using atyp_mptr = atyp<0x5>; +using atyp_shmp = atyp<0x6>; +using atyp_shcp = atyp<0x7>; +using atyp_uqmp = atyp<0x8>; +using atyp_uqcp = atyp<0x9>; +using atyp_udmp = atyp<0xA>; +using atyp_udcp = atyp<0xB>; + +// clang-format off + +atyp_valu rtrn_valu() { atyp_valu obj{"Valu"}; return obj; } +atyp_rref&& rtrn_rref() { static atyp_rref obj; obj.mtxt = "Rref"; return std::move(obj); } +atyp_cref const& rtrn_cref() { static atyp_cref obj; obj.mtxt = "Cref"; return obj; } +atyp_mref& rtrn_mref() { static atyp_mref obj; obj.mtxt = "Mref"; return obj; } +atyp_cptr const* rtrn_cptr() { return new atyp_cptr{"Cptr"}; } +atyp_mptr* rtrn_mptr() { return new atyp_mptr{"Mptr"}; } + +std::shared_ptr rtrn_shmp() { return std::shared_ptr(new atyp_shmp{"Shmp"}); } +std::shared_ptr rtrn_shcp() { return std::shared_ptr(new atyp_shcp{"Shcp"}); } + +std::unique_ptr rtrn_uqmp() { return std::unique_ptr(new atyp_uqmp{"Uqmp"}); } +std::unique_ptr rtrn_uqcp() { return std::unique_ptr(new atyp_uqcp{"Uqcp"}); } + +struct sddm : std::default_delete {}; +struct sddc : std::default_delete {}; + +std::unique_ptr rtrn_udmp() { return std::unique_ptr(new atyp_udmp{"Udmp"}); } +std::unique_ptr rtrn_udcp() { return std::unique_ptr(new atyp_udcp{"Udcp"}); } + +// clang-format on + +// Minimalistic approach to achieve full coverage of construct() overloads for constructing +// smart_holder from unique_ptr and shared_ptr returns. +struct with_alias { + int val = 0; + virtual ~with_alias() = default; + // Some compilers complain about implicitly defined versions of some of the following: + with_alias() = default; + with_alias(const with_alias &) = default; + with_alias(with_alias &&) = default; + with_alias &operator=(const with_alias &) = default; + with_alias &operator=(with_alias &&) = default; +}; +struct with_alias_alias : with_alias {}; +struct sddwaa : std::default_delete {}; + +} // namespace test_class_sh_factory_constructors +} // namespace pybind11_tests + +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_valu) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_rref) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_cref) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_mref) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_cptr) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_mptr) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_shmp) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_shcp) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_uqmp) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_uqcp) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_udmp) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::atyp_udcp) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::test_class_sh_factory_constructors::with_alias) + +TEST_SUBMODULE(class_sh_factory_constructors, m) { + using namespace pybind11_tests::test_class_sh_factory_constructors; + + py::classh(m, "atyp_valu") + .def(py::init(&rtrn_valu)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_rref") + .def(py::init(&rtrn_rref)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_cref") + // class_: ... must return a compatible ... + // classh: ... cannot pass object of non-trivial type ... + // .def(py::init(&rtrn_cref)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_mref") + // class_: ... must return a compatible ... + // classh: ... cannot pass object of non-trivial type ... + // .def(py::init(&rtrn_mref)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_cptr") + // class_: ... must return a compatible ... + // classh: ... must return a compatible ... + // .def(py::init(&rtrn_cptr)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_mptr") + .def(py::init(&rtrn_mptr)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_shmp") + .def(py::init(&rtrn_shmp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_shcp") + // py::class_>(m, "atyp_shcp") + // class_: ... must return a compatible ... + // classh: ... cannot pass object of non-trivial type ... + // .def(py::init(&rtrn_shcp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_uqmp") + .def(py::init(&rtrn_uqmp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_uqcp") + // class_: ... cannot pass object of non-trivial type ... + // classh: ... cannot pass object of non-trivial type ... + // .def(py::init(&rtrn_uqcp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_udmp") + .def(py::init(&rtrn_udmp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_udcp") + // py::class_>(m, "atyp_udcp") + // class_: ... must return a compatible ... + // classh: ... cannot pass object of non-trivial type ... + // .def(py::init(&rtrn_udcp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "with_alias") + .def_readonly("val", &with_alias::val) + .def(py::init([](int i) { + auto p = std::unique_ptr(new with_alias_alias); + p->val = i * 100; + return p; + })) + .def(py::init([](int i, int j) { + auto p = std::unique_ptr(new with_alias_alias); + p->val = i * 100 + j * 10; + return p; + })) + .def(py::init([](int i, int j, int k) { + auto p = std::shared_ptr(new with_alias_alias); + p->val = i * 100 + j * 10 + k; + return p; + })) + .def(py::init( + [](int, int, int, int) { return std::unique_ptr(new with_alias); }, + [](int, int, int, int) { + return std::unique_ptr(new with_alias); // Invalid alias factory. + })) + .def(py::init([](int, int, int, int, int) { return std::make_shared(); }, + [](int, int, int, int, int) { + return std::make_shared(); // Invalid alias factory. + })); +} diff --git a/tests/test_class_sh_factory_constructors.py b/tests/test_class_sh_factory_constructors.py new file mode 100644 index 0000000000..c5227b40d5 --- /dev/null +++ b/tests/test_class_sh_factory_constructors.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +import pytest + +from pybind11_tests import class_sh_factory_constructors as m + + +def test_atyp_factories(): + assert m.atyp_valu().get_mtxt() == "Valu" + assert m.atyp_rref().get_mtxt() == "Rref" + # sert m.atyp_cref().get_mtxt() == "Cref" + # sert m.atyp_mref().get_mtxt() == "Mref" + # sert m.atyp_cptr().get_mtxt() == "Cptr" + assert m.atyp_mptr().get_mtxt() == "Mptr" + assert m.atyp_shmp().get_mtxt() == "Shmp" + # sert m.atyp_shcp().get_mtxt() == "Shcp" + assert m.atyp_uqmp().get_mtxt() == "Uqmp" + # sert m.atyp_uqcp().get_mtxt() == "Uqcp" + assert m.atyp_udmp().get_mtxt() == "Udmp" + # sert m.atyp_udcp().get_mtxt() == "Udcp" + + +@pytest.mark.parametrize( + "init_args, expected", + [ + ((3,), 300), + ((5, 7), 570), + ((9, 11, 13), 1023), + ], +) +def test_with_alias_success(init_args, expected): + assert m.with_alias(*init_args).val == expected + + +@pytest.mark.parametrize( + "num_init_args, smart_ptr", + [ + (4, "std::unique_ptr"), + (5, "std::shared_ptr"), + ], +) +def test_with_alias_invalid(num_init_args, smart_ptr): + class PyDrvdWithAlias(m.with_alias): + pass + + with pytest.raises(TypeError) as excinfo: + PyDrvdWithAlias(*((0,) * num_init_args)) + assert ( + str(excinfo.value) + == "pybind11::init(): construction failed: returned " + + smart_ptr + + " pointee is not an alias instance" + ) diff --git a/tests/test_class_sh_inheritance.cpp b/tests/test_class_sh_inheritance.cpp new file mode 100644 index 0000000000..1c825d7f08 --- /dev/null +++ b/tests/test_class_sh_inheritance.cpp @@ -0,0 +1,105 @@ +#include "pybind11_tests.h" + +#include + +#include + +namespace pybind11_tests { +namespace class_sh_inheritance { + +template +struct base_template { + base_template() : base_id(Id) {} + virtual ~base_template() = default; + virtual int id() const { return base_id; } + int base_id; + + // Some compilers complain about implicitly defined versions of some of the following: + base_template(const base_template &) = default; + base_template(base_template &&) = default; + base_template &operator=(const base_template &) = default; + base_template &operator=(base_template &&) = default; +}; + +using base = base_template<100>; + +struct drvd : base { + int id() const override { return 2 * base_id; } +}; + +// clang-format off +inline drvd *rtrn_mptr_drvd() { return new drvd; } +inline base *rtrn_mptr_drvd_up_cast() { return new drvd; } + +inline int pass_cptr_base(base const *b) { return b->id() + 11; } +inline int pass_cptr_drvd(drvd const *d) { return d->id() + 12; } + +inline std::shared_ptr rtrn_shmp_drvd() { return std::shared_ptr(new drvd); } +inline std::shared_ptr rtrn_shmp_drvd_up_cast() { return std::shared_ptr(new drvd); } + +inline int pass_shcp_base(std::shared_ptr b) { return b->id() + 21; } +inline int pass_shcp_drvd(std::shared_ptr d) { return d->id() + 22; } +// clang-format on + +using base1 = base_template<110>; +using base2 = base_template<120>; + +// Not reusing base here because it would interfere with the single-inheritance test. +struct drvd2 : base1, base2 { + int id() const override { return 3 * base1::base_id + 4 * base2::base_id; } +}; + +// clang-format off +inline drvd2 *rtrn_mptr_drvd2() { return new drvd2; } +inline base1 *rtrn_mptr_drvd2_up_cast1() { return new drvd2; } +inline base2 *rtrn_mptr_drvd2_up_cast2() { return new drvd2; } + +inline int pass_cptr_base1(base1 const *b) { return b->id() + 21; } +inline int pass_cptr_base2(base2 const *b) { return b->id() + 22; } +inline int pass_cptr_drvd2(drvd2 const *d) { return d->id() + 23; } +// clang-format on + +} // namespace class_sh_inheritance +} // namespace pybind11_tests + +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_inheritance::base) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_inheritance::drvd) + +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_inheritance::base1) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_inheritance::base2) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_inheritance::drvd2) + +namespace pybind11_tests { +namespace class_sh_inheritance { + +TEST_SUBMODULE(class_sh_inheritance, m) { + py::classh(m, "base"); + py::classh(m, "drvd"); + + auto rvto = py::return_value_policy::take_ownership; + + m.def("rtrn_mptr_drvd", rtrn_mptr_drvd, rvto); + m.def("rtrn_mptr_drvd_up_cast", rtrn_mptr_drvd_up_cast, rvto); + m.def("pass_cptr_base", pass_cptr_base); + m.def("pass_cptr_drvd", pass_cptr_drvd); + + m.def("rtrn_shmp_drvd", rtrn_shmp_drvd); + m.def("rtrn_shmp_drvd_up_cast", rtrn_shmp_drvd_up_cast); + m.def("pass_shcp_base", pass_shcp_base); + m.def("pass_shcp_drvd", pass_shcp_drvd); + + // __init__ needed for Python inheritance. + py::classh(m, "base1").def(py::init<>()); + py::classh(m, "base2").def(py::init<>()); + py::classh(m, "drvd2"); + + m.def("rtrn_mptr_drvd2", rtrn_mptr_drvd2, rvto); + m.def("rtrn_mptr_drvd2_up_cast1", rtrn_mptr_drvd2_up_cast1, rvto); + m.def("rtrn_mptr_drvd2_up_cast2", rtrn_mptr_drvd2_up_cast2, rvto); + m.def("pass_cptr_base1", pass_cptr_base1); + m.def("pass_cptr_base2", pass_cptr_base2); + m.def("pass_cptr_drvd2", pass_cptr_drvd2); +} + +} // namespace class_sh_inheritance +} // namespace pybind11_tests diff --git a/tests/test_class_sh_inheritance.py b/tests/test_class_sh_inheritance.py new file mode 100644 index 0000000000..69f9131d70 --- /dev/null +++ b/tests/test_class_sh_inheritance.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +from pybind11_tests import class_sh_inheritance as m + + +def test_rtrn_mptr_drvd_pass_cptr_base(): + d = m.rtrn_mptr_drvd() + i = m.pass_cptr_base(d) # load_impl Case 2a + assert i == 2 * 100 + 11 + + +def test_rtrn_shmp_drvd_pass_shcp_base(): + d = m.rtrn_shmp_drvd() + i = m.pass_shcp_base(d) # load_impl Case 2a + assert i == 2 * 100 + 21 + + +def test_rtrn_mptr_drvd_up_cast_pass_cptr_drvd(): + b = m.rtrn_mptr_drvd_up_cast() + # the base return is down-cast immediately. + assert b.__class__.__name__ == "drvd" + i = m.pass_cptr_drvd(b) + assert i == 2 * 100 + 12 + + +def test_rtrn_shmp_drvd_up_cast_pass_shcp_drvd(): + b = m.rtrn_shmp_drvd_up_cast() + # the base return is down-cast immediately. + assert b.__class__.__name__ == "drvd" + i = m.pass_shcp_drvd(b) + assert i == 2 * 100 + 22 + + +def test_rtrn_mptr_drvd2_pass_cptr_bases(): + d = m.rtrn_mptr_drvd2() + i1 = m.pass_cptr_base1(d) # load_impl Case 2c + assert i1 == 3 * 110 + 4 * 120 + 21 + i2 = m.pass_cptr_base2(d) + assert i2 == 3 * 110 + 4 * 120 + 22 + + +def test_rtrn_mptr_drvd2_up_casts_pass_cptr_drvd2(): + b1 = m.rtrn_mptr_drvd2_up_cast1() + assert b1.__class__.__name__ == "drvd2" + i1 = m.pass_cptr_drvd2(b1) + assert i1 == 3 * 110 + 4 * 120 + 23 + b2 = m.rtrn_mptr_drvd2_up_cast2() + assert b2.__class__.__name__ == "drvd2" + i2 = m.pass_cptr_drvd2(b2) + assert i2 == 3 * 110 + 4 * 120 + 23 + + +def test_python_drvd2(): + class Drvd2(m.base1, m.base2): + def __init__(self): + m.base1.__init__(self) + m.base2.__init__(self) + + d = Drvd2() + i1 = m.pass_cptr_base1(d) # load_impl Case 2b + assert i1 == 110 + 21 + i2 = m.pass_cptr_base2(d) + assert i2 == 120 + 22 diff --git a/tests/test_class_sh_module_local.py b/tests/test_class_sh_module_local.py new file mode 100644 index 0000000000..aa4d1f3c5d --- /dev/null +++ b/tests/test_class_sh_module_local.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +import pytest + +import class_sh_module_local_0 as m0 +import class_sh_module_local_1 as m1 +import class_sh_module_local_2 as m2 + + +def test_cross_module_get_mtxt(): + obj1 = m1.atyp("A") + assert obj1.tag() == 1 + obj2 = m2.atyp("B") + assert obj2.tag() == 2 + assert m1.get_mtxt(obj1) == "A" + assert m2.get_mtxt(obj2) == "B" + assert m1.get_mtxt(obj2) == "B" + assert m2.get_mtxt(obj1) == "A" + assert m0.get_mtxt(obj1) == "A" + assert m0.get_mtxt(obj2) == "B" + + +def test_m0_rtrn_valu_atyp(): + with pytest.raises(TypeError) as exc_info: + m0.rtrn_valu_atyp() + assert str(exc_info.value).startswith( + "Unable to convert function return value to a Python type!" + ) diff --git a/tests/test_class_sh_unique_ptr_member.cpp b/tests/test_class_sh_unique_ptr_member.cpp new file mode 100644 index 0000000000..a34274a0ca --- /dev/null +++ b/tests/test_class_sh_unique_ptr_member.cpp @@ -0,0 +1,61 @@ +#include "pybind11_tests.h" + +#include + +#include + +namespace pybind11_tests { +namespace class_sh_unique_ptr_member { + +class pointee { // NOT copyable. +public: + pointee() = default; + + int get_int() const { return 213; } + +private: + pointee(const pointee &) = delete; + pointee(pointee &&) = delete; + pointee &operator=(const pointee &) = delete; + pointee &operator=(pointee &&) = delete; +}; + +inline std::unique_ptr make_unique_pointee() { + return std::unique_ptr(new pointee); +} + +class ptr_owner { +public: + explicit ptr_owner(std::unique_ptr ptr) : ptr_(std::move(ptr)) {} + + bool is_owner() const { return bool(ptr_); } + + std::unique_ptr give_up_ownership_via_unique_ptr() { return std::move(ptr_); } + std::shared_ptr give_up_ownership_via_shared_ptr() { return std::move(ptr_); } + +private: + std::unique_ptr ptr_; +}; + +} // namespace class_sh_unique_ptr_member +} // namespace pybind11_tests + +PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_unique_ptr_member::pointee) + +namespace pybind11_tests { +namespace class_sh_unique_ptr_member { + +TEST_SUBMODULE(class_sh_unique_ptr_member, m) { + py::classh(m, "pointee").def(py::init<>()).def("get_int", &pointee::get_int); + + m.def("make_unique_pointee", make_unique_pointee); + + py::class_(m, "ptr_owner") + .def(py::init>(), py::arg("ptr")) + .def("is_owner", &ptr_owner::is_owner) + .def("give_up_ownership_via_unique_ptr", &ptr_owner::give_up_ownership_via_unique_ptr) + .def("give_up_ownership_via_shared_ptr", &ptr_owner::give_up_ownership_via_shared_ptr); +} + +} // namespace class_sh_unique_ptr_member +} // namespace pybind11_tests diff --git a/tests/test_class_sh_unique_ptr_member.py b/tests/test_class_sh_unique_ptr_member.py new file mode 100644 index 0000000000..1528753a5e --- /dev/null +++ b/tests/test_class_sh_unique_ptr_member.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +import pytest + +from pybind11_tests import class_sh_unique_ptr_member as m + + +def test_make_unique_pointee(): + obj = m.make_unique_pointee() + assert obj.get_int() == 213 + + +@pytest.mark.parametrize( + "give_up_ownership_via", + ["give_up_ownership_via_unique_ptr", "give_up_ownership_via_shared_ptr"], +) +def test_pointee_and_ptr_owner(give_up_ownership_via): + obj = m.pointee() + assert obj.get_int() == 213 + owner = m.ptr_owner(obj) + with pytest.raises(RuntimeError) as exc_info: + obj.get_int() + assert ( + str(exc_info.value) + == "Missing value for wrapped C++ type: Python instance was disowned." + ) + assert owner.is_owner() + reclaimed = getattr(owner, give_up_ownership_via)() + assert not owner.is_owner() + assert reclaimed.get_int() == 213 diff --git a/tests/test_factory_constructors.cpp b/tests/test_factory_constructors.cpp index 7ff7e7b52c..26d081dc38 100644 --- a/tests/test_factory_constructors.cpp +++ b/tests/test_factory_constructors.cpp @@ -139,6 +139,11 @@ class TestFactoryHelper { static std::shared_ptr construct3(int a) { return std::shared_ptr(new TestFactory3(a)); } }; +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(TestFactory3, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(TestFactory4, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(TestFactory5, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(TestFactory7, std::shared_ptr) + TEST_SUBMODULE(factory_constructors, m) { // Define various trivial types to allow simpler overload resolution: diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index f99909bda7..53855c3671 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -112,8 +112,8 @@ UserType TestPropRVP::sv2(1); class NoneTester { public: int answer = 42; }; int none1(const NoneTester &obj) { return obj.answer; } int none2(NoneTester *obj) { return obj ? obj->answer : -1; } -int none3(std::shared_ptr &obj) { return obj ? obj->answer : -1; } -int none4(std::shared_ptr *obj) { return obj && *obj ? (*obj)->answer : -1; } +int none3(const std::shared_ptr &obj) { return obj ? obj->answer : -1; } +int none4(const std::shared_ptr *obj) { return obj && *obj ? (*obj)->answer : -1; } int none5(std::shared_ptr obj) { return obj ? obj->answer : -1; } struct StrIssue { @@ -148,6 +148,8 @@ struct RefQualified { int constRefQualified(int other) const & { return value + other; } }; +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(NoneTester, std::shared_ptr) + TEST_SUBMODULE(methods_and_attributes, m) { // test_methods_and_attributes py::class_ emna(m, "ExampleMandA"); diff --git a/tests/test_multiple_inheritance.cpp b/tests/test_multiple_inheritance.cpp index e67200809d..bf307fc7ea 100644 --- a/tests/test_multiple_inheritance.cpp +++ b/tests/test_multiple_inheritance.cpp @@ -11,6 +11,8 @@ #include "pybind11_tests.h" #include "constructor_stats.h" +namespace { + // Many bases for testing that multiple inheritance from many classes (i.e. requiring extra // space for holder constructed flags) works. template struct BaseN { @@ -43,6 +45,38 @@ int WithStatic2::static_value2 = 2; int VanillaStaticMix1::static_value = 12; int VanillaStaticMix2::static_value = 12; +// test_multiple_inheritance_virtbase +struct Base1a { + Base1a(int i) : i(i) { } + int foo() { return i; } + int i; +}; +struct Base2a { + Base2a(int i) : i(i) { } + int bar() { return i; } + int i; +}; +struct Base12a : Base1a, Base2a { + Base12a(int i, int j) : Base1a(i), Base2a(j) { } +}; + +// test_mi_unaligned_base +// test_mi_base_return +struct I801B1 { int a = 1; I801B1() = default; I801B1(const I801B1 &) = default; virtual ~I801B1() = default; }; +struct I801B2 { int b = 2; I801B2() = default; I801B2(const I801B2 &) = default; virtual ~I801B2() = default; }; +struct I801C : I801B1, I801B2 {}; +struct I801D : I801C {}; // Indirect MI + +} // namespace + +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(Base1a, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(Base2a, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(Base12a, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(I801B1, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(I801B2, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(I801C, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(I801D, std::shared_ptr) + TEST_SUBMODULE(multiple_inheritance, m) { // test_multiple_inheritance_mix1 @@ -99,27 +133,14 @@ TEST_SUBMODULE(multiple_inheritance, m) { // test_multiple_inheritance_virtbase // Test the case where not all base classes are specified, and where pybind11 requires the // py::multiple_inheritance flag to perform proper casting between types. - struct Base1a { - Base1a(int i) : i(i) { } - int foo() { return i; } - int i; - }; py::class_>(m, "Base1a") .def(py::init()) .def("foo", &Base1a::foo); - struct Base2a { - Base2a(int i) : i(i) { } - int bar() { return i; } - int i; - }; py::class_>(m, "Base2a") .def(py::init()) .def("bar", &Base2a::bar); - struct Base12a : Base1a, Base2a { - Base12a(int i, int j) : Base1a(i), Base2a(j) { } - }; py::class_>(m, "Base12a", py::multiple_inheritance()) .def(py::init()); @@ -130,10 +151,6 @@ TEST_SUBMODULE(multiple_inheritance, m) { // test_mi_unaligned_base // test_mi_base_return // Issue #801: invalid casting to derived type with MI bases - struct I801B1 { int a = 1; I801B1() = default; I801B1(const I801B1 &) = default; virtual ~I801B1() = default; }; - struct I801B2 { int b = 2; I801B2() = default; I801B2(const I801B2 &) = default; virtual ~I801B2() = default; }; - struct I801C : I801B1, I801B2 {}; - struct I801D : I801C {}; // Indirect MI // Unregistered classes: struct I801B3 { int c = 3; virtual ~I801B3() = default; }; struct I801E : I801B3, I801D {}; diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index 59996edeb4..6ffa2972f2 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -15,23 +15,7 @@ #include "pybind11_tests.h" #include "object.h" -// Make pybind aware of the ref-counted wrapper type (s): - -// ref is a wrapper for 'Object' which uses intrusive reference counting -// It is always possible to construct a ref from an Object* pointer without -// possible inconsistencies, hence the 'true' argument at the end. -PYBIND11_DECLARE_HOLDER_TYPE(T, ref, true); -// Make pybind11 aware of the non-standard getter member function -namespace pybind11 { namespace detail { - template - struct holder_helper> { - static const T *get(const ref &p) { return p.get_ptr(); } - }; -} // namespace detail -} // namespace pybind11 - -// The following is not required anymore for std::shared_ptr, but it should compile without error: -PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr); +namespace { // This is just a wrapper around unique_ptr, but with extra fields to deliberately bloat up the // holder size to trigger the non-simple-layout internal instance layout for single inheritance with @@ -43,7 +27,6 @@ template class huge_unique_ptr { huge_unique_ptr(T *p) : ptr(p) {}; T *get() { return ptr.get(); } }; -PYBIND11_DECLARE_HOLDER_TYPE(T, huge_unique_ptr); // Simple custom holder that works like unique_ptr template @@ -54,7 +37,6 @@ class custom_unique_ptr { T* get() const { return impl.get(); } T* release_ptr() { return impl.release(); } }; -PYBIND11_DECLARE_HOLDER_TYPE(T, custom_unique_ptr); // Simple custom holder that works like shared_ptr and has operator& overload // To obtain address of an instance of this holder pybind should use std::addressof @@ -68,7 +50,6 @@ class shared_ptr_with_addressof_operator { T* get() const { return impl.get(); } T** operator&() { throw std::logic_error("Call of overloaded operator& is not expected"); } }; -PYBIND11_DECLARE_HOLDER_TYPE(T, shared_ptr_with_addressof_operator); // Simple custom holder that works like unique_ptr and has operator& overload // To obtain address of an instance of this holder pybind should use std::addressof @@ -83,8 +64,245 @@ class unique_ptr_with_addressof_operator { T* release_ptr() { return impl.release(); } T** operator&() { throw std::logic_error("Call of overloaded operator& is not expected"); } }; + +// Custom object with builtin reference counting (see 'object.h' for the implementation) +class MyObject1 : public Object { +public: + MyObject1(int value) : value(value) { print_created(this, toString()); } + std::string toString() const override { return "MyObject1[" + std::to_string(value) + "]"; } +protected: + ~MyObject1() override { print_destroyed(this); } +private: + int value; +}; + +// Object managed by a std::shared_ptr<> +class MyObject2 { +public: + MyObject2(const MyObject2 &) = default; + MyObject2(int value) : value(value) { print_created(this, toString()); } + std::string toString() const { return "MyObject2[" + std::to_string(value) + "]"; } + virtual ~MyObject2() { print_destroyed(this); } +private: + int value; +}; + +// Object managed by a std::shared_ptr<>, additionally derives from std::enable_shared_from_this<> +class MyObject3 : public std::enable_shared_from_this { +public: + MyObject3(const MyObject3 &) = default; + MyObject3(int value) : value(value) { print_created(this, toString()); } + std::string toString() const { return "MyObject3[" + std::to_string(value) + "]"; } + virtual ~MyObject3() { print_destroyed(this); } +private: + int value; +}; + +// test_unique_nodelete +// Object with a private destructor +class MyObject4; +static std::unordered_set myobject4_instances; +class MyObject4 { +public: + MyObject4(int value) : value{value} { + print_created(this); + myobject4_instances.insert(this); + } + int value; + + static void cleanupAllInstances() { + auto tmp = std::move(myobject4_instances); + myobject4_instances.clear(); + for (auto o : tmp) + delete o; + } +private: + ~MyObject4() { + myobject4_instances.erase(this); + print_destroyed(this); + } +}; + +// test_unique_deleter +// Object with std::unique_ptr where D is not matching the base class +// Object with a protected destructor +class MyObject4a; +static std::unordered_set myobject4a_instances; +class MyObject4a { +public: + MyObject4a(int i) { + value = i; + print_created(this); + myobject4a_instances.insert(this); + }; + int value; + + static void cleanupAllInstances() { + auto tmp = std::move(myobject4a_instances); + myobject4a_instances.clear(); + for (auto o : tmp) + delete o; + } +protected: + virtual ~MyObject4a() { + myobject4a_instances.erase(this); + print_destroyed(this); + } +}; + +// Object derived but with public destructor and no Deleter in default holder +class MyObject4b : public MyObject4a { +public: + MyObject4b(int i) : MyObject4a(i) { print_created(this); } + ~MyObject4b() override { print_destroyed(this); } +}; + +// test_large_holder +class MyObject5 { // managed by huge_unique_ptr +public: + MyObject5(int value) : value{value} { print_created(this); } + ~MyObject5() { print_destroyed(this); } + int value; +}; + +// test_shared_ptr_and_references +struct SharedPtrRef { + struct A { + A() { print_created(this); } + A(const A &) { print_copy_created(this); } + A(A &&) { print_move_created(this); } + ~A() { print_destroyed(this); } + }; + + A value = {}; + std::shared_ptr shared = std::make_shared(); +}; + +// test_shared_ptr_from_this_and_references +struct SharedFromThisRef { + struct B : std::enable_shared_from_this { + B() { print_created(this); } + B(const B &) : std::enable_shared_from_this() { print_copy_created(this); } + B(B &&) : std::enable_shared_from_this() { print_move_created(this); } + ~B() { print_destroyed(this); } + }; + + B value = {}; + std::shared_ptr shared = std::make_shared(); +}; + +// Issue #865: shared_from_this doesn't work with virtual inheritance +struct SharedFromThisVBase : std::enable_shared_from_this { + SharedFromThisVBase() = default; + SharedFromThisVBase(const SharedFromThisVBase &) = default; + virtual ~SharedFromThisVBase() = default; +}; +struct SharedFromThisVirt : virtual SharedFromThisVBase {}; + +// test_move_only_holder +struct C { + C() { print_created(this); } + ~C() { print_destroyed(this); } +}; + +// test_holder_with_addressof_operator +struct TypeForHolderWithAddressOf { + TypeForHolderWithAddressOf() { print_created(this); } + TypeForHolderWithAddressOf(const TypeForHolderWithAddressOf &) { print_copy_created(this); } + TypeForHolderWithAddressOf(TypeForHolderWithAddressOf &&) { print_move_created(this); } + ~TypeForHolderWithAddressOf() { print_destroyed(this); } + std::string toString() const { + return "TypeForHolderWithAddressOf[" + std::to_string(value) + "]"; + } + int value = 42; +}; + +// test_move_only_holder_with_addressof_operator +struct TypeForMoveOnlyHolderWithAddressOf { + TypeForMoveOnlyHolderWithAddressOf(int value) : value{value} { print_created(this); } + ~TypeForMoveOnlyHolderWithAddressOf() { print_destroyed(this); } + std::string toString() const { + return "MoveOnlyHolderWithAddressOf[" + std::to_string(value) + "]"; + } + int value; +}; + +// test_smart_ptr_from_default +struct HeldByDefaultHolder { }; + +// test_shared_ptr_gc +// #187: issue involving std::shared_ptr<> return value policy & garbage collection +struct ElementBase { + virtual ~ElementBase() = default; /* Force creation of virtual table */ + ElementBase() = default; + ElementBase(const ElementBase&) = delete; +}; + +struct ElementA : ElementBase { + ElementA(int v) : v(v) { } + int value() { return v; } + int v; +}; + +struct ElementList { + void add(std::shared_ptr e) { l.push_back(e); } + std::vector> l; +}; + +} // namespace + +// ref is a wrapper for 'Object' which uses intrusive reference counting +// It is always possible to construct a ref from an Object* pointer without +// possible inconsistencies, hence the 'true' argument at the end. +// Make pybind11 aware of the non-standard getter member function +namespace pybind11 { namespace detail { + template + struct holder_helper> { + static const T *get(const ref &p) { return p.get_ptr(); } + }; +} // namespace detail +} // namespace pybind11 + +// Make pybind aware of the ref-counted wrapper type (s): +PYBIND11_DECLARE_HOLDER_TYPE(T, ref, true); +// The following is not required anymore for std::shared_ptr, but it should compile without error: +PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr); +PYBIND11_DECLARE_HOLDER_TYPE(T, huge_unique_ptr); +PYBIND11_DECLARE_HOLDER_TYPE(T, custom_unique_ptr); +PYBIND11_DECLARE_HOLDER_TYPE(T, shared_ptr_with_addressof_operator); PYBIND11_DECLARE_HOLDER_TYPE(T, unique_ptr_with_addressof_operator); +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(Object, ref) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MyObject1, ref) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MyObject2, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MyObject3, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MyObject4, std::unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MyObject4a, std::unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MyObject4b, std::unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(MyObject5, huge_unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(SharedPtrRef::A, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(SharedPtrRef, std::unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(SharedFromThisRef::B, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(SharedFromThisRef, std::unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(SharedFromThisVirt, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(C, custom_unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(TypeForHolderWithAddressOf, shared_ptr_with_addressof_operator) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(TypeForMoveOnlyHolderWithAddressOf, unique_ptr_with_addressof_operator) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(HeldByDefaultHolder, std::unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(ElementBase, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(ElementA, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(ElementList, std::shared_ptr) + +#ifdef PYBIND11_USE_SMART_HOLDER_AS_DEFAULT +// To prevent triggering a static_assert in the smart_holder code. +// This is a very special case, because the associated test exercises a holder mismatch. +namespace pybind11 { namespace detail { +template <> +class type_caster> + : public copyable_holder_caster> {}; +} // namespace detail +} // namespace pybind11 +#endif TEST_SUBMODULE(smart_ptr, m) { @@ -94,16 +312,6 @@ TEST_SUBMODULE(smart_ptr, m) { py::class_> obj(m, "Object"); obj.def("getRefCount", &Object::getRefCount); - // Custom object with builtin reference counting (see 'object.h' for the implementation) - class MyObject1 : public Object { - public: - MyObject1(int value) : value(value) { print_created(this, toString()); } - std::string toString() const override { return "MyObject1[" + std::to_string(value) + "]"; } - protected: - ~MyObject1() override { print_destroyed(this); } - private: - int value; - }; py::class_>(m, "MyObject1", obj) .def(py::init()); py::implicitly_convertible(); @@ -124,17 +332,6 @@ TEST_SUBMODULE(smart_ptr, m) { // Expose constructor stats for the ref type m.def("cstats_ref", &ConstructorStats::get); - - // Object managed by a std::shared_ptr<> - class MyObject2 { - public: - MyObject2(const MyObject2 &) = default; - MyObject2(int value) : value(value) { print_created(this, toString()); } - std::string toString() const { return "MyObject2[" + std::to_string(value) + "]"; } - virtual ~MyObject2() { print_destroyed(this); } - private: - int value; - }; py::class_>(m, "MyObject2") .def(py::init()); m.def("make_myobject2_1", []() { return new MyObject2(6); }); @@ -144,16 +341,6 @@ TEST_SUBMODULE(smart_ptr, m) { m.def("print_myobject2_3", [](const std::shared_ptr &obj) { py::print(obj->toString()); }); m.def("print_myobject2_4", [](const std::shared_ptr *obj) { py::print((*obj)->toString()); }); - // Object managed by a std::shared_ptr<>, additionally derives from std::enable_shared_from_this<> - class MyObject3 : public std::enable_shared_from_this { - public: - MyObject3(const MyObject3 &) = default; - MyObject3(int value) : value(value) { print_created(this, toString()); } - std::string toString() const { return "MyObject3[" + std::to_string(value) + "]"; } - virtual ~MyObject3() { print_destroyed(this); } - private: - int value; - }; py::class_>(m, "MyObject3") .def(py::init()); m.def("make_myobject3_1", []() { return new MyObject3(8); }); @@ -174,101 +361,26 @@ TEST_SUBMODULE(smart_ptr, m) { return good; }); - // test_unique_nodelete - // Object with a private destructor - class MyObject4; - static std::unordered_set myobject4_instances; - class MyObject4 { - public: - MyObject4(int value) : value{value} { - print_created(this); - myobject4_instances.insert(this); - } - int value; - - static void cleanupAllInstances() { - auto tmp = std::move(myobject4_instances); - myobject4_instances.clear(); - for (auto o : tmp) - delete o; - } - private: - ~MyObject4() { - myobject4_instances.erase(this); - print_destroyed(this); - } - }; py::class_>(m, "MyObject4") .def(py::init()) .def_readwrite("value", &MyObject4::value) .def_static("cleanup_all_instances", &MyObject4::cleanupAllInstances); - // test_unique_deleter - // Object with std::unique_ptr where D is not matching the base class - // Object with a protected destructor - class MyObject4a; - static std::unordered_set myobject4a_instances; - class MyObject4a { - public: - MyObject4a(int i) { - value = i; - print_created(this); - myobject4a_instances.insert(this); - }; - int value; - - static void cleanupAllInstances() { - auto tmp = std::move(myobject4a_instances); - myobject4a_instances.clear(); - for (auto o : tmp) - delete o; - } - protected: - virtual ~MyObject4a() { - myobject4a_instances.erase(this); - print_destroyed(this); - } - }; py::class_>(m, "MyObject4a") .def(py::init()) .def_readwrite("value", &MyObject4a::value) .def_static("cleanup_all_instances", &MyObject4a::cleanupAllInstances); - // Object derived but with public destructor and no Deleter in default holder - class MyObject4b : public MyObject4a { - public: - MyObject4b(int i) : MyObject4a(i) { print_created(this); } - ~MyObject4b() override { print_destroyed(this); } - }; - py::class_(m, "MyObject4b") + py::class_>(m, "MyObject4b") .def(py::init()); - // test_large_holder - class MyObject5 { // managed by huge_unique_ptr - public: - MyObject5(int value) : value{value} { print_created(this); } - ~MyObject5() { print_destroyed(this); } - int value; - }; py::class_>(m, "MyObject5") .def(py::init()) .def_readwrite("value", &MyObject5::value); - // test_shared_ptr_and_references - struct SharedPtrRef { - struct A { - A() { print_created(this); } - A(const A &) { print_copy_created(this); } - A(A &&) { print_move_created(this); } - ~A() { print_destroyed(this); } - }; - - A value = {}; - std::shared_ptr shared = std::make_shared(); - }; using A = SharedPtrRef::A; py::class_>(m, "A"); - py::class_(m, "SharedPtrRef") + py::class_>(m, "SharedPtrRef") .def(py::init<>()) .def_readonly("ref", &SharedPtrRef::value) .def_property_readonly("copy", [](const SharedPtrRef &s) { return s.value; }, @@ -279,21 +391,9 @@ TEST_SUBMODULE(smart_ptr, m) { .def("set_ref", [](SharedPtrRef &, const A &) { return true; }) .def("set_holder", [](SharedPtrRef &, std::shared_ptr) { return true; }); - // test_shared_ptr_from_this_and_references - struct SharedFromThisRef { - struct B : std::enable_shared_from_this { - B() { print_created(this); } - B(const B &) : std::enable_shared_from_this() { print_copy_created(this); } - B(B &&) : std::enable_shared_from_this() { print_move_created(this); } - ~B() { print_destroyed(this); } - }; - - B value = {}; - std::shared_ptr shared = std::make_shared(); - }; using B = SharedFromThisRef::B; py::class_>(m, "B"); - py::class_(m, "SharedFromThisRef") + py::class_>(m, "SharedFromThisRef") .def(py::init<>()) .def_readonly("bad_wp", &SharedFromThisRef::value) .def_property_readonly("ref", [](const SharedFromThisRef &s) -> const B & { return *s.shared; }) @@ -305,37 +405,14 @@ TEST_SUBMODULE(smart_ptr, m) { .def("set_ref", [](SharedFromThisRef &, const B &) { return true; }) .def("set_holder", [](SharedFromThisRef &, std::shared_ptr) { return true; }); - // Issue #865: shared_from_this doesn't work with virtual inheritance - struct SharedFromThisVBase : std::enable_shared_from_this { - SharedFromThisVBase() = default; - SharedFromThisVBase(const SharedFromThisVBase &) = default; - virtual ~SharedFromThisVBase() = default; - }; - struct SharedFromThisVirt : virtual SharedFromThisVBase {}; static std::shared_ptr sft(new SharedFromThisVirt()); py::class_>(m, "SharedFromThisVirt") .def_static("get", []() { return sft.get(); }); - // test_move_only_holder - struct C { - C() { print_created(this); } - ~C() { print_destroyed(this); } - }; py::class_>(m, "TypeWithMoveOnlyHolder") .def_static("make", []() { return custom_unique_ptr(new C); }) .def_static("make_as_object", []() { return py::cast(custom_unique_ptr(new C)); }); - // test_holder_with_addressof_operator - struct TypeForHolderWithAddressOf { - TypeForHolderWithAddressOf() { print_created(this); } - TypeForHolderWithAddressOf(const TypeForHolderWithAddressOf &) { print_copy_created(this); } - TypeForHolderWithAddressOf(TypeForHolderWithAddressOf &&) { print_move_created(this); } - ~TypeForHolderWithAddressOf() { print_destroyed(this); } - std::string toString() const { - return "TypeForHolderWithAddressOf[" + std::to_string(value) + "]"; - } - int value = 42; - }; using HolderWithAddressOf = shared_ptr_with_addressof_operator; py::class_(m, "TypeForHolderWithAddressOf") .def_static("make", []() { return HolderWithAddressOf(new TypeForHolderWithAddressOf); }) @@ -345,49 +422,22 @@ TEST_SUBMODULE(smart_ptr, m) { .def("print_object_3", [](const HolderWithAddressOf &obj) { py::print(obj.get()->toString()); }) .def("print_object_4", [](const HolderWithAddressOf *obj) { py::print((*obj).get()->toString()); }); - // test_move_only_holder_with_addressof_operator - struct TypeForMoveOnlyHolderWithAddressOf { - TypeForMoveOnlyHolderWithAddressOf(int value) : value{value} { print_created(this); } - ~TypeForMoveOnlyHolderWithAddressOf() { print_destroyed(this); } - std::string toString() const { - return "MoveOnlyHolderWithAddressOf[" + std::to_string(value) + "]"; - } - int value; - }; using MoveOnlyHolderWithAddressOf = unique_ptr_with_addressof_operator; py::class_(m, "TypeForMoveOnlyHolderWithAddressOf") .def_static("make", []() { return MoveOnlyHolderWithAddressOf(new TypeForMoveOnlyHolderWithAddressOf(0)); }) .def_readwrite("value", &TypeForMoveOnlyHolderWithAddressOf::value) .def("print_object", [](const TypeForMoveOnlyHolderWithAddressOf *obj) { py::print(obj->toString()); }); - // test_smart_ptr_from_default - struct HeldByDefaultHolder { }; - py::class_(m, "HeldByDefaultHolder") + py::class_>(m, "HeldByDefaultHolder") .def(py::init<>()) .def_static("load_shared_ptr", [](std::shared_ptr) {}); - // test_shared_ptr_gc - // #187: issue involving std::shared_ptr<> return value policy & garbage collection - struct ElementBase { - virtual ~ElementBase() = default; /* Force creation of virtual table */ - ElementBase() = default; - ElementBase(const ElementBase&) = delete; - }; py::class_>(m, "ElementBase"); - struct ElementA : ElementBase { - ElementA(int v) : v(v) { } - int value() { return v; } - int v; - }; py::class_>(m, "ElementA") .def(py::init()) .def("value", &ElementA::value); - struct ElementList { - void add(std::shared_ptr e) { l.push_back(e); } - std::vector> l; - }; py::class_>(m, "ElementList") .def(py::init<>()) .def("add", &ElementList::add) diff --git a/tests/test_smart_ptr.py b/tests/test_smart_ptr.py index 85f61a3223..d4e8e2e063 100644 --- a/tests/test_smart_ptr.py +++ b/tests/test_smart_ptr.py @@ -302,9 +302,9 @@ def test_smart_ptr_from_default(): instance = m.HeldByDefaultHolder() with pytest.raises(RuntimeError) as excinfo: m.HeldByDefaultHolder.load_shared_ptr(instance) - assert ( - "Unable to load a custom holder type from a " - "default-holder instance" in str(excinfo.value) + assert str(excinfo.value) in ( + "Unable to load a smart-pointer type from a non-smart_holder instance.", + "Unable to load a custom holder type from a default-holder instance", ) diff --git a/ubench/holder_comparison.cpp b/ubench/holder_comparison.cpp new file mode 100644 index 0000000000..720464a91b --- /dev/null +++ b/ubench/holder_comparison.cpp @@ -0,0 +1,54 @@ +#include + +#include "number_bucket.h" + +#include +#include + +namespace hc { // holder comparison + +using nb_up = pybind11_ubench::number_bucket<1>; +using nb_sp = pybind11_ubench::number_bucket<2>; +using nb_pu = pybind11_ubench::number_bucket<3>; +using nb_sh = pybind11_ubench::number_bucket<4>; + +namespace py = pybind11; + +template +void wrap_number_bucket(py::module m, const char *class_name) { + py::class_(m, class_name) + .def(py::init(), py::arg("data_size") = 0) + .def("sum", &WrappedType::sum) + .def("add", &WrappedType::add, py::arg("other")); +} + +template +class padded_unique_ptr { + std::unique_ptr ptr; + char padding[sizeof(py::smart_holder) - sizeof(std::unique_ptr)]; + +public: + padded_unique_ptr(T *p) : ptr(p) {} + T *get() { return ptr.get(); } +}; + +static_assert(sizeof(padded_unique_ptr) == sizeof(py::smart_holder), + "Unexpected sizeof mismatch."); + +} // namespace hc + +PYBIND11_DECLARE_HOLDER_TYPE(T, hc::padded_unique_ptr); + +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(hc::nb_up, std::unique_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(hc::nb_sp, std::shared_ptr) +PYBIND11_SMART_POINTER_HOLDER_TYPE_CASTERS(hc::nb_pu, hc::padded_unique_ptr) +PYBIND11_SMART_HOLDER_TYPE_CASTERS(hc::nb_sh) + +PYBIND11_MODULE(pybind11_ubench_holder_comparison, m) { + using namespace hc; + m.def("sizeof_smart_holder", []() { return sizeof(py::smart_holder); }); + wrap_number_bucket>(m, "number_bucket_up"); + wrap_number_bucket>(m, "number_bucket_sp"); + wrap_number_bucket>(m, "number_bucket_pu"); + wrap_number_bucket(m, "number_bucket_sh"); +} diff --git a/ubench/holder_comparison.py b/ubench/holder_comparison.py new file mode 100644 index 0000000000..f6277a8439 --- /dev/null +++ b/ubench/holder_comparison.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +"""Simple comparison of holder performances, relative to unique_ptr holder.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import pybind11_ubench_holder_comparison as m + +import collections +import sys +import time + +number_bucket_pc = None + + +def pflush(*args, **kwargs): + result = print(*args, **kwargs) + # Using "file" here because it is the name of the built-in keyword argument. + file = kwargs.get("file", sys.stdout) # pylint: disable=redefined-builtin + file.flush() # file object must have a flush method. + return result + + +def run(args): + if not args: + size_exponent_min = 0 + size_exponent_max = 16 + size_exponent_step = 4 + call_repetitions_first_pass = 100 + call_repetitions_target_elapsed_secs = 0.1 + num_samples = 10 + selected_holder_type = "all" + else: + assert len(args) == 7, ( + "size_exponent_min size_exponent_max size_exponent_step" + " call_repetitions_first_pass call_repetitions_target_elapsed_secs" + " num_samples selected_holder_type" + ) + size_exponent_min = int(args[0]) + size_exponent_max = int(args[1]) + size_exponent_step = int(args[2]) + call_repetitions_first_pass = int(args[3]) + call_repetitions_target_elapsed_secs = float(args[4]) + num_samples = int(args[5]) + selected_holder_type = args[6] + pflush( + "command-line arguments:", + size_exponent_min, + size_exponent_max, + size_exponent_step, + call_repetitions_first_pass, + "%.3f" % call_repetitions_target_elapsed_secs, + num_samples, + selected_holder_type, + ) + pflush("sizeof_smart_holder:", m.sizeof_smart_holder()) + + def find_call_repetitions( + callable, + time_delta_floor=1.0e-6, + target_elapsed_secs_multiplier=1.05, # Empirical. + target_elapsed_secs_tolerance=0.05, + max_iterations=100, + ): + td_target = ( + call_repetitions_target_elapsed_secs * target_elapsed_secs_multiplier + ) + crd = call_repetitions_first_pass + for _ in range(max_iterations): + td = callable(crd) + crd = max(1, int(td_target * crd / max(td, time_delta_floor))) + if abs(td - td_target) / td_target < target_elapsed_secs_tolerance: + return crd + raise RuntimeError("find_call_repetitions failure: max_iterations exceeded.") + + for size_exponent in range( + size_exponent_min, size_exponent_max + 1, size_exponent_step + ): + data_size = 2 ** size_exponent + pflush(data_size, "data_size") + ratios = collections.defaultdict(list) + call_repetitions = None + for _ in range(num_samples): + row_0 = None + for nb_label, nb_type in [ + ("up", m.number_bucket_up), + ("sp", m.number_bucket_sp), + ("pu", m.number_bucket_pu), + ("sh", m.number_bucket_sh), + ("pc", number_bucket_pc), + ]: + if nb_label == "pc" and nb_type is None: + continue + if selected_holder_type != "all" and nb_label != selected_holder_type: + continue + nb1 = nb_type(data_size) + nb2 = nb_type(data_size) + + def many_sum(call_repetitions): + assert int(round(nb1.sum())) == data_size + t0 = time.time() + for _ in range(call_repetitions): + nb1.sum() + return time.time() - t0 + + def many_add(call_repetitions): + assert nb1.add(nb2) == data_size + t0 = time.time() + for _ in range(call_repetitions): + nb1.add(nb2) + return time.time() - t0 + + if call_repetitions is None: + call_repetitions = find_call_repetitions(many_sum) + pflush(call_repetitions, "call_repetitions") + + td_sum = many_sum(call_repetitions) + td_add = many_add(call_repetitions) + row = [td_sum, td_add] + if row_0 is None: + pflush(" Sum Add ratS ratA") + row_0 = row + else: + for curr, prev in zip(row, row_0): + if prev: + rat = curr / prev + else: + rat = -1 + row.append(curr / prev) + ratios[nb_label + "_ratS"].append(row[-2]) + ratios[nb_label + "_ratA"].append(row[-1]) + pflush(nb_label, " ".join(["%.3f" % v for v in row])) + pflush(" Min Mean Max") + for key, rat in ratios.items(): + print(key, "%5.3f %5.3f %5.3f" % (min(rat), sum(rat) / len(rat), max(rat))) + + +if __name__ == "__main__": + run(args=sys.argv[1:]) diff --git a/ubench/holder_comparison_extract_sheet_data.py b/ubench/holder_comparison_extract_sheet_data.py new file mode 100644 index 0000000000..64ee27fc48 --- /dev/null +++ b/ubench/holder_comparison_extract_sheet_data.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +"""Extract mean ratios from holder_comparison.py output.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import sys + + +def run(args): + assert len(args) == 1, "log_holder_comparison.txt" + + log_lines = open(args[0]).read().splitlines() + + for ratx in ("_ratS ", "_ratA "): + print(ratx) + header = None + header_row = None + data_row = None + data_row_buffer = [] + + def show(): + if header_row: + if header is None: + print(",".join(header_row)) + else: + assert header == header_row + if data_row is not None: + print(",".join(data_row)) + data_row_buffer.append(data_row) + return header_row + + for line in log_lines: + if line.endswith(" data_size"): + header = show() + flds = line.split() + assert len(flds) == 2 + header_row = ["data_size"] + data_row = [flds[0]] + elif line.endswith(" call_repetitions"): + flds = line.split() + assert len(flds) == 2 + header_row.append("calls") + data_row.append(flds[0]) + header_row.append("up") + data_row.append("1.000") + elif line[2:].startswith(ratx): + flds = line.split() + assert len(flds) == 4 + header_row.append(line[:2]) + data_row.append(flds[2]) + show() + + print("Scaled to last column:") + print(",".join(header_row)) + for data_row in data_row_buffer: + data_row_rescaled = data_row[:2] + unit = float(data_row[-1]) + for fld in data_row[2:]: + data_row_rescaled.append("%.3f" % (float(fld) / unit)) + print(",".join(data_row_rescaled)) + + +if __name__ == "__main__": + run(args=sys.argv[1:]) diff --git a/ubench/number_bucket.h b/ubench/number_bucket.h new file mode 100644 index 0000000000..75523cb605 --- /dev/null +++ b/ubench/number_bucket.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +namespace pybind11_ubench { + +template +struct number_bucket { + std::vector data; + + explicit number_bucket(std::size_t data_size = 0) : data(data_size, 1.0) {} + + double sum() const { + std::size_t n = 0; + double s = 0; + const double *a = &*data.begin(); + const double *e = &*data.end(); + while (a != e) { + s += *a++; + n++; + } + if (n != data.size()) { + std::cerr << "Internal consistency failure (sum)." << std::endl; + std::terminate(); + } + return s; + } + + std::size_t add(const number_bucket &other) { + if (other.data.size() != data.size()) { + std::cerr << "Incompatible data sizes (add)." << std::endl; + std::terminate(); + } + std::size_t n = 0; + double *a = &*data.begin(); + const double *e = &*data.end(); + const double *b = &*other.data.begin(); + while (a != e) { + *a++ += *b++; + n++; + } + return n; + } + +private: + number_bucket(const number_bucket &) = delete; + number_bucket(number_bucket &&) = delete; + number_bucket &operator=(const number_bucket &) = delete; + number_bucket &operator=(number_bucket &&) = delete; +}; + +} // namespace pybind11_ubench diff --git a/ubench/python/number_bucket.clif b/ubench/python/number_bucket.clif new file mode 100644 index 0000000000..ef704c0905 --- /dev/null +++ b/ubench/python/number_bucket.clif @@ -0,0 +1,6 @@ +from "pybind11/ubench/number_bucket.h": + namespace `pybind11_ubench`: + class `number_bucket<0>` as number_bucket_pc: + def __init__(self, data_size: int = default) + def sum(self) -> float + def add(self, other: number_bucket_pc) -> int