Skip to content

Commit cfbefe8

Browse files
committed
Use re-entrant mutex to protect global state.
Add pymutex.hpp which implements a re-entrant mutex on top of Python's PyMutex. Add BOOST_PYTHON_LOCK_STATE() macro that uses RAII to lock mutable global state as required.
1 parent 6f5f3b6 commit cfbefe8

File tree

6 files changed

+147
-6
lines changed

6 files changed

+147
-6
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2025 Boost.Python Contributors
2+
// Distributed under the Boost Software License, Version 1.0. (See
3+
// accompanying file LICENSE_1_0.txt or copy at
4+
// http://www.boost.org/LICENSE_1_0.txt)
5+
6+
#ifndef BOOST_PYTHON_DETAIL_PYMUTEX_HPP
7+
#define BOOST_PYTHON_DETAIL_PYMUTEX_HPP
8+
9+
#include <boost/python/detail/prefix.hpp>
10+
#ifdef Py_GIL_DISABLED
11+
// needed for pymutex wrapper
12+
#include <atomic>
13+
#include <cstddef>
14+
#endif
15+
16+
namespace boost { namespace python { namespace detail {
17+
18+
#ifdef Py_GIL_DISABLED
19+
20+
// Re-entrant wrapper around PyMutex for free-threaded Python
21+
// Similar to _PyRecursiveMutex or threading.RLock
22+
class pymutex {
23+
PyMutex m_mutex;
24+
std::atomic<unsigned long> m_owner;
25+
std::size_t m_level;
26+
27+
public:
28+
pymutex() : m_mutex({}), m_owner(0), m_level(0) {}
29+
30+
// Non-copyable, non-movable
31+
pymutex(const pymutex&) = delete;
32+
pymutex& operator=(const pymutex&) = delete;
33+
34+
void lock() {
35+
unsigned long thread = PyThread_get_thread_ident();
36+
if (m_owner.load(std::memory_order_relaxed) == thread) {
37+
m_level++;
38+
return;
39+
}
40+
PyMutex_Lock(&m_mutex);
41+
m_owner.store(thread, std::memory_order_relaxed);
42+
// m_level should be 0 when we acquire the lock
43+
}
44+
45+
void unlock() {
46+
unsigned long thread = PyThread_get_thread_ident();
47+
// Verify current thread owns the lock
48+
if (m_owner.load(std::memory_order_relaxed) != thread) {
49+
// This should never happen - programming error
50+
return;
51+
}
52+
if (m_level > 0) {
53+
m_level--;
54+
return;
55+
}
56+
m_owner.store(0, std::memory_order_relaxed);
57+
PyMutex_Unlock(&m_mutex);
58+
}
59+
60+
bool is_locked_by_current_thread() const {
61+
unsigned long thread = PyThread_get_thread_ident();
62+
return m_owner.load(std::memory_order_relaxed) == thread;
63+
}
64+
};
65+
66+
67+
// RAII lock guard for pymutex
68+
class pymutex_guard {
69+
pymutex& m_mutex;
70+
71+
public:
72+
explicit pymutex_guard(pymutex& mutex) : m_mutex(mutex) {
73+
m_mutex.lock();
74+
}
75+
76+
~pymutex_guard() {
77+
m_mutex.unlock();
78+
}
79+
80+
// Non-copyable, non-movable
81+
pymutex_guard(const pymutex_guard&) = delete;
82+
pymutex_guard& operator=(const pymutex_guard&) = delete;
83+
};
84+
85+
// Global mutex for protecting all Boost.Python internal state
86+
// Similar to pybind11's internals.mutex
87+
BOOST_PYTHON_DECL pymutex& get_global_mutex();
88+
89+
// Macro for acquiring the global lock
90+
// Similar to pybind11's PYBIND11_LOCK_INTERNALS
91+
#define BOOST_PYTHON_LOCK_STATE() \
92+
::boost::python::detail::pymutex_guard lock(::boost::python::detail::get_global_mutex())
93+
94+
#else
95+
96+
// No-op macro when not in free-threaded mode
97+
#define BOOST_PYTHON_LOCK_STATE()
98+
99+
#endif // Py_GIL_DISABLED
100+
101+
}}} // namespace boost::python::detail
102+
103+
#endif // BOOST_PYTHON_DETAIL_PYMUTEX_HPP

src/converter/from_python.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include <boost/python/handle.hpp>
1313
#include <boost/python/detail/raw_pyobject.hpp>
14+
#include <boost/python/detail/pymutex.hpp>
1415
#include <boost/python/cast.hpp>
1516

1617
#include <vector>
@@ -145,6 +146,8 @@ namespace
145146

146147
inline bool visit(rvalue_from_python_chain const* chain)
147148
{
149+
BOOST_PYTHON_LOCK_STATE();
150+
148151
visited_t::iterator const p = std::lower_bound(visited.begin(), visited.end(), chain);
149152
if (p != visited.end() && *p == chain)
150153
return false;
@@ -157,9 +160,11 @@ namespace
157160
{
158161
unvisit(rvalue_from_python_chain const* chain)
159162
: chain(chain) {}
160-
163+
161164
~unvisit()
162165
{
166+
BOOST_PYTHON_LOCK_STATE();
167+
163168
visited_t::iterator const p = std::lower_bound(visited.begin(), visited.end(), chain);
164169
assert(p != visited.end());
165170
visited.erase(p);

src/converter/registry.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <boost/python/converter/registry.hpp>
66
#include <boost/python/converter/registrations.hpp>
77
#include <boost/python/converter/builtin_converters.hpp>
8+
#include <boost/python/detail/pymutex.hpp>
89

910
#include <set>
1011
#include <stdexcept>
@@ -112,9 +113,9 @@ registration::~registration()
112113
namespace // <unnamed>
113114
{
114115
typedef registration entry;
115-
116+
116117
typedef std::set<entry> registry_t;
117-
118+
118119
#ifndef BOOST_PYTHON_CONVERTER_REGISTRY_APPLE_MACH_WORKAROUND
119120
registry_t& entries()
120121
{
@@ -181,6 +182,8 @@ namespace // <unnamed>
181182

182183
entry* get(type_info type, bool is_shared_ptr = false)
183184
{
185+
BOOST_PYTHON_LOCK_STATE();
186+
184187
# ifdef BOOST_PYTHON_TRACE_REGISTRY
185188
registry_t::iterator p = entries().find(entry(type));
186189

@@ -293,6 +296,8 @@ namespace registry
293296

294297
registration const* query(type_info type)
295298
{
299+
BOOST_PYTHON_LOCK_STATE();
300+
296301
registry_t::iterator p = entries().find(entry(type));
297302
# ifdef BOOST_PYTHON_TRACE_REGISTRY
298303
std::cout << "querying " << type

src/converter/type_id.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <boost/python/type_id.hpp>
77
#include <boost/python/detail/decorated_type_id.hpp>
8+
#include <boost/python/detail/pymutex.hpp>
89
#include <utility>
910
#include <vector>
1011
#include <algorithm>
@@ -81,7 +82,7 @@ namespace
8182
{
8283
free_mem(char*p)
8384
: p(p) {}
84-
85+
8586
~free_mem()
8687
{
8788
std::free(p);
@@ -92,6 +93,7 @@ namespace
9293

9394
bool cxxabi_cxa_demangle_is_broken()
9495
{
96+
BOOST_PYTHON_LOCK_STATE();
9597
static bool was_tested = false;
9698
static bool is_broken = false;
9799
if (!was_tested) {
@@ -109,6 +111,8 @@ namespace detail
109111
{
110112
BOOST_PYTHON_DECL char const* gcc_demangle(char const* mangled)
111113
{
114+
BOOST_PYTHON_LOCK_STATE();
115+
112116
typedef std::vector<
113117
std::pair<char const*, char const*>
114118
> mangling_map;

src/errors.cpp

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,35 @@
1010
#include <boost/python/errors.hpp>
1111
#include <boost/cast.hpp>
1212
#include <boost/python/detail/exception_handler.hpp>
13+
#include <boost/python/detail/pymutex.hpp>
1314

1415
namespace boost { namespace python {
1516

17+
#ifdef Py_GIL_DISABLED
18+
namespace detail {
19+
// Global mutex for protecting all Boost.Python internal state
20+
pymutex& get_global_mutex()
21+
{
22+
static pymutex mutex;
23+
return mutex;
24+
}
25+
}
26+
#endif
27+
1628
error_already_set::~error_already_set() {}
1729

1830
// IMPORTANT: this function may only be called from within a catch block!
1931
BOOST_PYTHON_DECL bool handle_exception_impl(function0<void> f)
2032
{
2133
try
2234
{
23-
if (detail::exception_handler::chain)
24-
return detail::exception_handler::chain->handle(f);
35+
detail::exception_handler* handler_chain = nullptr;
36+
{
37+
BOOST_PYTHON_LOCK_STATE();
38+
handler_chain = detail::exception_handler::chain;
39+
}
40+
if (handler_chain)
41+
return handler_chain->handle(f);
2542
f();
2643
return false;
2744
}
@@ -80,6 +97,7 @@ exception_handler::exception_handler(handler_function const& impl)
8097
: m_impl(impl)
8198
, m_next(0)
8299
{
100+
BOOST_PYTHON_LOCK_STATE();
83101
if (chain != 0)
84102
tail->m_next = this;
85103
else

src/object/inheritance.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// http://www.boost.org/LICENSE_1_0.txt)
55
#include <boost/python/object/inheritance.hpp>
66
#include <boost/python/type_id.hpp>
7+
#include <boost/python/detail/pymutex.hpp>
78
#include <boost/graph/breadth_first_search.hpp>
89
#if _MSC_FULL_VER >= 13102171 && _MSC_FULL_VER <= 13102179
910
# include <boost/graph/reverse_graph.hpp>
@@ -390,6 +391,8 @@ namespace
390391

391392
inline void* convert_type(void* const p, class_id src_t, class_id dst_t, bool polymorphic)
392393
{
394+
BOOST_PYTHON_LOCK_STATE();
395+
393396
// Quickly rule out unregistered types
394397
index_entry* src_p = seek_type(src_t);
395398
if (src_p == 0)
@@ -452,6 +455,8 @@ BOOST_PYTHON_DECL void* find_static_type(void* p, class_id src_t, class_id dst_t
452455
BOOST_PYTHON_DECL void add_cast(
453456
class_id src_t, class_id dst_t, cast_function cast, bool is_downcast)
454457
{
458+
BOOST_PYTHON_LOCK_STATE();
459+
455460
// adding an edge will invalidate any record of unreachability in
456461
// the cache.
457462
static std::size_t expected_cache_len = 0;
@@ -490,6 +495,7 @@ BOOST_PYTHON_DECL void add_cast(
490495
BOOST_PYTHON_DECL void register_dynamic_id_aux(
491496
class_id static_id, dynamic_id_function get_dynamic_id)
492497
{
498+
BOOST_PYTHON_LOCK_STATE();
493499
tuples::get<kdynamic_id>(*demand_type(static_id)) = get_dynamic_id;
494500
}
495501

0 commit comments

Comments
 (0)