Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b863457
fix: Guard against using a uninitialized value after `__new__` alloca…
amjames Aug 28, 2026
66f8f37
fix: free lazily allocated storage on failed init and only permit laz…
henryiii Aug 29, 2026
4991f00
Merge branch 'master' into amjames→bugfix-6153
rwgk Aug 30, 2026
b80c224
fix: isolate old-style constructor storage
rwgk Aug 30, 2026
4455e3f
test: skip constructor thread test on Emscripten
rwgk Aug 30, 2026
14e32ae
fix: bump internals version to 13
rwgk Aug 30, 2026
9468fce
Revert "fix: bump internals version to 13"
rwgk Sep 2, 2026
91122b1
Merge branch 'master' into amjames→bugfix-6153
rwgk Sep 2, 2026
bda1151
fix: recover from legacy constructor storage collisions
rwgk Sep 2, 2026
23f2d0a
test: fix collision subprocess imports
rwgk Sep 2, 2026
89a5f72
refactor: simplify old-style constructor storage tracking
henryiii Sep 3, 2026
7a7e9f3
test: reject later self alias during old-style init
rwgk Sep 3, 2026
955cb19
fix: restrict old-style constructor self permission to self's own loa…
Sep 4, 2026
607d3c6
style: pre-commit fixes
pre-commit-ci[bot] Sep 8, 2026
ad4544b
style: clang-tidy fixes
amjames Sep 10, 2026
f061048
fix: GraalPY exceptions
amjames Sep 10, 2026
bf7f96f
test: pin the two gaps identified in the load-phase review
amjames Sep 11, 2026
d1fbf07
perf: only test the old-style frame pointer where the phase can change
amjames Sep 11, 2026
ab56b0f
docs: describe status_value_constructing with the other status bits
amjames Sep 11, 2026
c10fcc5
style: pre-commit fixes
pre-commit-ci[bot] Sep 11, 2026
182a9d0
style: clang-tidy/format
amjames Sep 11, 2026
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
8 changes: 8 additions & 0 deletions docs/advanced/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1427,4 +1427,12 @@ You can do that using ``py::custom_type_setup``:
cls.def("size", &ContainerOwnsPythonObjects::size);
cls.def("clear", &ContainerOwnsPythonObjects::clear);

.. note::

The ``py::detail::is_holder_constructed()`` guards above are required. During garbage
collection, ``tp_traverse`` and ``tp_clear`` may be handed an instance whose C++ value has
not been constructed yet -- for example one created with ``__new__`` before ``__init__``
has run. Casting such an instance raises ``ValueError``, and an exception must not be
allowed to escape either of these slots.

.. versionadded:: 2.8
33 changes: 28 additions & 5 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -2160,7 +2160,12 @@ class argument_loader {
static constexpr auto arg_names
= ::pybind11::detail::concat(type_descr(make_caster<Args>::name)...);

bool load_args(function_call &call) { return load_impl_sequence(call, indices{}); }
/// `old_style_init_frame` is non-null only for an old-style constructor candidate, whose
/// `self` (positional argument 0) may reach not-yet-constructed storage. Every other call
/// passes nullptr and pays nothing for the per-argument bookkeeping.
bool load_args(function_call &call, loader_life_support *old_style_init_frame = nullptr) {
return load_impl_sequence(call, indices{}, old_style_init_frame);
}

template <typename Return, typename Guard, typename Func>
// NOLINTNEXTLINE(readability-const-return-type)
Expand All @@ -2177,21 +2182,39 @@ class argument_loader {
}

private:
static bool load_impl_sequence(function_call &, index_sequence<>) { return true; }
static bool load_impl_sequence(function_call &, index_sequence<>, loader_life_support *) {
return true;
}

// Loads one positional argument, telling an old-style constructor frame (if any) which
// argument is being loaded: only argument 0 is the constructor's `self`.
template <size_t I>
bool load_one(loader_life_support *old_style_init_frame, function_call &call) {
// The phase only *changes* at argument 0 (`self_argument`) and argument 1
// (`later_argument`); from argument 2 on it is already `later_argument`. `I` is a
// template parameter, so `I < 2` folds at compile time and arguments 2 and beyond
// do not even test the pointer.
if (I < 2 && old_style_init_frame != nullptr) {
old_style_init_frame->begin_argument_load(I);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Where is begin_argument_load() defined? I don't see it in this PR or on master.

I wonder if we could add a I == 0 && or similar to the check here, so that we only pay for the null check once per call instead of once per argument.

@amjames amjames Sep 11, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

begin_argument_load is defined here.

I wonder if we could add a I == 0 && or similar to the check here, so that we only pay for the null check once per call instead of once per argument.

We can pay for 2x per call. The phase only changes on the slot-0 to slot-1 transition; from argument 2 on it is already later_argument, and the state carries across every remaining load.

The phase is still checked on every argument load, but we skip the bookkeeping.

From claude
Getting it to exactly one check per call would mean initializing the frame to self_argument and transitioning only at I == 1, dropping the before_arguments state. I would rather not: that trades a local invariant for a non-local assumption that nothing loads a bound type
between frame construction and argument 0.

}
return std::get<I>(argcasters).load(call.args[I], call.args_convert[I]);
}

template <size_t... Is>
bool load_impl_sequence(function_call &call, index_sequence<Is...>) {
bool load_impl_sequence(function_call &call,
index_sequence<Is...>,
loader_life_support *old_style_init_frame) {
PYBIND11_WARNING_PUSH
#if !defined(__clang__) && defined(__GNUC__) && __GNUC__ >= 13
// Work around a GCC -Warray-bounds false positive in argument_vector usage.
PYBIND11_WARNING_DISABLE_GCC("-Warray-bounds")
#endif
#ifdef __cpp_fold_expressions
if ((... || !std::get<Is>(argcasters).load(call.args[Is], call.args_convert[Is]))) {
if ((... || !load_one<Is>(old_style_init_frame, call))) {
return false;
}
#else
for (bool r : {std::get<Is>(argcasters).load(call.args[Is], call.args_convert[Is])...}) {
for (bool r : {load_one<Is>(old_style_init_frame, call)...}) {
if (!r) {
return false;
}
Expand Down
9 changes: 7 additions & 2 deletions include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -664,8 +664,10 @@ struct instance {
* the [bb...] block (but not independently allocated).
*
* Status bits indicate whether the associated holder is constructed (&
* status_holder_constructed) and whether the value pointer is registered (&
* status_instance_registered) in `registered_instances`.
* status_holder_constructed), whether the value pointer is registered (&
* status_instance_registered) in `registered_instances`, and whether a constructor is
* currently constructing the C++ value in that slot (& status_value_constructing), during
* which the value pointer must not be treated as denoting a live C++ object.
*/
bool simple_layout : 1;
/// For simple layout, tracks whether the holder has been constructed
Expand All @@ -676,6 +678,8 @@ struct instance {
bool has_patients : 1;
/// If true, this Python object needs to be kept alive for the lifetime of the C++ value.
bool is_alias : 1;
/// For simple layout, tracks whether a constructor is currently constructing the C++ value.
bool simple_value_constructing : 1;

/// Initializes all of the above type/values/holders data (but not the instance values
/// themselves)
Expand All @@ -693,6 +697,7 @@ struct instance {
/// Bit values for the non-simple status flags
static constexpr uint8_t status_holder_constructed = 1;
static constexpr uint8_t status_instance_registered = 2;
static constexpr uint8_t status_value_constructing = 4;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A reference to this should be added in the comment above that describes the other two flags.

};

static_assert(std::is_standard_layout<instance>::value,
Expand Down
Loading
Loading