@@ -62,11 +62,117 @@ ERL_NIF_TERM ATOM_CANCEL_TIMER;
6262ERL_NIF_TERM ATOM_EVENT_LOOP ;
6363ERL_NIF_TERM ATOM_DISPATCH ;
6464
65+ /* ============================================================================
66+ * Per-Interpreter Event Loop Storage
67+ * ============================================================================
68+ *
69+ * Event loop references are stored as module attributes in py_event_loop,
70+ * using PyCapsule for safe C pointer storage. This approach:
71+ *
72+ * - Works uniformly for main interpreter and sub-interpreters
73+ * - Each interpreter has its own py_event_loop module with its own attribute
74+ * - Thread-safe for free-threading (Python 3.13+)
75+ * - Uses gil_acquire()/gil_release() for safe GIL management
76+ *
77+ * Flow:
78+ * NIF set_python_event_loop() -> stores capsule in py_event_loop._loop
79+ * Python _is_initialized() -> checks if _loop attribute exists and is valid
80+ * Python operations -> retrieve loop from py_event_loop._loop
81+ */
82+
83+ /** @brief Name for the PyCapsule storing event loop pointer */
84+ static const char * EVENT_LOOP_CAPSULE_NAME = "erlang_python.event_loop" ;
85+
86+ /** @brief Module attribute name for storing the event loop */
87+ static const char * EVENT_LOOP_ATTR_NAME = "_loop" ;
88+
89+ /**
90+ * Get the py_event_loop module for the current interpreter.
91+ * MUST be called with GIL held.
92+ * Returns borrowed reference.
93+ */
94+ static PyObject * get_event_loop_module (void ) {
95+ PyObject * modules = PyImport_GetModuleDict ();
96+ if (modules == NULL ) {
97+ return NULL ;
98+ }
99+ return PyDict_GetItemString (modules , "py_event_loop" );
100+ }
101+
102+ /**
103+ * Get the event loop for the current Python interpreter.
104+ * MUST be called with GIL held.
105+ * Retrieves from py_event_loop._loop module attribute.
106+ *
107+ * @return Event loop pointer or NULL if not set
108+ */
109+ static erlang_event_loop_t * get_interpreter_event_loop (void ) {
110+ PyObject * module = get_event_loop_module ();
111+ if (module == NULL ) {
112+ return NULL ;
113+ }
114+
115+ PyObject * capsule = PyObject_GetAttrString (module , EVENT_LOOP_ATTR_NAME );
116+ if (capsule == NULL ) {
117+ PyErr_Clear (); /* Attribute doesn't exist */
118+ return NULL ;
119+ }
120+
121+ if (!PyCapsule_IsValid (capsule , EVENT_LOOP_CAPSULE_NAME )) {
122+ Py_DECREF (capsule );
123+ return NULL ;
124+ }
125+
126+ erlang_event_loop_t * loop = (erlang_event_loop_t * )PyCapsule_GetPointer (
127+ capsule , EVENT_LOOP_CAPSULE_NAME );
128+ Py_DECREF (capsule );
129+
130+ return loop ;
131+ }
132+
133+ /**
134+ * Set the event loop for the current interpreter.
135+ * MUST be called with GIL held.
136+ * Stores as py_event_loop._loop module attribute.
137+ *
138+ * @param loop Event loop to set
139+ * @return 0 on success, -1 on error
140+ */
141+ static int set_interpreter_event_loop (erlang_event_loop_t * loop ) {
142+ PyObject * module = get_event_loop_module ();
143+ if (module == NULL ) {
144+ return -1 ;
145+ }
146+
147+ if (loop == NULL ) {
148+ /* Clear the event loop attribute */
149+ if (PyObject_SetAttrString (module , EVENT_LOOP_ATTR_NAME , Py_None ) < 0 ) {
150+ PyErr_Clear ();
151+ }
152+ return 0 ;
153+ }
154+
155+ PyObject * capsule = PyCapsule_New (loop , EVENT_LOOP_CAPSULE_NAME , NULL );
156+ if (capsule == NULL ) {
157+ return -1 ;
158+ }
159+
160+ int result = PyObject_SetAttrString (module , EVENT_LOOP_ATTR_NAME , capsule );
161+ Py_DECREF (capsule );
162+
163+ if (result < 0 ) {
164+ PyErr_Clear ();
165+ return -1 ;
166+ }
167+
168+ return 0 ;
169+ }
170+
65171/* ============================================================================
66172 * Resource Callbacks
67173 * ============================================================================ */
68174
69- /* Forward declaration of global Python event loop pointer (defined later in file) */
175+ /* Global Python event loop pointer - kept for fast access from C code */
70176static erlang_event_loop_t * g_python_event_loop ;
71177
72178/* Forward declaration */
@@ -2089,29 +2195,48 @@ ERL_NIF_TERM nif_set_udp_broadcast(ErlNifEnv *env, int argc,
20892195/**
20902196 * Initialize the global Python event loop.
20912197 * Called from Erlang when setting up the event loop for Python use.
2198+ * This function expects to be called when the GIL is held (during py_init).
20922199 */
20932200int py_event_loop_init_python (ErlNifEnv * env , erlang_event_loop_t * loop ) {
20942201 (void )env ;
20952202 g_python_event_loop = loop ;
2203+
2204+ /* Store in per-interpreter storage if GIL is held */
2205+ if (PyGILState_Check ()) {
2206+ set_interpreter_event_loop (loop );
2207+ }
2208+
20962209 return 0 ;
20972210}
20982211
20992212/**
21002213 * NIF to set the global Python event loop.
21012214 * Called from Erlang: py_nif:set_python_event_loop(LoopRef)
2215+ *
2216+ * Stores the event loop both in the global C variable (for fast access)
2217+ * and in the Python module attribute (for sub-interpreter support).
21022218 */
21032219ERL_NIF_TERM nif_set_python_event_loop (ErlNifEnv * env , int argc ,
21042220 const ERL_NIF_TERM argv []) {
21052221 (void )argc ;
2106- (void )env ;
21072222
21082223 erlang_event_loop_t * loop ;
21092224 if (!enif_get_resource (env , argv [0 ], EVENT_LOOP_RESOURCE_TYPE , (void * * )& loop )) {
21102225 return make_error (env , "invalid_event_loop" );
21112226 }
21122227
2228+ /* Set global C variable for fast access */
21132229 g_python_event_loop = loop ;
21142230
2231+ /* Also store in Python module for sub-interpreter support */
2232+ gil_guard_t guard = gil_acquire ();
2233+ int result = set_interpreter_event_loop (loop );
2234+ gil_release (guard );
2235+
2236+ if (result < 0 ) {
2237+ return make_error (env , "failed_to_set_event_loop" );
2238+ }
2239+
21152240 return ATOM_OK ;
21162241}
21172242
@@ -2703,6 +2828,11 @@ int create_default_event_loop(ErlNifEnv *env) {
27032828 /* Set as global Python event loop */
27042829 g_python_event_loop = loop ;
27052830
2831+ /* Store in per-interpreter storage if GIL is held */
2832+ if (PyGILState_Check ()) {
2833+ set_interpreter_event_loop (loop );
2834+ }
2835+
27062836 /* Keep a reference to prevent garbage collection */
27072837 /* Note: This loop will be replaced when py_event_loop:init runs */
27082838
0 commit comments