Current section

Files

Jump to
erlang_python c_src py_subinterp_thread.c
Raw

c_src/py_subinterp_thread.c

/*
* Copyright 2026 Benoit Chesneau
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @file py_subinterp_thread.c
* @brief OWN_GIL subinterpreter thread pool implementation
* @author Benoit Chesneau
*
* Implements a pthread pool where each thread owns a Python subinterpreter
* with OWN_GIL for true parallelism.
*/
#include "py_subinterp_thread.h"
#include "py_nif.h"
#include "py_buffer.h"
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#ifdef HAVE_SUBINTERPRETERS
/* ============================================================================
* Global State
* ============================================================================ */
/** @brief Global thread pool instance */
subinterp_thread_pool_t g_thread_pool = {0};
/* Forward declarations */
static void *worker_thread_main(void *arg);
static int worker_create_namespace(subinterp_thread_worker_t *w, uint64_t handle_id);
static void worker_destroy_namespace(subinterp_thread_worker_t *w, uint64_t handle_id);
static subinterp_namespace_t *worker_find_namespace(subinterp_thread_worker_t *w, uint64_t handle_id);
static int write_full(int fd, const void *buf, size_t count);
static int read_full(int fd, void *buf, size_t count);
/* Defined in py_callback.c */
extern int create_erlang_module(void);
/* ============================================================================
* Pool Management
* ============================================================================ */
int subinterp_thread_pool_init(int num_workers) {
if (atomic_load(&g_thread_pool.initialized)) {
return 0; /* Already initialized */
}
/* Set default/cap worker count */
if (num_workers <= 0) {
num_workers = SUBINTERP_THREAD_POOL_DEFAULT;
}
if (num_workers > SUBINTERP_THREAD_POOL_MAX) {
num_workers = SUBINTERP_THREAD_POOL_MAX;
}
/* Initialize pool state */
memset(&g_thread_pool, 0, sizeof(g_thread_pool));
g_thread_pool.num_workers = num_workers;
atomic_store(&g_thread_pool.next_worker, 0);
atomic_store(&g_thread_pool.next_handle_id, 1);
atomic_store(&g_thread_pool.next_request_id, 1);
/* Create workers - declare i outside loop for cleanup_workers label */
int i;
for (i = 0; i < num_workers; i++) {
subinterp_thread_worker_t *w = &g_thread_pool.workers[i];
w->worker_id = i;
atomic_store(&w->running, false);
atomic_store(&w->shutdown_requested, false);
atomic_store(&w->initialized, false);
atomic_store(&w->requests_processed, 0);
atomic_store(&w->errors_count, 0);
w->num_namespaces = 0;
/* Initialize mutexes */
if (pthread_mutex_init(&w->dispatch_mutex, NULL) != 0) {
fprintf(stderr, "subinterp_thread_pool_init: failed to init dispatch_mutex for worker %d\n", i);
goto cleanup_workers;
}
if (pthread_mutex_init(&w->ns_mutex, NULL) != 0) {
fprintf(stderr, "subinterp_thread_pool_init: failed to init ns_mutex for worker %d\n", i);
pthread_mutex_destroy(&w->dispatch_mutex);
goto cleanup_workers;
}
/* Create pipes */
if (pipe(w->cmd_pipe) < 0) {
fprintf(stderr, "subinterp_thread_pool_init: failed to create cmd_pipe for worker %d: %s\n",
i, strerror(errno));
pthread_mutex_destroy(&w->dispatch_mutex);
pthread_mutex_destroy(&w->ns_mutex);
goto cleanup_workers;
}
/* Non-blocking cmd_pipe write end so the dispatch NIFs can bound their
* writes with write_all_with_deadline (no dirty-scheduler stall on a full
* pipe). The worker thread reads cmd_pipe[0] blocking, which is fine. */
{
int wfl = fcntl(w->cmd_pipe[1], F_GETFL, 0);
if (wfl >= 0) (void)fcntl(w->cmd_pipe[1], F_SETFL, wfl | O_NONBLOCK);
}
if (pipe(w->result_pipe) < 0) {
fprintf(stderr, "subinterp_thread_pool_init: failed to create result_pipe for worker %d: %s\n",
i, strerror(errno));
close(w->cmd_pipe[0]);
close(w->cmd_pipe[1]);
pthread_mutex_destroy(&w->dispatch_mutex);
pthread_mutex_destroy(&w->ns_mutex);
goto cleanup_workers;
}
/* Set non-blocking on read ends for proper timeout handling */
/* Actually, keep blocking for simplicity - we control timeouts via protocol */
/* Start worker thread */
if (pthread_create(&w->thread, NULL, worker_thread_main, w) != 0) {
fprintf(stderr, "subinterp_thread_pool_init: failed to create thread for worker %d: %s\n",
i, strerror(errno));
close(w->cmd_pipe[0]);
close(w->cmd_pipe[1]);
close(w->result_pipe[0]);
close(w->result_pipe[1]);
pthread_mutex_destroy(&w->dispatch_mutex);
pthread_mutex_destroy(&w->ns_mutex);
goto cleanup_workers;
}
/* Wait for worker to initialize */
int timeout_ms = 5000; /* 5 second timeout */
int waited = 0;
while (!atomic_load(&w->initialized) && waited < timeout_ms) {
usleep(10000); /* 10ms */
waited += 10;
}
if (!atomic_load(&w->initialized)) {
fprintf(stderr, "subinterp_thread_pool_init: worker %d failed to initialize\n", i);
atomic_store(&w->shutdown_requested, true);
/* Write shutdown message to unblock worker if stuck */
owngil_header_t shutdown = {
.magic = OWNGIL_MAGIC,
.version = OWNGIL_PROTOCOL_VERSION,
.msg_type = MSG_REQUEST,
.req_type = REQ_SHUTDOWN,
.payload_len = 0,
};
write(w->cmd_pipe[1], &shutdown, sizeof(shutdown));
pthread_join(w->thread, NULL);
close(w->cmd_pipe[0]);
close(w->cmd_pipe[1]);
close(w->result_pipe[0]);
close(w->result_pipe[1]);
pthread_mutex_destroy(&w->dispatch_mutex);
pthread_mutex_destroy(&w->ns_mutex);
goto cleanup_workers;
}
}
atomic_store(&g_thread_pool.initialized, true);
#ifdef DEBUG
fprintf(stderr, "subinterp_thread_pool_init: created %d OWN_GIL workers\n", num_workers);
#endif
return 0;
cleanup_workers:
/* Clean up already created workers */
for (int j = 0; j < i; j++) {
subinterp_thread_worker_t *w = &g_thread_pool.workers[j];
atomic_store(&w->shutdown_requested, true);
owngil_header_t shutdown = {
.magic = OWNGIL_MAGIC,
.version = OWNGIL_PROTOCOL_VERSION,
.msg_type = MSG_REQUEST,
.req_type = REQ_SHUTDOWN,
.payload_len = 0,
};
write(w->cmd_pipe[1], &shutdown, sizeof(shutdown));
pthread_join(w->thread, NULL);
close(w->cmd_pipe[0]);
close(w->cmd_pipe[1]);
close(w->result_pipe[0]);
close(w->result_pipe[1]);
pthread_mutex_destroy(&w->dispatch_mutex);
pthread_mutex_destroy(&w->ns_mutex);
}
return -1;
}
void subinterp_thread_pool_shutdown(void) {
if (!atomic_load(&g_thread_pool.initialized)) {
return;
}
/* Mark as not initialized to prevent new work */
atomic_store(&g_thread_pool.initialized, false);
/* Signal all workers to shut down */
for (int i = 0; i < g_thread_pool.num_workers; i++) {
subinterp_thread_worker_t *w = &g_thread_pool.workers[i];
if (!atomic_load(&w->running)) {
continue;
}
atomic_store(&w->shutdown_requested, true);
/* Send shutdown message */
owngil_header_t shutdown = {
.magic = OWNGIL_MAGIC,
.version = OWNGIL_PROTOCOL_VERSION,
.msg_type = MSG_REQUEST,
.req_type = REQ_SHUTDOWN,
.payload_len = 0,
};
write_full(w->cmd_pipe[1], &shutdown, sizeof(shutdown));
}
/* Wait for all workers to exit */
for (int i = 0; i < g_thread_pool.num_workers; i++) {
subinterp_thread_worker_t *w = &g_thread_pool.workers[i];
if (w->thread != 0) {
pthread_join(w->thread, NULL);
}
/* Close pipes */
if (w->cmd_pipe[0] >= 0) close(w->cmd_pipe[0]);
if (w->cmd_pipe[1] >= 0) close(w->cmd_pipe[1]);
if (w->result_pipe[0] >= 0) close(w->result_pipe[0]);
if (w->result_pipe[1] >= 0) close(w->result_pipe[1]);
/* Destroy mutexes */
pthread_mutex_destroy(&w->dispatch_mutex);
pthread_mutex_destroy(&w->ns_mutex);
}
g_thread_pool.num_workers = 0;
#ifdef DEBUG
fprintf(stderr, "subinterp_thread_pool_shutdown: complete\n");
#endif
}
bool subinterp_thread_pool_is_ready(void) {
return atomic_load(&g_thread_pool.initialized);
}
void subinterp_thread_pool_stats(int *num_workers, uint64_t *total_requests,
uint64_t *total_errors) {
if (num_workers) *num_workers = g_thread_pool.num_workers;
uint64_t reqs = 0, errs = 0;
for (int i = 0; i < g_thread_pool.num_workers; i++) {
reqs += atomic_load(&g_thread_pool.workers[i].requests_processed);
errs += atomic_load(&g_thread_pool.workers[i].errors_count);
}
if (total_requests) *total_requests = reqs;
if (total_errors) *total_errors = errs;
}
/* ============================================================================
* Worker Thread Main Loop
* ============================================================================ */
static void *worker_thread_main(void *arg) {
subinterp_thread_worker_t *w = (subinterp_thread_worker_t *)arg;
/* Create OWN_GIL subinterpreter.
* For OWN_GIL, we need the main GIL to create the subinterpreter,
* then the subinterpreter gets its own GIL. After creation,
* we're switched to the new subinterpreter's thread state. */
PyInterpreterConfig config = {
.use_main_obmalloc = 0,
.allow_fork = 0,
.allow_exec = 0,
.allow_threads = 1,
.allow_daemon_threads = 0,
.check_multi_interp_extensions = 1,
.gil = PyInterpreterConfig_OWN_GIL,
};
/* Acquire main GIL to create subinterpreter */
PyGILState_STATE gstate = PyGILState_Ensure();
/* Save main thread state before creating subinterpreter */
PyThreadState *main_tstate = PyThreadState_Get();
PyStatus status = Py_NewInterpreterFromConfig(&w->tstate, &config);
if (PyStatus_Exception(status) || w->tstate == NULL) {
fprintf(stderr, "worker %d: failed to create OWN_GIL subinterpreter\n", w->worker_id);
/* Restore main thread state and release */
PyThreadState_Swap(main_tstate);
PyGILState_Release(gstate);
return NULL;
}
/* Now we're in the new subinterpreter's thread state with its own GIL.
* The main GIL was released when Py_NewInterpreterFromConfig switched to OWN_GIL. */
w->interp = PyThreadState_GetInterpreter(w->tstate);
/* Create erlang module in this subinterpreter */
if (create_erlang_module() < 0) {
fprintf(stderr, "worker %d: failed to create erlang module\n", w->worker_id);
PyErr_Clear();
/* Continue without erlang module - callbacks won't work */
} else {
/* Register PyBuffer with erlang module in this subinterpreter */
if (PyBuffer_register_with_module() < 0) {
PyErr_Clear();
/* Non-fatal - PyBuffer just won't be available */
}
}
/* Initialize asyncio for this worker */
w->asyncio_module = PyImport_ImportModule("asyncio");
if (w->asyncio_module == NULL) {
fprintf(stderr, "worker %d: failed to import asyncio\n", w->worker_id);
PyErr_Clear();
} else {
/* Create a new event loop for this worker */
PyObject *new_event_loop = PyObject_CallMethod(w->asyncio_module,
"new_event_loop", NULL);
if (new_event_loop == NULL) {
fprintf(stderr, "worker %d: failed to create asyncio event loop\n", w->worker_id);
PyErr_Clear();
} else {
w->asyncio_loop = new_event_loop;
/* Set as the running event loop for this thread */
PyObject *result = PyObject_CallMethod(w->asyncio_module,
"set_event_loop", "O", w->asyncio_loop);
Py_XDECREF(result);
PyErr_Clear();
}
}
/* Release the subinterpreter's GIL (we'll acquire it per-request) */
PyEval_SaveThread();
/* Signal that we're initialized */
atomic_store(&w->running, true);
atomic_store(&w->initialized, true);
/* Main command loop */
while (!atomic_load(&w->shutdown_requested)) {
owngil_header_t header;
int n = read_full(w->cmd_pipe[0], &header, sizeof(header));
if (n <= 0) {
if (errno == EINTR) continue;
break; /* Pipe closed or error */
}
/* Validate header */
if (header.magic != OWNGIL_MAGIC || header.version != OWNGIL_PROTOCOL_VERSION) {
fprintf(stderr, "worker %d: invalid protocol header\n", w->worker_id);
continue;
}
/* Handle shutdown */
if (header.req_type == REQ_SHUTDOWN) {
break;
}
/* Read payload if present */
unsigned char *payload = NULL;
if (header.payload_len > 0) {
payload = malloc(header.payload_len);
if (payload == NULL) {
fprintf(stderr, "worker %d: failed to allocate payload\n", w->worker_id);
continue;
}
n = read_full(w->cmd_pipe[0], payload, header.payload_len);
if (n != (int)header.payload_len) {
fprintf(stderr, "worker %d: failed to read payload\n", w->worker_id);
free(payload);
continue;
}
}
/* Acquire our GIL for Python execution */
PyEval_RestoreThread(w->tstate);
/* Handle namespace management (needs GIL for Python dict operations) */
if (header.req_type == REQ_CREATE_NS) {
worker_create_namespace(w, header.handle_id);
PyEval_SaveThread();
/* Send simple OK response */
owngil_header_t resp = {
.magic = OWNGIL_MAGIC,
.version = OWNGIL_PROTOCOL_VERSION,
.msg_type = MSG_RESPONSE,
.request_id = header.request_id,
.payload_len = 0,
};
write_full(w->result_pipe[1], &resp, sizeof(resp));
free(payload);
continue;
}
if (header.req_type == REQ_DESTROY_NS) {
worker_destroy_namespace(w, header.handle_id);
PyEval_SaveThread();
/* Send simple OK response */
owngil_header_t resp = {
.magic = OWNGIL_MAGIC,
.version = OWNGIL_PROTOCOL_VERSION,
.msg_type = MSG_RESPONSE,
.request_id = header.request_id,
.payload_len = 0,
};
write_full(w->result_pipe[1], &resp, sizeof(resp));
free(payload);
continue;
}
/* Handle apply imports - imports modules into sys.modules */
if (header.req_type == REQ_APPLY_IMPORTS) {
/* Payload is ETF list of {ModuleBin, FuncBin | all} tuples */
if (payload != NULL && header.payload_len > 0) {
ErlNifEnv *tmp_env = enif_alloc_env();
if (tmp_env != NULL) {
ERL_NIF_TERM imports_list;
if (enif_binary_to_term(tmp_env, payload, header.payload_len,
&imports_list, ERL_NIF_BIN2TERM_SAFE) != 0) {
ERL_NIF_TERM head, tail = imports_list;
int arity;
const ERL_NIF_TERM *tuple;
while (enif_get_list_cell(tmp_env, tail, &head, &tail)) {
if (enif_get_tuple(tmp_env, head, &arity, &tuple) && arity == 2) {
ErlNifBinary module_bin;
if (enif_inspect_binary(tmp_env, tuple[0], &module_bin)) {
char *module_name = enif_alloc(module_bin.size + 1);
if (module_name != NULL) {
memcpy(module_name, module_bin.data, module_bin.size);
module_name[module_bin.size] = '\0';
/* Skip __main__ */
if (strcmp(module_name, "__main__") != 0) {
PyObject *mod = PyImport_ImportModule(module_name);
if (mod != NULL) {
Py_DECREF(mod);
} else {
PyErr_Clear();
}
}
enif_free(module_name);
}
}
}
}
}
enif_free_env(tmp_env);
}
}
PyEval_SaveThread();
/* Send OK response */
owngil_header_t resp = {
.magic = OWNGIL_MAGIC,
.version = OWNGIL_PROTOCOL_VERSION,
.msg_type = MSG_RESPONSE,
.request_id = header.request_id,
.payload_len = 0,
};
write_full(w->result_pipe[1], &resp, sizeof(resp));
free(payload);
continue;
}
/* Handle apply paths - add paths to sys.path */
if (header.req_type == REQ_APPLY_PATHS) {
/* Payload is ETF list of path binaries */
if (payload != NULL && header.payload_len > 0) {
ErlNifEnv *tmp_env = enif_alloc_env();
if (tmp_env != NULL) {
ERL_NIF_TERM paths_list;
if (enif_binary_to_term(tmp_env, payload, header.payload_len,
&paths_list, ERL_NIF_BIN2TERM_SAFE) != 0) {
PyObject *sys_path = PySys_GetObject("path");
if (sys_path != NULL && PyList_Check(sys_path)) {
ERL_NIF_TERM head, tail = paths_list;
/* Insert in reverse order so first path ends up first */
while (enif_get_list_cell(tmp_env, tail, &head, &tail)) {
ErlNifBinary path_bin;
if (enif_inspect_binary(tmp_env, head, &path_bin)) {
PyObject *path_str = PyUnicode_FromStringAndSize(
(const char *)path_bin.data, path_bin.size);
if (path_str != NULL) {
/* Check if path already in sys.path */
int contains = PySequence_Contains(sys_path, path_str);
if (contains == 0) {
PyList_Insert(sys_path, 0, path_str);
}
Py_DECREF(path_str);
}
}
}
}
}
enif_free_env(tmp_env);
}
}
PyEval_SaveThread();
/* Send OK response */
owngil_header_t resp = {
.magic = OWNGIL_MAGIC,
.version = OWNGIL_PROTOCOL_VERSION,
.msg_type = MSG_RESPONSE,
.request_id = header.request_id,
.payload_len = 0,
};
write_full(w->result_pipe[1], &resp, sizeof(resp));
free(payload);
continue;
}
/* Find namespace for this handle */
subinterp_namespace_t *ns = worker_find_namespace(w, header.handle_id);
if (ns == NULL) {
/* Namespace not found - create default one on the fly */
worker_create_namespace(w, header.handle_id);
ns = worker_find_namespace(w, header.handle_id);
}
/* Process the request */
owngil_header_t resp_header = {
.magic = OWNGIL_MAGIC,
.version = OWNGIL_PROTOCOL_VERSION,
.msg_type = MSG_RESPONSE,
.request_id = header.request_id,
.payload_len = 0,
};
unsigned char *resp_payload = NULL;
size_t resp_payload_len = 0;
/* Decode payload using temporary env */
ErlNifEnv *tmp_env = enif_alloc_env();
ERL_NIF_TERM payload_term;
int arity;
const ERL_NIF_TERM *elements;
bool success = false;
if (tmp_env != NULL && header.payload_len > 0) {
if (enif_binary_to_term(tmp_env, payload, header.payload_len,
&payload_term, ERL_NIF_BIN2TERM_SAFE) != 0) {
if (enif_get_tuple(tmp_env, payload_term, &arity, &elements)) {
/* Execute based on request type */
PyObject *result = NULL;
PyObject *globals = ns ? ns->globals : PyDict_New();
PyObject *locals = ns ? ns->locals : PyDict_New();
bool owns_globals = (ns == NULL);
bool owns_locals = (ns == NULL);
/* Check allocation if we own the dicts */
if ((owns_globals && globals == NULL) || (owns_locals && locals == NULL)) {
if (owns_globals) Py_XDECREF(globals);
if (owns_locals) Py_XDECREF(locals);
/* Per-request dict allocation failure: respond with an
* error and keep serving. This previously `break`ed the
* worker command loop, permanently killing the thread (and
* leaving the GIL held), wedging every session routed to it. */
resp_header.msg_type = MSG_ERROR;
goto send_response;
}
switch (header.req_type) {
case REQ_CALL:
case REQ_CAST: {
/* Payload: {Module, Func, Args, Kwargs} */
if (arity >= 3) {
ErlNifBinary mod_bin, func_bin;
char mod_str[256], func_str[256];
/* Get module name */
if (enif_inspect_binary(tmp_env, elements[0], &mod_bin)) {
size_t len = mod_bin.size < 255 ? mod_bin.size : 255;
memcpy(mod_str, mod_bin.data, len);
mod_str[len] = '\0';
} else if (enif_get_atom(tmp_env, elements[0], mod_str, 256, ERL_NIF_LATIN1)) {
/* Already filled */
} else {
if (owns_globals) Py_DECREF(globals);
if (owns_locals) Py_DECREF(locals);
break;
}
/* Get function name */
if (enif_inspect_binary(tmp_env, elements[1], &func_bin)) {
size_t len = func_bin.size < 255 ? func_bin.size : 255;
memcpy(func_str, func_bin.data, len);
func_str[len] = '\0';
} else if (enif_get_atom(tmp_env, elements[1], func_str, 256, ERL_NIF_LATIN1)) {
/* Already filled */
} else {
if (owns_globals) Py_DECREF(globals);
if (owns_locals) Py_DECREF(locals);
break;
}
/* Import module */
PyObject *module = NULL;
if (ns && ns->module_cache) {
PyObject *key = PyUnicode_FromString(mod_str);
module = PyDict_GetItem(ns->module_cache, key);
if (module == NULL) {
module = PyImport_ImportModule(mod_str);
if (module) {
PyDict_SetItem(ns->module_cache, key, module);
}
} else {
Py_INCREF(module);
}
Py_DECREF(key);
} else {
module = PyImport_ImportModule(mod_str);
}
if (module == NULL) {
PyErr_Clear();
if (owns_globals) Py_DECREF(globals);
if (owns_locals) Py_DECREF(locals);
break;
}
/* Get function */
PyObject *func = PyObject_GetAttrString(module, func_str);
Py_DECREF(module);
if (func == NULL) {
PyErr_Clear();
if (owns_globals) Py_DECREF(globals);
if (owns_locals) Py_DECREF(locals);
break;
}
/* Convert args list to Python tuple */
ERL_NIF_TERM args_list = elements[2];
unsigned int args_len;
PyObject *py_args = NULL;
if (enif_get_list_length(tmp_env, args_list, &args_len)) {
py_args = PyTuple_New(args_len);
if (py_args) {
ERL_NIF_TERM head, tail = args_list;
for (unsigned int idx = 0; idx < args_len; idx++) {
if (!enif_get_list_cell(tmp_env, tail, &head, &tail)) {
Py_DECREF(py_args);
py_args = NULL;
break;
}
PyObject *py_arg = term_to_py(tmp_env, head);
if (py_arg == NULL) {
Py_DECREF(py_args);
py_args = NULL;
break;
}
PyTuple_SET_ITEM(py_args, idx, py_arg);
}
}
}
if (py_args == NULL) {
py_args = PyTuple_New(0);
}
/* Call function */
result = PyObject_Call(func, py_args, NULL);
Py_DECREF(py_args);
Py_DECREF(func);
if (result == NULL) {
PyErr_Clear();
} else {
success = true;
}
}
break;
}
case REQ_ASYNC_CALL: {
/* Payload: {Module, Func, Args, Kwargs, CallerPid, Ref} */
/* For async calls, we run the coroutine and send result via erlang.send() */
if (arity >= 6) {
ErlNifBinary mod_bin, func_bin;
char mod_str[256], func_str[256];
/* Get module name */
if (enif_inspect_binary(tmp_env, elements[0], &mod_bin)) {
size_t len = mod_bin.size < 255 ? mod_bin.size : 255;
memcpy(mod_str, mod_bin.data, len);
mod_str[len] = '\0';
} else if (enif_get_atom(tmp_env, elements[0], mod_str, 256, ERL_NIF_LATIN1)) {
/* Already filled */
} else {
if (owns_globals) Py_DECREF(globals);
if (owns_locals) Py_DECREF(locals);
break;
}
/* Get function name */
if (enif_inspect_binary(tmp_env, elements[1], &func_bin)) {
size_t len = func_bin.size < 255 ? func_bin.size : 255;
memcpy(func_str, func_bin.data, len);
func_str[len] = '\0';
} else if (enif_get_atom(tmp_env, elements[1], func_str, 256, ERL_NIF_LATIN1)) {
/* Already filled */
} else {
if (owns_globals) Py_DECREF(globals);
if (owns_locals) Py_DECREF(locals);
break;
}
/* Import module */
PyObject *module = NULL;
if (ns && ns->module_cache) {
PyObject *key = PyUnicode_FromString(mod_str);
module = PyDict_GetItem(ns->module_cache, key);
if (module == NULL) {
module = PyImport_ImportModule(mod_str);
if (module) {
PyDict_SetItem(ns->module_cache, key, module);
}
} else {
Py_INCREF(module);
}
Py_DECREF(key);
} else {
module = PyImport_ImportModule(mod_str);
}
if (module == NULL) {
PyErr_Clear();
if (owns_globals) Py_DECREF(globals);
if (owns_locals) Py_DECREF(locals);
break;
}
/* Get function */
PyObject *func = PyObject_GetAttrString(module, func_str);
Py_DECREF(module);
if (func == NULL) {
PyErr_Clear();
if (owns_globals) Py_DECREF(globals);
if (owns_locals) Py_DECREF(locals);
break;
}
/* Convert args list to Python tuple */
ERL_NIF_TERM args_list = elements[2];
unsigned int args_len;
PyObject *py_args = NULL;
if (enif_get_list_length(tmp_env, args_list, &args_len)) {
py_args = PyTuple_New(args_len);
if (py_args) {
ERL_NIF_TERM head, tail = args_list;
for (unsigned int idx = 0; idx < args_len; idx++) {
if (!enif_get_list_cell(tmp_env, tail, &head, &tail)) {
Py_DECREF(py_args);
py_args = NULL;
break;
}
PyObject *py_arg = term_to_py(tmp_env, head);
if (py_arg == NULL) {
Py_DECREF(py_args);
py_args = NULL;
break;
}
PyTuple_SET_ITEM(py_args, idx, py_arg);
}
}
}
if (py_args == NULL) {
py_args = PyTuple_New(0);
}
/* Call function */
result = PyObject_Call(func, py_args, NULL);
Py_DECREF(py_args);
Py_DECREF(func);
if (result == NULL) {
PyErr_Clear();
} else {
/* Check if result is a coroutine and run it */
if (w->asyncio_loop != NULL && PyCoro_CheckExact(result)) {
PyObject *final_result = PyObject_CallMethod(
w->asyncio_loop, "run_until_complete", "O", result);
Py_DECREF(result);
result = final_result;
if (result == NULL) {
PyErr_Clear();
}
}
if (result != NULL) {
success = true;
}
}
/* Send result via erlang.send() to CallerPid */
/* elements[4] = CallerPid, elements[5] = Ref */
ERL_NIF_TERM result_term;
if (success && result != NULL) {
ERL_NIF_TERM py_result = py_to_term(tmp_env, result);
result_term = enif_make_tuple2(tmp_env,
enif_make_atom(tmp_env, "ok"), py_result);
} else {
result_term = enif_make_tuple2(tmp_env,
enif_make_atom(tmp_env, "error"),
enif_make_atom(tmp_env, "execution_failed"));
}
/* Build {async_result, Ref, Result} message */
ERL_NIF_TERM msg = enif_make_tuple3(tmp_env,
enif_make_atom(tmp_env, "async_result"),
elements[5], /* Ref */
result_term);
/* Get CallerPid and send */
ErlNifPid caller_pid;
if (enif_get_local_pid(tmp_env, elements[4], &caller_pid)) {
enif_send(NULL, &caller_pid, tmp_env, msg);
}
Py_XDECREF(result);
result = NULL; /* Don't process result in normal path */
success = false; /* Already handled */
}
if (owns_globals) Py_DECREF(globals);
if (owns_locals) Py_DECREF(locals);
break;
}
case REQ_EVAL: {
/* Payload: {Code, Locals} */
if (arity >= 1) {
ErlNifBinary code_bin;
if (enif_inspect_binary(tmp_env, elements[0], &code_bin)) {
char *code_str = malloc(code_bin.size + 1);
if (code_str) {
memcpy(code_str, code_bin.data, code_bin.size);
code_str[code_bin.size] = '\0';
result = PyRun_String(code_str, Py_eval_input, globals, locals);
free(code_str);
if (result == NULL) {
PyErr_Clear();
} else {
success = true;
}
}
}
}
break;
}
case REQ_EXEC: {
/* Payload: {Code} */
if (arity >= 1) {
ErlNifBinary code_bin;
if (enif_inspect_binary(tmp_env, elements[0], &code_bin)) {
char *code_str = malloc(code_bin.size + 1);
if (code_str) {
memcpy(code_str, code_bin.data, code_bin.size);
code_str[code_bin.size] = '\0';
result = PyRun_String(code_str, Py_file_input, globals, locals);
free(code_str);
if (result == NULL) {
PyErr_Clear();
} else {
Py_DECREF(result);
result = Py_None;
Py_INCREF(result);
success = true;
}
}
}
}
break;
}
default:
break;
}
/* Clean up owned dicts after switch completes */
if (owns_globals) Py_DECREF(globals);
if (owns_locals) Py_DECREF(locals);
/* Serialize result using py_to_term for full type support */
if (success && result != NULL) {
ERL_NIF_TERM result_term = py_to_term(tmp_env, result);
Py_XDECREF(result);
/* Wrap in {ok, Result} */
ERL_NIF_TERM ok_tuple = enif_make_tuple2(tmp_env,
enif_make_atom(tmp_env, "ok"), result_term);
/* Serialize to ETF */
ErlNifBinary etf_bin;
if (enif_term_to_binary(tmp_env, ok_tuple, &etf_bin)) {
resp_payload = malloc(etf_bin.size);
if (resp_payload) {
memcpy(resp_payload, etf_bin.data, etf_bin.size);
resp_payload_len = etf_bin.size;
}
enif_release_binary(&etf_bin);
}
} else {
resp_header.msg_type = MSG_ERROR;
/* Serialize error */
ERL_NIF_TERM err_tuple = enif_make_tuple2(tmp_env,
enif_make_atom(tmp_env, "error"),
enif_make_atom(tmp_env, "execution_failed"));
ErlNifBinary etf_bin;
if (enif_term_to_binary(tmp_env, err_tuple, &etf_bin)) {
resp_payload = malloc(etf_bin.size);
if (resp_payload) {
memcpy(resp_payload, etf_bin.data, etf_bin.size);
resp_payload_len = etf_bin.size;
}
enif_release_binary(&etf_bin);
}
}
}
}
}
send_response:
if (tmp_env) {
enif_free_env(tmp_env);
}
/* Release GIL */
PyEval_SaveThread();
/* Send response (except for cast) */
if (header.req_type != REQ_CAST) {
resp_header.payload_len = resp_payload_len;
write_full(w->result_pipe[1], &resp_header, sizeof(resp_header));
if (resp_payload_len > 0) {
write_full(w->result_pipe[1], resp_payload, resp_payload_len);
}
}
free(payload);
free(resp_payload);
atomic_fetch_add(&w->requests_processed, 1);
}
/* Cleanup */
PyEval_RestoreThread(w->tstate);
/* Clean up all namespaces */
pthread_mutex_lock(&w->ns_mutex);
for (int i = 0; i < w->num_namespaces; i++) {
subinterp_namespace_t *ns = &w->namespaces[i];
if (ns->initialized) {
Py_XDECREF(ns->asyncio_loop);
Py_XDECREF(ns->module_cache);
Py_XDECREF(ns->globals);
Py_XDECREF(ns->locals);
}
}
w->num_namespaces = 0;
pthread_mutex_unlock(&w->ns_mutex);
/* Clean up worker asyncio resources */
Py_XDECREF(w->asyncio_loop);
w->asyncio_loop = NULL;
Py_XDECREF(w->asyncio_module);
w->asyncio_module = NULL;
/* End interpreter */
Py_EndInterpreter(w->tstate);
w->tstate = NULL;
w->interp = NULL;
atomic_store(&w->running, false);
return NULL;
}
/* ============================================================================
* Namespace Management
* ============================================================================ */
static int worker_create_namespace(subinterp_thread_worker_t *w, uint64_t handle_id) {
pthread_mutex_lock(&w->ns_mutex);
/* Check if namespace already exists */
for (int i = 0; i < w->num_namespaces; i++) {
if (w->namespaces[i].handle_id == handle_id && w->namespaces[i].initialized) {
pthread_mutex_unlock(&w->ns_mutex);
return 0; /* Already exists */
}
}
/* Find empty slot or add new */
int slot = -1;
for (int i = 0; i < w->num_namespaces; i++) {
if (!w->namespaces[i].initialized) {
slot = i;
break;
}
}
if (slot < 0) {
if (w->num_namespaces >= MAX_NAMESPACES_PER_WORKER) {
pthread_mutex_unlock(&w->ns_mutex);
return -1; /* Full */
}
slot = w->num_namespaces++;
}
subinterp_namespace_t *ns = &w->namespaces[slot];
ns->handle_id = handle_id;
ns->globals = PyDict_New();
ns->locals = PyDict_New();
ns->module_cache = PyDict_New();
ns->asyncio_loop = NULL; /* Uses worker's shared event loop */
memset(&ns->owner_pid, 0, sizeof(ns->owner_pid));
if (ns->globals && ns->locals && ns->module_cache) {
/* Import __builtins__ */
PyObject *builtins = PyEval_GetBuiltins();
PyDict_SetItemString(ns->globals, "__builtins__", builtins);
ns->initialized = true;
} else {
Py_XDECREF(ns->globals);
Py_XDECREF(ns->locals);
Py_XDECREF(ns->module_cache);
ns->globals = NULL;
ns->locals = NULL;
ns->module_cache = NULL;
pthread_mutex_unlock(&w->ns_mutex);
return -1;
}
pthread_mutex_unlock(&w->ns_mutex);
return 0;
}
static void worker_destroy_namespace(subinterp_thread_worker_t *w, uint64_t handle_id) {
/* Note: Caller must hold the GIL */
pthread_mutex_lock(&w->ns_mutex);
for (int i = 0; i < w->num_namespaces; i++) {
if (w->namespaces[i].handle_id == handle_id && w->namespaces[i].initialized) {
subinterp_namespace_t *ns = &w->namespaces[i];
Py_XDECREF(ns->module_cache);
Py_XDECREF(ns->globals);
Py_XDECREF(ns->locals);
ns->initialized = false;
ns->globals = NULL;
ns->locals = NULL;
ns->module_cache = NULL;
break;
}
}
pthread_mutex_unlock(&w->ns_mutex);
}
static subinterp_namespace_t *worker_find_namespace(subinterp_thread_worker_t *w, uint64_t handle_id) {
pthread_mutex_lock(&w->ns_mutex);
for (int i = 0; i < w->num_namespaces; i++) {
if (w->namespaces[i].handle_id == handle_id && w->namespaces[i].initialized) {
pthread_mutex_unlock(&w->ns_mutex);
return &w->namespaces[i];
}
}
pthread_mutex_unlock(&w->ns_mutex);
return NULL;
}
/* ============================================================================
* Utility Functions
* ============================================================================ */
static int write_full(int fd, const void *buf, size_t count) {
const unsigned char *p = buf;
size_t remaining = count;
while (remaining > 0) {
ssize_t n = write(fd, p, remaining);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
p += n;
remaining -= n;
}
return count;
}
static int read_full(int fd, void *buf, size_t count) {
unsigned char *p = buf;
size_t remaining = count;
while (remaining > 0) {
ssize_t n = read(fd, p, remaining);
if (n < 0) {
if (errno == EINTR) continue;
return -1;
}
if (n == 0) {
return count - remaining; /* EOF */
}
p += n;
remaining -= n;
}
return count;
}
#endif /* HAVE_SUBINTERPRETERS */