Skip to content

R07. The stable ABI - #187

Merged
tamnd merged 3 commits into
mainfrom
r07-the-stable-abi
Sep 6, 2026
Merged

R07. The stable ABI#187
tamnd merged 3 commits into
mainfrom
r07-the-stable-abi

Conversation

@tamnd

@tamnd tamnd commented Sep 6, 2026

Copy link
Copy Markdown
Owner

R06 sorted out which C functions an extension is allowed to call. R07 picks up where that leaves off, when the thing you compiled turns up on somebody else's machine as a file, and asks what stands between that file and a working module.

The answer is two gates, and the point of the lesson is that both of them can be driven from Python rather than described.

The first gate is the file name. It is read before anything is opened, so it costs one stat call, and the tag has to be one of the six entries in the C array at Python/dynload_shlib.c:39-60. The lesson drives it by writing empty files into temporary directories and asking PathFinder.find_spec what it sees. Nothing loads, because the files are empty, but the rule falls out completely: a free threaded build takes abi3t and refuses abi3, an ordinary 3.15 build takes both and prefers abi3, and 3.14 has never heard of abi3t. Putting all five candidates plus a demo.py in one directory shows that the order comes straight from that array, and that a compiled file quietly shadows a Python file of the same name.

There is a small piece of history in here too. The 3 in abi3 is not a Python version. It is PYTHON_ABI_VERSION, which Include/patchlevel.h:53-62 says has not moved since 2010, sitting next to PYTHON_API_VERSION 1013, which has not moved since 2006.

The second gate is PyABIInfo, twelve bytes and new in 3.15. This is the part I was happiest to find. PyABIInfo_Check is a plain PyAPI_FUNC in the public headers, and the struct is five integers, so ctypes can build one and call the real check. Eight hypothetical extensions go in, four refusals come back, and they print with CPython's own wording rather than a paraphrase, because ctypes.pythonapi notices the ImportError and raises it.

The lesson is also honest about how weak that gate is. It is opt in, an extension that does not carry the struct is not checked at all, and the extension's own initialisation has already run by the time the check happens. Doc/c-api/stable.rst:107-113 says as much in the documentation.

The stable ABI as a list that grew. The last two cells walk every public header as a preprocessor stack and date each declaration by the version gate around it. 173 functions added since 3.2, with the busiest releases being 3.13, 3.14 and 3.15, and the 528 that were already there in 3.2 not on the chart at all because there is no version to gate on. The same scan pointed at four names gives the file, the line and the version for each, which includes the mildly funny result that PyABIInfo_Check is itself gated on 3.15.

Two Tier 1 recordings run both gates in containers, one on a release build and one on a build made with --disable-gil. Six file name tags become four and four refusals become six. Both gates tightened, and they tightened in the same direction, which is the whole argument for abi3t in two files.

Browser story. Ten cells, all ten run clean under Pyodide, and six of them do real work there rather than printing a guard message. Only the two PyABIInfo cells (3.15 only) and the two header reading cells (Pyodide ships no C headers, see #186) are guarded. That is a good deal better than R06, where five cells sat behind the header flag.

Six diagrams, all under 1100 px. Three new glossary terms: extension module, SOABI, PyABIInfo. Word counts are hook 106, tour 1493, lesson 2080, all inside the caps.

Every citation is checked against the pin, the notebook executes clean on 3.15.0rc1 and 3.14, and the differs= notes were written from actual 3.14 runs rather than guessed.

The two gates an extension has to get past before it is a module, and
both of them driven from Python rather than described.

The first gate is the file name. It is read before anything is opened,
and the tag has to be one of the handful in the C array at
Python/dynload_shlib.c. The lesson drives it by writing empty files into
temporary directories and asking PathFinder what it sees, which makes
the whole rule fall out: a free threaded build takes abi3t and refuses
abi3, an ordinary 3.15 build takes both and prefers abi3, and 3.14 has
never heard of abi3t. Putting all of them in one directory shows the
order comes from the array and that a compiled file shadows a Python
one.

The second gate is PyABIInfo, twelve bytes and new in 3.15. Because
PyABIInfo_Check is a plain exported function the struct can be built in
ctypes and handed to the real thing, so the four refusals print with
CPython's own wording rather than a paraphrase.

The stable ABI itself is dated by reading the preprocessor gates around
the declarations: 173 functions added since 3.2, with the busiest
releases being the recent ones.

Two Tier 1 recordings run both gates on a release build and on a build
made with --disable-gil, where six tags become four and four refusals
become six. That is the argument for abi3t in two files.

Six diagrams, three glossary terms, and ten of the notebook's cells run
under Pyodide, with only the PyABIInfo cells and the two header reading
cells sitting behind a guard.
@tamnd tamnd added this to the M8 Concurrency and runtime milestone Sep 6, 2026
@tamnd tamnd added kind/lesson A chapter: prose, notebook, experiments, boss fight area/runtime Startup, shutdown, import, extension modules and the C API labels Sep 6, 2026
The two R07 experiments printed file name tags with the build's machine
in them, which is aarch64-linux-gnu when they are recorded here and
x86_64-linux-gnu when CI verifies them, so tier1 verify failed on every
line that carried a tag.

The machine says nothing about either gate, so the program now replaces
it with the word PLATFORM before printing, and the docstring says so.
Both experiments re-recorded. The three measured lines are unchanged.
The lesson embeds both recordings, so taking the machine out of them
changed the notebook too.
@tamnd
tamnd merged commit 9c75c63 into main Sep 6, 2026
16 checks passed
@tamnd
tamnd deleted the r07-the-stable-abi branch September 6, 2026 11:16
@tamnd tamnd mentioned this pull request Sep 6, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/runtime Startup, shutdown, import, extension modules and the C API kind/lesson A chapter: prose, notebook, experiments, boss fight

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant