Skip to content

Commit 72128fe

Browse files
authored
Merge pull request #74 from benoitc/docs-architecture
Add architecture, code map and glossary
2 parents b8e5cbf + c8f77f0 commit 72128fe

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)