Current section

Files

Jump to
erlang_python c_src py_nif.h
Raw

c_src/py_nif.h

/*
* 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_nif.h
* @brief Shared header for the Python-Erlang NIF integration
* @author Benoit Chesneau
*
* @mainpage Python-Erlang NIF Integration
*
* @section intro_sec Introduction
*
* This NIF (Native Implemented Function) library provides seamless integration
* between Erlang/OTP and Python. It embeds a Python interpreter within the
* Erlang VM and provides bidirectional communication capabilities.
*
* @section arch_sec Architecture
*
* The implementation follows a modular design with four main components:
*
* - **py_nif.h** - Shared types, macros, and declarations
* - **py_convert.c** - Bidirectional type conversion (Python ↔ Erlang)
* - **py_exec.c** - Python execution engine and GIL management
* - **py_callback.c** - Erlang callback support and asyncio integration
*
* @section modes_sec Execution Modes
*
* The library supports three execution modes based on Python version:
*
* | Mode | Python Version | Description |
* |------|----------------|-------------|
* | FREE_THREADED | 3.13+ (no-GIL) | Direct execution without GIL |
* | SUBINTERP | 3.12+ | Per-interpreter GIL isolation |
* | MULTI_EXECUTOR | Any | Multiple executor threads with GIL |
*
* @section gil_sec GIL Management
*
* The GIL (Global Interpreter Lock) is managed following PyO3/Granian patterns:
*
* - `Py_BEGIN_ALLOW_THREADS` / `Py_END_ALLOW_THREADS` around blocking ops
* - Executor threads hold the GIL and process queued requests
* - Dirty I/O schedulers are used for Python-calling NIFs
*
* @section callback_sec Callback Mechanism
*
* Python code can call back to Erlang using a suspension/resume pattern:
*
* 1. Python calls `erlang.call('func', args)`
* 2. NIF raises `SuspensionRequired` exception
* 3. Dirty scheduler is released, callback sent to Erlang
* 4. Erlang processes callback, calls `resume_callback/2`
* 5. Python execution resumes with cached result
*
* @section mem_sec Memory Management
*
* - Erlang resources wrap Python objects (prevent GC)
* - Thread-local storage for callback context
* - Proper cleanup in resource destructors
*/
#ifndef PY_NIF_H
#define PY_NIF_H
/**
* @defgroup includes Required Headers
* @brief System and library headers required for the NIF
* @{
*/
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <erl_nif.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdatomic.h>
#include <time.h>
#include <math.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
#include <dlfcn.h>
/** @brief Flag indicating dlopen with RTLD_GLOBAL is needed for Python extensions */
#define NEED_DLOPEN_GLOBAL 1
#endif
#include <sys/select.h>
/** @} */
/* ============================================================================
* Feature Detection Macros
* ============================================================================ */
/**
* @defgroup features Feature Detection
* @brief Compile-time feature detection based on Python version
* @{
*/
/**
* @def HAVE_SUBINTERPRETERS
* @brief Defined when Python 3.12+ sub-interpreter support is available
*
* Python 3.12 introduced per-interpreter GIL support, allowing true
* parallelism between sub-interpreters.
*/
#if PY_VERSION_HEX >= 0x030C0000
#define HAVE_SUBINTERPRETERS 1
#endif
/**
* @def HAVE_FREE_THREADED
* @brief Defined when Python 3.13+ free-threaded (no-GIL) build is detected
*
* Python 3.13 introduced experimental free-threaded builds where the GIL
* is completely disabled, allowing true multi-threaded Python execution.
*/
#if PY_VERSION_HEX >= 0x030D0000
#ifdef Py_GIL_DISABLED
#define HAVE_FREE_THREADED 1
#endif
#endif
/** @} */
/* ============================================================================
* Execution Mode
* ============================================================================ */
/**
* @defgroup exec_mode Execution Mode
* @brief Python execution mode selection
* @{
*/
/**
* @enum py_execution_mode_t
* @brief Execution mode for Python operations
*
* The execution mode determines how Python code is executed and how
* the GIL is managed. The mode is auto-detected at initialization based
* on the Python version and build configuration.
*/
typedef enum {
/**
* @brief Free-threaded mode (Python 3.13+ no-GIL build)
*
* In this mode, Python has no GIL and code executes directly
* without any GIL acquisition. This provides maximum parallelism.
*/
PY_MODE_FREE_THREADED,
/**
* @brief Sub-interpreter mode (Python 3.12+)
*
* Each sub-interpreter has its own GIL, allowing parallel execution
* across interpreters while maintaining GIL semantics within each.
*/
PY_MODE_SUBINTERP,
/**
* @brief Multi-executor mode (all Python versions)
*
* Multiple executor threads share the GIL using a work-stealing
* pattern. This is the fallback mode for older Python versions.
*/
PY_MODE_MULTI_EXECUTOR
} py_execution_mode_t;
/** @} */
/* ============================================================================
* Core Type Definitions
* ============================================================================ */
/**
* @defgroup types Core Types
* @brief Primary data structures for the NIF
* @{
*/
/**
* @struct py_worker_t
* @brief Represents a Python worker with its own namespace
*
* A worker encapsulates a Python execution context with isolated
* global and local namespaces. Workers are created per-process in
* Erlang and can execute Python code independently.
*
* @note Workers should be created via `py:worker_new/0` and destroyed
* via `py:worker_destroy/1` or automatically via GC.
*
* @see nif_worker_new
* @see nif_worker_destroy
*/
typedef struct {
/** @brief Python thread state for this worker */
PyThreadState *thread_state;
/** @brief Global namespace dictionary (`__globals__`) */
PyObject *globals;
/** @brief Local namespace dictionary (`__locals__`) */
PyObject *locals;
/** @brief Whether this worker currently owns the GIL */
bool owns_gil;
/* Callback support fields */
/**
* @brief Pipe file descriptors for callback IPC
*
* - `callback_pipe[0]` - Read end (Python reads responses)
* - `callback_pipe[1]` - Write end (Erlang writes responses)
*/
int callback_pipe[2];
/** @brief PID of the Erlang callback handler process */
ErlNifPid callback_handler;
/** @brief Whether a callback handler is registered */
bool has_callback_handler;
/** @brief Environment for building callback messages */
ErlNifEnv *callback_env;
} py_worker_t;
/**
* @struct async_pending_t
* @brief Represents a pending asynchronous Python operation
*
* Used to track asyncio coroutines submitted to the event loop.
* Forms a linked list for efficient queue management.
*/
typedef struct async_pending {
/** @brief Unique identifier for this async operation */
uint64_t id;
/** @brief Python Future object from `asyncio.run_coroutine_threadsafe` */
PyObject *future;
/** @brief PID of the Erlang process awaiting the result */
ErlNifPid caller;
/** @brief Next pending operation in the queue */
struct async_pending *next;
} async_pending_t;
/**
* @struct py_async_worker_t
* @brief Async worker managing an asyncio event loop
*
* Provides support for Python async/await operations by running
* an asyncio event loop in a dedicated background thread.
*
* @see nif_async_worker_new
* @see nif_async_call
*/
typedef struct {
/** @brief Background thread running the event loop */
pthread_t loop_thread;
/** @brief Python asyncio event loop object */
PyObject *event_loop;
/**
* @brief Notification pipe for waking the event loop
*
* - `notify_pipe[0]` - Read end (event loop monitors)
* - `notify_pipe[1]` - Write end (main thread signals)
*/
int notify_pipe[2];
/** @brief Flag indicating the event loop is running */
volatile bool loop_running;
/** @brief Flag to signal shutdown */
volatile bool shutdown;
/** @brief Mutex protecting the pending queue */
pthread_mutex_t queue_mutex;
/** @brief Head of pending operations queue */
async_pending_t *pending_head;
/** @brief Tail of pending operations queue */
async_pending_t *pending_tail;
/** @brief Environment for sending async result messages */
ErlNifEnv *msg_env;
} py_async_worker_t;
/**
* @struct py_object_t
* @brief Wrapper for Python objects preventing garbage collection
*
* Erlang resources wrapping Python objects to prevent them from
* being collected by Python's GC while Erlang holds a reference.
*
* @warning The wrapped object must be properly DECREF'd in the
* resource destructor.
*/
typedef struct {
/** @brief The wrapped Python object (owned reference) */
PyObject *obj;
} py_object_t;
/** @} */
/* ============================================================================
* Request Types and Structures
* ============================================================================ */
/**
* @defgroup requests Request Handling
* @brief Structures for executor request processing
* @{
*/
/**
* @enum py_request_type_t
* @brief Types of requests that can be submitted to the executor
*/
typedef enum {
PY_REQ_CALL, /**< Call a Python function */
PY_REQ_EVAL, /**< Evaluate a Python expression */
PY_REQ_EXEC, /**< Execute Python statements */
PY_REQ_NEXT, /**< Get next value from iterator/generator */
PY_REQ_IMPORT, /**< Import a Python module */
PY_REQ_GETATTR, /**< Get attribute from Python object */
PY_REQ_MEMORY_STATS, /**< Get Python memory statistics */
PY_REQ_GC, /**< Trigger Python garbage collection */
PY_REQ_SHUTDOWN /**< Signal executor shutdown */
} py_request_type_t;
/**
* @struct py_request_t
* @brief Request submitted to the executor thread for processing
*
* Encapsulates all information needed to execute a Python operation.
* The caller thread blocks on the condition variable until the
* executor signals completion.
*
* @note Requests are allocated on the stack by the caller NIF and
* passed to the executor. The executor processes them with
* the GIL held.
*/
typedef struct py_request {
/** @brief Type of operation to perform */
py_request_type_t type;
/* Synchronization primitives */
/** @brief Mutex for condition variable */
pthread_mutex_t mutex;
/** @brief Condition variable for completion signaling */
pthread_cond_t cond;
/** @brief Flag set when processing is complete */
volatile bool completed;
/* Common parameters */
/** @brief Worker context (may be NULL for global ops) */
py_worker_t *worker;
/** @brief Caller's NIF environment for term creation */
ErlNifEnv *env;
/* Call/Import parameters */
/** @brief Module name as binary */
ErlNifBinary module_bin;
/** @brief Function name as binary */
ErlNifBinary func_bin;
/** @brief Code string for eval/exec */
ErlNifBinary code_bin;
/** @brief Arguments list term */
ERL_NIF_TERM args_term;
/** @brief Keyword arguments map term */
ERL_NIF_TERM kwargs_term;
/** @brief Local variables map for eval */
ERL_NIF_TERM locals_term;
/** @brief Execution timeout in milliseconds (0 = no timeout) */
unsigned long timeout_ms;
/* Iterator parameters */
/** @brief Generator/iterator wrapper for PY_REQ_NEXT */
py_object_t *gen_wrapper;
/* Getattr parameters */
/** @brief Object wrapper for PY_REQ_GETATTR */
py_object_t *obj_wrapper;
/** @brief Attribute name as binary */
ErlNifBinary attr_bin;
/* GC parameters */
/** @brief Generation to collect (0, 1, or 2) */
int gc_generation;
/* Result */
/** @brief Result term set by executor */
ERL_NIF_TERM result;
/* Queue linkage */
/** @brief Next request in executor queue */
struct py_request *next;
} py_request_t;
/** @} */
/* ============================================================================
* Callback/Suspension State
* ============================================================================ */
/**
* @defgroup callback Callback Support
* @brief Structures for Python-to-Erlang callbacks
* @{
*/
/**
* @struct suspended_state_t
* @brief State for a suspended Python execution awaiting callback result
*
* When Python code calls `erlang.call()`, execution is suspended and
* this structure captures all state needed to resume after Erlang
* processes the callback.
*
* @par Suspension Flow:
* 1. Python calls `erlang.call('func', args)`
* 2. `erlang_call_impl` raises `SuspensionRequired` exception
* 3. `process_request` catches exception, creates `suspended_state_t`
* 4. Returns `{suspended, CallbackId, StateRef, {Func, Args}}` to Erlang
* 5. Erlang executes callback, calls `resume_callback(StateRef, Result)`
* 6. `nif_resume_callback_dirty` replays Python with cached result
*
* @see erlang_call_impl
* @see nif_resume_callback
*/
typedef struct {
/** @brief Worker context for replay */
py_worker_t *worker;
/** @brief Unique identifier for this callback */
uint64_t callback_id;
/* Callback invocation info */
/** @brief Name of Erlang function being called */
char *callback_func_name;
/** @brief Length of callback_func_name */
size_t callback_func_len;
/** @brief Arguments passed to the callback */
PyObject *callback_args;
/* Original request context for replay */
/** @brief Original module name binary */
ErlNifBinary orig_module;
/** @brief Original function name binary */
ErlNifBinary orig_func;
/** @brief Original arguments (copied to orig_env) */
ERL_NIF_TERM orig_args;
/** @brief Original keyword arguments */
ERL_NIF_TERM orig_kwargs;
/** @brief Environment owning copied terms */
ErlNifEnv *orig_env;
/** @brief Original timeout setting */
int orig_timeout_ms;
/** @brief Original request type (PY_REQ_CALL, PY_REQ_EVAL) */
int request_type;
/** @brief Original code for eval/exec replay */
ErlNifBinary orig_code;
/** @brief Original locals map for eval replay */
ERL_NIF_TERM orig_locals;
/* Callback result */
/** @brief Raw result data from Erlang callback */
unsigned char *result_data;
/** @brief Length of result_data */
size_t result_len;
/** @brief Flag: result is available for replay */
volatile bool has_result;
/** @brief Flag: result represents an error */
volatile bool is_error;
/* Synchronization */
/** @brief Mutex for result access */
pthread_mutex_t mutex;
/** @brief Condition for blocking callback mode */
pthread_cond_t cond;
} suspended_state_t;
/** @} */
/* ============================================================================
* Sub-interpreter Support
* ============================================================================ */
/**
* @defgroup subinterp Sub-interpreter Support
* @brief Structures for Python 3.12+ sub-interpreters
* @{
*/
#ifdef HAVE_SUBINTERPRETERS
/**
* @struct py_subinterp_worker_t
* @brief Worker running in an isolated sub-interpreter
*
* Sub-interpreters provide true isolation with their own GIL,
* enabling parallel Python execution on Python 3.12+.
*
* @note Only available when compiled with Python 3.12+
*
* @see nif_subinterp_worker_new
* @see nif_subinterp_call
*/
typedef struct {
/** @brief Python interpreter state */
PyInterpreterState *interp;
/** @brief Thread state for this interpreter */
PyThreadState *tstate;
/** @brief Global namespace dictionary */
PyObject *globals;
/** @brief Local namespace dictionary */
PyObject *locals;
} py_subinterp_worker_t;
#endif
/** @} */
/* ============================================================================
* Executor Pool
* ============================================================================ */
/**
* @defgroup executor Executor Pool
* @brief Multi-executor thread pool for GIL management
* @{
*/
/**
* @def MAX_EXECUTORS
* @brief Maximum number of executor threads in the pool
*/
#define MAX_EXECUTORS 16
/**
* @struct executor_t
* @brief Single executor thread in the multi-executor pool
*
* Each executor has its own request queue and processes requests
* independently. The GIL is acquired/released around queue operations.
*/
typedef struct {
/** @brief Executor thread handle */
pthread_t thread;
/** @brief Mutex protecting the request queue */
pthread_mutex_t mutex;
/** @brief Condition variable for queue signaling */
pthread_cond_t cond;
/** @brief Head of request queue */
struct py_request *queue_head;
/** @brief Tail of request queue */
struct py_request *queue_tail;
/** @brief Flag: executor is running */
volatile bool running;
/** @brief Flag: executor should shut down */
volatile bool shutdown;
/** @brief Executor ID (0 to MAX_EXECUTORS-1) */
int id;
} executor_t;
/** @} */
/* ============================================================================
* Global State Declarations
* ============================================================================ */
/**
* @defgroup globals Global State
* @brief External declarations for global variables
* @{
*/
/** @brief Resource type for py_worker_t */
extern ErlNifResourceType *WORKER_RESOURCE_TYPE;
/** @brief Resource type for py_object_t */
extern ErlNifResourceType *PYOBJ_RESOURCE_TYPE;
/** @brief Resource type for py_async_worker_t */
extern ErlNifResourceType *ASYNC_WORKER_RESOURCE_TYPE;
/** @brief Resource type for suspended_state_t */
extern ErlNifResourceType *SUSPENDED_STATE_RESOURCE_TYPE;
#ifdef HAVE_SUBINTERPRETERS
/** @brief Resource type for py_subinterp_worker_t */
extern ErlNifResourceType *SUBINTERP_WORKER_RESOURCE_TYPE;
#endif
/** @brief Flag: Python interpreter is initialized */
extern bool g_python_initialized;
/** @brief Main Python thread state (saved on init) */
extern PyThreadState *g_main_thread_state;
/** @brief Current execution mode */
extern py_execution_mode_t g_execution_mode;
/** @brief Number of active executors */
extern int g_num_executors;
/** @brief Multi-executor pool array */
extern executor_t g_executors[MAX_EXECUTORS];
/** @brief Round-robin counter for executor selection */
extern _Atomic int g_next_executor;
/** @brief Flag: multi-executor pool is initialized */
extern bool g_multi_executor_initialized;
/* Single executor state */
/** @brief Single executor thread handle */
extern pthread_t g_executor_thread;
/** @brief Single executor queue mutex */
extern pthread_mutex_t g_executor_mutex;
/** @brief Single executor queue condition */
extern pthread_cond_t g_executor_cond;
/** @brief Single executor queue head */
extern py_request_t *g_executor_queue_head;
/** @brief Single executor queue tail */
extern py_request_t *g_executor_queue_tail;
/** @brief Single executor running flag */
extern volatile bool g_executor_running;
/** @brief Single executor shutdown flag */
extern volatile bool g_executor_shutdown;
/** @brief Global counter for unique callback IDs */
extern _Atomic uint64_t g_callback_id_counter;
/** @brief Python exception class for suspension */
extern PyObject *SuspensionRequiredException;
/** @brief Cached numpy.ndarray type for fast isinstance checks (NULL if numpy unavailable) */
extern PyObject *g_numpy_ndarray_type;
/* Thread-local state */
/** @brief Current worker for callback context */
extern __thread py_worker_t *tl_current_worker;
/** @brief Current NIF environment for callbacks */
extern __thread ErlNifEnv *tl_callback_env;
/** @brief Current suspended state (for replay) */
extern __thread suspended_state_t *tl_current_suspended;
/** @brief Flag: suspension is allowed in current context */
extern __thread bool tl_allow_suspension;
/** @brief Flag: pending callback detected (checked before exception type) */
extern __thread bool tl_pending_callback;
extern __thread uint64_t tl_pending_callback_id;
extern __thread char *tl_pending_func_name;
extern __thread size_t tl_pending_func_name_len;
extern __thread PyObject *tl_pending_args;
/** @brief Timeout deadline (nanoseconds, monotonic clock) */
extern __thread uint64_t tl_timeout_deadline;
/* Logging/Tracing global state */
/** @brief PID of the Erlang process receiving log messages */
extern ErlNifPid g_log_receiver_pid;
/** @brief Flag: log receiver is registered */
extern volatile bool g_has_log_receiver;
/** @brief Minimum log level threshold (Python levelno) */
extern volatile int g_log_level_threshold;
/** @brief PID of the Erlang process receiving trace spans */
extern ErlNifPid g_trace_receiver_pid;
/** @brief Flag: trace receiver is registered */
extern volatile bool g_has_trace_receiver;
/** @brief Flag: timeout checking is enabled */
extern __thread bool tl_timeout_enabled;
/** @} */
/* ============================================================================
* Atom Declarations
* ============================================================================ */
/**
* @defgroup atoms Erlang Atoms
* @brief Pre-created atoms for efficient term construction
* @{
*/
extern ERL_NIF_TERM ATOM_OK; /**< @brief `ok` atom */
extern ERL_NIF_TERM ATOM_ERROR; /**< @brief `error` atom */
extern ERL_NIF_TERM ATOM_TRUE; /**< @brief `true` atom */
extern ERL_NIF_TERM ATOM_FALSE; /**< @brief `false` atom */
extern ERL_NIF_TERM ATOM_NONE; /**< @brief `none` atom (Python None) */
extern ERL_NIF_TERM ATOM_NIL; /**< @brief `nil` atom (Elixir nil) */
extern ERL_NIF_TERM ATOM_UNDEFINED; /**< @brief `undefined` atom */
extern ERL_NIF_TERM ATOM_NIF_NOT_LOADED; /**< @brief `nif_not_loaded` atom */
extern ERL_NIF_TERM ATOM_GENERATOR; /**< @brief `generator` atom */
extern ERL_NIF_TERM ATOM_STOP_ITERATION; /**< @brief `stop_iteration` atom */
extern ERL_NIF_TERM ATOM_TIMEOUT; /**< @brief `timeout` atom */
extern ERL_NIF_TERM ATOM_NAN; /**< @brief `nan` atom (float NaN) */
extern ERL_NIF_TERM ATOM_INFINITY; /**< @brief `infinity` atom */
extern ERL_NIF_TERM ATOM_NEG_INFINITY; /**< @brief `neg_infinity` atom */
extern ERL_NIF_TERM ATOM_ERLANG_CALLBACK;/**< @brief `erlang_callback` atom */
extern ERL_NIF_TERM ATOM_ASYNC_RESULT; /**< @brief `async_result` atom */
extern ERL_NIF_TERM ATOM_ASYNC_ERROR; /**< @brief `async_error` atom */
extern ERL_NIF_TERM ATOM_SUSPENDED; /**< @brief `suspended` atom */
/* Logging atoms */
extern ERL_NIF_TERM ATOM_PY_LOG; /**< @brief `py_log` atom */
extern ERL_NIF_TERM ATOM_SPAN_START; /**< @brief `span_start` atom */
extern ERL_NIF_TERM ATOM_SPAN_END; /**< @brief `span_end` atom */
extern ERL_NIF_TERM ATOM_SPAN_EVENT; /**< @brief `span_event` atom */
/** @} */
/* ============================================================================
* Type Conversion Functions (py_convert.c)
* ============================================================================ */
/**
* @defgroup convert Type Conversion
* @brief Functions for converting between Python and Erlang types
* @{
*/
/**
* @brief Convert a Python object to an Erlang term
*
* Recursively converts Python objects to their Erlang equivalents:
*
* | Python Type | Erlang Type |
* |-------------|-------------|
* | None | `none` atom |
* | True/False | `true`/`false` atoms |
* | int | integer |
* | float | float (with NaN/Inf handling) |
* | str | binary |
* | bytes | binary |
* | list | list |
* | tuple | tuple |
* | dict | map |
* | numpy.ndarray | nested list |
* | other | string representation |
*
* @param env NIF environment for term allocation
* @param obj Python object to convert (borrowed reference)
* @return Erlang term representing the Python object
*
* @note Does not consume a reference to obj
* @note May return ATOM_ERROR on allocation failure
*/
static ERL_NIF_TERM py_to_term(ErlNifEnv *env, PyObject *obj);
/**
* @brief Convert an Erlang term to a Python object
*
* Recursively converts Erlang terms to Python objects:
*
* | Erlang Type | Python Type |
* |-------------|-------------|
* | atom `true` | True |
* | atom `false` | False |
* | atom `nil`/`none`/`undefined` | None |
* | other atoms | str |
* | integer | int |
* | float | float |
* | binary | str (UTF-8) |
* | list | list |
* | tuple | tuple |
* | map | dict |
* | resource (py_object) | unwrapped object |
*
* @param env NIF environment containing the term
* @param term Erlang term to convert
* @return New Python object (caller owns reference), or NULL on error
*
* @warning Caller must DECREF the returned object
*/
static PyObject *term_to_py(ErlNifEnv *env, ERL_NIF_TERM term);
/**
* @brief Create an error tuple `{error, Reason}`
*
* @param env NIF environment for term allocation
* @param reason Error reason as C string (converted to atom)
* @return `{error, reason}` tuple
*/
static ERL_NIF_TERM make_error(ErlNifEnv *env, const char *reason);
/**
* @brief Create an error tuple from the current Python exception
*
* Fetches the current Python exception, formats it as an Erlang
* error tuple, and clears the Python error state.
*
* @param env NIF environment for term allocation
* @return `{error, {ExceptionType, Message}}` tuple
*
* @note Always clears the Python exception state
*/
static ERL_NIF_TERM make_py_error(ErlNifEnv *env);
/**
* @brief Convert ErlNifBinary to null-terminated C string
*
* Allocates a new buffer using enif_alloc(), copies the binary
* data, and adds a null terminator.
*
* @param bin Binary to convert
* @return Newly allocated null-terminated string, or NULL on failure
*
* @warning Caller must call enif_free() on the returned string
*/
static char *binary_to_string(const ErlNifBinary *bin) {
char *str = enif_alloc(bin->size + 1);
if (str != NULL) {
memcpy(str, bin->data, bin->size);
str[bin->size] = '\0';
}
return str;
}
/**
* @brief Read from a file descriptor with optional timeout
*
* Uses select() to implement a timeout on blocking read operations.
* This prevents indefinite blocking on pipe reads.
*
* @param fd File descriptor to read from
* @param buf Buffer to read into
* @param count Number of bytes to read
* @param timeout_ms Timeout in milliseconds (0 = no timeout, wait indefinitely)
*
* @return Number of bytes read on success, 0 on timeout, -1 on error
*
* @note On timeout, errno is set to ETIMEDOUT
*/
static ssize_t read_with_timeout(int fd, void *buf, size_t count, int timeout_ms) {
if (timeout_ms > 0) {
struct timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd, &fds);
int ret = select(fd + 1, &fds, NULL, NULL, &tv);
if (ret < 0) {
return -1; /* select error */
}
if (ret == 0) {
errno = ETIMEDOUT;
return 0; /* timeout */
}
}
return read(fd, buf, count);
}
/**
* @brief Read length-prefixed data from a file descriptor
*
* Reads a 4-byte length prefix followed by the data payload.
* Uses read_with_timeout for optional timeout support.
*
* @param fd File descriptor to read from
* @param data_out Pointer to store allocated data buffer (caller must free with enif_free)
* @param len_out Pointer to store data length
* @param timeout_ms Timeout in milliseconds (0 = no timeout)
*
* @return 0 on success, -1 on read error/timeout, -2 on allocation failure
*
* @note On success with len > 0, caller must call enif_free(*data_out)
*/
static int read_length_prefixed_data(int fd, char **data_out, uint32_t *len_out, int timeout_ms) {
uint32_t len;
ssize_t n = read_with_timeout(fd, &len, sizeof(len), timeout_ms);
if (n != sizeof(len)) {
return -1;
}
*data_out = NULL;
*len_out = len;
if (len > 0) {
*data_out = enif_alloc(len);
if (*data_out == NULL) {
return -2;
}
n = read_with_timeout(fd, *data_out, len, timeout_ms);
if (n != (ssize_t)len) {
enif_free(*data_out);
*data_out = NULL;
return -1;
}
}
return 0;
}
/** @} */
/* ============================================================================
* Timeout Support (py_exec.c)
* ============================================================================ */
/**
* @defgroup timeout Timeout Support
* @brief Functions for Python execution timeout handling
* @{
*/
/**
* @brief Get current monotonic time in nanoseconds
*
* Uses CLOCK_MONOTONIC for timeout calculations to avoid issues
* with system clock adjustments.
*
* @return Current time in nanoseconds
*/
static inline uint64_t get_monotonic_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
}
/**
* @brief Start timeout monitoring for Python execution
*
* Sets up a trace callback that checks elapsed time and raises
* `TimeoutError` if the deadline is exceeded.
*
* @param timeout_ms Timeout in milliseconds (0 = no timeout)
*
* @see stop_timeout
*/
static void start_timeout(unsigned long timeout_ms);
/**
* @brief Stop timeout monitoring
*
* Removes the trace callback and resets timeout state.
*
* @see start_timeout
*/
static void stop_timeout(void);
/**
* @brief Check if current Python exception is a timeout error
*
* @return true if TimeoutError is pending, false otherwise
*/
static bool check_timeout_error(void);
/** @} */
/* ============================================================================
* Executor Functions (py_exec.c)
* ============================================================================ */
/**
* @defgroup exec Executor Functions
* @brief Functions for the executor thread pool
* @{
*/
/**
* @brief Process a single request with GIL held
*
* Main dispatch function called by executor threads. Handles all
* request types and stores results in the request structure.
*
* @param req Request to process (must not be NULL)
*
* @note Caller must hold the GIL
* @note Sets req->result on completion
*/
static void process_request(py_request_t *req);
/**
* @brief Submit a request to the executor
*
* Routes the request based on execution mode:
* - FREE_THREADED: Execute directly
* - MULTI_EXECUTOR: Route to executor pool
* - SUBINTERP: Use single executor
*
* @param req Request to submit
*/
static void executor_enqueue(py_request_t *req);
/**
* @brief Wait for a request to complete
*
* Blocks until the executor signals completion by setting
* req->completed and signaling req->cond.
*
* @param req Request to wait for
*/
static void executor_wait(py_request_t *req);
/**
* @brief Initialize a request structure
*
* Zeroes the structure and initializes mutex/condvar.
*
* @param req Request to initialize
*/
static void request_init(py_request_t *req);
/**
* @brief Clean up a request structure
*
* Destroys mutex and condvar. Does not free the request itself.
*
* @param req Request to clean up
*/
static void request_cleanup(py_request_t *req);
/**
* @brief Start the single executor thread
*
* Creates and starts the executor thread, waiting for it to
* become ready before returning.
*
* @return 0 on success, -1 on failure
*/
static int executor_start(void);
/**
* @brief Stop the single executor thread
*
* Sends shutdown request and waits for thread to terminate.
*/
static void executor_stop(void);
/**
* @brief Main function for multi-executor threads
*
* Thread entry point for executor pool threads. Processes
* requests from its queue until shutdown.
*
* @param arg Pointer to executor_t for this thread
* @return NULL
*/
static void *multi_executor_thread_main(void *arg);
/**
* @brief Start the multi-executor pool
*
* Creates and starts num_executors threads.
*
* @param num_executors Number of executors (capped at MAX_EXECUTORS)
* @return 0 on success, -1 on failure
*/
static int multi_executor_start(int num_executors);
/**
* @brief Stop the multi-executor pool
*
* Signals shutdown and waits for all executor threads.
*/
static void multi_executor_stop(void);
/**
* @brief Select an executor using round-robin
*
* @return Executor index (0 to g_num_executors-1)
*/
static int select_executor(void);
/**
* @brief Submit a request to a specific executor
*
* @param exec_id Executor index
* @param req Request to submit
*/
static void multi_executor_enqueue(int exec_id, struct py_request *req);
/** @} */
/* ============================================================================
* Callback Functions (py_callback.c)
* ============================================================================ */
/**
* @defgroup cb Callback Functions
* @brief Functions for Python-to-Erlang callback support
* @{
*/
/**
* @brief Create and register the 'erlang' Python module
*
* Creates a Python module named 'erlang' that provides:
* - `erlang.call(name, *args)` - Call Erlang function
* - `erlang.func_name(*args)` - Shorthand syntax
* - `erlang.SuspensionRequired` - Internal exception
*
* @return 0 on success, -1 on failure
*/
static int create_erlang_module(void);
/**
* @brief Implementation of `erlang.call(name, *args)`
*
* Allows Python code to call registered Erlang callback functions.
* Uses suspension/resume mechanism to free dirty schedulers.
*
* @param self Module reference (unused)
* @param args Tuple: (func_name, arg1, arg2, ...)
* @return Result from Erlang, or NULL with exception set
*
* @see create_suspended_state
*/
static PyObject *erlang_call_impl(PyObject *self, PyObject *args);
/**
* @brief Module __getattr__ for `erlang.func_name` syntax
*
* Enables calling Erlang functions as `erlang.my_func(args)`.
*
* @param module Module reference
* @param name Attribute name
* @return ErlangFunction wrapper object
*/
static PyObject *erlang_module_getattr(PyObject *module, PyObject *name);
/**
* @brief Background thread running asyncio event loop
*
* Manages async Python operations submitted via async_call.
*
* @param arg Pointer to py_async_worker_t
* @return NULL
*/
static void *async_event_loop_thread(void *arg);
/**
* @brief Create suspended state for callback handling
*
* Captures all state needed to resume Python execution after
* Erlang processes the callback.
*
* @param env NIF environment
* @param exc_args Exception args tuple (callback_id, func_name, args)
* @param req Original request being processed
* @return New suspended state resource, or NULL on error
*/
static suspended_state_t *create_suspended_state(ErlNifEnv *env, PyObject *exc_args,
py_request_t *req);
/**
* @brief Parse callback response from Erlang
*
* Response format: status_byte (0=ok, 1=error) + python_repr_string
*
* @param response_data Raw response bytes
* @param response_len Length of response
* @return Parsed Python object, or NULL with exception set
*/
static PyObject *parse_callback_response(unsigned char *response_data, size_t response_len);
/**
* @brief Detect and set the execution mode
*
* Auto-detects based on Python version and build configuration.
*/
static void detect_execution_mode(void);
/** @} */
/* ============================================================================
* Thread Worker Functions (py_thread_worker.c)
* ============================================================================ */
/**
* @defgroup thread_worker Thread Worker Support
* @brief Functions for ThreadPoolExecutor thread support
* @{
*/
/**
* @brief Initialize the thread worker system
*
* Creates pthread key for automatic cleanup on thread exit.
* Called during NIF initialization.
*
* @return 0 on success, -1 on failure
*/
static int thread_worker_init(void);
/**
* @brief Clean up the thread worker system
*
* Releases all workers in the pool and destroys the pthread key.
* Called during NIF unload.
*/
static void thread_worker_cleanup(void);
/**
* @brief Set the thread coordinator PID
*
* @param pid PID of the coordinator process
*/
static void thread_worker_set_coordinator(ErlNifPid pid);
/**
* @brief Execute an erlang.call() from a spawned thread
*
* Called when a Python thread that is NOT an executor thread
* tries to call erlang.call().
*
* @param func_name Name of the Erlang function to call
* @param func_name_len Length of function name
* @param call_args Python tuple of arguments
* @return Python result object, or NULL with exception set
*/
static PyObject *thread_worker_call(const char *func_name, size_t func_name_len,
PyObject *call_args);
/** @} */
/* ============================================================================
* Safe GIL/Thread State Acquisition
* ============================================================================
*
* These helpers provide safe GIL acquisition that works across:
* - Python 3.9-3.11 (standard GIL)
* - Python 3.12-3.13 (stricter thread state checks)
* - Python 3.13t free-threaded (no GIL, but thread attachment required)
*
* The pattern follows PyO3's Python::attach() approach:
* 1. Check if already attached via PyGILState_Check()
* 2. If not, use PyGILState_Ensure() to attach
* 3. Track whether we acquired so we release correctly
*
* Per Python docs, PyGILState_Ensure/Release work in free-threaded builds
* to manage thread attachment even without a GIL.
*/
/**
* @defgroup gil_helpers Safe GIL/Thread State Helpers
* @brief Thread-safe GIL acquisition for Python 3.12+ compatibility
* @{
*/
/**
* @brief Guard structure for safe GIL acquisition/release
*/
typedef struct {
PyGILState_STATE gstate; /**< GIL state from PyGILState_Ensure */
int acquired; /**< 1 if we acquired, 0 if already held */
} gil_guard_t;
/**
* @brief Safely acquire the GIL/attach to Python runtime.
*
* This function is reentrant - if the current thread already holds the GIL
* (or is attached in free-threaded builds), it returns immediately without
* double-acquiring.
*
* @return Guard structure that must be passed to gil_release()
*/
static inline gil_guard_t gil_acquire(void) {
gil_guard_t guard = {.gstate = PyGILState_UNLOCKED, .acquired = 0};
/* Check if already attached to Python runtime */
if (PyGILState_Check()) {
return guard;
}
/* Attach to Python runtime (acquires GIL in GIL-enabled builds) */
guard.gstate = PyGILState_Ensure();
guard.acquired = 1;
return guard;
}
/**
* @brief Release the GIL/detach from Python runtime.
*
* Only releases if we actually acquired in gil_acquire().
*
* @param guard The guard structure returned by gil_acquire()
*/
static inline void gil_release(gil_guard_t guard) {
if (guard.acquired) {
PyGILState_Release(guard.gstate);
}
}
/** @} */
#endif /* PY_NIF_H */