@@ -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 */
@@ -2099,6 +2205,10 @@ int py_event_loop_init_python(ErlNifEnv *env, erlang_event_loop_t *loop) {
20992205/**
21002206 * NIF to set the global Python event loop.
21012207 * Called from Erlang: py_nif:set_python_event_loop(LoopRef)
2208+ *
2209+ * Only updates the global C variable. The per-interpreter storage
2210+ * is set during initialization when the GIL is already held.
2211+ * This avoids needing to acquire the GIL from an arbitrary Erlang thread.
21022212 */
21032213ERL_NIF_TERM nif_set_python_event_loop (ErlNifEnv * env , int argc ,
21042214 const ERL_NIF_TERM argv []) {
@@ -2110,6 +2220,7 @@ ERL_NIF_TERM nif_set_python_event_loop(ErlNifEnv *env, int argc,
21102220 return make_error (env , "invalid_event_loop" );
21112221 }
21122222
2223+ /* Set global C variable for fast access from C code */
21132224 g_python_event_loop = loop ;
21142225
21152226 return ATOM_OK ;
0 commit comments