Skip to content

Commit c8f77f0

Browse files
committed
Add architecture, code map and glossary
A map of the code: processes and threads, the life of a call in each context mode, the paths Python uses to call Erlang, which code is live and which is legacy, and one meaning per overloaded word (worker, context, pool). The stale file list in the NIF headers now points at these pages. Also completes the 4.2.0 changelog.
1 parent b8e5cbf commit c8f77f0

10 files changed

Lines changed: 536 additions & 54 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
such a region with ring backpressure, usable as `wsgi.input` in isolated
3535
contexts. Handles are plain terms and travel inside any argument or result;
3636
`py_shm:read_only/1` and `new(Size, #{writable => false})` hand Python a
37-
read-only mapping.
37+
read-only mapping. `py_buffer:write/3` takes a timeout for the case where
38+
the ring is full and nobody reads (default 30 s).
3839
- `py:python_executable/0`, `py:kill/1`, `py_nif:os_kill/2`.
3940
- `py_isolated` is a `gen_statem` (states `idle`, `{busy, Id}`, `looping`,
4041
`stopping_loop`, `{restarting, Reason}`): `sys:get_state/1` and
@@ -48,6 +49,11 @@
4849
storms, loop churn, 60 s mixed workload with resource counters checked.
4950
- Guide: `docs/isolated.md`, with what each of the three modes guarantees.
5051

52+
### Fixed
53+
54+
- `pthread_timedjoin_np` was called without `_GNU_SOURCE`, an implicit
55+
declaration on Linux that newer compilers reject.
56+
5157
## 4.1.0 (2026-08-15)
5258

5359
### Added

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,7 @@ py:execution_mode(). %% => worker | owngil
654654

655655
## Documentation
656656

657+
- [Architecture](docs/architecture.md), [Code map](docs/code-map.md), [Glossary](docs/glossary.md) - how the pieces fit
657658
- [Getting Started](docs/getting-started.md)
658659
- [Process-Bound Environments](docs/process-bound-envs.md) - Isolated Python state per Erlang process
659660
- [AI Integration Guide](docs/ai-integration.md)

‎c_src/README.md‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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.

‎c_src/py_nif.c‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,10 @@
2929
* - Resource types for Python objects to ensure proper cleanup
3030
* - Dirty NIF flags for GIL-holding operations
3131
*
32-
* This file is the main entry point. It includes the following modules:
33-
* - py_nif.h: Shared header with types and declarations
34-
* - py_convert.c: Type conversion (Python <-> Erlang)
35-
* - py_exec.c: Python execution and GIL management
36-
* - py_callback.c: Callback system and asyncio support
32+
* This file is the main entry point and the single translation unit: it
33+
* includes the other .c files (see "Include module implementations"). The
34+
* file map is c_src/README.md; the request lifecycle per context mode is
35+
* docs/architecture.md.
3736
*/
3837

3938
/* pthread_timedjoin_np (used to bound the owngil worker join on Linux)

‎c_src/py_nif.h‎

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,54 +21,19 @@
2121
*
2222
* @mainpage Python-Erlang NIF Integration
2323
*
24-
* @section intro_sec Introduction
25-
*
26-
* This NIF (Native Implemented Function) library provides seamless integration
27-
* between Erlang/OTP and Python. It embeds a Python interpreter within the
28-
* Erlang VM and provides bidirectional communication capabilities.
29-
*
30-
* @section arch_sec Architecture
31-
*
32-
* The implementation follows a modular design with four main components:
33-
*
34-
* - **py_nif.h** - Shared types, macros, and declarations
35-
* - **py_convert.c** - Bidirectional type conversion (Python ↔ Erlang)
36-
* - **py_exec.c** - Python execution engine and GIL management
37-
* - **py_callback.c** - Erlang callback support and asyncio integration
38-
*
39-
* @section modes_sec Execution Modes
40-
*
41-
* The library supports three execution modes based on Python version:
42-
*
43-
* | Mode | Python Version | Description |
44-
* |------|----------------|-------------|
45-
* | FREE_THREADED | 3.13+ (no-GIL) | Direct execution without GIL |
46-
* | SUBINTERP | 3.12+ | Per-interpreter GIL isolation |
47-
* | MULTI_EXECUTOR | Any | Multiple executor threads with GIL |
48-
*
49-
* @section gil_sec GIL Management
50-
*
51-
* The GIL (Global Interpreter Lock) is managed following PyO3/Granian patterns:
52-
*
53-
* - `Py_BEGIN_ALLOW_THREADS` / `Py_END_ALLOW_THREADS` around blocking ops
54-
* - Executor threads hold the GIL and process queued requests
55-
* - Dirty I/O schedulers are used for Python-calling NIFs
56-
*
57-
* @section callback_sec Callback Mechanism
58-
*
59-
* Python code can call back to Erlang using a suspension/resume pattern:
60-
*
61-
* 1. Python calls `erlang.call('func', args)`
62-
* 2. NIF raises `SuspensionRequired` exception
63-
* 3. Dirty scheduler is released, callback sent to Erlang
64-
* 4. Erlang processes callback, calls `resume_callback/2`
65-
* 5. Python execution resumes with cached result
66-
*
67-
* @section mem_sec Memory Management
68-
*
69-
* - Erlang resources wrap Python objects (prevent GC)
70-
* - Thread-local storage for callback context
71-
* - Proper cleanup in resource destructors
24+
* This NIF embeds CPython in the Erlang VM. The map of the code, the life
25+
* of a call in each context mode, the callback paths and the locking rules
26+
* are documented in docs/architecture.md, c_src/README.md and
27+
* docs/glossary.md; keep those current instead of this comment.
28+
*
29+
* In one paragraph: py_nif.c is the single translation unit and includes
30+
* the other .c files; a context (py_context_t) owns a request queue and a
31+
* pthread that runs Python for it (worker mode: main interpreter, shared
32+
* GIL; owngil mode: a sub-interpreter with its own GIL); Erlang enqueues
33+
* through nif_context_call_async and receives {py_result, Ref, Result};
34+
* Python calls Erlang through erlang_call_impl (py_callback.c), by
35+
* suspension in worker mode and a blocking pipe in owngil mode. Isolated
36+
* mode runs Python in a child process and uses no C code beyond os_kill.
7237
*/
7338

7439
#ifndef PY_NIF_H

0 commit comments

Comments
 (0)