|
| 1 | +# c_src |
| 2 | + |
| 3 | +The NIF that embeds CPython in the VM. One translation unit: `py_nif.c` |
| 4 | +`#include`s every other `.c` file (see the "Include module implementations" |
| 5 | +section near line 265), so build with `rebar3 compile` (CMake through |
| 6 | +`do_cmake.sh` / `do_build.sh`), never a single file. Headers declare what |
| 7 | +the included files share. |
| 8 | + |
| 9 | +Read `docs/architecture.md` first for how a call travels; this file says |
| 10 | +where things are. |
| 11 | + |
| 12 | +## Files |
| 13 | + |
| 14 | +| File | What it owns | Notes | |
| 15 | +|---|---|---| |
| 16 | +| `py_nif.h` | All shared types: `py_context_t` and its request queue, request types, callback and suspension state, runtime state machine, atoms, globals, declarations | 2.4k lines. The struct comments carry the locking rules; read `py_context_t` before touching threads | |
| 17 | +| `py_nif.c` | Runtime init and finalize, resource types, context create/destroy, the request queue, `worker_context_thread_main` and `owngil_context_thread_main`, `owngil_execute_*` (used by both thread kinds), the `nif_context_*` NIFs, process-local envs, `py_ref`, the NIF function table at the end | Sections are banner-separated; `grep -n '^ \* ===\|^/\* ==='` lists them | |
| 18 | +| `py_convert.c` | `py_to_term`, `term_to_py`, depth limits, tagged tuples (`{bytes, B}`, `{'$py_shm', ...}`), error tuples `{error, {Type, Msg}}` | The type mapping tables in the comments are the reference for `_etf.py` | |
| 19 | +| `py_exec.c` | Executing a call/eval/exec with suspension support; the legacy single executor thread | | |
| 20 | +| `py_callback.c` | The `erlang` Python module: `call`, `send`, `whereis`, `schedule*`, `Atom`/`Pid`/`Ref` types, callback delivery paths (suspension, blocking pipe, async pipe), channel and shared-dict methods, callback name registry | `erlang_call_impl` documents the path precedence | |
| 21 | +| `py_thread_worker.c` | Python threads calling Erlang through `py_thread_handler` | | |
| 22 | +| `py_subinterp_thread.c/.h` | Thread pool of sub-interpreters (owngil contexts, loop pools) | | |
| 23 | +| `py_event_loop.c/.h` | `ErlangEventLoop` support: `enif_select` readers and writers, timers, task injection into loops, reactor dispatch, fd registry, Python module `py_event_loop`; plus test-only fd/TCP/UDP NIFs (section "Test Helper Functions") | Largest file | |
| 24 | +| `py_channel.c/.h`, `py_buffer.c/.h`, `py_reactor_buffer.c/.h`, `py_shared_dict.c` | Resources with a Python-facing object each | | |
| 25 | +| `py_logging.c` | Logging and tracing NIFs | | |
| 26 | +| `py_mem_limit.c` | obmalloc arena accounting for owngil memory caps | | |
| 27 | +| `py_worker_pool.c/.h` | Legacy pool, no caller in `src/` | Candidate for removal | |
| 28 | +| `py_util.c/.h` | Macros, small helpers | | |
| 29 | + |
| 30 | +## Where the live paths are |
| 31 | + |
| 32 | +- `py:call/3` in worker or owngil mode: `nif_context_call_async` (`py_nif.c`) |
| 33 | + enqueues; `worker_context_thread_main` or `owngil_context_thread_main` |
| 34 | + dequeues and calls `owngil_execute_request`; the reply goes out as |
| 35 | + `{py_result, Ref, Result}`. |
| 36 | +- `erlang.call` from Python: `erlang_call_impl` (`py_callback.c`). |
| 37 | +- Interrupt: `nif_context_interrupt` (`py_nif.c`), `interrupt_mutex` rules on |
| 38 | + `py_context_t`. |
| 39 | +- Type conversion: `py_to_term` / `term_to_py` (`py_convert.c`). |
| 40 | +- Isolated mode has no C code of its own: `os_kill` is the only NIF it uses. |
| 41 | + |
| 42 | +## Rules that are easy to break |
| 43 | + |
| 44 | +- Only the context's thread touches the context's Python objects. NIFs |
| 45 | + called from Erlang processes enqueue requests and return; they do not run |
| 46 | + Python for a context that has a thread. |
| 47 | +- `Py_BEGIN_ALLOW_THREADS` around every blocking wait; never block on an |
| 48 | + Erlang-side resource with the GIL held. |
| 49 | +- Never call a Future method while holding `async_futures_mutex` |
| 50 | + (`py_callback.c`, comment above the struct explains the pattern). |
| 51 | +- `interrupt_mutex` is taken only by threads that do not hold the GIL. |
| 52 | +- `queue_mutex` protects the request queue of a context and is taken before |
| 53 | + a request's own mutex (`ctx_queue_cancel_all`), never the other way. |
| 54 | +- A NIF that can block or run Python is registered with a dirty scheduler |
| 55 | + flag in the table at the end of `py_nif.c`. |
| 56 | + |
| 57 | +## Adding a NIF |
| 58 | + |
| 59 | +1. Implement `static ERL_NIF_TERM nif_x(ErlNifEnv*, int, const ERL_NIF_TERM[])` |
| 60 | + next to related code. |
| 61 | +2. Add `{"x", Arity, nif_x, Flags}` to `nif_funcs[]` at the end of `py_nif.c`. |
| 62 | +3. Add the stub and its `-spec` and doc to `src/py_nif.erl`. |
| 63 | +4. Cover it in a suite; `rebar3 dialyzer` and `rebar3 xref` must stay clean. |
| 64 | + |
| 65 | +## Legacy code, for orientation |
| 66 | + |
| 67 | +Not on the path of contexts created today: the `worker_*` NIFs and |
| 68 | +"Worker management" section, `async_worker_*` NIFs (return `deprecated`), |
| 69 | +the "Legacy mode" inline branches in `nif_context_call/eval/exec`, |
| 70 | +`py_worker_pool.c`, and the `cancel_reader/writer` aliases. When in doubt, |
| 71 | +follow `nif_context_call_async` and ignore the rest. |
0 commit comments