@@ -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+
553630template <typename InternalsType>
554631class internals_pp_manager {
555632public:
@@ -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 }
0 commit comments