Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
262a609
ChatGPT-generated diamond virtual-inheritance test case.
rwgk Sep 14, 2025
5f77da3
Report "virtual base at offset 0" but don't skip test.
rwgk Sep 14, 2025
a50ff8d
Remove Left/Right virtual default dtors, to resolve clang-tidy errors:
rwgk Sep 14, 2025
72c40ab
Add assert(ptr) in register_instance_impl, deregister_instance_impl
rwgk Sep 13, 2025
598d4ec
Proper bug fix
rwgk Sep 13, 2025
3620ceb
Also exercise smart_holder_from_unique_ptr
rwgk Sep 14, 2025
5f1a454
[skip ci] Merge branch 'master' into mi_thunks
rwgk Sep 14, 2025
b3cc792
[skip ci] ChatGPT-generated bug fix: smart_holder::from_unique_ptr()
rwgk Sep 14, 2025
ecf0252
Exception-safe ownership transfer from unique_ptr to shared_ptr
rwgk Sep 14, 2025
7d047e8
[skip ci] Rename alias_ptr to mi_subobject_ptr to distinguish from tr…
rwgk Sep 14, 2025
00257be
[skip ci] Also exercise smart_holder::from_raw_ptr_take_ownership
rwgk Sep 14, 2025
8f87bc9
[skip ci] Add st.first comments (generated by ChatGPT)
rwgk Sep 14, 2025
4efd7e7
[skip ci] Copy and extend (raw_ptr, unique_ptr) reproducer from PR #5796
rwgk Sep 14, 2025
77be20f
Some polishing: comments, add back Left/Right dtors for consistency w…
rwgk Sep 14, 2025
cd9458b
explicitly default copy/move for VBase to silence -Wdeprecated-copy-w…
rwgk Sep 14, 2025
5e86d87
Resolve clang-tidy error:
rwgk Sep 15, 2025
1abad47
Merge branch 'master' into mi_thunks
rwgk Sep 19, 2025
4c95a2a
Merge branch 'master' into mi_thunks
rwgk Sep 28, 2025
31886ec
Expand comment in `smart_holder::from_unique_ptr()`
rwgk Sep 28, 2025
ba89e1d
Better Left/Right padding to make it more likely that we avoid "all a…
rwgk Sep 28, 2025
0df90e6
Give up on `alignas(16)` to resolve MSVC warning:
rwgk Sep 28, 2025
9dbd9f1
Rename test_virtual_base_at_offset_0() → test_virtual_base_not_at_off…
rwgk Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/pybind11/detail/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -334,13 +334,15 @@ inline void enable_try_inc_ref(PyObject *obj) {
#endif

inline bool register_instance_impl(void *ptr, instance *self) {
assert(ptr);
#ifdef Py_GIL_DISABLED
enable_try_inc_ref(reinterpret_cast<PyObject *>(self));
#endif
with_instance_map(ptr, [&](instance_map &instances) { instances.emplace(ptr, self); });
return true; // unused, but gives the same signature as the deregister func
}
inline bool deregister_instance_impl(void *ptr, instance *self) {
assert(ptr);
return with_instance_map(ptr, [&](instance_map &instances) {
auto range = instances.equal_range(ptr);
for (auto it = range.first; it != range.second; ++it) {
Expand Down
28 changes: 17 additions & 11 deletions include/pybind11/detail/struct_smart_holder.h
Original file line number Diff line number Diff line change
Expand Up @@ -339,22 +339,28 @@ struct smart_holder {

template <typename T, typename D>
static smart_holder from_unique_ptr(std::unique_ptr<T, D> &&unq_ptr,
void *void_ptr = nullptr) {
void *mi_subobject_ptr = nullptr) {
smart_holder hld;
hld.rtti_uqp_del = &typeid(D);
hld.vptr_is_using_std_default_delete = uqp_del_is_std_default_delete<T, D>();
guarded_delete gd{nullptr, false};
if (hld.vptr_is_using_std_default_delete) {
gd = make_guarded_std_default_delete<T>(true);
} else {
gd = make_guarded_custom_deleter<T, D>(std::move(unq_ptr.get_deleter()), true);
}
if (void_ptr != nullptr) {
hld.vptr.reset(void_ptr, std::move(gd));

// Build the owning control block on the *real object start* (T*).
guarded_delete gd
= hld.vptr_is_using_std_default_delete
? make_guarded_std_default_delete<T>(true)
: make_guarded_custom_deleter<T, D>(std::move(unq_ptr.get_deleter()), true);
// Critical: construct owner with pointer we intend to delete
std::shared_ptr<T> owner(unq_ptr.get(), std::move(gd));
// Relinquish ownership only after successful construction of owner
(void) unq_ptr.release();

// Publish either the subobject alias (for identity/VI) or the full object.
Copy link
Contributor

Choose a reason for hiding this comment

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

Personally, I'd elaborate a bit in this comment. This part feels a bit like magic to me why this is necessary and fixes a bug when this is used in msvc. I think I understand the problem to be that the shared pointer 'owner' must always point to the start of the entire object (and the deleter operates on that), but that the actual smart holder should 'hold' the subclass, which may not be at the start of the object itself?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done: commit 31886ec

if (mi_subobject_ptr) {
hld.vptr = std::shared_ptr<void>(owner, mi_subobject_ptr);
} else {
hld.vptr.reset(unq_ptr.get(), std::move(gd));
hld.vptr = std::static_pointer_cast<void>(owner);
}
(void) unq_ptr.release();

hld.is_populated = true;
return hld;
}
Expand Down
10 changes: 6 additions & 4 deletions include/pybind11/detail/type_caster_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,8 @@ handle smart_holder_from_unique_ptr(std::unique_ptr<T, D> &&src,
if (!src) {
return none().release();
}
// st.first is the subobject pointer appropriate for tinfo (may differ from src.get()
// under MI/VI). Use this for Python identity/registration, but keep ownership on T*.
void *src_raw_void_ptr = const_cast<void *>(st.first);
assert(st.second != nullptr);
const detail::type_info *tinfo = st.second;
Expand Down Expand Up @@ -657,9 +659,10 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr<T> &src,
return none().release();
}

auto src_raw_ptr = src.get();
// st.first is the subobject pointer appropriate for tinfo (may differ from src.get()
// under MI/VI). Use this for Python identity/registration, but keep ownership on T*.
void *src_raw_void_ptr = const_cast<void *>(st.first);
assert(st.second != nullptr);
void *src_raw_void_ptr = static_cast<void *>(src_raw_ptr);
const detail::type_info *tinfo = st.second;
if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) {
// PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing smart_holder.
Expand All @@ -673,8 +676,7 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr<T> &src,
void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr();
valueptr = src_raw_void_ptr;

auto smhldr
= smart_holder::from_shared_ptr(std::shared_ptr<void>(src, const_cast<void *>(st.first)));
auto smhldr = smart_holder::from_shared_ptr(std::shared_ptr<void>(src, src_raw_void_ptr));
tinfo->init_instance(inst_raw_ptr, static_cast<const void *>(&smhldr));

if (policy == return_value_policy::reference_internal) {
Expand Down
134 changes: 134 additions & 0 deletions tests/test_class_sh_mi_thunks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace test_class_sh_mi_thunks {
// C++ vtables - Part 2 - Multiple Inheritance
// ... the compiler creates a 'thunk' method that corrects `this` ...

// This test was added under PR #4380

struct Base0 {
virtual ~Base0() = default;
Base0() = default;
Expand All @@ -30,6 +32,107 @@ struct Derived : Base1, Base0 {
Derived(const Derived &) = delete;
};

// ChatGPT-generated Diamond added under PR #5836

struct VBase {
VBase() = default;
VBase(const VBase &) = default; // silence -Wdeprecated-copy-with-dtor
VBase &operator=(const VBase &) = default;
VBase(VBase &&) = default;
VBase &operator=(VBase &&) = default;
virtual ~VBase() = default;
virtual int ping() const { return 1; }
int vbase_tag = 42; // ensure it's not empty
};

// Left/right add some weight to steer layout differences across compilers
Copy link
Contributor

@iwanders iwanders Sep 28, 2025

Choose a reason for hiding this comment

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

I'm not sure this matters, but Left and Right are pretty similar a long long is guaranteed to be 64 bits, and the 7 long array is likely going to be padded to 8, which would make them the same size? To me the comment contradicts the code a bit in that sense. Maybe just having any attribute is enough, but if the sizes ought to be different I'd make it very clear, say char pad_l[4] and char pad_r[16].

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done: commit ba89e1d

Here is what ChatGPT is saying:

Good catch — the original comment was misleading, since char[7] and long long can end up the same size after padding. The real intent is simply to make the virtual bases non-empty and, ideally, asymmetrically sized/aligned so compilers are more likely to give us non-zero subobject offsets. I’ve updated the code to use small but clearly asymmetric paddings (char[4] vs char[16], with optional alignment) and clarified the comment to reflect this. The test still works correctly even if a compiler chooses offset 0, since that case is explicitly logged via test_virtual_base_at_offset_0().


In Ralf's own words: I was just super happy when the GhatGPT-generated test triggered failures on both Linux and Windows [see comment from two weeks ago] and didn't look as closely at the test code as you did. However, now I pushed ChatGPT harder, to "really bend things out of shape", but it assured me that the new code is as good as we can make it.

I'll rerun the CI. When it's done I'll look for test_virtual_base_at_offset_0() skip messages again. (I think I did that before, although I forgot to log that here.)

Copy link
Contributor

Choose a reason for hiding this comment

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

I was just super happy when the GhatGPT-generated test triggered failures on both Linux and Windows

Agreed, that is pretty impressive and definitely speeds things up. Thanks for sharing your ChatGPT conversation btw, I’ve bookmarked that to read through when I’m not on the road. I don’t have much experience using llms, so expect it’ll be educational for me.

Copy link
Collaborator Author

@rwgk rwgk Sep 29, 2025

Choose a reason for hiding this comment

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

I was just super happy when the GhatGPT-generated test triggered failures on both Linux and Windows

Agreed, that is pretty impressive and definitely speeds things up. Thanks for sharing your ChatGPT conversation btw, I’ve bookmarked that to read through when I’m not on the road. I don’t have much experience using llms, so expect it’ll be educational for me.

My time budget for pybind11 maintenance work has become super tiny. I think without ChatGPT I'd have to give up.

Thanks for your review, this was super helpful.

A couple minutes ago I replaced the pytest.skip() with an assert, to guarantee test quality in the future. It'd be great if you could take a final look and formally approve if it looks good to you.

Copy link
Contributor

Choose a reason for hiding this comment

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

A couple minutes ago I replaced the pytest.skip() with an assert, to guarantee test quality in the future.

Good idea, and helpful to have that comment there for future developers that may run into the assert failing. I've approved it!

struct Left : virtual VBase {
char pad_l[7];
~Left() override = default;
};
struct Right : virtual VBase {
long long pad_r;
~Right() override = default;
};

struct Diamond : Left, Right {
Diamond() = default;
Diamond(const Diamond &) = default;
~Diamond() override = default;
int ping() const override { return 7; }
int self_tag = 99;
};

VBase *make_diamond_as_vbase_raw_ptr() {
auto *ptr = new Diamond;
return ptr; // upcast
}

std::shared_ptr<VBase> make_diamond_as_vbase_shared_ptr() {
auto shptr = std::make_shared<Diamond>();
return shptr; // upcast
}

std::unique_ptr<VBase> make_diamond_as_vbase_unique_ptr() {
auto uqptr = std::unique_ptr<Diamond>(new Diamond);
return uqptr; // upcast
}

// For diagnostics
struct DiamondAddrs {
uintptr_t as_self;
uintptr_t as_vbase;
uintptr_t as_left;
uintptr_t as_right;
};

DiamondAddrs diamond_addrs() {
auto sp = std::make_shared<Diamond>();
return DiamondAddrs{reinterpret_cast<uintptr_t>(sp.get()),
reinterpret_cast<uintptr_t>(static_cast<VBase *>(sp.get())),
reinterpret_cast<uintptr_t>(static_cast<Left *>(sp.get())),
reinterpret_cast<uintptr_t>(static_cast<Right *>(sp.get()))};
}

// Animal-Cat-Tiger reproducer copied from PR #5796
// clone_raw_ptr, clone_unique_ptr added under PR #5836

class Animal {
public:
Animal() = default;
Animal(const Animal &) = default;
Animal &operator=(const Animal &) = default;
virtual Animal *clone_raw_ptr() const = 0;
virtual std::shared_ptr<Animal> clone_shared_ptr() const = 0;
virtual std::unique_ptr<Animal> clone_unique_ptr() const = 0;
virtual ~Animal() = default;
};

class Cat : virtual public Animal {
public:
Cat() = default;
Cat(const Cat &) = default;
Cat &operator=(const Cat &) = default;
~Cat() override = default;
};

class Tiger : virtual public Cat {
public:
Tiger() = default;
Tiger(const Tiger &) = default;
Tiger &operator=(const Tiger &) = default;
~Tiger() override = default;
Animal *clone_raw_ptr() const override {
return new Tiger(*this); // upcast
}
std::shared_ptr<Animal> clone_shared_ptr() const override {
return std::make_shared<Tiger>(*this); // upcast
}
std::unique_ptr<Animal> clone_unique_ptr() const override {
return std::unique_ptr<Tiger>(new Tiger(*this)); // upcast
}
};

} // namespace test_class_sh_mi_thunks

TEST_SUBMODULE(class_sh_mi_thunks, m) {
Expand Down Expand Up @@ -90,4 +193,35 @@ TEST_SUBMODULE(class_sh_mi_thunks, m) {
}
return obj_der->vec.size();
});

py::class_<VBase, py::smart_holder>(m, "VBase").def("ping", &VBase::ping);

py::class_<Left, VBase, py::smart_holder>(m, "Left");
py::class_<Right, VBase, py::smart_holder>(m, "Right");

py::class_<Diamond, Left, Right, py::smart_holder>(m, "Diamond", py::multiple_inheritance())
.def(py::init<>())
.def("ping", &Diamond::ping);

m.def("make_diamond_as_vbase_raw_ptr",
&make_diamond_as_vbase_raw_ptr,
py::return_value_policy::take_ownership);
m.def("make_diamond_as_vbase_shared_ptr", &make_diamond_as_vbase_shared_ptr);
m.def("make_diamond_as_vbase_unique_ptr", &make_diamond_as_vbase_unique_ptr);

py::class_<DiamondAddrs, py::smart_holder>(m, "DiamondAddrs")
.def_readonly("as_self", &DiamondAddrs::as_self)
.def_readonly("as_vbase", &DiamondAddrs::as_vbase)
.def_readonly("as_left", &DiamondAddrs::as_left)
.def_readonly("as_right", &DiamondAddrs::as_right);

m.def("diamond_addrs", &diamond_addrs);

py::classh<Animal>(m, "Animal");
py::classh<Cat, Animal>(m, "Cat");
py::classh<Tiger, Cat>(m, "Tiger", py::multiple_inheritance())
.def(py::init<>())
.def("clone_raw_ptr", &Tiger::clone_raw_ptr)
.def("clone_shared_ptr", &Tiger::clone_shared_ptr)
.def("clone_unique_ptr", &Tiger::clone_unique_ptr);
}
36 changes: 36 additions & 0 deletions tests/test_class_sh_mi_thunks.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,39 @@ def test_get_shared_vec_size_unique():
assert (
str(exc_info.value) == "Cannot disown external shared_ptr (load_as_unique_ptr)."
)


def test_virtual_base_at_offset_0():
addrs = m.diamond_addrs()
if addrs.as_vbase - addrs.as_self == 0:
# Not an actual skip, just a trick generate a message in the pytest summary
pytest.skip("virtual base at offset 0 on this compiler/layout")


@pytest.mark.parametrize(
"make_fn",
[
m.make_diamond_as_vbase_raw_ptr, # exercises smart_holder::from_raw_ptr_take_ownership
m.make_diamond_as_vbase_shared_ptr, # exercises smart_holder_from_shared_ptr
m.make_diamond_as_vbase_unique_ptr, # exercises smart_holder_from_unique_ptr
],
)
def test_make_diamond_as_vbase(make_fn):
# Added under PR #5836
vb = make_fn()
assert vb.ping() == 7


@pytest.mark.parametrize(
"clone_fn",
[
m.Tiger.clone_raw_ptr,
m.Tiger.clone_shared_ptr,
m.Tiger.clone_unique_ptr,
],
)
def test_animal_cat_tiger(clone_fn):
# Based on Animal-Cat-Tiger reproducer under PR #5796
tiger = m.Tiger()
cloned = clone_fn(tiger)
assert isinstance(cloned, m.Tiger)
Loading