Skip to content

Commit 96d2eaf

Browse files
committed
Extract reusable functions
1 parent db22bb4 commit 96d2eaf

File tree

2 files changed

+83
-80
lines changed

2 files changed

+83
-80
lines changed

include/pybind11/detail/internals.h

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,83 @@ inline object get_python_state_dict() {
550550
return state_dict;
551551
}
552552

553+
// Get or create per-storage capsule in the current interpreter's state dict.
554+
// Use test-and-set pattern with `PyDict_SetDefault` for thread-safe concurrent access.
555+
// WARNING: There can be multiple threads creating the storage at the same time, while only one
556+
// will succeed in inserting its capsule into the dict. Therefore, the deleter will be
557+
// used to clean up the storage of the unused capsules.
558+
template <typename Payload, bool LeakOnInterpreterShutdown = false>
559+
Payload *atomic_get_or_create_in_state_dict(const char *key) {
560+
error_scope err_scope; // preserve any existing Python error states
561+
562+
auto state_dict = reinterpret_borrow<dict>(get_python_state_dict());
563+
PyObject *capsule_obj = nullptr;
564+
565+
// First, try to get existing storage (fast path).
566+
{
567+
capsule_obj = dict_getitemstring(state_dict.ptr(), key);
568+
if (capsule_obj == nullptr && PyErr_Occurred()) {
569+
throw error_already_set();
570+
}
571+
// Fallthrough if capsule_obj is nullptr (not found).
572+
// Otherwise, we have found the existing storage (most common case) and return it below.
573+
}
574+
575+
if (capsule_obj == nullptr) {
576+
// Storage doesn't exist yet, create a new one.
577+
// Use unique_ptr for exception safety: if capsule creation throws, the storage is
578+
// automatically deleted.
579+
auto storage_ptr = std::unique_ptr<Payload>(new Payload{});
580+
// Create capsule with destructor to clean up when the interpreter shuts down.
581+
auto new_capsule = capsule(
582+
storage_ptr.get(),
583+
// The destructor will be called when the capsule is GC'ed.
584+
// - If our capsule is inserted into the dict below, it will be kept alive until
585+
// interpreter shutdown, so the destructor will be called at that time.
586+
// - If our capsule is NOT inserted (another thread inserted first), it will be
587+
// destructed when going out of scope here, so the destructor will be called
588+
// immediately, which will also free the storage.
589+
/*destructor=*/[](void *ptr) -> void { delete static_cast<Payload *>(ptr); });
590+
// At this point, the capsule object is created successfully.
591+
// Release the unique_ptr and let the capsule object own the storage to avoid
592+
// double-free.
593+
(void) storage_ptr.release();
594+
595+
// Use `PyDict_SetDefault` for atomic test-and-set:
596+
// - If key doesn't exist, inserts our capsule and returns it.
597+
// - If key exists (another thread inserted first), returns the existing value.
598+
// This is thread-safe because `PyDict_SetDefault` will hold a lock on the dict.
599+
//
600+
// NOTE: Here we use `PyDict_SetDefault` instead of `PyDict_SetDefaultRef` because the
601+
// capsule is kept alive until interpreter shutdown, so we do not need to handle
602+
// incref and decref here.
603+
capsule_obj = dict_setdefaultstring(state_dict.ptr(), key, new_capsule.ptr());
604+
if (capsule_obj == nullptr) {
605+
throw error_already_set();
606+
}
607+
if (LeakOnInterpreterShutdown && capsule_obj == new_capsule.ptr()) {
608+
// Our capsule was inserted.
609+
// Remove the destructor to leak the storage on interpreter shutdown.
610+
if (PyCapsule_SetDestructor(capsule_obj, nullptr) < 0) {
611+
throw error_already_set();
612+
}
613+
}
614+
}
615+
// - If key already existed, our `new_capsule` is not inserted, it will be destructed
616+
// when going out of scope here, which will also free the storage.
617+
// - Otherwise, our `new_capsule` is now in the dict, and it owns the storage and the
618+
// state dict will incref it.
619+
}
620+
621+
// Get the storage pointer from the capsule.
622+
void *raw_ptr = PyCapsule_GetPointer(capsule_obj, /*name=*/nullptr);
623+
if (!raw_ptr) {
624+
raise_from(PyExc_SystemError, "pybind11::detail::atomic_get_or_create_in_state_dict() FAILED");
625+
throw error_already_set();
626+
}
627+
return static_cast<Payload *>(raw_ptr);
628+
}
629+
553630
template <typename InternalsType>
554631
class internals_pp_manager {
555632
public:
@@ -618,26 +695,11 @@ class internals_pp_manager {
618695
: holder_id_(id), on_fetch_(on_fetch) {}
619696

620697
std::unique_ptr<InternalsType> *get_or_create_pp_in_state_dict() {
621-
error_scope err_scope;
622-
dict state_dict = get_python_state_dict();
623-
auto internals_obj
624-
= reinterpret_steal<object>(dict_getitemstringref(state_dict.ptr(), holder_id_));
625-
std::unique_ptr<InternalsType> *pp = nullptr;
626-
if (internals_obj) {
627-
void *raw_ptr = PyCapsule_GetPointer(internals_obj.ptr(), /*name=*/nullptr);
628-
if (!raw_ptr) {
629-
raise_from(PyExc_SystemError,
630-
"pybind11::detail::internals_pp_manager::get_pp_from_dict() FAILED");
631-
throw error_already_set();
632-
}
633-
pp = reinterpret_cast<std::unique_ptr<InternalsType> *>(raw_ptr);
634-
if (on_fetch_ && pp) {
635-
on_fetch_(pp->get());
636-
}
637-
} else {
638-
pp = new std::unique_ptr<InternalsType>;
639-
// NOLINTNEXTLINE(bugprone-casting-through-void)
640-
state_dict[holder_id_] = capsule(reinterpret_cast<void *>(pp));
698+
auto *pp
699+
= atomic_get_or_create_in_state_dict<std::unique_ptr<InternalsType>,
700+
/*LeakOnInterpreterShutdown=*/true>(holder_id_);
701+
if (on_fetch_ && pp) {
702+
on_fetch_(pp->get());
641703
}
642704
return pp;
643705
}

include/pybind11/gil_safe_call_once.h

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -248,67 +248,8 @@ class gil_safe_call_once_and_store {
248248
}
249249

250250
// Get or create per-storage capsule in the current interpreter's state dict.
251-
// Use test-and-set pattern with `PyDict_SetDefault` for thread-safe concurrent access.
252251
storage_type *get_or_create_storage_in_state_dict() {
253-
error_scope err_scope; // preserve any existing Python error states
254-
255-
auto state_dict = reinterpret_borrow<dict>(detail::get_python_state_dict());
256-
const std::string key = get_storage_key();
257-
PyObject *capsule_obj = nullptr;
258-
259-
// First, try to get existing storage (fast path).
260-
{
261-
capsule_obj = detail::dict_getitemstring(state_dict.ptr(), key.c_str());
262-
if (capsule_obj == nullptr && PyErr_Occurred()) {
263-
throw error_already_set();
264-
}
265-
// Fallthrough if capsule_obj is nullptr (not found).
266-
// Otherwise, we have found the existing storage (most common case) and return it
267-
// below.
268-
}
269-
270-
if (capsule_obj == nullptr) {
271-
// Storage doesn't exist yet, create a new one.
272-
// Use unique_ptr for exception safety: if capsule creation throws, the storage is
273-
// automatically deleted.
274-
auto storage_ptr = std::unique_ptr<storage_type>(new storage_type{});
275-
// Create capsule with destructor to clean up when the interpreter shuts down.
276-
auto new_capsule = capsule(storage_ptr.get(), [](void *ptr) -> void {
277-
delete static_cast<storage_type *>(ptr);
278-
});
279-
// At this point, the capsule object is created successfully.
280-
// Release the unique_ptr and let the capsule object own the storage to avoid
281-
// double-free.
282-
(void) storage_ptr.release();
283-
284-
// Use `PyDict_SetDefault` for atomic test-and-set:
285-
// - If key doesn't exist, inserts our capsule and returns it.
286-
// - If key exists (another thread inserted first), returns the existing value.
287-
// This is thread-safe because `PyDict_SetDefault` will hold a lock on the dict.
288-
//
289-
// NOTE: Here we use `PyDict_SetDefault` instead of `PyDict_SetDefaultRef` because the
290-
// capsule is kept alive until interpreter shutdown, so we do not need to handle incref
291-
// and decref here.
292-
capsule_obj
293-
= detail::dict_setdefaultstring(state_dict.ptr(), key.c_str(), new_capsule.ptr());
294-
if (capsule_obj == nullptr) {
295-
throw error_already_set();
296-
}
297-
// - If key already existed, our `new_capsule` is not inserted, it will be destructed
298-
// when going out of scope here, which will also free the storage.
299-
// - Otherwise, our `new_capsule` is now in the dict, and it owns the storage and the
300-
// state dict will incref it.
301-
}
302-
303-
// Get the storage pointer from the capsule.
304-
void *raw_ptr = PyCapsule_GetPointer(capsule_obj, /*name=*/nullptr);
305-
if (!raw_ptr) {
306-
raise_from(PyExc_SystemError,
307-
"pybind11::gil_safe_call_once_and_store::"
308-
"get_or_create_storage_in_state_dict() FAILED");
309-
throw error_already_set();
310-
}
311-
return static_cast<storage_type *>(raw_ptr);
252+
return detail::atomic_get_or_create_in_state_dict<storage_type>(get_storage_key().c_str());
312253
}
313254

314255
// No storage needed when subinterpreter support is enabled.

0 commit comments

Comments
 (0)