%% 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. %%% @doc Low-level NIF wrapper for Python integration. %%% %%% This module provides the direct NIF interface. Most users should use %%% the higher-level `py' module instead. %%% %%% @private -module(py_nif). -export([ init/0, init/1, finalize/0, worker_new/0, worker_new/1, worker_destroy/1, worker_call/5, worker_call/6, worker_eval/3, worker_eval/4, worker_exec/2, worker_next/2, import_module/2, get_attr/3, version/0, memory_stats/0, get_debug_counters/0, gc/0, gc/1, tracemalloc_start/0, tracemalloc_start/1, tracemalloc_stop/0, set_callback_handler/2, send_callback_response/2, resume_callback/2, %% Async workers async_worker_new/0, async_worker_destroy/1, async_call/6, async_gather/3, async_stream/6, %% Sub-interpreters (Python 3.12+) - shared GIL pool model subinterp_supported/0, owngil_supported/0, subinterp_worker_new/0, subinterp_worker_destroy/1, subinterp_call/5, parallel_execute/2, %% OWN_GIL subinterpreter thread pool (true parallelism) subinterp_thread_pool_start/0, subinterp_thread_pool_start/1, subinterp_thread_pool_stop/0, subinterp_thread_pool_ready/0, subinterp_thread_pool_stats/0, subinterp_thread_create/0, subinterp_thread_destroy/1, subinterp_thread_call/4, subinterp_thread_call/5, subinterp_thread_eval/2, subinterp_thread_eval/3, subinterp_thread_exec/2, subinterp_thread_cast/4, subinterp_thread_async_call/6, %% OWN_GIL session management for event loop pool owngil_create_session/1, owngil_submit_task/7, owngil_destroy_session/2, owngil_apply_imports/3, owngil_apply_paths/3, %% Execution mode info execution_mode/0, num_executors/0, %% Thread worker support (ThreadPoolExecutor) thread_worker_set_coordinator/1, thread_worker_write/2, thread_worker_signal_ready/1, %% Async callback support (for erlang.async_call) async_callback_response/3, %% Callback name registry (prevents torch introspection issues) register_callback_name/1, unregister_callback_name/1, %% Logging and tracing set_log_receiver/2, clear_log_receiver/0, set_trace_receiver/1, clear_trace_receiver/0, %% Erlang-native event loop (for asyncio integration) set_event_loop_priv_dir/1, event_loop_new/0, event_loop_destroy/1, event_loop_set_router/2, event_loop_set_worker/2, event_loop_set_id/2, event_loop_wakeup/1, event_loop_run_async/7, %% Async task queue NIFs (uvloop-inspired) submit_task/7, submit_task_with_env/8, process_ready_tasks/1, event_loop_set_py_loop/2, %% Per-process namespace NIFs event_loop_exec/2, event_loop_eval/2, %% Per-interpreter import caching NIFs interp_apply_imports/2, interp_apply_paths/2, add_reader/3, remove_reader/2, add_writer/3, remove_writer/2, call_later/3, cancel_timer/2, poll_events/2, get_pending/1, dispatch_callback/3, dispatch_timer/2, get_fd_callback_id/2, reselect_reader/2, reselect_writer/2, reselect_reader_fd/1, reselect_writer_fd/1, %% FD lifecycle management (uvloop-like API) handle_fd_event/2, handle_fd_event_and_reselect/2, stop_reader/1, start_reader/1, stop_writer/1, start_writer/1, cancel_reader/2, %% Legacy alias for stop_reader cancel_writer/2, %% Legacy alias for stop_writer close_fd/1, %% File descriptor utilities dup_fd/1, %% Test helpers for fd monitoring (using pipes) create_test_pipe/0, close_test_fd/1, write_test_fd/2, read_test_fd/2, %% TCP test helpers create_test_tcp_listener/1, accept_test_tcp/1, connect_test_tcp/2, %% UDP test helpers create_test_udp_socket/1, recvfrom_test_udp/2, sendto_test_udp/4, set_udp_broadcast/2, %% Python event loop integration set_python_event_loop/1, set_isolation_mode/1, set_shared_worker/1, %% Worker pool pool_start/1, pool_stop/0, pool_submit/5, pool_stats/0, %% Process-per-context API (no mutex) context_create/1, context_destroy/1, context_call/5, context_call/6, context_eval/3, context_eval/4, context_exec/2, context_exec/3, context_call_method/4, create_local_env/1, context_to_term/1, context_interp_id/1, context_set_callback_handler/2, context_get_callback_pipe/1, context_write_callback_response/2, context_resume/3, context_cancel_resume/2, context_get_event_loop/1, %% py_ref API (Python object references with interp_id) ref_wrap/2, is_ref/1, ref_interp_id/1, ref_to_term/1, ref_getattr/2, ref_call_method/3, %% Reactor NIFs - Erlang-as-Reactor architecture reactor_register_fd/3, reactor_reselect_read/1, reactor_select_write/1, get_fd_from_resource/1, reactor_on_read_ready/2, reactor_on_write_ready/2, reactor_init_connection/3, reactor_close_fd/2, %% Direct FD operations fd_read/2, fd_write/2, fd_select_read/1, fd_select_write/1, fd_close/1, socketpair/0, %% Channel API - bidirectional message passing channel_create/0, channel_create/1, channel_send/2, channel_receive/2, channel_try_receive/1, channel_reply/3, channel_close/1, channel_info/1, channel_wait/3, channel_cancel_wait/2, channel_register_sync_waiter/1, %% ByteChannel API - raw bytes, no term conversion byte_channel_send_bytes/2, byte_channel_try_receive_bytes/1, byte_channel_wait_bytes/3, %% PyBuffer API - zero-copy WSGI input py_buffer_create/1, py_buffer_write/2, py_buffer_close/1, %% SharedDict API - process-scoped shared dictionary shared_dict_new/0, shared_dict_get/3, shared_dict_set/3, shared_dict_del/2, shared_dict_keys/1, shared_dict_destroy/1 ]). -on_load(load_nif/0). -define(NIF_STUB, erlang:nif_error(nif_not_loaded)). %%% ============================================================================ %%% NIF Loading %%% ============================================================================ load_nif() -> PrivDir = case code:priv_dir(erlang_python) of {error, bad_name} -> %% Fallback for development case code:which(?MODULE) of Filename when is_list(Filename) -> filename:join([filename:dirname(Filename), "..", "priv"]); _ -> "priv" end; Dir -> Dir end, NifPath = filename:join(PrivDir, "py_nif"), erlang:load_nif(NifPath, 0). %%% ============================================================================ %%% Initialization %%% ============================================================================ %% @doc Initialize the Python interpreter. %% Must be called before any other functions. %% Usually called automatically by the application. -spec init() -> ok | {error, term()}. init() -> init(#{}). %% @doc Initialize with options. %% Options: %% python_home => string() - Python installation directory %% python_path => [string()] - Additional module search paths %% isolated => boolean() - Run in isolated mode (default: false) -spec init(map()) -> ok | {error, term()}. init(_Opts) -> ?NIF_STUB. %% @doc Finalize the Python interpreter. %% Call this when shutting down. After this, no Python calls can be made. -spec finalize() -> ok. finalize() -> ?NIF_STUB. %%% ============================================================================ %%% Worker Management %%% ============================================================================ %% @doc Create a new Python worker context. %% Returns an opaque reference to be used with other worker functions. -spec worker_new() -> {ok, reference()} | {error, term()}. worker_new() -> worker_new(#{}). %% @doc Create a worker with options. %% Options: %% use_subinterpreter => boolean() - Use a separate sub-interpreter (Python 3.12+) -spec worker_new(map()) -> {ok, reference()} | {error, term()}. worker_new(_Opts) -> ?NIF_STUB. %% @doc Destroy a worker context. -spec worker_destroy(reference()) -> ok. worker_destroy(_WorkerRef) -> ?NIF_STUB. %% @doc Call a Python function from a worker. %% This is a dirty NIF that acquires the GIL. %% May return {suspended, ...} if Python calls erlang.call() (reentrant callback). -spec worker_call(reference(), binary(), binary(), list(), map()) -> {ok, term()} | {error, term()} | {suspended, term(), reference(), {binary(), term()}}. worker_call(_WorkerRef, _Module, _Func, _Args, _Kwargs) -> ?NIF_STUB. %% @doc Call a Python function from a worker with timeout. %% May return {suspended, ...} if Python calls erlang.call() (reentrant callback). -spec worker_call(reference(), binary(), binary(), list(), map(), non_neg_integer()) -> {ok, term()} | {error, term()} | {suspended, term(), reference(), {binary(), term()}}. worker_call(_WorkerRef, _Module, _Func, _Args, _Kwargs, _TimeoutMs) -> ?NIF_STUB. %% @doc Evaluate a Python expression in a worker. %% May return {suspended, ...} if Python calls erlang.call() (reentrant callback). -spec worker_eval(reference(), binary(), map()) -> {ok, term()} | {error, term()} | {suspended, term(), reference(), {binary(), term()}}. worker_eval(_WorkerRef, _Code, _Locals) -> ?NIF_STUB. %% @doc Evaluate a Python expression in a worker with timeout. %% May return {suspended, ...} if Python calls erlang.call() (reentrant callback). -spec worker_eval(reference(), binary(), map(), non_neg_integer()) -> {ok, term()} | {error, term()} | {suspended, term(), reference(), {binary(), term()}}. worker_eval(_WorkerRef, _Code, _Locals, _TimeoutMs) -> ?NIF_STUB. %% @doc Execute Python statements in a worker. -spec worker_exec(reference(), binary()) -> ok | {error, term()}. worker_exec(_WorkerRef, _Code) -> ?NIF_STUB. %% @doc Get next item from a generator/iterator. %% Returns {ok, Value} | {error, stop_iteration} | {error, Error} -spec worker_next(reference(), reference()) -> {ok, term()} | {error, term()}. worker_next(_WorkerRef, _GeneratorRef) -> ?NIF_STUB. %%% ============================================================================ %%% Module Operations %%% ============================================================================ %% @doc Import a Python module in a worker context. -spec import_module(reference(), binary()) -> {ok, reference()} | {error, term()}. import_module(_WorkerRef, _ModuleName) -> ?NIF_STUB. %% @doc Get an attribute from a Python object. -spec get_attr(reference(), reference(), binary()) -> {ok, term()} | {error, term()}. get_attr(_WorkerRef, _ObjRef, _AttrName) -> ?NIF_STUB. %%% ============================================================================ %%% Info %%% ============================================================================ %% @doc Get Python version info. -spec version() -> {ok, binary()} | {error, term()}. version() -> ?NIF_STUB. %%% ============================================================================ %%% Memory and GC %%% ============================================================================ %% @doc Get Python memory statistics. %% Returns a map with gc_stats, gc_count, gc_threshold, and optionally %% traced_memory_current and traced_memory_peak if tracemalloc is enabled. -spec memory_stats() -> {ok, map()} | {error, term()}. memory_stats() -> ?NIF_STUB. %% @doc Get debug counters for tracking resource lifecycle. %% Returns a map with counter names and their values. Used for detecting leaks. -spec get_debug_counters() -> map(). get_debug_counters() -> ?NIF_STUB. %% @doc Force Python garbage collection. %% Returns the number of unreachable objects collected. -spec gc() -> {ok, integer()} | {error, term()}. gc() -> ?NIF_STUB. %% @doc Force garbage collection of a specific generation. %% Generation: 0, 1, or 2 (default 2 = full collection). -spec gc(0..2) -> {ok, integer()} | {error, term()}. gc(_Generation) -> ?NIF_STUB. %% @doc Start memory tracing with tracemalloc. %% This allows tracking memory allocations. -spec tracemalloc_start() -> ok | {error, term()}. tracemalloc_start() -> ?NIF_STUB. %% @doc Start memory tracing with specified number of frames. -spec tracemalloc_start(pos_integer()) -> ok | {error, term()}. tracemalloc_start(_NFrame) -> ?NIF_STUB. %% @doc Stop memory tracing. -spec tracemalloc_stop() -> ok | {error, term()}. tracemalloc_stop() -> ?NIF_STUB. %%% ============================================================================ %%% Callback Support %%% ============================================================================ %% @doc Set callback handler process for a worker. %% Returns {ok, Fd} where Fd is the file descriptor for sending responses. -spec set_callback_handler(reference(), pid()) -> {ok, integer()} | {error, term()}. set_callback_handler(_WorkerRef, _HandlerPid) -> ?NIF_STUB. %% @doc Send a callback response to a worker via file descriptor. -spec send_callback_response(integer(), binary()) -> ok | {error, term()}. send_callback_response(_Fd, _Response) -> ?NIF_STUB. %% @doc Resume a suspended Python callback with the result. %% StateRef is the reference returned in the {suspended, ...} tuple. %% Result is the callback result as a binary (status byte + data). %% Returns {ok, FinalResult}, {error, Reason}, or another {suspended, ...} for nested callbacks. -spec resume_callback(reference(), binary()) -> {ok, term()} | {error, term()} | {suspended, term(), reference(), {binary(), term()}}. resume_callback(_StateRef, _Result) -> ?NIF_STUB. %%% ============================================================================ %%% Async Worker Support %%% ============================================================================ %% @doc Create a new async worker with background event loop. %% Returns an opaque reference to be used with async functions. -spec async_worker_new() -> {ok, reference()} | {error, term()}. async_worker_new() -> ?NIF_STUB. %% @doc Destroy an async worker. -spec async_worker_destroy(reference()) -> ok. async_worker_destroy(_WorkerRef) -> ?NIF_STUB. %% @doc Submit an async call to the event loop. %% Args: AsyncWorkerRef, Module, Func, Args, Kwargs, CallerPid %% Returns: {ok, AsyncId} | {ok, {immediate, Result}} | {error, term()} -spec async_call(reference(), binary(), binary(), list(), map(), pid()) -> {ok, non_neg_integer() | {immediate, term()}} | {error, term()}. async_call(_WorkerRef, _Module, _Func, _Args, _Kwargs, _CallerPid) -> ?NIF_STUB. %% @doc Execute multiple async calls concurrently using asyncio.gather. %% Args: AsyncWorkerRef, CallsList (list of {Module, Func, Args}), CallerPid %% Returns: {ok, AsyncId} | {ok, {immediate, Results}} | {error, term()} -spec async_gather(reference(), [{binary(), binary(), list()}], pid()) -> {ok, non_neg_integer() | {immediate, list()}} | {error, term()}. async_gather(_WorkerRef, _Calls, _CallerPid) -> ?NIF_STUB. %% @doc Stream from an async generator. %% Args: AsyncWorkerRef, Module, Func, Args, Kwargs, CallerPid %% Returns: {ok, AsyncId} | {error, term()} -spec async_stream(reference(), binary(), binary(), list(), map(), pid()) -> {ok, non_neg_integer()} | {error, term()}. async_stream(_WorkerRef, _Module, _Func, _Args, _Kwargs, _CallerPid) -> ?NIF_STUB. %%% ============================================================================ %%% Sub-interpreter Support (Python 3.12+) %%% ============================================================================ %% @doc Check if sub-interpreters with per-interpreter GIL are supported. %% Returns true on Python 3.12+, false otherwise. -spec subinterp_supported() -> boolean(). subinterp_supported() -> ?NIF_STUB. %% @doc Check if OWN_GIL mode is supported (Python 3.14+). %% OWN_GIL requires Python 3.14+ due to C extension global state bugs %% in earlier versions (e.g., _decimal). See gh-106078. -spec owngil_supported() -> boolean(). owngil_supported() -> ?NIF_STUB. %% @doc Create a new sub-interpreter worker with its own GIL. %% Returns an opaque reference to be used with subinterp functions. -spec subinterp_worker_new() -> {ok, reference()} | {error, term()}. subinterp_worker_new() -> ?NIF_STUB. %% @doc Destroy a sub-interpreter worker. -spec subinterp_worker_destroy(reference()) -> ok | {error, term()}. subinterp_worker_destroy(_WorkerRef) -> ?NIF_STUB. %% @doc Call a Python function in a sub-interpreter. %% Args: WorkerRef, Module (binary), Func (binary), Args (list), Kwargs (map) -spec subinterp_call(reference(), binary(), binary(), list(), map()) -> {ok, term()} | {error, term()}. subinterp_call(_WorkerRef, _Module, _Func, _Args, _Kwargs) -> ?NIF_STUB. %% @doc Execute multiple calls in parallel across sub-interpreters. %% Args: WorkerRefs (list of refs), Calls (list of {Module, Func, Args}) %% Returns: List of results (one per call) -spec parallel_execute([reference()], [{binary(), binary(), list()}]) -> {ok, list()} | {error, term()}. parallel_execute(_WorkerRefs, _Calls) -> ?NIF_STUB. %%% ============================================================================ %%% OWN_GIL Subinterpreter Thread Pool (True Parallelism) %%% ============================================================================ %% @doc Start the OWN_GIL subinterpreter thread pool with default workers. %% Creates a pool of pthreads, each with an OWN_GIL subinterpreter. -spec subinterp_thread_pool_start() -> ok | {error, term()}. subinterp_thread_pool_start() -> ?NIF_STUB. %% @doc Start the OWN_GIL subinterpreter thread pool with N workers. -spec subinterp_thread_pool_start(non_neg_integer()) -> ok | {error, term()}. subinterp_thread_pool_start(_NumWorkers) -> ?NIF_STUB. %% @doc Stop the OWN_GIL subinterpreter thread pool. -spec subinterp_thread_pool_stop() -> ok. subinterp_thread_pool_stop() -> ?NIF_STUB. %% @doc Check if the OWN_GIL thread pool is ready. -spec subinterp_thread_pool_ready() -> boolean(). subinterp_thread_pool_ready() -> ?NIF_STUB. %% @doc Get OWN_GIL thread pool statistics. -spec subinterp_thread_pool_stats() -> map(). subinterp_thread_pool_stats() -> ?NIF_STUB. %% @doc Create a new OWN_GIL subinterpreter handle. %% The handle is bound to a worker thread and has isolated namespace. -spec subinterp_thread_create() -> {ok, reference()} | {error, term()}. subinterp_thread_create() -> ?NIF_STUB. %% @doc Destroy an OWN_GIL subinterpreter handle. -spec subinterp_thread_destroy(reference()) -> ok | {error, term()}. subinterp_thread_destroy(_Handle) -> ?NIF_STUB. %% @doc Call a Python function through OWN_GIL subinterpreter (blocking). -spec subinterp_thread_call(reference(), binary(), binary(), list()) -> {ok, term()} | {error, term()}. subinterp_thread_call(_Handle, _Module, _Func, _Args) -> ?NIF_STUB. %% @doc Call a Python function through OWN_GIL subinterpreter with kwargs. -spec subinterp_thread_call(reference(), binary(), binary(), list(), map()) -> {ok, term()} | {error, term()}. subinterp_thread_call(_Handle, _Module, _Func, _Args, _Kwargs) -> ?NIF_STUB. %% @doc Evaluate Python expression through OWN_GIL subinterpreter. -spec subinterp_thread_eval(reference(), binary()) -> {ok, term()} | {error, term()}. subinterp_thread_eval(_Handle, _Code) -> ?NIF_STUB. %% @doc Evaluate Python expression with locals through OWN_GIL subinterpreter. -spec subinterp_thread_eval(reference(), binary(), map()) -> {ok, term()} | {error, term()}. subinterp_thread_eval(_Handle, _Code, _Locals) -> ?NIF_STUB. %% @doc Execute Python statements through OWN_GIL subinterpreter (no return). -spec subinterp_thread_exec(reference(), binary()) -> ok | {error, term()}. subinterp_thread_exec(_Handle, _Code) -> ?NIF_STUB. %% @doc Cast (fire-and-forget) through OWN_GIL subinterpreter. %% Returns immediately, result is discarded. -spec subinterp_thread_cast(reference(), binary(), binary(), list()) -> ok. subinterp_thread_cast(_Handle, _Module, _Func, _Args) -> ?NIF_STUB. %% @doc Async call through OWN_GIL subinterpreter. %% Args: Handle, Module, Func, Args, CallerPid, Ref %% Result is sent to CallerPid as {py_subinterp_result, Ref, Result}. -spec subinterp_thread_async_call(reference(), binary(), binary(), list(), pid(), reference()) -> ok | {error, term()}. subinterp_thread_async_call(_Handle, _Module, _Func, _Args, _CallerPid, _Ref) -> ?NIF_STUB. %%% ============================================================================ %%% OWN_GIL Session Management (for event loop pool) %%% ============================================================================ %% @doc Create a new OWN_GIL session for event loop pool. %% The WorkerHint is used for worker assignment (typically loop index). %% Returns {ok, WorkerId, HandleId} where: %% - WorkerId is the assigned worker thread index %% - HandleId is the unique namespace handle within that worker -spec owngil_create_session(non_neg_integer()) -> {ok, non_neg_integer(), non_neg_integer()} | {error, term()}. owngil_create_session(_WorkerHint) -> ?NIF_STUB. %% @doc Submit an async task to an OWN_GIL worker. %% Args: WorkerId, HandleId, CallerPid, Ref, Module, Func, Args %% The task runs in the worker's asyncio event loop. %% Result is sent to CallerPid as {async_result, Ref, Result}. -spec owngil_submit_task(non_neg_integer(), non_neg_integer(), pid(), reference(), binary(), binary(), list()) -> ok | {error, term()}. owngil_submit_task(_WorkerId, _HandleId, _CallerPid, _Ref, _Module, _Func, _Args) -> ?NIF_STUB. %% @doc Destroy an OWN_GIL session. %% Cleans up the namespace within the worker. -spec owngil_destroy_session(non_neg_integer(), non_neg_integer()) -> ok | {error, term()}. owngil_destroy_session(_WorkerId, _HandleId) -> ?NIF_STUB. %% @doc Apply imports to an OWN_GIL session. %% Imports modules into the worker's sys.modules. -spec owngil_apply_imports(non_neg_integer(), non_neg_integer(), [{binary(), binary() | all}]) -> ok | {error, term()}. owngil_apply_imports(_WorkerId, _HandleId, _Imports) -> ?NIF_STUB. %% @doc Apply paths to an OWN_GIL session. %% Adds paths to the worker's sys.path. -spec owngil_apply_paths(non_neg_integer(), non_neg_integer(), [binary()]) -> ok | {error, term()}. owngil_apply_paths(_WorkerId, _HandleId, _Paths) -> ?NIF_STUB. %%% ============================================================================ %%% Execution Mode Info %%% ============================================================================ %% @doc Get the current execution mode. %% Returns one of: free_threaded | subinterp | multi_executor %% - free_threaded: Python 3.13+ with no GIL (Py_GIL_DISABLED) %% - subinterp: Python 3.12+ with per-interpreter GIL %% - multi_executor: Traditional Python with N executor threads -spec execution_mode() -> free_threaded | subinterp | multi_executor. execution_mode() -> ?NIF_STUB. %% @doc Get the number of executor threads. %% For multi_executor mode, this is the number of executor threads. %% For other modes, returns 1. -spec num_executors() -> pos_integer(). num_executors() -> ?NIF_STUB. %%% ============================================================================ %%% Thread Worker Support (ThreadPoolExecutor) %%% ============================================================================ %% @doc Set the thread worker coordinator process. %% This process receives spawn and callback messages from Python threads %% spawned via ThreadPoolExecutor. -spec thread_worker_set_coordinator(pid()) -> ok | {error, term()}. thread_worker_set_coordinator(_Pid) -> ?NIF_STUB. %% @doc Write a callback response to a thread worker's pipe. %% Fd is the write end of the response pipe. %% Response is the result binary (status byte + python repr). -spec thread_worker_write(integer(), binary()) -> ok | {error, term()}. thread_worker_write(_Fd, _Response) -> ?NIF_STUB. %% @doc Signal that a thread worker handler is ready. %% Writes a zero-length response to the pipe to indicate readiness. -spec thread_worker_signal_ready(integer()) -> ok | {error, term()}. thread_worker_signal_ready(_Fd) -> ?NIF_STUB. %%% ============================================================================ %%% Async Callback Support (for erlang.async_call) %%% ============================================================================ %% @doc Write an async callback response to the async callback pipe. %% Fd is the write end of the async callback pipe. %% CallbackId is the unique identifier for this callback. %% Response is the result binary (status byte + encoded result). %% %% This is called when an async_callback message is processed. %% The response is written in the format: %% callback_id (8 bytes) + response_len (4 bytes) + response_data -spec async_callback_response(integer(), non_neg_integer(), binary()) -> ok | {error, term()}. async_callback_response(_Fd, _CallbackId, _Response) -> ?NIF_STUB. %%% ============================================================================ %%% Callback Name Registry %%% ============================================================================ %% @doc Register a callback name in the C-side registry. %% This allows the Python erlang module's __getattr__ to return %% ErlangFunction wrappers only for registered callbacks, preventing %% introspection issues with libraries like torch. -spec register_callback_name(atom() | binary()) -> ok | {error, term()}. register_callback_name(_Name) -> ?NIF_STUB. %% @doc Unregister a callback name from the C-side registry. -spec unregister_callback_name(atom() | binary()) -> ok. unregister_callback_name(_Name) -> ?NIF_STUB. %%% ============================================================================ %%% Logging and Tracing %%% ============================================================================ %% @doc Set the log receiver process and minimum level threshold. %% Level is the Python levelno (0=DEBUG to 50=CRITICAL). -spec set_log_receiver(pid(), integer()) -> ok | {error, term()}. set_log_receiver(_Pid, _Level) -> ?NIF_STUB. %% @doc Clear the log receiver - log messages will be dropped. -spec clear_log_receiver() -> ok. clear_log_receiver() -> ?NIF_STUB. %% @doc Set the trace receiver process for span events. -spec set_trace_receiver(pid()) -> ok | {error, term()}. set_trace_receiver(_Pid) -> ?NIF_STUB. %% @doc Clear the trace receiver - trace events will be dropped. -spec clear_trace_receiver() -> ok. clear_trace_receiver() -> ?NIF_STUB. %%% ============================================================================ %%% Erlang-native Event Loop (asyncio integration) %%% ============================================================================ %% @doc Set the priv_dir path for module imports in subinterpreters. %% Must be called during application startup before creating event loops. -spec set_event_loop_priv_dir(binary() | string()) -> ok | {error, term()}. set_event_loop_priv_dir(_Path) -> ?NIF_STUB. %% @doc Create a new Erlang-backed asyncio event loop. %% Returns an opaque reference to be used with event loop functions. -spec event_loop_new() -> {ok, reference()} | {error, term()}. event_loop_new() -> ?NIF_STUB. %% @doc Destroy an event loop. -spec event_loop_destroy(reference()) -> ok | {error, term()}. event_loop_destroy(_LoopRef) -> ?NIF_STUB. %% @doc Set the router process for an event loop (legacy). %% The router receives enif_select messages and timer events. -spec event_loop_set_router(reference(), pid()) -> ok | {error, term()}. event_loop_set_router(_LoopRef, _RouterPid) -> ?NIF_STUB. %% @doc Set the worker process for an event loop (scalable I/O model). %% The worker receives FD events and timers directly. -spec event_loop_set_worker(reference(), pid()) -> ok | {error, term()}. event_loop_set_worker(_LoopRef, _WorkerPid) -> ?NIF_STUB. %% @doc Set the loop identifier for multi-loop routing. -spec event_loop_set_id(reference(), binary() | atom()) -> ok | {error, term()}. event_loop_set_id(_LoopRef, _LoopId) -> ?NIF_STUB. %% @doc Wake up an event loop from a wait. -spec event_loop_wakeup(reference()) -> ok | {error, term()}. event_loop_wakeup(_LoopRef) -> ?NIF_STUB. %% @doc Submit an async coroutine to run on the event loop. %% When the coroutine completes, the result is sent to CallerPid via erlang.send(). %% This replaces the pthread+usleep polling model with direct message passing. -spec event_loop_run_async(reference(), pid(), reference(), binary(), binary(), list(), map()) -> ok | {error, term()}. event_loop_run_async(_LoopRef, _CallerPid, _Ref, _Module, _Func, _Args, _Kwargs) -> ?NIF_STUB. %%% ============================================================================ %%% Async Task Queue NIFs (uvloop-inspired) %%% ============================================================================ %% @doc Submit an async task to the event loop (thread-safe). %% %% This NIF can be called from any thread including dirty schedulers. %% It serializes the task info, enqueues to the task queue, and sends %% a 'task_ready' wakeup to the worker via enif_send. %% %% The result will be sent to CallerPid as: %% {async_result, Ref, {ok, Result}} - on success %% {async_result, Ref, {error, Reason}} - on failure -spec submit_task(reference(), pid(), reference(), binary(), binary(), list(), map()) -> ok | {error, term()}. submit_task(_LoopRef, _CallerPid, _Ref, _Module, _Func, _Args, _Kwargs) -> ?NIF_STUB. %% @doc Submit an async task with process-local env. %% %% Like submit_task but includes an env resource reference. The env's globals %% dict is used for function lookup, allowing functions defined via py:exec() %% to be called from the event loop. -spec submit_task_with_env(reference(), pid(), reference(), binary(), binary(), list(), map(), reference()) -> ok | {error, term()}. submit_task_with_env(_LoopRef, _CallerPid, _Ref, _Module, _Func, _Args, _Kwargs, _EnvRef) -> ?NIF_STUB. %% @doc Process all pending tasks from the task queue. %% %% Called by the event worker when it receives 'task_ready' message. %% Dequeues all tasks, creates coroutines, and schedules them on the loop. %% Returns 'more' if batch limit was hit and more tasks remain. -spec process_ready_tasks(reference()) -> ok | more | {error, term()}. process_ready_tasks(_LoopRef) -> ?NIF_STUB. %% @doc Store a Python event loop reference in the C struct. %% %% This avoids thread-local lookup issues when processing tasks. %% Called from Python after creating the ErlangEventLoop. -spec event_loop_set_py_loop(reference(), reference()) -> ok | {error, term()}. event_loop_set_py_loop(_LoopRef, _PyLoopRef) -> ?NIF_STUB. %% @doc Execute Python code in the calling process's namespace. %% Each Erlang process gets an isolated namespace for the event loop. %% Functions defined via exec can be called via create_task with __main__ module. -spec event_loop_exec(reference(), binary() | iolist()) -> ok | {error, term()}. event_loop_exec(_LoopRef, _Code) -> ?NIF_STUB. %% @doc Evaluate a Python expression in the calling process's namespace. %% Returns the result of the expression. -spec event_loop_eval(reference(), binary() | iolist()) -> {ok, term()} | {error, term()}. event_loop_eval(_LoopRef, _Expr) -> ?NIF_STUB. %%% ============================================================================ %%% Per-Interpreter Import Caching %%% ============================================================================ %% @doc Apply a list of imports to an interpreter's module cache. %% %% Called when a new interpreter is created to pre-warm the cache with %% all modules registered via py:import/1,2. %% %% @param Ref Context reference (from context_create/1) %% @param Imports List of {ModuleBin, FuncBin | all} tuples %% @returns ok | {error, Reason} -spec interp_apply_imports(reference(), [{binary(), binary() | all}]) -> ok | {error, term()}. interp_apply_imports(_Ref, _Imports) -> ?NIF_STUB. %% @doc Apply a list of paths to an interpreter's sys.path. %% %% Paths are inserted at the beginning of sys.path to take precedence %% over system paths. Called when a new context is created. %% %% @param Ref Context reference (from context_create/1) %% @param Paths List of path binaries %% @returns ok | {error, Reason} -spec interp_apply_paths(reference(), [binary()]) -> ok | {error, term()}. interp_apply_paths(_Ref, _Paths) -> ?NIF_STUB. %% @doc Register a file descriptor for read monitoring. %% Uses enif_select to register with the Erlang scheduler. -spec add_reader(reference(), integer(), non_neg_integer()) -> {ok, reference()} | {error, term()}. add_reader(_LoopRef, _Fd, _CallbackId) -> ?NIF_STUB. %% @doc Stop monitoring a file descriptor for reads. %% FdRef must be the same resource returned by add_reader. -spec remove_reader(reference(), reference()) -> ok | {error, term()}. remove_reader(_LoopRef, _FdRef) -> ?NIF_STUB. %% @doc Register a file descriptor for write monitoring. -spec add_writer(reference(), integer(), non_neg_integer()) -> {ok, reference()} | {error, term()}. add_writer(_LoopRef, _Fd, _CallbackId) -> ?NIF_STUB. %% @doc Stop monitoring a file descriptor for writes. %% FdRef must be the same resource returned by add_writer. -spec remove_writer(reference(), reference()) -> ok | {error, term()}. remove_writer(_LoopRef, _FdRef) -> ?NIF_STUB. %% @doc Schedule a timer callback. %% DelayMs is the delay in milliseconds. -spec call_later(reference(), integer(), non_neg_integer()) -> {ok, reference()} | {error, term()}. call_later(_LoopRef, _DelayMs, _CallbackId) -> ?NIF_STUB. %% @doc Cancel a pending timer. -spec cancel_timer(reference(), reference()) -> ok | {error, term()}. cancel_timer(_LoopRef, _TimerRef) -> ?NIF_STUB. %% @doc Wait for events with timeout. %% Returns the number of pending events. %% This is a dirty NIF that releases the GIL while waiting. -spec poll_events(reference(), integer()) -> {ok, integer()} | {error, term()}. poll_events(_LoopRef, _TimeoutMs) -> ?NIF_STUB. %% @doc Get list of pending events. %% Returns [{CallbackId, Type}] where Type is read | write | timer. -spec get_pending(reference()) -> [{non_neg_integer(), read | write | timer}]. get_pending(_LoopRef) -> ?NIF_STUB. %% @doc Dispatch a callback from the router. %% Called when an fd becomes ready. -spec dispatch_callback(reference(), non_neg_integer(), read | write | timer) -> ok. dispatch_callback(_LoopRef, _CallbackId, _Type) -> ?NIF_STUB. %% @doc Dispatch a timer callback. %% Called when a timer expires. -spec dispatch_timer(reference(), non_neg_integer()) -> ok. dispatch_timer(_LoopRef, _CallbackId) -> ?NIF_STUB. %% @doc Get callback ID from an fd resource. %% Type is read or write. -spec get_fd_callback_id(reference(), read | write) -> non_neg_integer() | undefined. get_fd_callback_id(_FdRes, _Type) -> ?NIF_STUB. %% @doc Re-register an fd resource for read monitoring. %% Called after an event is delivered since enif_select is one-shot. -spec reselect_reader(reference(), reference()) -> ok | {error, term()}. reselect_reader(_LoopRef, _FdRes) -> ?NIF_STUB. %% @doc Re-register an fd resource for write monitoring. %% Called after an event is delivered since enif_select is one-shot. -spec reselect_writer(reference(), reference()) -> ok | {error, term()}. reselect_writer(_LoopRef, _FdRes) -> ?NIF_STUB. %% @doc Re-register an fd resource for read monitoring using fd_res->loop. %% This variant doesn't require LoopRef since the fd resource already %% has a back-reference to its parent loop. -spec reselect_reader_fd(reference()) -> ok | {error, term()}. reselect_reader_fd(_FdRes) -> ?NIF_STUB. %% @doc Re-register an fd resource for write monitoring using fd_res->loop. %% This variant doesn't require LoopRef since the fd resource already %% has a back-reference to its parent loop. -spec reselect_writer_fd(reference()) -> ok | {error, term()}. reselect_writer_fd(_FdRes) -> ?NIF_STUB. %%% ============================================================================ %%% FD Lifecycle Management (uvloop-like API) %%% ============================================================================ %% @doc Handle a select event (dispatch + auto-reselect). %% Called by py_event_worker when receiving {select, FdRes, Ref, ready_input/output}. %% This combines get_fd_callback_id + dispatch_callback + reselect into one NIF call. %% Type: read | write -spec handle_fd_event(reference(), read | write) -> ok | {error, term()}. handle_fd_event(_FdRef, _Type) -> ?NIF_STUB. %% @doc Handle FD event and immediately reselect for next event. %% Combined operation that eliminates one roundtrip - dispatch and reselect in one NIF call. %% Type: read | write -spec handle_fd_event_and_reselect(reference(), read | write) -> ok | {error, term()}. handle_fd_event_and_reselect(_FdRef, _Type) -> ?NIF_STUB. %% @doc Stop/pause read monitoring without closing the FD. %% The watcher still exists and can be restarted with start_reader. -spec stop_reader(reference()) -> ok | {error, term()}. stop_reader(_FdRef) -> ?NIF_STUB. %% @doc Start/resume read monitoring on an existing watcher. %% Must have been created with add_reader first. -spec start_reader(reference()) -> ok | {error, term()}. start_reader(_FdRef) -> ?NIF_STUB. %% @doc Stop/pause write monitoring without closing the FD. %% The watcher still exists and can be restarted with start_writer. -spec stop_writer(reference()) -> ok | {error, term()}. stop_writer(_FdRef) -> ?NIF_STUB. %% @doc Start/resume write monitoring on an existing watcher. %% Must have been created with add_writer first. -spec start_writer(reference()) -> ok | {error, term()}. start_writer(_FdRef) -> ?NIF_STUB. %% @doc Cancel read monitoring (legacy alias for stop_reader). %% Kept for backward compatibility. -spec cancel_reader(reference(), reference()) -> ok | {error, term()}. cancel_reader(_LoopRef, _FdRef) -> ?NIF_STUB. %% @doc Cancel write monitoring (legacy alias for stop_writer). %% Kept for backward compatibility. -spec cancel_writer(reference(), reference()) -> ok | {error, term()}. cancel_writer(_LoopRef, _FdRef) -> ?NIF_STUB. %% @doc Explicitly close an FD with proper lifecycle cleanup. %% Transfers ownership and triggers proper cleanup via ERL_NIF_SELECT_STOP. %% Safe to call multiple times (idempotent). -spec close_fd(reference()) -> ok | {error, term()}. close_fd(_FdRef) -> ?NIF_STUB. %%% ============================================================================ %%% Test Helper Functions (for fd monitoring tests using pipes) %%% ============================================================================ %% @doc Create a pipe for testing fd monitoring. %% Returns {ReadFd, WriteFd} where both are non-blocking. -spec create_test_pipe() -> {ok, {integer(), integer()}} | {error, term()}. create_test_pipe() -> ?NIF_STUB. %% @doc Duplicate a file descriptor. %% Creates an independent copy of the fd. -spec dup_fd(integer()) -> {ok, integer()} | {error, term()}. dup_fd(_Fd) -> ?NIF_STUB. %% @doc Close a test file descriptor. -spec close_test_fd(integer()) -> ok | {error, term()}. close_test_fd(_Fd) -> ?NIF_STUB. %% @doc Write binary data to a test file descriptor. -spec write_test_fd(integer(), binary()) -> ok | {error, term()}. write_test_fd(_Fd, _Data) -> ?NIF_STUB. %% @doc Read data from a test file descriptor. -spec read_test_fd(integer(), integer()) -> {ok, binary()} | {error, term()}. read_test_fd(_Fd, _MaxSize) -> ?NIF_STUB. %%% ============================================================================ %%% TCP Test Helper Functions %%% ============================================================================ %% @doc Create a TCP listener socket for testing. %% Port 0 will cause the OS to assign an ephemeral port. %% Returns {ListenFd, ActualPort}. -spec create_test_tcp_listener(integer()) -> {ok, {integer(), integer()}} | {error, term()}. create_test_tcp_listener(_Port) -> ?NIF_STUB. %% @doc Accept a connection on a TCP listener socket. %% Returns the client socket fd. -spec accept_test_tcp(integer()) -> {ok, integer()} | {error, term()}. accept_test_tcp(_ListenFd) -> ?NIF_STUB. %% @doc Connect to a TCP server. %% Host should be a binary like `<<"127.0.0.1">>'. -spec connect_test_tcp(binary(), integer()) -> {ok, integer()} | {error, term()}. connect_test_tcp(_Host, _Port) -> ?NIF_STUB. %%% ============================================================================ %%% UDP Test Helper Functions %%% ============================================================================ %% @doc Create a UDP socket for testing. %% Port 0 will cause the OS to assign an ephemeral port. %% Returns {Fd, ActualPort}. -spec create_test_udp_socket(integer()) -> {ok, {integer(), integer()}} | {error, term()}. create_test_udp_socket(_Port) -> ?NIF_STUB. %% @doc Receive data from a UDP socket. %% Returns the data and source address: {Data, {Host, Port}}. -spec recvfrom_test_udp(integer(), integer()) -> {ok, {binary(), {binary(), integer()}}} | {error, term()}. recvfrom_test_udp(_Fd, _MaxSize) -> ?NIF_STUB. %% @doc Send data to a UDP destination address. %% Host should be a binary like `<<"127.0.0.1">>'. -spec sendto_test_udp(integer(), binary(), binary(), integer()) -> ok | {error, term()}. sendto_test_udp(_Fd, _Data, _Host, _Port) -> ?NIF_STUB. %% @doc Enable or disable SO_BROADCAST on a UDP socket. -spec set_udp_broadcast(integer(), boolean()) -> ok | {error, term()}. set_udp_broadcast(_Fd, _Enable) -> ?NIF_STUB. %%% ============================================================================ %%% Python Event Loop Integration %%% ============================================================================ %% @doc Set the global Python event loop. %% This makes the event loop available to Python asyncio code. -spec set_python_event_loop(reference()) -> ok | {error, term()}. set_python_event_loop(_LoopRef) -> ?NIF_STUB. %% @doc Set the event loop isolation mode. %% - global: all ErlangEventLoop instances share the global loop (default) %% - per_loop: each ErlangEventLoop creates its own isolated native loop -spec set_isolation_mode(global | per_loop) -> ok | {error, term()}. set_isolation_mode(_Mode) -> ?NIF_STUB. %% @doc Set the shared worker PID for task_ready notifications. %% The worker receives task_ready messages from dispatch_timer and other %% event sources to trigger process_ready_tasks. -spec set_shared_worker(pid()) -> ok | {error, term()}. set_shared_worker(_WorkerPid) -> ?NIF_STUB. %%% ============================================================================ %%% Worker Pool %%% ============================================================================ %% @doc Start the worker pool with the specified number of workers. %% %% Creates a pool of worker threads that process Python operations. %% Each worker may have its own subinterpreter (Python 3.12+) for true %% parallelism, or share the GIL with optimized batching. %% %% If NumWorkers is 0, the pool will use the number of CPU cores. %% %% @param NumWorkers Number of worker threads (0 = auto-detect) %% @returns ok on success, or {error, Reason} -spec pool_start(non_neg_integer()) -> ok | {error, term()}. pool_start(_NumWorkers) -> ?NIF_STUB. %% @doc Stop the worker pool. %% %% Signals all workers to shut down and waits for them to terminate. %% Any pending requests will receive {error, pool_shutdown}. %% %% @returns ok -spec pool_stop() -> ok. pool_stop() -> ?NIF_STUB. %% @doc Submit a request to the worker pool. %% %% Submits an asynchronous request to the pool. The caller will receive %% a {py_response, RequestId, Result} message when the request completes. %% %% Request types and arguments: %%