pybind11 icon indicating copy to clipboard operation
pybind11 copied to clipboard

[WIP] Bake smart_holder functionality into `class_` and `type_caster_base`

Open rwgk opened this issue 1 year ago • 2 comments

Description

WORK IN PROGRESS

Best to ignore this PR until all tests pass.

High-level approach:

  1. Start with pybind11 master branch.
  2. Make smart_holder the default holder.
  3. Fix until all tests pass, while also adding in all tests from the smart_holder branch. (This is the hard part. Could take a while.)

Suggested changelog entry:


rwgk avatar Jun 29 '24 22:06 rwgk

Current state testing with the Google-internal toolchain, using ASAN:

7 failing tests:

//third_party/pybind11/tests:test_copy_move
//third_party/pybind11/tests:test_opaque_types
//third_party/pybind11/tests:test_smart_ptr
//third_party/pybind11/tests:test_stl
//third_party/pybind11/tests:test_stl_binders
//third_party/pybind11/tests:test_tagbased_polymorphic
//third_party/pybind11/tests:test_vector_unique_ptr_member

All other tests run ASAN-clean. For completeness, this is the list of passing tests:

40 passing tests:

//third_party/pybind11/tests:test_async
//third_party/pybind11/tests:test_buffers
//third_party/pybind11/tests:test_builtin_casters
//third_party/pybind11/tests:test_call_policies
//third_party/pybind11/tests:test_callbacks
//third_party/pybind11/tests:test_chrono
//third_party/pybind11/tests:test_class
//third_party/pybind11/tests:test_const_name
//third_party/pybind11/tests:test_constants_and_functions
//third_party/pybind11/tests:test_custom_type_casters
//third_party/pybind11/tests:test_custom_type_setup
//third_party/pybind11/tests:test_docstring_options
//third_party/pybind11/tests:test_eigen_matrix
//third_party/pybind11/tests:test_eigen_tensor
//third_party/pybind11/tests:test_enum
//third_party/pybind11/tests:test_eval
//third_party/pybind11/tests:test_exceptions
//third_party/pybind11/tests:test_factory_constructors
//third_party/pybind11/tests:test_gil_scoped
//third_party/pybind11/tests:test_iostream
//third_party/pybind11/tests:test_kwargs_and_defaults
//third_party/pybind11/tests:test_local_bindings
//third_party/pybind11/tests:test_methods_and_attributes
//third_party/pybind11/tests:test_modules
//third_party/pybind11/tests:test_multiple_inheritance
//third_party/pybind11/tests:test_numpy_array
//third_party/pybind11/tests:test_numpy_dtypes
//third_party/pybind11/tests:test_numpy_vectorize
//third_party/pybind11/tests:test_operator_overloading
//third_party/pybind11/tests:test_pickling
//third_party/pybind11/tests:test_python_multiple_inheritance
//third_party/pybind11/tests:test_pytypes
//third_party/pybind11/tests:test_sequences_and_iterators
//third_party/pybind11/tests:test_thread
//third_party/pybind11/tests:test_type_caster_pyobject_ptr
//third_party/pybind11/tests:test_union
//third_party/pybind11/tests:test_unnamed_namespace_a
//third_party/pybind11/tests:test_unnamed_namespace_b
//third_party/pybind11/tests:test_virtual_functions
//third_party/pybind11/tests:test_wip

rwgk avatar Jun 29 '24 22:06 rwgk

Tracking progress:

test_factory_constructors builds and runs successfully WITH ALL BAKEIN_BREAK removed.

This is still the same:

https://github.com/pybind/pybind11/pull/5213#issuecomment-2198362173

rwgk avatar Jun 30 '24 18:06 rwgk

Tracking progress (@ commit 66a775eee9b1dff862f5b1af9dd9cdeb423c026c):

All existing tests pass, with just two obvious minor changes to the tests itself:

$ git diff master tests/test_class.cpp tests/test_smart_ptr.py
diff --git a/tests/test_class.cpp b/tests/test_class.cpp
index 9001d86b..e2981aca 100644
--- a/tests/test_class.cpp
+++ b/tests/test_class.cpp
@@ -606,11 +606,15 @@ CHECK_NOALIAS(8);
     static_assert(std::is_same<typename DoesntBreak##N::holder_type,                              \
                                std::TYPE##_ptr<BreaksBase<(N)>>>::value,                          \
                   "DoesntBreak" #N " has wrong holder_type!")
+#define CHECK_SMART_HOLDER(N)                                                                     \
+    static_assert(std::is_same<typename DoesntBreak##N::holder_type,                              \
+                               pybindit::memory::smart_holder>::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);
+CHECK_SMART_HOLDER(4);
+CHECK_SMART_HOLDER(5);
 CHECK_HOLDER(6, shared);
 CHECK_HOLDER(7, shared);
 CHECK_HOLDER(8, shared);
diff --git a/tests/test_smart_ptr.py b/tests/test_smart_ptr.py
index bf0ae4ae..0d196199 100644
--- a/tests/test_smart_ptr.py
+++ b/tests/test_smart_ptr.py
@@ -298,6 +298,7 @@ def test_move_only_holder_with_addressof_operator():


 def test_smart_ptr_from_default():
+    pytest.skip("BAKEIN_EXPECTED: Failed: DID NOT RAISE <class 'RuntimeError'>")
     instance = m.HeldByDefaultHolder()
     with pytest.raises(RuntimeError) as excinfo:
         m.HeldByDefaultHolder.load_shared_ptr(instance)

rwgk avatar Jul 02 '24 19:07 rwgk

Tracking progress (@ commit a332fe8cf4db42435e917a43e82b0775d6a42161):

GHA: all tests pass

ASAN, MSAN testing with Google-internal toolchain: all tests pass

rwgk avatar Jul 02 '24 22:07 rwgk

Tracking progress (@ commit 0b7a628a04a11d0c6be4a4e6ed7ea1e9e6d61708):

GHA: all tests pass

ASAN, MSAN testing with Google-internal toolchain: all tests pass

test_class_sh_basic.cpp and test_class_sh_basic.py are identical to the smart_holder branch.

rwgk avatar Jul 05 '24 08:07 rwgk

Tracking progress (@ commit 91d40035a2be318f6d33e955c11fe17ac5e1999d):

All smart_holder-specific tests (i.e. tests that only exist on the smart_holder branch, but not on master) were transferred to this PR, except test_exc_namespace_visibility and test_type_caster_odr_guard_[12] (these will not be included here).

Note the BAKEIN_WIP diffs below, which are the only diffs between smart_holder-specific tests.

GHA: all tests pass BUT NOTE that 🐍 3 • GCC 7 • C++17• x64 is disabled

ASAN, MSAN testing with Google-internal toolchain: all tests pass (including test_class_sh_shared_ptr_copy_move.py WITHOUT the diff below).

diff --git a/tests/test_class_sh_disowning.py b/tests/test_class_sh_disowning.py
index 63cdd4ed..195faff9 100644
--- a/tests/test_class_sh_disowning.py
+++ b/tests/test_class_sh_disowning.py
@@ -50,8 +50,10 @@ def test_mixed():
         # Either obj1b or obj2b was disowned in the expected failed m.mixed() calls above, but not
         # both.
         is_disowned_results = (is_disowned(obj1b), is_disowned(obj2b))
-        assert is_disowned_results.count(True) == 1
-        if first_pass:
+        # BAKEIN_WIP: Why is the behavior different?
+        assert is_disowned_results.count(True) == 0
+        # BAKEIN_WIP: Cleanup condition:
+        if first_pass and is_disowned_results.count(True):
             first_pass = False
             print(
                 "\nC++ function argument %d is evaluated first."
diff --git a/tests/test_class_sh_property.cpp b/tests/test_class_sh_property.cpp
index 35615cec..a553935b 100644
--- a/tests/test_class_sh_property.cpp
+++ b/tests/test_class_sh_property.cpp
@@ -73,9 +73,9 @@ TEST_SUBMODULE(class_sh_property, m) {
         .def_readwrite("m_cptr_readwrite", &Outer::m_cptr)
 
         // .def_readonly("m_uqmp_readonly", &Outer::m_uqmp) // Custom compilation Error.
-        .def_readwrite("m_uqmp_readwrite", &Outer::m_uqmp)
+        // BAKEIN_BREAK .def_readwrite("m_uqmp_readwrite", &Outer::m_uqmp)
         // .def_readonly("m_uqcp_readonly", &Outer::m_uqcp) // Custom compilation Error.
-        .def_readwrite("m_uqcp_readwrite", &Outer::m_uqcp)
+        // BAKEIN_BREAK .def_readwrite("m_uqcp_readwrite", &Outer::m_uqcp)
 
         .def_readwrite("m_shmp_readonly", &Outer::m_shmp)
         .def_readwrite("m_shmp_readwrite", &Outer::m_shmp)
diff --git a/tests/test_class_sh_property.py b/tests/test_class_sh_property.py
index 9aeef44e..a2530247 100644
--- a/tests/test_class_sh_property.py
+++ b/tests/test_class_sh_property.py
@@ -16,6 +16,7 @@ def test_valu_getter(m_attr):
     outer = m.Outer()
     field = getattr(outer, m_attr)
     assert field.num == -99
+    pytest.skip("BAKEIN_BREAK: Failed: DID NOT RAISE <class 'ValueError'>")
     with pytest.raises(ValueError) as excinfo:
         m.DisownOuter(outer)
     assert str(excinfo.value) == "Cannot disown use_count != 1 (loaded_as_unique_ptr)."
@@ -83,6 +84,7 @@ def test_ptr(field_type, num_default, outer_type, m_attr, r_kind):
 
 @pytest.mark.parametrize("m_attr_readwrite", ["m_uqmp_readwrite", "m_uqcp_readwrite"])
 def test_uqp(m_attr_readwrite):
+    pytest.skip(f"BAKEIN_BREAK: {m_attr_readwrite} does not build")
     outer = m.Outer()
     assert getattr(outer, m_attr_readwrite) is None
     field_orig = m.Field()
@@ -135,6 +137,7 @@ def _proxy_dereference(proxy, xxxattr, *args, **kwargs):
 @pytest.mark.parametrize("m_attr", ["m_uqmp", "m_uqcp"])
 def test_unique_ptr_field_proxy_poc(m_attr):
     m_attr_readwrite = m_attr + "_readwrite"
+    pytest.skip(f"BAKEIN_BREAK: {m_attr_readwrite} does not build")
     outer = m.Outer()
     field_orig = m.Field()
     field_orig.num = 45
diff --git a/tests/test_class_sh_property_non_owning.cpp b/tests/test_class_sh_property_non_owning.cpp
index cd7fc5c6..5591fa96 100644
--- a/tests/test_class_sh_property_non_owning.cpp
+++ b/tests/test_class_sh_property_non_owning.cpp
@@ -60,7 +60,8 @@ TEST_SUBMODULE(class_sh_property_non_owning, m) {
         .def_readwrite("core_fld_shared_ptr_rw", &DataField::core_fld_shared_ptr)
         .def_readonly("core_fld_raw_ptr_ro", &DataField::core_fld_raw_ptr)
         .def_readwrite("core_fld_raw_ptr_rw", &DataField::core_fld_raw_ptr)
-        .def_readwrite("core_fld_unique_ptr_rw", &DataField::core_fld_unique_ptr);
+        // BAKEIN_BREAK .def_readwrite("core_fld_unique_ptr_rw", &DataField::core_fld_unique_ptr)
+        ;
 
     py::classh<DataFieldsHolder>(m, "DataFieldsHolder")
         .def(py::init<std::size_t>())
diff --git a/tests/test_class_sh_property_non_owning.py b/tests/test_class_sh_property_non_owning.py
index 33a9d450..7e38684a 100644
--- a/tests/test_class_sh_property_non_owning.py
+++ b/tests/test_class_sh_property_non_owning.py
@@ -15,7 +15,7 @@ from pybind11_tests import class_sh_property_non_owning as m
         ("core_fld_shared_ptr_rw", (14, 25)),
         ("core_fld_raw_ptr_ro", (14, 25)),
         ("core_fld_raw_ptr_rw", (14, 25)),
-        ("core_fld_unique_ptr_rw", (15, 26)),
+        # BAKEIN_BREAK ("core_fld_unique_ptr_rw", (15, 26)),
     ],
 )
 def test_core_fld_common(core_fld, expected, persistent_holder):
diff --git a/tests/test_class_sh_shared_ptr_copy_move.py b/tests/test_class_sh_shared_ptr_copy_move.py
index 067bb47d..cfefe9fd 100644
--- a/tests/test_class_sh_shared_ptr_copy_move.py
+++ b/tests/test_class_sh_shared_ptr_copy_move.py
@@ -1,7 +1,13 @@
 from __future__ import annotations
 
+import pytest
+
 from pybind11_tests import class_sh_shared_ptr_copy_move as m
 
+pytest.skip(
+    "BAKEIN_BREAK: Windows fatal exception: access violation", allow_module_level=True
+)
+
 
 def test_shptr_copy():
     txt = m.test_ShPtr_copy()[0].get_history()

rwgk avatar Jul 07 '24 22:07 rwgk

Keeping track of the

🐍 3 • GCC 7 • C++17• x64

failure for easy future reference:

Sun, 07 Jul 2024 17:26:09 GMT ...
...
============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-3.10.1, py-1.7.0, pluggy-0.8.0
C++ Info: 7.5.0 C++17 __pybind11_internals_v4_gcc_libstdcpp_cxxabi1011__ PYBIND11_SIMPLE_GIL_MANAGEMENT=False PYBIND11_NUMPY_1_ONLY=False
rootdir: /__w/pybind11/pybind11/tests, inifile: pytest.ini
collected 1033 items / 1 skipped

../../tests/test_async.py ..                                             [  0%]
../../tests/test_buffers.py ..........................                   [  2%]
../../tests/test_builtin_casters.py ....................                 [  4%]
../../tests/test_call_policies.py ........                               [  5%]
../../tests/test_callbacks.py .............s.                            [  6%]
../../tests/test_chrono.py ...........................................   [ 11%]
../../tests/test_class.py ....................................           [ 14%]
../../tests/test_class_sh_basic.py ..................................... [ 18%]
s........                                                                [ 18%]
../../tests/test_class_sh_disowning.py ...                               [ 19%]
../../tests/test_class_sh_disowning_mi.py .........................      [ 21%]
../../tests/test_class_sh_factory_constructors.py ......                 [ 22%]
../../tests/test_class_sh_inheritance.py .......                         [ 22%]
../../tests/test_class_sh_mi_thunks.py ..........                        [ 23%]
../../tests/test_class_sh_trampoline_basic.py .....                      [ 24%]
../../tests/test_class_sh_trampoline_self_life_support.py .....          [ 24%]
../../tests/test_class_sh_trampoline_shared_from_this.py ..............  [ 26%]
../../tests/test_class_sh_trampoline_shared_ptr_cpp_arg.py ........      [ 27%]
../../tests/test_class_sh_trampoline_unique_ptr.py ..                    [ 27%]
../../tests/test_class_sh_unique_ptr_custom_deleter.py .                 [ 27%]
Segmentation fault (core dumped)
make[3]: *** [tests/CMakeFiles/pytest.dir/build.make:74: tests/CMakeFiles/pytest] Error 139
make[2]: *** [CMakeFiles/Makefile2:404: tests/CMakeFiles/pytest.dir/all] Error 2
make[1]: *** [CMakeFiles/Makefile2:411: tests/CMakeFiles/pytest.dir/rule] Error 2
../../tests/test_class_sh_unique_ptr_member.py .make[3]: Leaving directory '/__w/pybind11/pybind11/build'
make[2]: Leaving directory '/__w/pybind11/pybind11/build'
make[1]: Leaving directory '/__w/pybind11/pybind11/build'
make: *** [Makefile:248: pytest] Error 2

rwgk avatar Jul 08 '24 18:07 rwgk

Keeping track of the 17 jobs failing before the module-level pytest.skip() in test_class_sh_shared_ptr_copy_move.py was added:

NOTE that [🐍 3 • GCC 7 • C++17• x64] also failed with

Sun, 07 Jul 2024 16:55:30 GMT ...
...
../../tests/test_class_sh_shared_ptr_copy_move.py make[3]: Leaving directory '/__w/pybind11/pybind11/build'
make[2]: Leaving directory '/__w/pybind11/pybind11/build'
Segmentation fault (core dumped)

🐍 3 • Clang 3.7 • C++11 • x64 Process completed with exit code 2. 🐍 3 • Clang 5 • C++14 • x64 Process completed with exit code 2. 🐍 3 • almalinux:8 • x64 Process completed with exit code 2. 🐍 3.8 • ubuntu-20.04 • x64 -DPYBIND11_FINDPYTHON=ON -DCMAKE_CXX_FLAGS="-D_=1" Process completed with exit code 2. 🐍 3 • GCC 8 • C++17• x64 Process completed with exit code 2. 🐍 3.13 • ubuntu-20.04 • x64 Process completed with exit code 2. 🐍 3.9 • MSVC 2022 C++20 • x64 Process completed with exit code 1. 🐍 3 • GCC 7 • C++17• x64 Process completed with exit code 2. 🐍 3.9 • windows-2019 • x64 Process completed with exit code 1. 🐍 3.8 • windows-2022 • x64 Process completed with exit code 1. 🐍 pypy-3.8 • windows-2022 • x64 Process completed with exit code 1. 🐍 3 • windows-latest • mingw64 Process completed with exit code 2. 🐍 3 • ICC latest • x64 Process completed with exit code 2. 🐍 3.9 • ubuntu-20.04 • x64 Process completed with exit code 2. 🐍 3.9 • windows-2022 • x64 Process completed with exit code 1. 🐍 3.8 • macos-13 • x64 Process completed with exit code 2. 🐍 pypy-3.9 • windows-2022 • x64 Process completed with exit code 1.

rwgk avatar Jul 08 '24 18:07 rwgk

Regarding commit 127058cb16961526d185b6600511854723651ad5, the key observation leading to that bug fix was this:

< LOOOK DEREGISTER_INSTANCE START(PTR=AAA=, pytype=pybind11_tests.ccccccccccccccccccccc.D, cpptype=pybind11_tests::ccccccccccccccccccccc::D)
---
> LOOOK DEREGISTER_INSTANCE START(PTR=AAA=, pytype=pybind11_tests.ccccccccccccccccccccc.D, cpptype=pybind11_tests::ccccccccccccccccccccc::B)

Note cpptype D (correct) vs B (incorrect).

The correct line is from running test_ ccccccccccccccccccccc with the smart_holder branch, the incorrect line this PR before commit 127058cb16961526d185b6600511854723651ad5.

rwgk avatar Jul 10 '24 15:07 rwgk

Milestone reached at commit ed27c37c4e064216aa2ceae64d1fe7d8e1e75a09:

  • Testing via GHA passes:

    • All existing tests under master, with the only minor exceptions as noted previously under https://github.com/pybind/pybind11/pull/5213#issuecomment-2204180286.
    • All** existing tests under the smart_holder branch, exactly as-is. (** 2 tests were intentionally not transferred to this PR: test_exc_namespace_visibility, test_type_caster_odr_guard_1)
  • Google-global testing, with several minor Google-internal adjustments outside the pybind11 source tree.

NOTE: smart_holder is still the default holder. — This is mainly intended to be a stress test.

The Google-global testing (internal ID: OCL:652909258:BASE:653492046:1721283711718:c0b9e029) identified only 3 use cases that break with smart_holder as the holder. These cases were fixed by explicitly requesting std::unique_ptr as the holder:

rwgk avatar Jul 18 '24 07:07 rwgk

Closing, as explained in the PR description.

All checks have passed: 2 skipped and 155 successful checks

CI — https://github.com/pybind/pybind11/actions/runs/10038035526

CI-SH-DEF — https://github.com/pybind/pybind11/actions/runs/10038035539

rwgk avatar Jul 22 '24 10:07 rwgk