Skip to content

Commit 7d51202

Browse files
authored
Merge pull request #30157 from rwgk/pybind11clif_merge_sh
git merge smart_holder
2 parents 326741b + 8ee7373 commit 7d51202

15 files changed

+257
-109
lines changed

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ set(PYBIND11_HEADERS
170170
include/pybind11/stl/filesystem.h
171171
include/pybind11/trampoline_self_life_support.h
172172
include/pybind11/type_caster_pyobject_ptr.h
173-
include/pybind11/typing.h)
173+
include/pybind11/typing.h
174+
include/pybind11/warnings.h)
174175

175176
# Compare with grep and warn if mismatched
176177
if(PYBIND11_MASTER_PROJECT)

docs/faq.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,50 @@ been received, you must either explicitly interrupt execution by throwing
247247
});
248248
}
249249
250+
What is a highly conclusive and simple way to find memory leaks (e.g. in pybind11 bindings)?
251+
============================================================================================
252+
253+
Use ``while True`` & ``top`` (Linux, macOS).
254+
255+
For example, locally change tests/test_type_caster_pyobject_ptr.py like this:
256+
257+
.. code-block:: diff
258+
259+
def test_return_list_pyobject_ptr_reference():
260+
+ while True:
261+
vec_obj = m.return_list_pyobject_ptr_reference(ValueHolder)
262+
assert [e.value for e in vec_obj] == [93, 186]
263+
# Commenting out the next `assert` will leak the Python references.
264+
# An easy way to see evidence of the leaks:
265+
# Insert `while True:` as the first line of this function and monitor the
266+
# process RES (Resident Memory Size) with the Unix top command.
267+
- assert m.dec_ref_each_pyobject_ptr(vec_obj) == 2
268+
+ # assert m.dec_ref_each_pyobject_ptr(vec_obj) == 2
269+
270+
Then run the test as you would normally do, which will go into the infinite loop.
271+
272+
**In another shell, but on the same machine** run:
273+
274+
.. code-block:: bash
275+
276+
top
277+
278+
This will show:
279+
280+
.. code-block::
281+
282+
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
283+
1266095 rwgk 20 0 5207496 611372 45696 R 100.0 0.3 0:08.01 test_type_caste
284+
285+
Look for the number under ``RES`` there. You'll see it going up very quickly.
286+
287+
**Don't forget to Ctrl-C the test command** before your machine becomes
288+
unresponsive due to swapping.
289+
290+
This method only takes a couple minutes of effort and is very conclusive.
291+
What you want to see is that the ``RES`` number is stable after a couple
292+
seconds.
293+
250294
CMake doesn't detect the right Python version
251295
=============================================
252296

include/pybind11/detail/struct_smart_holder.h

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ struct smart_holder {
215215
// race conditions, but in the context of Python it is a bug (elsewhere)
216216
// if the Global Interpreter Lock (GIL) is not being held when this code
217217
// is reached.
218-
// SMART_HOLDER_WIP: IMPROVABLE: assert(GIL is held).
218+
// PYBIND11:REMINDER: This may need to be protected by a mutex in free-threaded Python.
219219
if (vptr.use_count() != 1) {
220220
throw std::invalid_argument(std::string("Cannot disown use_count != 1 (") + context
221221
+ ").");
@@ -277,29 +277,32 @@ struct smart_holder {
277277
return hld;
278278
}
279279

280-
// Caller is responsible for ensuring preconditions (SMART_HOLDER_WIP: details).
280+
// Caller is responsible for ensuring the complex preconditions
281+
// (see `smart_holder_type_caster_support::load_helper`).
281282
void disown() {
282283
reset_vptr_deleter_armed_flag(false);
283284
is_disowned = true;
284285
}
285286

286-
// Caller is responsible for ensuring preconditions (SMART_HOLDER_WIP: details).
287+
// Caller is responsible for ensuring the complex preconditions
288+
// (see `smart_holder_type_caster_support::load_helper`).
287289
void reclaim_disowned() {
288290
reset_vptr_deleter_armed_flag(true);
289291
is_disowned = false;
290292
}
291293

292-
// Caller is responsible for ensuring preconditions (SMART_HOLDER_WIP: details).
294+
// Caller is responsible for ensuring the complex preconditions
295+
// (see `smart_holder_type_caster_support::load_helper`).
293296
void release_disowned() { vptr.reset(); }
294297

295-
// SMART_HOLDER_WIP: review this function.
296298
void ensure_can_release_ownership(const char *context = "ensure_can_release_ownership") const {
297299
ensure_is_not_disowned(context);
298300
ensure_vptr_is_using_builtin_delete(context);
299301
ensure_use_count_1(context);
300302
}
301303

302-
// Caller is responsible for ensuring preconditions (SMART_HOLDER_WIP: details).
304+
// Caller is responsible for ensuring the complex preconditions
305+
// (see `smart_holder_type_caster_support::load_helper`).
303306
void release_ownership() {
304307
reset_vptr_deleter_armed_flag(false);
305308
release_disowned();

include/pybind11/detail/type_caster_base.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ inline PyObject *make_new_instance(PyTypeObject *type);
476476

477477
#ifdef PYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT
478478

479-
// SMART_HOLDER_WIP: Needs refactoring of existing pybind11 code.
479+
// PYBIND11:REMINDER: Needs refactoring of existing pybind11 code.
480480
inline bool deregister_instance(instance *self, void *valptr, const type_info *tinfo);
481481

482482
PYBIND11_NAMESPACE_BEGIN(smart_holder_type_caster_support)
@@ -569,8 +569,7 @@ handle smart_holder_from_unique_ptr(std::unique_ptr<T, D> &&src,
569569

570570
if (static_cast<void *>(src.get()) == src_raw_void_ptr) {
571571
// This is a multiple-inheritance situation that is incompatible with the current
572-
// shared_from_this handling (see PR #3023).
573-
// SMART_HOLDER_WIP: IMPROVABLE: Is there a better solution?
572+
// shared_from_this handling (see PR #3023). Is there a better solution?
574573
src_raw_void_ptr = nullptr;
575574
}
576575
auto smhldr = smart_holder::from_unique_ptr(std::move(src), src_raw_void_ptr);
@@ -626,8 +625,8 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr<T> &src,
626625
void *src_raw_void_ptr = static_cast<void *>(src_raw_ptr);
627626
const detail::type_info *tinfo = st.second;
628627
if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) {
629-
// SMART_HOLDER_WIP: MISSING: Enforcement of consistency with existing smart_holder.
630-
// SMART_HOLDER_WIP: MISSING: keep_alive.
628+
// PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing smart_holder.
629+
// PYBIND11:REMINDER: MISSING: keep_alive.
631630
return existing_inst;
632631
}
633632

include/pybind11/trampoline_self_life_support.h

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@
1515
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
1616

1717
PYBIND11_NAMESPACE_BEGIN(detail)
18-
// SMART_HOLDER_WIP: Needs refactoring of existing pybind11 code.
18+
// PYBIND11:REMINDER: Needs refactoring of existing pybind11 code.
1919
inline bool deregister_instance(instance *self, void *valptr, const type_info *tinfo);
2020
PYBIND11_NAMESPACE_END(detail)
2121

2222
// The original core idea for this struct goes back to PyCLIF:
2323
// https://github.com/google/clif/blob/07f95d7e69dca2fcf7022978a55ef3acff506c19/clif/python/runtime.cc#L37
24-
// URL provided here mainly to give proper credit. To fully explain the `HoldPyObj` feature, more
25-
// context is needed (SMART_HOLDER_WIP).
24+
// URL provided here mainly to give proper credit.
2625
struct trampoline_self_life_support {
2726
detail::value_and_holder v_h;
2827

include/pybind11/typing.h

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,7 @@ class Never : public none {
100100
using none::none;
101101
};
102102

103-
#if defined(__cpp_nontype_template_parameter_class) \
104-
&& (/* See #5201 */ !defined(__GNUC__) \
105-
|| (__GNUC__ > 10 || (__GNUC__ == 10 && __GNUC_MINOR__ >= 3)))
103+
#if defined(__cpp_nontype_template_args) && __cpp_nontype_template_args >= 201911L
106104
# define PYBIND11_TYPING_H_HAS_STRING_LITERAL
107105
template <size_t N>
108106
struct StringLiteral {

include/pybind11/warnings.h

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
pybind11/warnings.h: Python warnings wrappers.
3+
4+
Copyright (c) 2024 Jan Iwaszkiewicz <[email protected]>
5+
6+
All rights reserved. Use of this source code is governed by a
7+
BSD-style license that can be found in the LICENSE file.
8+
*/
9+
10+
#pragma once
11+
12+
#include "pybind11.h"
13+
#include "detail/common.h"
14+
15+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
16+
17+
PYBIND11_NAMESPACE_BEGIN(detail)
18+
19+
inline bool PyWarning_Check(PyObject *obj) {
20+
int result = PyObject_IsSubclass(obj, PyExc_Warning);
21+
if (result == 1) {
22+
return true;
23+
}
24+
if (result == -1) {
25+
raise_from(PyExc_SystemError,
26+
"pybind11::detail::PyWarning_Check(): PyObject_IsSubclass() call failed.");
27+
throw error_already_set();
28+
}
29+
return false;
30+
}
31+
32+
PYBIND11_NAMESPACE_END(detail)
33+
34+
PYBIND11_NAMESPACE_BEGIN(warnings)
35+
36+
inline object
37+
new_warning_type(handle scope, const char *name, handle base = PyExc_RuntimeWarning) {
38+
if (!detail::PyWarning_Check(base.ptr())) {
39+
pybind11_fail("pybind11::warnings::new_warning_type(): cannot create custom warning, base "
40+
"must be a subclass of "
41+
"PyExc_Warning!");
42+
}
43+
if (hasattr(scope, name)) {
44+
pybind11_fail("pybind11::warnings::new_warning_type(): an attribute with name \""
45+
+ std::string(name) + "\" exists already.");
46+
}
47+
std::string full_name = scope.attr("__name__").cast<std::string>() + std::string(".") + name;
48+
handle h(PyErr_NewException(full_name.c_str(), base.ptr(), nullptr));
49+
if (!h) {
50+
raise_from(PyExc_SystemError,
51+
"pybind11::warnings::new_warning_type(): PyErr_NewException() call failed.");
52+
throw error_already_set();
53+
}
54+
auto obj = reinterpret_steal<object>(h);
55+
scope.attr(name) = obj;
56+
return obj;
57+
}
58+
59+
// Similar to Python `warnings.warn()`
60+
inline void
61+
warn(const char *message, handle category = PyExc_RuntimeWarning, int stack_level = 2) {
62+
if (!detail::PyWarning_Check(category.ptr())) {
63+
pybind11_fail(
64+
"pybind11::warnings::warn(): cannot raise warning, category must be a subclass of "
65+
"PyExc_Warning!");
66+
}
67+
68+
if (PyErr_WarnEx(category.ptr(), message, stack_level) == -1) {
69+
throw error_already_set();
70+
}
71+
}
72+
73+
PYBIND11_NAMESPACE_END(warnings)
74+
75+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

tests/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ set(PYBIND11_TEST_FILES
135135
test_class_sh_unique_ptr_member
136136
test_class_sh_virtual_py_cpp_mix
137137
test_class_sh_void_ptr_capsule
138-
test_classh_mock
139138
test_const_name
140139
test_constants_and_functions
141140
test_copy_move
@@ -182,7 +181,8 @@ set(PYBIND11_TEST_FILES
182181
test_unnamed_namespace_a
183182
test_unnamed_namespace_b
184183
test_vector_unique_ptr_member
185-
test_virtual_functions)
184+
test_virtual_functions
185+
test_warnings)
186186

187187
# Invoking cmake with something like:
188188
# cmake -DPYBIND11_TEST_OVERRIDE="test_callbacks.cpp;test_pickling.cpp" ..

tests/extra_python_package/test_files.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"include/pybind11/trampoline_self_life_support.h",
5151
"include/pybind11/type_caster_pyobject_ptr.h",
5252
"include/pybind11/typing.h",
53+
"include/pybind11/warnings.h",
5354
}
5455

5556
detail_headers = {

tests/test_class_sh_trampoline_shared_ptr_cpp_arg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def test_shared_ptr_arg_identity():
6262
del obj
6363
pytest.gc_collect()
6464

65-
# SMART_HOLDER_WIP: the behavior below is DIFFERENT from PR #2839
65+
# NOTE: the behavior below is DIFFERENT from PR #2839
6666
# python reference is gone because it is not an Alias instance
6767
assert objref() is None
6868
assert tester.has_python_instance() is False

tests/test_classh_mock.cpp

Lines changed: 0 additions & 73 deletions
This file was deleted.

tests/test_classh_mock.py

Lines changed: 0 additions & 13 deletions
This file was deleted.

tests/test_pytypes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,7 +1026,7 @@ def test_optional_object_annotations(doc):
10261026

10271027
@pytest.mark.skipif(
10281028
not m.defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL,
1029-
reason="C++20 feature not available.",
1029+
reason="C++20 non-type template args feature not available.",
10301030
)
10311031
def test_literal(doc):
10321032
assert (
@@ -1037,7 +1037,7 @@ def test_literal(doc):
10371037

10381038
@pytest.mark.skipif(
10391039
not m.defined_PYBIND11_TYPING_H_HAS_STRING_LITERAL,
1040-
reason="C++20 feature not available.",
1040+
reason="C++20 non-type template args feature not available.",
10411041
)
10421042
def test_typevar(doc):
10431043
assert (

0 commit comments

Comments
 (0)