Skip to content

Commit d557289

Browse files
committed
Add safe GIL helpers for future use
- Add gil_acquire()/gil_release() helpers that check PyGILState_Check() before calling PyGILState_Ensure() to avoid double-acquisition - Add per-interpreter event loop storage infrastructure (not yet used) for future sub-interpreter support The per-interpreter storage functions are defined but not called yet. This commit only adds infrastructure without changing behavior.
1 parent 952ed3b commit d557289

File tree

2 files changed

+182
-1
lines changed

2 files changed

+182
-1
lines changed

c_src/py_event_loop.c

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,117 @@ ERL_NIF_TERM ATOM_CANCEL_TIMER;
6262
ERL_NIF_TERM ATOM_EVENT_LOOP;
6363
ERL_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 */
70176
static 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
*/
21032213
ERL_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;

c_src/py_nif.h

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,4 +1303,74 @@ static PyObject *thread_worker_call(const char *func_name, size_t func_name_len,
13031303

13041304
/** @} */
13051305

1306+
/* ============================================================================
1307+
* Safe GIL/Thread State Acquisition
1308+
* ============================================================================
1309+
*
1310+
* These helpers provide safe GIL acquisition that works across:
1311+
* - Python 3.9-3.11 (standard GIL)
1312+
* - Python 3.12-3.13 (stricter thread state checks)
1313+
* - Python 3.13t free-threaded (no GIL, but thread attachment required)
1314+
*
1315+
* The pattern follows PyO3's Python::attach() approach:
1316+
* 1. Check if already attached via PyGILState_Check()
1317+
* 2. If not, use PyGILState_Ensure() to attach
1318+
* 3. Track whether we acquired so we release correctly
1319+
*
1320+
* Per Python docs, PyGILState_Ensure/Release work in free-threaded builds
1321+
* to manage thread attachment even without a GIL.
1322+
*/
1323+
1324+
/**
1325+
* @defgroup gil_helpers Safe GIL/Thread State Helpers
1326+
* @brief Thread-safe GIL acquisition for Python 3.12+ compatibility
1327+
* @{
1328+
*/
1329+
1330+
/**
1331+
* @brief Guard structure for safe GIL acquisition/release
1332+
*/
1333+
typedef struct {
1334+
PyGILState_STATE gstate; /**< GIL state from PyGILState_Ensure */
1335+
int acquired; /**< 1 if we acquired, 0 if already held */
1336+
} gil_guard_t;
1337+
1338+
/**
1339+
* @brief Safely acquire the GIL/attach to Python runtime.
1340+
*
1341+
* This function is reentrant - if the current thread already holds the GIL
1342+
* (or is attached in free-threaded builds), it returns immediately without
1343+
* double-acquiring.
1344+
*
1345+
* @return Guard structure that must be passed to gil_release()
1346+
*/
1347+
static inline gil_guard_t gil_acquire(void) {
1348+
gil_guard_t guard = {.gstate = PyGILState_UNLOCKED, .acquired = 0};
1349+
1350+
/* Check if already attached to Python runtime */
1351+
if (PyGILState_Check()) {
1352+
return guard;
1353+
}
1354+
1355+
/* Attach to Python runtime (acquires GIL in GIL-enabled builds) */
1356+
guard.gstate = PyGILState_Ensure();
1357+
guard.acquired = 1;
1358+
return guard;
1359+
}
1360+
1361+
/**
1362+
* @brief Release the GIL/detach from Python runtime.
1363+
*
1364+
* Only releases if we actually acquired in gil_acquire().
1365+
*
1366+
* @param guard The guard structure returned by gil_acquire()
1367+
*/
1368+
static inline void gil_release(gil_guard_t guard) {
1369+
if (guard.acquired) {
1370+
PyGILState_Release(guard.gstate);
1371+
}
1372+
}
1373+
1374+
/** @} */
1375+
13061376
#endif /* PY_NIF_H */

0 commit comments

Comments
 (0)