diff --git a/GLOSSARY.md b/GLOSSARY.md index 260ae20..a67ab4c 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -8,7 +8,7 @@ This file is generated from `pyxray/src/pyxray/glossary.py`. Edit that and run ` ## Index -[ASDL](#asdl) | [Argument Clinic](#argument-clinic) | [C stack](#c-stack) | [C3 linearization](#c3-linearization) | [DEBUG_SAVEALL](#debug_saveall) | [DISABLE](#disable) | [EXTENDED_ARG](#extended_arg) | [GC pre header](#gc-pre-header) | [GIL](#gil) | [JIT](#jit) | [MRO](#mro) | [PEG parser](#peg-parser) | [PYTHONMALLOC](#pythonmalloc) | [PyVarObject](#pyvarobject) | [Pyodide](#pyodide) | [WebAssembly](#webassembly) | [abi flags](#abi-flags) | [abstract interpreter](#abstract-interpreter) | [abstract syntax tree](#abstract-syntax-tree) | [adaptive counter](#adaptive-counter) | [adaptive instruction](#adaptive-instruction) | [allocator domain](#allocator-domain) | [arena](#arena) | [assembler](#assembler) | [asynchronous exception](#asynchronous-exception) | [attach and detach](#attach-and-detach) | [backtrace](#backtrace) | [basic block](#basic-block) | [biased reference counting](#biased-reference-counting) | [binding](#binding) | [block](#block) | [blurb](#blurb) | [borrowed reference](#borrowed-reference) | [bound method](#bound-method) | [bytecode](#bytecode) | [cached hash](#cached-hash) | [calling convention](#calling-convention) | [cases generator](#cases-generator) | [cell](#cell) | [class cell](#class-cell) | [closure](#closure) | [code generation](#code-generation) | [code object](#code-object) | [code point](#code-point) | [code unit](#code-unit) | [coding cookie](#coding-cookie) | [cold block](#cold-block) | [collection threshold](#collection-threshold) | [compact dict](#compact-dict) | [compact int](#compact-int) | [compact string](#compact-string) | [computed goto](#computed-goto) | [configure](#configure) | [constant folding](#constant-folding) | [control flow graph](#control-flow-graph) | [copy and patch](#copy-and-patch) | [critical section](#critical-section) | [cross interpreter data](#cross-interpreter-data) | [cycle collector](#cycle-collector) | [daemon thread](#daemon-thread) | [data descriptor](#data-descriptor) | [data stack](#data-stack) | [deallocation](#deallocation) | [debug build](#debug-build) | [debug hooks](#debug-hooks) | [deferred reference counting](#deferred-reference-counting) | [deoptimization](#deoptimization) | [descriptor](#descriptor) | [devguide](#devguide) | [digit array](#digit-array) | [dispatch](#dispatch) | [dispatch table](#dispatch-table) | [environment changed](#environment-changed) | [eval breaker](#eval-breaker) | [eval loop](#eval-loop) | [evaluation order](#evaluation-order) | [exact type check](#exact-type-check) | [exception table](#exception-table) | [executor](#executor) | [f string](#f-string) | [finalized bit](#finalized-bit) | [finalizer](#finalizer) | [frame](#frame) | [frame object](#frame-object) | [free list](#free-list) | [free threaded build](#free-threaded-build) | [free variable](#free-variable) | [frozen module](#frozen-module) | [gdb](#gdb) | [generated file](#generated-file) | [generation](#generation) | [global import lock](#global-import-lock) | [grammar](#grammar) | [guard](#guard) | [header file](#header-file) | [heap type](#heap-type) | [immortal object](#immortal-object) | [immortalization](#immortalization) | [import bootstrap](#import-bootstrap) | [import placeholder](#import-placeholder) | [indent and dedent](#indent-and-dedent) | [inline cache](#inline-cache) | [inline values](#inline-values) | [instance dictionary](#instance-dictionary) | [instruction](#instruction) | [instruction DSL](#instruction-dsl) | [instruction pointer](#instruction-pointer) | [instrumented instruction](#instrumented-instruction) | [interning](#interning) | [interpreter pool](#interpreter-pool) | [interpreter state](#interpreter-state) | [lazy import](#lazy-import) | [left recursion](#left-recursion) | [line table](#line-table) | [loader state](#loader-state) | [magic number](#magic-number) | [mark alive pass](#mark-alive-pass) | [marshal](#marshal) | [meta path finder](#meta-path-finder) | [metaclass](#metaclass) | [micro operation](#micro-operation) | [mimalloc](#mimalloc) | [mimalloc heap](#mimalloc-heap) | [module alias](#module-alias) | [module lock](#module-lock) | [module spec](#module-spec) | [monitoring events](#monitoring-events) | [new reference](#new-reference) | [object](#object) | [object header](#object-header) | [obmalloc](#obmalloc) | [oparg](#oparg) | [opcode](#opcode) | [optimistic read](#optimistic-read) | [over allocation](#over-allocation) | [ownership](#ownership) | [parser generator](#parser-generator) | [path configuration](#path-configuration) | [path entry finder](#path-entry-finder) | [pdb](#pdb) | [pending call](#pending-call) | [per object lock](#per-object-lock) | [periodic check](#periodic-check) | [permanent generation](#permanent-generation) | [pointer](#pointer) | [pool](#pool) | [pool header](#pool-header) | [probe sequence](#probe-sequence) | [product type](#product-type) | [profile guided optimization](#profile-guided-optimization) | [pseudo instruction](#pseudo-instruction) | [pyc file](#pyc-file) | [pyconfig](#pyconfig) | [quickening](#quickening) | [race condition](#race-condition) | [reentrancy](#reentrancy) | [reference count](#reference-count) | [reference count contention](#reference-count-contention) | [reference cycle](#reference-cycle) | [reference leak](#reference-leak) | [referrer](#referrer) | [regen](#regen) | [regrtest](#regrtest) | [replacement field](#replacement-field) | [resource](#resource) | [resurrection](#resurrection) | [runtime state](#runtime-state) | [safe memory reclamation](#safe-memory-reclamation) | [safe path](#safe-path) | [scope](#scope) | [segment](#segment) | [segmentation fault](#segmentation-fault) | [shareable object](#shareable-object) | [short circuiting](#short-circuiting) | [side exit](#side-exit) | [signal handler](#signal-handler) | [single character cache](#single-character-cache) | [size class](#size-class) | [slot](#slot) | [slot wrapper](#slot-wrapper) | [small int cache](#small-int-cache) | [small integer cache](#small-integer-cache) | [soft keyword](#soft-keyword) | [specialization](#specialization) | [specialization family](#specialization-family) | [split table](#split-table) | [stack depth](#stack-depth) | [stack effect](#stack-effect) | [stack reference](#stack-reference) | [stateless function](#stateless-function) | [static object](#static-object) | [static type](#static-type) | [stencil](#stencil) | [stolen reference](#stolen-reference) | [stop the world](#stop-the-world) | [string kind](#string-kind) | [struct](#struct) | [subinterpreter](#subinterpreter) | [sum type](#sum-type) | [switch interval](#switch-interval) | [symbol table](#symbol-table) | [symbol table pass](#symbol-table-pass) | [t string](#t-string) | [tagged integer](#tagged-integer) | [tagged pointer](#tagged-pointer) | [tail call](#tail-call) | [test case](#test-case) | [thread safety declaration](#thread-safety-declaration) | [thread state](#thread-state) | [tier one](#tier-one) | [tier two](#tier-two) | [token](#token) | [tokenizer](#tokenizer) | [tool id](#tool-id) | [trace](#trace) | [trace function](#trace-function) | [traceback](#traceback) | [transient GIL](#transient-gil) | [two phase initialisation](#two-phase-initialisation) | [type object](#type-object) | [underflow](#underflow) | [unwinding](#unwinding) | [value stack](#value-stack) | [varint](#varint) | [watcher](#watcher) | [weak reference](#weak-reference) | [weakref callback](#weakref-callback) | [weakref offset](#weakref-offset) | [zero cost exceptions](#zero-cost-exceptions) +[ASDL](#asdl) | [Argument Clinic](#argument-clinic) | [C API](#c-api) | [C stack](#c-stack) | [C3 linearization](#c3-linearization) | [DEBUG_SAVEALL](#debug_saveall) | [DISABLE](#disable) | [EXTENDED_ARG](#extended_arg) | [GC pre header](#gc-pre-header) | [GIL](#gil) | [JIT](#jit) | [MRO](#mro) | [PEG parser](#peg-parser) | [PYTHONMALLOC](#pythonmalloc) | [PyAPI_FUNC](#pyapi_func) | [PyVarObject](#pyvarobject) | [Pyodide](#pyodide) | [WebAssembly](#webassembly) | [abi flags](#abi-flags) | [abstract interpreter](#abstract-interpreter) | [abstract syntax tree](#abstract-syntax-tree) | [adaptive counter](#adaptive-counter) | [adaptive instruction](#adaptive-instruction) | [allocator domain](#allocator-domain) | [arena](#arena) | [assembler](#assembler) | [asynchronous exception](#asynchronous-exception) | [attach and detach](#attach-and-detach) | [backtrace](#backtrace) | [basic block](#basic-block) | [biased reference counting](#biased-reference-counting) | [binding](#binding) | [block](#block) | [blurb](#blurb) | [borrowed reference](#borrowed-reference) | [bound method](#bound-method) | [bytecode](#bytecode) | [cached hash](#cached-hash) | [calling convention](#calling-convention) | [cases generator](#cases-generator) | [cell](#cell) | [class cell](#class-cell) | [closure](#closure) | [code generation](#code-generation) | [code object](#code-object) | [code point](#code-point) | [code unit](#code-unit) | [coding cookie](#coding-cookie) | [cold block](#cold-block) | [collection threshold](#collection-threshold) | [compact dict](#compact-dict) | [compact int](#compact-int) | [compact string](#compact-string) | [computed goto](#computed-goto) | [configure](#configure) | [constant folding](#constant-folding) | [control flow graph](#control-flow-graph) | [copy and patch](#copy-and-patch) | [critical section](#critical-section) | [cross interpreter data](#cross-interpreter-data) | [cycle collector](#cycle-collector) | [daemon thread](#daemon-thread) | [data descriptor](#data-descriptor) | [data stack](#data-stack) | [deallocation](#deallocation) | [debug build](#debug-build) | [debug hooks](#debug-hooks) | [deferred reference counting](#deferred-reference-counting) | [deoptimization](#deoptimization) | [descriptor](#descriptor) | [devguide](#devguide) | [digit array](#digit-array) | [dispatch](#dispatch) | [dispatch table](#dispatch-table) | [environment changed](#environment-changed) | [eval breaker](#eval-breaker) | [eval loop](#eval-loop) | [evaluation order](#evaluation-order) | [exact type check](#exact-type-check) | [exception table](#exception-table) | [executor](#executor) | [f string](#f-string) | [finalized bit](#finalized-bit) | [finalizer](#finalizer) | [frame](#frame) | [frame object](#frame-object) | [free list](#free-list) | [free threaded build](#free-threaded-build) | [free variable](#free-variable) | [frozen module](#frozen-module) | [gdb](#gdb) | [generated file](#generated-file) | [generation](#generation) | [global import lock](#global-import-lock) | [grammar](#grammar) | [guard](#guard) | [header file](#header-file) | [heap type](#heap-type) | [immortal object](#immortal-object) | [immortalization](#immortalization) | [import bootstrap](#import-bootstrap) | [import placeholder](#import-placeholder) | [indent and dedent](#indent-and-dedent) | [inline cache](#inline-cache) | [inline values](#inline-values) | [instance dictionary](#instance-dictionary) | [instruction](#instruction) | [instruction DSL](#instruction-dsl) | [instruction pointer](#instruction-pointer) | [instrumented instruction](#instrumented-instruction) | [interning](#interning) | [interpreter pool](#interpreter-pool) | [interpreter state](#interpreter-state) | [lazy import](#lazy-import) | [left recursion](#left-recursion) | [limited API](#limited-api) | [line table](#line-table) | [loader state](#loader-state) | [magic number](#magic-number) | [mark alive pass](#mark-alive-pass) | [marshal](#marshal) | [meta path finder](#meta-path-finder) | [metaclass](#metaclass) | [micro operation](#micro-operation) | [mimalloc](#mimalloc) | [mimalloc heap](#mimalloc-heap) | [module alias](#module-alias) | [module lock](#module-lock) | [module spec](#module-spec) | [monitoring events](#monitoring-events) | [new reference](#new-reference) | [object](#object) | [object header](#object-header) | [obmalloc](#obmalloc) | [oparg](#oparg) | [opcode](#opcode) | [optimistic read](#optimistic-read) | [over allocation](#over-allocation) | [ownership](#ownership) | [parser generator](#parser-generator) | [path configuration](#path-configuration) | [path entry finder](#path-entry-finder) | [pdb](#pdb) | [pending call](#pending-call) | [per object lock](#per-object-lock) | [periodic check](#periodic-check) | [permanent generation](#permanent-generation) | [pointer](#pointer) | [pool](#pool) | [pool header](#pool-header) | [probe sequence](#probe-sequence) | [product type](#product-type) | [profile guided optimization](#profile-guided-optimization) | [pseudo instruction](#pseudo-instruction) | [pyc file](#pyc-file) | [pyconfig](#pyconfig) | [quickening](#quickening) | [race condition](#race-condition) | [reentrancy](#reentrancy) | [reference count](#reference-count) | [reference count contention](#reference-count-contention) | [reference cycle](#reference-cycle) | [reference leak](#reference-leak) | [referrer](#referrer) | [regen](#regen) | [regrtest](#regrtest) | [replacement field](#replacement-field) | [resource](#resource) | [resurrection](#resurrection) | [runtime state](#runtime-state) | [safe memory reclamation](#safe-memory-reclamation) | [safe path](#safe-path) | [scope](#scope) | [segment](#segment) | [segmentation fault](#segmentation-fault) | [shareable object](#shareable-object) | [short circuiting](#short-circuiting) | [side exit](#side-exit) | [signal handler](#signal-handler) | [single character cache](#single-character-cache) | [size class](#size-class) | [slot](#slot) | [slot wrapper](#slot-wrapper) | [small int cache](#small-int-cache) | [small integer cache](#small-integer-cache) | [soft keyword](#soft-keyword) | [specialization](#specialization) | [specialization family](#specialization-family) | [split table](#split-table) | [stable ABI](#stable-abi) | [stack depth](#stack-depth) | [stack effect](#stack-effect) | [stack reference](#stack-reference) | [stateless function](#stateless-function) | [static object](#static-object) | [static type](#static-type) | [stencil](#stencil) | [stolen reference](#stolen-reference) | [stop the world](#stop-the-world) | [string kind](#string-kind) | [struct](#struct) | [subinterpreter](#subinterpreter) | [sum type](#sum-type) | [switch interval](#switch-interval) | [symbol table](#symbol-table) | [symbol table pass](#symbol-table-pass) | [t string](#t-string) | [tagged integer](#tagged-integer) | [tagged pointer](#tagged-pointer) | [tail call](#tail-call) | [test case](#test-case) | [thread safety declaration](#thread-safety-declaration) | [thread state](#thread-state) | [tier one](#tier-one) | [tier two](#tier-two) | [token](#token) | [tokenizer](#tokenizer) | [tool id](#tool-id) | [trace](#trace) | [trace function](#trace-function) | [traceback](#traceback) | [transient GIL](#transient-gil) | [two phase initialisation](#two-phase-initialisation) | [type object](#type-object) | [underflow](#underflow) | [unwinding](#unwinding) | [value stack](#value-stack) | [varint](#varint) | [watcher](#watcher) | [weak reference](#weak-reference) | [weakref callback](#weakref-callback) | [weakref offset](#weakref-offset) | [zero cost exceptions](#zero-cost-exceptions) ## Reading the source @@ -1770,6 +1770,38 @@ This is what runs when you open one of these lessons in a browser without a loca First met in B01. See also [WebAssembly](#webassembly). +### C API + +**The C functions and macros an extension may call to talk to the interpreter.** + +It is not one list but three, and which one you get is decided before you include anything. `Include/` is open to everybody, `Include/cpython/` needs `Py_LIMITED_API` to be undefined, and `Include/internal/` stops the compiler with an error unless you define `Py_BUILD_CORE`. More than half the header lines are in the third directory, the one no extension is meant to open. + +Also written `Python.h`. First met in R06. See also [limited API](#limited-api), [PyAPI_FUNC](#pyapi_func), [stable ABI](#stable-abi). In the source: [`Doc/c-api/stable.rst:21-35@v3.15.0rc1`](https://github.com/python/cpython/blob/v3.15.0rc1/Doc/c-api/stable.rst#L21-L35). + +### limited API + +**The subset of the C API you get when you define Py_LIMITED_API before Python.h.** + +It takes away about a quarter of the public functions, which is the part people notice, and all of the struct layouts, which is the part that costs. With no fields to read, `Py_TYPE` stops being a pointer dereference and becomes a function call, and `Py_DECREF` becomes a call to `_Py_DecRef`. What you buy with that is one built file that keeps working across releases. + +Also written `Py_LIMITED_API`. First met in R06. See also [C API](#c-api), [stable ABI](#stable-abi). In the source: [`Include/object.h:124-126@v3.15.0rc1#_Py_OPAQUE_PYOBJECT`](https://github.com/python/cpython/blob/v3.15.0rc1/Include/object.h#L124-L126). + +### PyAPI_FUNC + +**The macro that marks a declaration as a symbol other binaries may link against.** + +It expands to a compiler attribute asking for default visibility, so a declaration that uses it leaves the shared library and a plain `extern` does not. The internal headers use both spellings, often two lines apart, and the choice is deliberate: CPython's own bundled extensions are separate shared libraries that need to reach back in, and a comment above each export usually names which one. + +First met in R06. See also [C API](#c-api). In the source: [`Include/exports.h:88-93@v3.15.0rc1#PyAPI_FUNC`](https://github.com/python/cpython/blob/v3.15.0rc1/Include/exports.h#L88-L93). + +### stable ABI + +**An extension build that keeps working across releases, marked abi3 in its name.** + +The tiers are a compile time idea and none of them survive into a built extension. The file name does. An interpreter decides what it is willing to load from the suffix alone, so `something.abi3.so` will load on many releases while `something.cpython-315-darwin.so` will load on exactly one. Python 3.15 adds `abi3t` for free threaded builds. + +Also written abi3, abi3t. First met in R06. See also [limited API](#limited-api), [abi flags](#abi-flags). + ## Stopping a running interpreter The words for looking at a program that is halfway through doing something. Most of them come from B02, and about half are things you already have on your machine without knowing it. diff --git a/README.md b/README.md index c161322..c95ac9c 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ A lesson can also end with a boss fight, which is a problem the text does not so | R03 | [What import does](lessons/r03-what-import-does/r03.ipynb) | An import statement compiles to one IMPORT_NAME plus zero or more IMPORT_FROM, and IMPORT_NAME looks the name __import__ up in builtins every single time, which is why replacing it works. Compiling the four spellings shows that import a.b binds a rather than a.b, and that a relative import is the empty string at a level above zero. A finder put on the front of sys.meta_path that answers nothing and writes down every question shows a dotted import searching for each part in turn from the outside in, with everything after the first part looked for in the parent package's __path__. A fresh interpreter has three finders, and import os stops at the second of them, so os.py is never opened. The module object goes into sys.modules before its body runs, which is what makes circular imports work and what decides how much of a half loaded module the other side can see, and if the body raises the entry is taken back out. Three caches sit in the way of a repeat import, and a directory created after it was first looked for stays invisible until importlib.invalidate_caches is called. Two recordings then settle the import lock: it is one lock per module name, so four threads importing four different modules keep one core busy on a build with the GIL and three and a half on a build without | M8 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/r03-what-import-does/r03.ipynb) | | R04 | [Frozen modules](lessons/r04-frozen-modules/r04.ipynb) | The import system is written in Python, so it cannot be imported, and the way out of that is to compile a handful of modules during CPython's own build and write the bytecode into the binary as C arrays. The module body of _frozen_importlib contains zero IMPORT_NAME opcodes, which is what makes it loadable with no import system running, and init_importlib in C hands sys and _imp to it as arguments. Thirty three names are frozen in a stock 3.15 build, in three groups that the flag treats differently: three for the import system that can never be switched off, nineteen for what a bare startup needs, and eleven hello world modules for the test suite. A frozen module still knows where it came from, because the loader puts the original path in loader_state and copies it onto __file__, which is why inspect.getsource and tracebacks still work while co_filename says . Switching the flag off in process with _imp._override_frozen_modules_for_tests moves os from FrozenImporter to SourceFileLoader and leaves the same constants behind. What freezing actually buys is the finder search and the file read, not the unmarshal, which is identical on both paths, and two recordings put it at about a tenth of a startup on a release build and a tenth on a debug build, where it is off by default | M8 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/r04-frozen-modules/r04.ipynb) | | R05 | [Lazy imports](lessons/r05-lazy-imports/r05.ipynb) | PEP 810 adds one word to the import statement in 3.15, and lazy import json binds the name now and does the finding, reading and running of the module the first time something reads that name back. It compiles to the same IMPORT_NAME opcode as a plain import, with the name index shifted up by two bits and the two bits underneath saying lazy, forced eager or ordinary, so dis prints json, json + lazy or json + eager. What gets bound is not a module but a five field placeholder that is not in sys.modules, that you can look at without setting it off, and whose name sits in sys.lazy_modules until it resolves. Only two opcodes resolve one, reading it as a bare name and reading it as an attribute of a module, so dict lookups, membership tests and reprs all leave it alone, and a placeholder copied into another variable resolves when that variable is read rather than when it was copied. The scope rules come from the symbol table rather than the code generator, which is why a lazy import inside a try block is refused with its own message. A failure inside a deferred module arrives with a second exception attached as its cause, pointing back at the lazy import line from information the placeholder was carrying. Three recordings put the startup saving at about three quarters of a run, and show that resolution takes the interpreter wide import lock rather than a lock per module name, so on a free threaded build four threads waking four different deferred imports keep 0.98 cores busy while four ordinary imports keep 3.60 | M8 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/r05-lazy-imports/r05.ipynb) | +| R06 | [The C API tiers](lessons/r06-the-c-api-tiers/r06.ipynb) | The C API is three directories and two macros. Include is open to any extension, Include/cpython needs Py_LIMITED_API to be undefined, and Include/internal starts nearly every file with three lines that stop the compiler unless you define Py_BUILD_CORE. More than half the header lines are in that third directory. Defining Py_LIMITED_API hides 186 of the 766 functions the public headers declare and all 974 in the other two, and the part that costs is not the functions but the struct layouts, because with no fields to read Py_TYPE becomes a call and Py_DECREF becomes a call to _Py_DecRef. The naming convention nearly matches the directories and the exceptions have a reason: a private name has to be exported when a public macro expands to it, which is what the 17 underscore names in the public tier are. None of it survives the build. Every tier resolves through ctypes.pythonapi, and calling _PyDict_SizeOf by hand gives the same number dict.__sizeof__ does, 16 bytes short of sys.getsizeof because that adds the collector header. Two recordings show the split is deliberate: inside the internal headers 93 percent of the names spelled PyAPI_FUNC resolve against 0.4 percent of the ones spelled plain extern, with 168 comments naming which bundled extension needs each export | M8 | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/r06-the-c-api-tiers/r06.ipynb) | More are landing in order. [lessons/README.md](lessons/README.md) explains how one is put together and how to run them locally. diff --git a/citations.lock.json b/citations.lock.json index 5c19a3d..1c5df40 100644 --- a/citations.lock.json +++ b/citations.lock.json @@ -1,5 +1,15 @@ { "citations": { + "Doc/c-api/stable.rst:21-35@v3.15.0rc1": { + "digest": "9299cb98031d1463", + "first_line": "There are two tiers of C API with different stability expectations:", + "lines": 15 + }, + "Doc/c-api/stable.rst:44-52@v3.15.0rc1": { + "digest": "16edf2164cc5ad02", + "first_line": "Any API named with the ``PyUnstable`` prefix exposes CPython implementation", + "lines": 9 + }, "Grammar/python.gram:121-124@v3.15.0rc1": { "digest": "816f44568467b4ce", "first_line": "simple_stmt[stmt_ty] (memo):", @@ -290,6 +300,11 @@ "first_line": "#define E_EOF 11 /* End Of File */", "lines": 2 }, + "Include/exports.h:88-93@v3.15.0rc1": { + "digest": "a1f585b73b10bbb8", + "first_line": "#ifndef PyAPI_FUNC", + "lines": 6 + }, "Include/internal/mimalloc/mimalloc/types.h:203-211@v3.15.0rc1": { "digest": "ec93e4470862f3c7", "first_line": "#define MI_SEGMENT_SLICE_SHIFT (13 + MI_INTPTR_SHIFT) // 64KiB (32KiB on 32-bit)", @@ -400,6 +415,11 @@ "first_line": "static inline uint8_t *", "lines": 17 }, + "Include/internal/pycore_dict.h:50-54@v3.15.0rc1": { + "digest": "67e2c9427216744a", + "first_line": "", + "lines": 5 + }, "Include/internal/pycore_dict.h:79-90@v3.15.0rc1": { "digest": "c3ccf876d4deb3d8", "first_line": "", @@ -645,6 +665,11 @@ "first_line": "/* Tries to incref the object op and ensures that *src still points to it. */", "lines": 16 }, + "Include/internal/pycore_object.h:7-9@v3.15.0rc1": { + "digest": "880508f79b0c0812", + "first_line": "#ifndef Py_BUILD_CORE", + "lines": 3 + }, "Include/internal/pycore_object.h:83-89@v3.15.0rc1": { "digest": "70fca12f791324cc", "first_line": "#if SIZEOF_VOID_P > 4", @@ -935,6 +960,11 @@ "first_line": "#define PyList_Check(op) \\", "lines": 3 }, + "Include/modsupport.h:85-101@v3.15.0rc1": { + "digest": "88ce91d467657676", + "first_line": "/* ABI info & checking (new in 3.15) */", + "lines": 17 + }, "Include/moduleobject.h:78-89@v3.15.0rc1": { "digest": "391087226de90dc4", "first_line": "/* for Py_mod_multiple_interpreters: */", @@ -945,6 +975,11 @@ "first_line": "/* for Py_mod_gil: */", "lines": 5 }, + "Include/object.h:124-126@v3.15.0rc1": { + "digest": "1f03faa6c6b4cdc0", + "first_line": "#ifdef _Py_OPAQUE_PYOBJECT", + "lines": 3 + }, "Include/object.h:127-149@v3.15.0rc1": { "digest": "c7e17988b3729573", "first_line": "struct _object {", @@ -980,6 +1015,11 @@ "first_line": "struct PyVarObject {", "lines": 5 }, + "Include/object.h:185-198@v3.15.0rc1": { + "digest": "82cf6802185e7cbd", + "first_line": "// Test if the 'x' object is the 'y' object, the same as \"x is y\" in Python.", + "lines": 14 + }, "Include/object.h:237-244@v3.15.0rc1": { "digest": "dc45c7bd6a91c2b3", "first_line": "// bpo-39573: The Py_SET_SIZE() function must be used to set an object size.", @@ -1010,6 +1050,11 @@ "first_line": "/* Macro for returning Py_None from a function.", "lines": 7 }, + "Include/object.h:740-744@v3.15.0rc1": { + "digest": "209c33f89cca4826", + "first_line": "#ifndef Py_LIMITED_API", + "lines": 5 + }, "Include/opcode_ids.h:1-4@v3.15.0rc1": { "digest": "427ac74efb62f52a", "first_line": "// This file is generated by Tools/cases_generator/opcode_id_generator.py", @@ -1045,11 +1090,21 @@ "first_line": "#if SIZEOF_VOID_P > 4", "lines": 28 }, + "Include/refcount.h:238-241@v3.15.0rc1": { + "digest": "da14159986fa309d", + "first_line": "#endif // Py_REF_DEBUG && !Py_LIMITED_API", + "lines": 4 + }, "Include/refcount.h:285-292@v3.15.0rc1": { "digest": "8ae8e77b2ccded13", "first_line": "#elif SIZEOF_VOID_P > 4", "lines": 8 }, + "Include/refcount.h:327-338@v3.15.0rc1": { + "digest": "92fb9a053d91b091", + "first_line": "#if (defined(Py_LIMITED_API) && (Py_LIMITED_API+0 >= 0x030c0000 || defined(Py_REF_DEBUG))) \\", + "lines": 12 + }, "Include/refcount.h:417-429@v3.15.0rc1": { "digest": "a2af4e3c17465fe0", "first_line": "static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op)", @@ -4980,6 +5035,11 @@ "first_line": "size_t", "lines": 16 }, + "Python/sysmodule.c:1970-1979@v3.15.0rc1": { + "digest": "5090fe7bc65883c4", + "first_line": "", + "lines": 10 + }, "Python/sysmodule.c:2015-2033@v3.15.0rc1": { "digest": "2aa5d7a83af6b2ee", "first_line": "/*[clinic input]", diff --git a/experiments/README.md b/experiments/README.md index cb2f79f..a2c908e 100644 --- a/experiments/README.md +++ b/experiments/README.md @@ -47,6 +47,8 @@ So those programs run somewhere else. They run in the images this project publis | [r05-what-deferring-an-import-is-worth](tier1/r05-what-deferring-an-import-is-worth.md) | R05 | release | What does a program get back for not importing what it turns out not to need? | | [r05-how-much-of-a-wake-up-is-parallel](tier1/r05-how-much-of-a-wake-up-is-parallel.md) | R05 | release | Do two threads waking up two different deferred imports wait for each other? | | [r05-how-much-of-a-wake-up-is-parallel-without-the-lock](tier1/r05-how-much-of-a-wake-up-is-parallel-without-the-lock.md) | R05 | freethreaded | With the global interpreter lock gone, does waking up a deferred import scale? | +| [r06-what-leaves-the-binary](tier1/r06-what-leaves-the-binary.md) | R06 | release | How much of the C API that the headers call private is callable anyway? | +| [r06-what-leaves-the-binary-on-a-free-threaded-build](tier1/r06-what-leaves-the-binary-on-a-free-threaded-build.md) | R06 | freethreaded | Does dropping the global interpreter lock change what the C API exports? | ## The commands diff --git a/experiments/tier1/r06-what-leaves-the-binary-on-a-free-threaded-build.md b/experiments/tier1/r06-what-leaves-the-binary-on-a-free-threaded-build.md new file mode 100644 index 0000000..f7f0c9f --- /dev/null +++ b/experiments/tier1/r06-what-leaves-the-binary-on-a-free-threaded-build.md @@ -0,0 +1,136 @@ +# The same sweep on the build that compiles a different half of the headers + +Generated by `just build-tier1`. Do not edit by hand, the change will be overwritten. + +Does dropping the global interpreter lock change what the C API exports? + +- Lesson: R06 +- Build: freethreaded +- Image: ghcr.io/tamnd/cpython-internals/cpython:freethreaded@sha256:db72284e3a49f43c38b96bec2baed1380b8348e27ea6f54f6e8d0810b59c3144 +- Interpreter: 3.15.0rc1 free-threading build (37e98da:37e98da, Aug 29 2026, 09:25:00) [GCC 14.2.0] +- Recorded: 2026-09-06 + +Why this needs the freethreaded build: it needs a build configured with --disable-gil, because the question is whether the names behind Py_GIL_DISABLED are the ones that were missing on the ordinary build. + +## The program + +```python +"""Which of the C API's private declarations actually leave the binary. + +The headers put the C API in three directories. `Include/` is what any extension may use, +`Include/cpython/` is the part that only makes sense compiled against this exact CPython, and +`Include/internal/` says at the top of nearly every file that it will not compile unless you +claim to be CPython itself. + +That is all a compile time arrangement. This program asks what survives into the built +interpreter, by taking every name the headers declare and asking the dynamic linker for it +through `ctypes.pythonapi`. A name that resolves is a name any program can call, whatever the +header said about it. + +The interesting split is inside the internal headers, which use two different spellings. +`PyAPI_FUNC` means the symbol leaves the shared library. A plain `extern` means it does not. +Both spellings sit in the same file, often two lines apart. +""" + +import ctypes +import pathlib +import re +import sys +import sysconfig +from collections import Counter + +API = re.compile(r"^PyAPI_FUNC\([^)]*\)\s*\**\s*(\w+)", re.M) +EXTERN = re.compile(r"^extern\s+[\w *]+?\**\s*(\w+)\s*\(", re.M) +NOTE = re.compile(r"^//\s*Export for (.+?)\.?$", re.M) + +TIERS = (("public", "*.h"), ("cpython only", "cpython/*.h"), ("internal", "internal/*.h")) + +api = ctypes.pythonapi +include = pathlib.Path(sysconfig.get_paths()["include"]) + + +def resolves(name): + """Ask the linker for a name, the way any program with a handle on the process can.""" + return hasattr(api, name) + + +def read(pattern): + """Every header matching the pattern, as one blob of text per file.""" + return [path.read_text(errors="replace") for path in sorted(include.glob(pattern))] + + +print("include directory:", include) +print("build has the gil disabled:", sysconfig.get_config_var("Py_GIL_DISABLED")) +print("abi flags:", repr(sys.abiflags)) +print() + +for tier, pattern in TIERS: + blobs = read(pattern) + declared = set() + for blob in blobs: + declared |= set(API.findall(blob)) + found = sum(1 for name in declared if resolves(name)) + print(f"{tier}: {len(blobs)} header files") + print(f" declared with PyAPI_FUNC: {len(declared)}") + print(f" of those, resolve in this process: {found}") + +internal = read("internal/*.h") +exported = set() +kept_in = set() +notes = [] +for blob in internal: + exported |= set(API.findall(blob)) + kept_in |= set(EXTERN.findall(blob)) + notes += NOTE.findall(blob) +kept_in -= exported + +leaked = sum(1 for name in exported if resolves(name)) +held = sum(1 for name in kept_in if resolves(name)) + +print() +print("inside the internal headers") +print(" names spelled PyAPI_FUNC:", len(exported)) +print(" names spelled plain extern:", len(kept_in)) +print(" comments naming who needs the export:", len(notes)) +for who, count in Counter(notes).most_common(5): + print(f" {count} for {who}") + +print() +exported_share = leaked / len(exported) * 100 +extern_share = held / len(kept_in) * 100 +print("~ private names that leave the binary: {}".format(leaked)) +print("~ share of PyAPI_FUNC internal names that resolve: {:.1f} percent".format(exported_share)) +print("~ share of plain extern internal names that resolve: {:.1f} percent".format(extern_share)) +``` + +## What it printed + +```text +include directory: /opt/python/include/python3.15t +build has the gil disabled: 1 +abi flags: 't' + +public: 79 header files + declared with PyAPI_FUNC: 770 + of those, resolve in this process: 754 +cpython only: 63 header files + declared with PyAPI_FUNC: 445 + of those, resolve in this process: 440 +internal: 148 header files + declared with PyAPI_FUNC: 530 + of those, resolve in this process: 499 + +inside the internal headers + names spelled PyAPI_FUNC: 530 + names spelled plain extern: 757 + comments naming who needs the export: 168 + 25 for '_testinternalcapi' shared extension + 12 for test_peg_generator + 10 for '_datetime' shared extension + 9 for '_asyncio' shared extension + 7 for 'math' shared extension + +~ private names that leave the binary: 499 +~ share of PyAPI_FUNC internal names that resolve: 94.2 percent +~ share of plain extern internal names that resolve: 0.4 percent +``` diff --git a/experiments/tier1/r06-what-leaves-the-binary.md b/experiments/tier1/r06-what-leaves-the-binary.md new file mode 100644 index 0000000..f1c7c65 --- /dev/null +++ b/experiments/tier1/r06-what-leaves-the-binary.md @@ -0,0 +1,136 @@ +# Every name the headers declare, handed to the linker one at a time + +Generated by `just build-tier1`. Do not edit by hand, the change will be overwritten. + +How much of the C API that the headers call private is callable anyway? + +- Lesson: R06 +- Build: release +- Image: ghcr.io/tamnd/cpython-internals/cpython:release@sha256:fb55d6afcf053c974de6447fafbd2be6af20cdb9f596e25a0445607b8af981e3 +- Interpreter: 3.15.0rc1 (37e98da:37e98da, Aug 29 2026, 09:24:54) [GCC 14.2.0] +- Recorded: 2026-09-06 + +Why this needs the release build: it needs the headers next to the interpreter, which a normal install has and a browser tab does not, and it needs ctypes to reach the process it is running in. + +## The program + +```python +"""Which of the C API's private declarations actually leave the binary. + +The headers put the C API in three directories. `Include/` is what any extension may use, +`Include/cpython/` is the part that only makes sense compiled against this exact CPython, and +`Include/internal/` says at the top of nearly every file that it will not compile unless you +claim to be CPython itself. + +That is all a compile time arrangement. This program asks what survives into the built +interpreter, by taking every name the headers declare and asking the dynamic linker for it +through `ctypes.pythonapi`. A name that resolves is a name any program can call, whatever the +header said about it. + +The interesting split is inside the internal headers, which use two different spellings. +`PyAPI_FUNC` means the symbol leaves the shared library. A plain `extern` means it does not. +Both spellings sit in the same file, often two lines apart. +""" + +import ctypes +import pathlib +import re +import sys +import sysconfig +from collections import Counter + +API = re.compile(r"^PyAPI_FUNC\([^)]*\)\s*\**\s*(\w+)", re.M) +EXTERN = re.compile(r"^extern\s+[\w *]+?\**\s*(\w+)\s*\(", re.M) +NOTE = re.compile(r"^//\s*Export for (.+?)\.?$", re.M) + +TIERS = (("public", "*.h"), ("cpython only", "cpython/*.h"), ("internal", "internal/*.h")) + +api = ctypes.pythonapi +include = pathlib.Path(sysconfig.get_paths()["include"]) + + +def resolves(name): + """Ask the linker for a name, the way any program with a handle on the process can.""" + return hasattr(api, name) + + +def read(pattern): + """Every header matching the pattern, as one blob of text per file.""" + return [path.read_text(errors="replace") for path in sorted(include.glob(pattern))] + + +print("include directory:", include) +print("build has the gil disabled:", sysconfig.get_config_var("Py_GIL_DISABLED")) +print("abi flags:", repr(sys.abiflags)) +print() + +for tier, pattern in TIERS: + blobs = read(pattern) + declared = set() + for blob in blobs: + declared |= set(API.findall(blob)) + found = sum(1 for name in declared if resolves(name)) + print(f"{tier}: {len(blobs)} header files") + print(f" declared with PyAPI_FUNC: {len(declared)}") + print(f" of those, resolve in this process: {found}") + +internal = read("internal/*.h") +exported = set() +kept_in = set() +notes = [] +for blob in internal: + exported |= set(API.findall(blob)) + kept_in |= set(EXTERN.findall(blob)) + notes += NOTE.findall(blob) +kept_in -= exported + +leaked = sum(1 for name in exported if resolves(name)) +held = sum(1 for name in kept_in if resolves(name)) + +print() +print("inside the internal headers") +print(" names spelled PyAPI_FUNC:", len(exported)) +print(" names spelled plain extern:", len(kept_in)) +print(" comments naming who needs the export:", len(notes)) +for who, count in Counter(notes).most_common(5): + print(f" {count} for {who}") + +print() +exported_share = leaked / len(exported) * 100 +extern_share = held / len(kept_in) * 100 +print("~ private names that leave the binary: {}".format(leaked)) +print("~ share of PyAPI_FUNC internal names that resolve: {:.1f} percent".format(exported_share)) +print("~ share of plain extern internal names that resolve: {:.1f} percent".format(extern_share)) +``` + +## What it printed + +```text +include directory: /opt/python/include/python3.15 +build has the gil disabled: 0 +abi flags: '' + +public: 79 header files + declared with PyAPI_FUNC: 770 + of those, resolve in this process: 750 +cpython only: 63 header files + declared with PyAPI_FUNC: 445 + of those, resolve in this process: 439 +internal: 148 header files + declared with PyAPI_FUNC: 530 + of those, resolve in this process: 493 + +inside the internal headers + names spelled PyAPI_FUNC: 530 + names spelled plain extern: 757 + comments naming who needs the export: 168 + 25 for '_testinternalcapi' shared extension + 12 for test_peg_generator + 10 for '_datetime' shared extension + 9 for '_asyncio' shared extension + 7 for 'math' shared extension + +~ private names that leave the binary: 493 +~ share of PyAPI_FUNC internal names that resolve: 93.0 percent +~ share of plain extern internal names that resolve: 0.4 percent +``` diff --git a/lessons/CLAIMS.md b/lessons/CLAIMS.md index c22565f..8b99f80 100644 --- a/lessons/CLAIMS.md +++ b/lessons/CLAIMS.md @@ -12,7 +12,7 @@ header, what the allocator does with a freed block, the shape of the eval loop. marked with the reason, and a lesson is allowed at most 3 of them. The cap is the point. Without it the exception becomes the rule and this goes back to being a book. -601 claims across 76 lessons, 59 of them not observable from Python. +610 claims across 77 lessons, 59 of them not observable from Python. ## B01. Building CPython, and whether you need to @@ -781,6 +781,20 @@ Without it the exception becomes the rule and this goes back to being a book. | A file with twelve imports at the top that uses one runs several times faster with them deferred, and eleven of the twelve module bodies never run | [`r05-37`](r05-lazy-imports/r05.ipynb) | | On a free threaded build, four threads importing four different modules run about three and a half times over, while four threads waking four different deferred imports run one at a time, because reification takes the interpreter wide import lock | not observable from Python: it needs a build configured with --disable-gil and several processors, and one notebook is only ever one build | +## R06. The C API tiers + +| Claim | Proved by | +| --- | --- | +| Most of CPython's header surface is in the directory extensions are not meant to open. | [`r06-07`](r06-the-c-api-tiers/r06.ipynb) | +| Defining Py_LIMITED_API removes about a quarter of the functions declared in the public headers, and all of the other two directories. | [`r06-10`](r06-the-c-api-tiers/r06.ipynb) | +| The internal headers are protected by a compiler error and nothing else. | [`r06-13`](r06-the-c-api-tiers/r06.ipynb) | +| The naming convention and the directory disagree about a few dozen functions, in both directions. | [`r06-16`](r06-the-c-api-tiers/r06.ipynb) | +| The tier a function belongs to makes no difference to whether you can find it at run time. | [`r06-19`](r06-the-c-api-tiers/r06.ipynb) | +| Inside the internal headers, the two spellings decide what leaves the binary, and almost nothing crosses over. | [`r06-21`](r06-the-c-api-tiers/r06.ipynb) | +| The internal function, the dunder and sys.getsizeof are the same measurement with one thing added. | [`r06-24`](r06-the-c-api-tiers/r06.ipynb) | +| Some C API names are a macro and an exported function at once, and which one you get depends on your build. | [`r06-26`](r06-the-c-api-tiers/r06.ipynb) | +| Which tier an installed extension was built against is written on the outside of the file. | [`r06-29`](r06-the-c-api-tiers/r06.ipynb) | + ## T01. One line, seven stages | Claim | Proved by | diff --git a/lessons/README.md b/lessons/README.md index 3a35820..e1b020b 100644 --- a/lessons/README.md +++ b/lessons/README.md @@ -80,6 +80,7 @@ Each lesson is a notebook you can run. There is nothing to install and nothing t | [R03. What import does](r03-what-import-does/r03.ipynb) | The four spellings of the statement compiled and read back opcode by opcode, a finder on the front of sys.meta_path that answers nothing and logs every question a dotted import asks, the three finders asked for the same three names side by side, a circular import in a temporary directory caught reading a module halfway through its own body, a module served out of a string by fourteen lines of class, the three caches an import passes through including the one that hides a directory you just made, a timing of a first import against a repeat one, and two recordings showing that what serialises two threads importing two different modules is the GIL rather than the import lock | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/r03-what-import-does/r03.ipynb) | | [R04. Frozen modules](r04-frozen-modules/r04.ipynb) | The compiled body of the import bootstrap disassembled to show it contains no imports at all, the thirty three frozen names split into the three groups frozen.c keeps them in, a frozen code object pulled straight out of the binary and weighed, os asked five questions about where it came from and answering to one of them and the real path to another, the flag turned off inside a running interpreter and os moving from one loader to another, four steps of a load timed separately to find out which of them freezing actually removes, and a startup with -v counting the files a second interpreter reads that the first one did not | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/r04-frozen-modules/r04.ipynb) | | [R05. Lazy imports](r05-lazy-imports/r05.ipynb) | The whole xml.etree family weighed to show what one unused import line costs, the pre 3.15 LazyLoader waking itself up the moment anything reads an attribute of it, four import statements disassembled to show the two spare bits of the IMPORT_NAME argument, a placeholder inspected without being resolved and then resolved by a single bare name read, a copy of one escaping into another variable and resolving there while the entry it came from stays a placeholder, six spellings of the keyword compiled to see which four are refused and with what message, a filter installed that defers one name and not the other, a module that raises during resolution and the two part traceback that comes back, a deferred import that queues another one behind it, and twelve imports timed as written and again deferred | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/r05-lazy-imports/r05.ipynb) | +| [R06. The C API tiers](r06-the-c-api-tiers/r06.ipynb) | The three include directories measured file by file and line by line, the Py_LIMITED_API guard walked as a preprocessor stack to count what a limited build may call and what it may not, the three line #error that guards 140 of the 148 internal headers printed as it appears in the file, the three naming conventions cross tabulated against the three directories, eight names from all three tiers handed to ctypes.pythonapi to show that every one of them resolves, the internal headers split by their two spellings so the deliberate half of the export shows up, _PyDict_SizeOf called by hand and checked against the dunder and against sys.getsizeof, seven names asked whether they are a macro or an exported function or both, and every shared object on sys.path sorted by what its file name says it was built against | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/r06-the-c-api-tiers/r06.ipynb) | ## The three programs diff --git a/lessons/r06-the-c-api-tiers/build.py b/lessons/r06-the-c-api-tiers/build.py new file mode 100644 index 0000000..8799c03 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/build.py @@ -0,0 +1,529 @@ +#!/usr/bin/env python +"""R06. The C API tiers. + +The sixth runtime lesson, and the first one that is really about C rather than about Python. +CPython's C API lives in three directories, and which one you are allowed to reach into is +decided by two macros you set before you include anything. + +The awkward part of writing this lesson is that half of it wants to read the headers, and the +headers are not always there. A normal install ships them, Colab usually does, and a browser +tab never does. So the header cells all sit behind one flag set in the first of them, and they +say so out loud rather than printing nothing. + +The two Tier 1 recordings answer the question the notebook cannot answer on its own, which is +whether the sweep is telling the truth about builds other than yours. They run the same +program on the ordinary build and on the free threaded one. + +Run this file to regenerate the notebook, or `just build-lessons` to regenerate all of them. +`just lessons` checks that the committed notebook still matches this file. +""" + +from nbbuild import BANNER, Lesson +from nbdiagram import Diagrams +from tier1 import show as recording + +lesson = Lesson("r06-the-c-api-tiers", "r06") +badge = lesson.badge +cite = lesson.cite +term = lesson.term +figure = Diagrams("r06-the-c-api-tiers").figure + +LEAVES = "r06-what-leaves-the-binary" +FREE = "r06-what-leaves-the-binary-on-a-free-threaded-build" + + +lesson.md(f""" +# R06. The C API tiers + +{badge} + +Everything so far has looked at CPython from inside Python. This one looks at it from the other side, the way a C extension does. + +An extension gets at the interpreter by including `Python.h` and calling functions. Which functions it is allowed to call is not one list. It is three, they live in three directories, and you pick which ones you get by defining a macro before the include. + +{figure("three-doors", "three nested rings, the public headers inside the cpython only headers inside the internal headers")} +""") + + +lesson.md(""" +## About the source references + +Now and then this lesson points at CPython's own source, like this: `Include/exports.h:88-93@v3.15.0rc1`. + +Read it as three parts: the file, the lines, and the release those line numbers belong to. Sometimes there is a fourth part after a `#`, which is the name of the thing those lines are inside. + +Every reference is a link, and every one is checked against the pinned source on each change, so a stale reference fails the build instead of sending you somewhere wrong. You never have to read any of it. The references are there so you can go deeper when you want to, and so you can check that this lesson is not making things up. + +## Setup + +Colab does not come with the small package these lessons use, so the next cell installs it. If you are running this from a checkout of the repository it is already installed and the cell does nothing. +""") + + +lesson.code(""" +import sys + +if sys.version_info < (3, 14): + print("This lesson needs CPython 3.14 or newer.") + print(f"This runtime is {sys.version.split()[0]}, and the cells below will not run on it.") +else: + try: + import pyxray + except ImportError: + %pip install -q "pyxray @ git+https://github.com/tamnd/cpython-internals@main#subdirectory=pyxray" + import pyxray +""") + + +lesson.md(""" +## Which Python is this + +About half the cells here read the header files that sit next to the interpreter. A normal install has them and a browser tab does not, so the first cell below works out whether they are there and the rest check that answer before they do anything. The other half only need `ctypes`, which works everywhere. + +## Which interpreter is this +""") + + +lesson.code( + """ +import pyxray + +pyxray.show() +""", + differs=BANNER, + quiet=True, +) + + +lesson.md(f""" +## Three directories + +{term("C API", "the set of C functions and macros an extension may call to talk to the interpreter")} is not one thing. `Include/` holds the headers any extension may use. `Include/cpython/` holds the ones that only make sense if you were compiled against this exact CPython. `Include/internal/` holds the ones CPython uses on itself. + +The first cell finds where they live and measures how much is in each. The interesting number is the third one. + +{lesson.claim("Most of CPython's header surface is in the directory extensions are not meant to open.")} +""") + + +lesson.code( + """ +import pathlib +import sysconfig + +INCLUDE = pathlib.Path(sysconfig.get_paths()["include"]) +TIERS = (("public", "*.h"), ("cpython only", "cpython/*.h"), ("internal", "internal/*.h")) +HEADERS = INCLUDE.is_dir() and any(INCLUDE.glob("*.h")) + + +def headers(pattern): + \"\"\"Every header in one tier as text, or nothing when this runtime did not ship them.\"\"\" + return [path.read_text(errors="replace") for path in sorted(INCLUDE.glob(pattern))] + + +if not HEADERS: + print(" no headers next to this interpreter, looked in", INCLUDE) + print(" the cells that read them say so and skip themselves") +else: + print(" headers live in", INCLUDE) + for tier, pattern in TIERS: + blobs = headers(pattern) + lines = sum(len(blob.splitlines()) for blob in blobs) + print(f" {tier:13} {len(blobs):4} files {lines:7} lines") +""", + differs=( + "On 3.14 the internal directory is a good deal smaller, 139 files and about thirty " + "thousand lines rather than 148 and forty two thousand." + ), +) + + +lesson.md(f""" +More than half the lines are in the directory nobody outside CPython is supposed to read. That is not a mistake. The interpreter is written against its own headers, and those headers describe the parts that change from release to release. + +{figure("who-can-see-what", "a table of the three tiers, the directory each one lives in, the macro that opens it, how its names look and how long it lasts")} + +The right hand column is the whole point. The public tier is covered by CPython's backwards compatibility policy. The middle one may change in any minor release. The internal one may change in a patch release, and the documentation says so plainly in {cite("Doc/c-api/stable.rst:21-35@v3.15.0rc1")}. + +## What one macro hides + +The gate is `Py_LIMITED_API`. Define it before you include `Python.h` and a large amount of the header tree stops existing for you. + +The mechanism is dull and worth seeing once. At the bottom of {cite("Include/object.h:740-744@v3.15.0rc1#Py_CPYTHON_OBJECT_H")} there is an `#ifndef Py_LIMITED_API` around an `#include "cpython/object.h"`. Almost every public header ends the same way. That is the entire second door. + +{term("limited API", "the subset of the C API you get when you define Py_LIMITED_API before including Python.h")} is not a separate set of files. It is the same files with a lot of them switched off. + +{lesson.claim("Defining Py_LIMITED_API removes about a quarter of the functions declared in the public headers, and all of the other two directories.")} +""") + + +lesson.code( + """ +import re + +DECLARED = re.compile(r"^PyAPI_FUNC\\([^)]*\\)\\s*\\**\\s*(\\w+)", re.M) +OPENS = re.compile(r"^\\s*#\\s*if") +CLOSES = re.compile(r"^\\s*#\\s*endif") +ASKED = ("PyList_Append", "PyErr_SetString", "PyBuffer_FillInfo", "PyThreadState_GetFrame") + + +def split_by_the_guard(text): + \"\"\"Split one header's function names into what a limited build sees and what it does not.\"\"\" + shut = [] + seen, hidden = set(), set() + for line in text.splitlines(): + if OPENS.match(line): + shut.append("Py_LIMITED_API" in line and ("ifndef" in line or "!defined" in line)) + elif CLOSES.match(line) and shut: + shut.pop() + found = DECLARED.match(line) + if found: + (hidden if any(shut) else seen).add(found.group(1)) + return seen, hidden + + +if HEADERS: + seen, hidden = set(), set() + for blob in headers("*.h"): + one, other = split_by_the_guard(blob) + seen |= one + hidden |= other + away = set() + for pattern in ("cpython/*.h", "internal/*.h"): + for blob in headers(pattern): + away |= set(DECLARED.findall(blob)) + print(" in the public headers") + print(" functions a limited build may call:", len(seen)) + print(" functions the guard takes away: ", len(hidden)) + print(" in the other two directories, which it never opens:", len(away)) + print() + for name in ASKED: + answer = "yes" if name in seen else "no, the guard hides it" + print(f" can a limited build call {name:24} {answer}") +""", + differs=( + "On 3.14 the numbers are 575 and 162, and 824 in the other two directories. The four " + "answers underneath are the same." + ), +) + + +lesson.md(f""" +{figure("the-gate", "a four step flow, including Python.h, checking whether Py_LIMITED_API is defined, and either pulling in cpython/object.h or skipping it")} + +Losing a quarter of the functions is the part people notice. It is not the part that hurts. + +What actually hurts is that the struct definitions go too. With `_Py_OPAQUE_PYOBJECT` set, {cite("Include/object.h:124-126@v3.15.0rc1#_Py_OPAQUE_PYOBJECT")} says only `/* PyObject is opaque */`, so there is no `ob_refcnt` field to read and no `ob_type` field to follow. + +{figure("what-limited-costs", "a comparison of an ordinary build against a limited one, showing that the fields disappear and the macros become calls")} + +That is the trade. You give up knowing where anything is, and in return one built file keeps working across releases, because the thing that changes between releases is where everything is. + +## The lock on the third door + +The third door has a different kind of lock, and it is worth seeing because it is so much cruder than you would expect. + +{cite("Include/internal/pycore_object.h:7-9@v3.15.0rc1#Py_BUILD_CORE")} is three lines: if `Py_BUILD_CORE` is not defined, stop the compiler with an error message. Nearly every file in that directory starts with the same three lines. + +{lesson.claim("The internal headers are protected by a compiler error and nothing else.")} +""") + + +lesson.code( + """ +if HEADERS: + blobs = headers("internal/*.h") + refuse = [blob for blob in blobs if "#ifndef Py_BUILD_CORE" in blob] + print(" internal headers: ", len(blobs)) + print(" that stop the compiler without the macro:", len(refuse)) + print() + start = refuse[0].index("#ifndef Py_BUILD_CORE") + for line in refuse[0][start:].splitlines()[:3]: + print(" ", line) +""", + differs="On 3.14 it is 139 headers and 132 of them, because the directory grew in 3.15.", +) + + +lesson.md(f""" +Nothing stops you defining `Py_BUILD_CORE` yourself. People do. It compiles, it links, and it breaks on the next patch release when a struct grows a field. + +## The names and the directories + +There is a second convention running alongside the directories, and it is the one you meet first, because it is in the names. + +A plain `Py` name is public. A `PyUnstable_` name is the middle tier saying so out loud, which {cite("Doc/c-api/stable.rst:44-52@v3.15.0rc1")} describes as intended for debuggers and tools that follow CPython development. A leading underscore means private. + +You would expect the three conventions to line up exactly with the three directories. They nearly do. + +{lesson.claim("The naming convention and the directory disagree about a few dozen functions, in both directions.")} +""") + + +lesson.code( + """ +if HEADERS: + + def convention(name): + \"\"\"Which of the three naming conventions a name follows.\"\"\" + if name.startswith("PyUnstable"): + return "PyUnstable" + return "underscore" if name.startswith("_") else "plain Py" + + print(f" {'tier':13} {'plain Py':>9} {'PyUnstable':>11} {'underscore':>11}") + for tier, pattern in TIERS: + names = set() + for blob in headers(pattern): + names |= set(DECLARED.findall(blob)) + counted = {"plain Py": 0, "PyUnstable": 0, "underscore": 0} + for name in names: + counted[convention(name)] += 1 + print( + f" {tier:13} {counted['plain Py']:>9} " + f"{counted['PyUnstable']:>11} {counted['underscore']:>11}" + ) +""", + differs=( + "The three rows on 3.14 read 716, 1, 20 then 304, 30, 89 then 8, 0, 394. The shape is " + "the same and the internal row is a lot shorter." + ), +) + + +lesson.md(f""" +{figure("names-against-tiers", "a table counting plain Py names, PyUnstable names and underscore names in each of the three directories")} + +The seventeen underscore names in the public row are the interesting ones, and they all have the same explanation. `Py_DECREF` is a macro, and at {cite("Include/refcount.h:417-429@v3.15.0rc1#Py_DECREF")} you can see what it expands to: a decrement, and then a call to `_Py_Dealloc` if the count reached zero. + +That call happens in your code, after the macro is pasted in. So `_Py_Dealloc` has to be a symbol your extension can link against, private name or not, and it is declared two hundred lines earlier at {cite("Include/refcount.h:238-241@v3.15.0rc1#_Py_Dealloc")}. + +A private name has to be public when a public macro expands to it. That is the whole rule, and it accounts for nearly the entire list. + +## Ask the linker instead + +Everything up to here has been about compiling. Now the interesting bit, which is what happens once the compiling is over. + +Python can look up C symbols by name. `ctypes.pythonapi` is a handle on the running interpreter, and asking it for an attribute asks the dynamic linker for a symbol with that name. + +{lesson.claim("The tier a function belongs to makes no difference to whether you can find it at run time.")} +""") + + +lesson.code(""" +import ctypes + +api = ctypes.pythonapi +SAMPLE = ( + ("public", "PyList_Append"), + ("public", "PyErr_SetString"), + ("cpython only", "PyUnstable_Code_New"), + ("cpython only", "PyFrame_GetLasti"), + ("internal", "_PyDict_SizeOf"), + ("internal", "_PyEval_EvalFrameDefault"), + ("internal", "_PyObject_GC_New"), + ("no such name", "PyNothingLikeThis"), +) +for tier, name in SAMPLE: + found = "the linker hands it over" if hasattr(api, name) else "not found" + print(f" {tier:13} {name:26} {found}") +""") + + +lesson.md(f""" +Seven for seven, and the made up name is the only one that fails. The three tiers are a compile time arrangement. By the time there is a binary, they are gone. + +Which raises the obvious question: is the internal tier exported by accident, or on purpose? The answer is in the headers, and it is on purpose. + +{term("PyAPI_FUNC", "the macro that marks a declaration as a symbol other binaries may link against")} expands, at {cite("Include/exports.h:88-93@v3.15.0rc1#PyAPI_FUNC")}, to `Py_EXPORTED_SYMBOL`, which is the compiler attribute for default visibility. A declaration that does not use it is not exported. + +The internal headers use both spellings, often two lines apart. `_PyDict_SizeOf` at {cite("Include/internal/pycore_dict.h:50-54@v3.15.0rc1#_PyDict_SizeOf")} is `PyAPI_FUNC`, with a comment above it saying which shipped extension needs it, and `_PyDict_SizeOf_LockHeld` right underneath is a plain `extern`. + +{lesson.claim("Inside the internal headers, the two spellings decide what leaves the binary, and almost nothing crosses over.")} +""") + + +lesson.code( + """ +EXTERN = re.compile(r"^extern\\s+[\\w *]+?\\**\\s*(\\w+)\\s*\\(", re.M) +WHO = re.compile(r"^//\\s*Export for (.+?)\\.?$", re.M) + +if HEADERS: + loud, quiet, asked = set(), set(), [] + for blob in headers("internal/*.h"): + loud |= set(DECLARED.findall(blob)) + quiet |= set(EXTERN.findall(blob)) + asked += WHO.findall(blob) + quiet -= loud + out = sum(1 for name in loud if hasattr(api, name)) + kept = sum(1 for name in quiet if hasattr(api, name)) + print(f" spelled PyAPI_FUNC: {len(loud):4}, and {out} of them resolve") + print(f" spelled plain extern: {len(quiet):4}, and {kept} of them resolve") + print() + print(" comments naming who needs the export:", len(asked)) + print(" the first three:", ", ".join(asked[:3])) +""", + differs=( + "On 3.14 it is 402 exported and 388 resolving, against 783 held back and 2 resolving, " + "with 161 comments rather than 168. The gap between the two spellings is the same shape." + ), +) + + +lesson.md(f""" +So the internal tier is deliberately half exported. The reason is in those comments: CPython's own bundled extensions, `math` and `_asyncio` and `_pickle` and the rest, are compiled as separate shared libraries, and they need to reach back into the interpreter that loaded them. + +The handful of exported names that did not resolve are the ones behind `Py_GIL_DISABLED` and the Windows only ones, which is why the free threaded recording at the end finds a few more. + +## Calling one of them + +Finding a symbol is not the same as calling it. Calling it means telling `ctypes` what the arguments and the return value are, and getting that wrong crashes the process rather than raising. + +`_PyDict_SizeOf` is a safe one to try. It takes a dict and returns a count of bytes, and there is a familiar function sitting on top of it. + +{lesson.claim("The internal function, the dunder and sys.getsizeof are the same measurement with one thing added.")} +""") + + +lesson.code(""" +size_of = api._PyDict_SizeOf +size_of.argtypes = [ctypes.py_object] +size_of.restype = ctypes.c_ssize_t + +small = {"a": 1, "b": 2} +print(" what the internal function says:", size_of(small)) +print(" what the dunder says: ", small.__sizeof__()) +print(" what sys.getsizeof says: ", sys.getsizeof(small)) +print(" the difference: ", sys.getsizeof(small) - small.__sizeof__()) +try: + import _testinternalcapi + + print(" and the header the collector adds:", _testinternalcapi.SIZEOF_PYGC_HEAD, "bytes") +except ImportError: + print(" and _testinternalcapi is not here to say what that difference is") +""") + + +lesson.md(f""" +`dict.__sizeof__` is a thin wrapper around the internal function, so those two agree exactly. `sys.getsizeof` adds the bytes the cycle collector keeps in front of the object, which you can see it doing at {cite("Python/sysmodule.c:1970-1979@v3.15.0rc1#_PyType_PreHeaderSize")}. + +We went round the outside and got the same answer the front door gives. That is the honest summary of what the internal tier is: not hidden, just unsupported. + +## A macro and a function with the same name + +One more thing falls out of all this, and it explains the shape of a lot of the header tree. + +If a limited build cannot see the struct, it cannot read `ob_type` to implement `Py_TYPE`. So `Py_TYPE` has to exist twice: as a macro for everybody else, and as a real exported function for the limited build. + +{cite("Include/object.h:185-198@v3.15.0rc1#Py_Is")} does this in the open. A `PyAPI_FUNC` declaration, then a `#define` of the same name on the very next line. The macro wins when it is visible, and the function is there when it is not. + +{lesson.claim("Some C API names are a macro and an exported function at once, and which one you get depends on your build.")} +""") + + +lesson.code( + """ +for name in ("Py_TYPE", "Py_REFCNT", "Py_SIZE", "Py_IS_TYPE", "Py_Is", "Py_DECREF", "Py_CLEAR"): + both = "declared as well, so a limited build can call it" + print(f" {name:11} {both if hasattr(api, name) else 'a macro and nothing else'}") +""", + differs=( + "On 3.14 Py_SIZE and Py_IS_TYPE are macros and nothing else. They got their function " + "forms in 3.15, which the comment above them in object.h dates." + ), +) + + +lesson.md(f""" +`Py_DECREF` and `Py_CLEAR` never got function forms, which is why a limited build calls `_Py_DecRef` instead, as {cite("Include/refcount.h:327-338@v3.15.0rc1#Py_DECREF")} shows. + +That is the cost of the limited API in one line. The single most common operation in the whole C API stops being a decrement and becomes a function call. + +## What the file name says + +None of the tiers show up in a built extension. One thing does, and it is the file name. + +{term("stable ABI", "a build of an extension that keeps working across releases, marked by abi3 in its file name")} extensions are named `something.abi3.so`. Version locked ones carry the exact version, like `something.cpython-315-darwin.so`. The interpreter decides what it is willing to load from that suffix alone. + +{lesson.claim("Which tier an installed extension was built against is written on the outside of the file.")} +""") + + +lesson.code( + """ +import importlib.machinery + +print(" suffixes this interpreter will load:") +for one in importlib.machinery.EXTENSION_SUFFIXES: + print(" ", one) +print() +counted = {"built for one version": 0, "built for the stable abi": 0, "no tag at all": 0} +example = {} +for entry in {p for p in sys.path if p and pathlib.Path(p).is_dir()}: + for found in pathlib.Path(entry).rglob("*.so"): + tag = ( + "built for the stable abi" + if ".abi3" in found.name + else "built for one version" + if ".cpython-" in found.name + else "no tag at all" + ) + counted[tag] += 1 + example.setdefault(tag, found.name) +for tag, total in counted.items(): + print(f" {tag:26} {total:4} {example.get(tag, '')}") +""", + varies=( + "The counts underneath are whatever happens to be installed where you are running " + "this, so they will not match. The suffix list is the part that depends on the " + "version: 3.14 offers three and 3.15 offers six, because 3.15 added abi3t for free " + "threaded builds." + ), +) + + +lesson.md(f""" +Three suffixes on 3.14 and six on 3.15. The new ones end in `abi3t`, and they belong to PEP 803, which gives free threaded builds their own stable ABI. R07 is about that list. + +## How much really leaves the binary + +Everything above was measured on whatever machine you are reading this on. The claim that the internal tier is half exported deserves better than that, so here is the same sweep run in a container, on a build made from the pinned source. + +{recording(LEAVES)} + +Ninety three percent of the private declarations that are spelled `PyAPI_FUNC` can be found by name, and four in a thousand of the ones spelled `extern` can. Two spellings, two completely different outcomes, in the same files. + +{figure("what-leaves-the-binary", "a bar chart comparing the share of internal PyAPI_FUNC names that resolve against the share of plain extern names")} + +The other run is the same program on a build made with `--disable-gil`, which compiles a different half of the headers. + +{recording(FREE)} + +Six more names resolve there, and the numbers on the three tiers are otherwise identical. The extra six are the ones guarded by `Py_GIL_DISABLED`, which is the answer to whether the missing names were a mystery or just a build flag. + +## Try it yourself + +**One.** Take the sweep in the cell above and print the names in the internal headers that are spelled `PyAPI_FUNC` but do not resolve. Then go and look two of them up in the pinned source and work out what they have in common. + +**Two.** `PyUnstable_Code_New` resolved in the linker cell. Find its declaration in `Include/cpython/code.h` and work out from the comments around it why a name with `Unstable` in it is declared in a header at all rather than being kept private. + +**Three.** The cross tabulation cell counts eight plain `Py` names in the internal directory. Print them. They are all from the same area of the interpreter, and the reason is a piece of history rather than a design decision. + +**Four.** `ctypes.pythonapi` also finds data, not just functions. Look up `PyLong_Type` with `ctypes.cast` and confirm the address you get back is the same one `id(int)` reports. + +## What you now know + +The C API is three directories. `Include/` is for anybody, `Include/cpython/` needs you not to have defined `Py_LIMITED_API`, and `Include/internal/` needs you to define `Py_BUILD_CORE` and stops the compiler with an error if you do not. + +The limited API takes away about a quarter of the public functions, and all of the struct layouts. The second half is the expensive one, because it turns field reads into function calls, `Py_DECREF` included. + +The naming convention nearly matches the directories, and the exceptions have a reason. A private name has to be exported when a public macro expands to it. + +None of this survives the build. The linker exports ninety three percent of the internal declarations that ask for it, on purpose, because CPython's own bundled extensions are separate shared libraries that need to reach back in. What stops you using them is a comment and a compiler error, not the loader. + +## What is next + +R07 takes the last cell and expands it. The stable ABI is what `abi3` means, `abi3t` is what 3.15 added for free threaded builds, and `PyABIInfo` at {cite("Include/modsupport.h:85-101@v3.15.0rc1#PyABIInfo")} is the struct an extension carries so the interpreter can check, at load time, that the two of you agree about which tier you compiled against. +""") + + +raise SystemExit(lesson.save()) diff --git a/lessons/r06-the-c-api-tiers/diagrams.py b/lessons/r06-the-c-api-tiers/diagrams.py new file mode 100644 index 0000000..0d86cc4 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +"""The diagrams for R06, the three doors into the C API. + +Each scene is written out twice, as an editable `.excalidraw` and as the `.svg` the lesson +embeds. Run this file to regenerate them, or `just build-diagrams` for every lesson. + +The order follows the lesson. The three directories first, then the one macro that opens or +closes the second door, then what a limited build gives up, then how the naming convention +lines up against the directories, and last the sweep that shows none of it survives the build. +""" + +from nbdiagram import Gallery, figures + +gallery = Gallery("r06-the-c-api-tiers") + +gallery.add( + figures.nest( + "three-doors", + ( + "Include/internal, 148 files, needs Py_BUILD_CORE", + [ + ( + "Include/cpython, 63 files, needs Py_LIMITED_API to be undefined", + [("Include, 79 files, anybody may use these", [])], + ) + ], + ), + title="The C API is three directories, one inside the next", + caption="An extension gets the inner ring. CPython itself gets all three.", + ) +) + + +gallery.add( + figures.table( + "who-can-see-what", + ["tier", "directory", "what opens it", "how the names look", "how long it lasts"], + [ + ["public", "Include/", "always open", "PyList_Append", "PEP 387"], + [ + "cpython only", + "Include/cpython/", + "Py_LIMITED_API undefined", + "PyUnstable_Code_New", + "one release", + ], + [ + "internal", + "Include/internal/", + "Py_BUILD_CORE defined", + "_PyDict_SizeOf", + "no promise", + ], + ], + title="The three tiers, and what each one asks of you", + caption="The middle row is the one most extensions actually live in without noticing.", + tones=["durable", "intermediate", "warning"], + ) +) + + +gallery.add( + figures.flow( + "the-gate", + [ + "you include Python.h", + "is Py_LIMITED_API defined", + "no, so pull in cpython/object.h", + "yes, so skip it", + ], + title="How the second door opens and closes", + tones=["input", "focus", "durable", "warning"], + labels=("", "one macro", "the whole struct", "an opaque pointer"), + ) +) + + +gallery.add( + figures.compare( + "what-limited-costs", + ( + "an ordinary build", + [ + "PyObject has fields", + "Py_DECREF is a decrement", + "Py_TYPE reads ob_type", + "recompile for every minor release", + ], + ), + ( + "a limited build", + [ + "PyObject is an opaque type", + "Py_DECREF calls _Py_DecRef", + "Py_TYPE calls Py_TYPE", + "one build runs on many releases", + ], + ), + title="What defining Py_LIMITED_API actually changes", + verdict="It takes away the layout, not the functions. Every field read becomes a call.", + ) +) + + +gallery.add( + figures.table( + "names-against-tiers", + ["tier", "plain Py names", "PyUnstable names", "underscore names"], + [ + ["Include/", "752", "1", "17"], + ["Include/cpython/", "322", "33", "90"], + ["Include/internal/", "8", "0", "522"], + ], + title="The naming convention against the directory it sits in", + caption="Close, but not the same thing. The 17 in the top right are what public macros expand to.", + tones=["durable", "intermediate", "warning"], + ) +) + + +gallery.add( + figures.bars( + "what-leaves-the-binary", + [ + ["internal, spelled PyAPI_FUNC", 93.0], + ["internal, spelled plain extern", 0.4], + ], + unit="percent", + title="How much of the private C API the linker hands out", + caption="Both spellings sit in the same headers, often two lines apart.", + tones=["warning", "durable"], + width=460, + ) +) + + +raise SystemExit(gallery.save()) diff --git a/lessons/r06-the-c-api-tiers/diagrams/names-against-tiers.excalidraw b/lessons/r06-the-c-api-tiers/diagrams/names-against-tiers.excalidraw new file mode 100644 index 0000000..c03d443 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/names-against-tiers.excalidraw @@ -0,0 +1,983 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "7ab083a351c7c250c8c6", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 705.24, + "height": 30.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "The naming convention against the directory it sits in", + "originalText": "The naming convention against the directory it sits in", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "b7a0ff007c3725327d5f", + "type": "text", + "x": 14.0, + "y": 50.0, + "width": 27.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "tier", + "originalText": "tier", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "fa04c9fdf823d54a33e5", + "type": "text", + "x": 205.2, + "y": 50.0, + "width": 132.95999999999998, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "plain Py names", + "originalText": "plain Py names", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "4bf8546c55e6f311e2cb", + "type": "text", + "x": 367.6, + "y": 50.0, + "width": 152.16000000000003, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "PyUnstable names", + "originalText": "PyUnstable names", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "829d9ae58f7f16940c2e", + "type": "text", + "x": 549.2, + "y": 50.0, + "width": 152.16000000000003, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "underscore names", + "originalText": "underscore names", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "3639254791d2f53366aa", + "type": "line", + "x": 0.0, + "y": 76.0, + "width": 716.8, + "height": 0.0, + "angle": 0, + "strokeColor": "#ced4da", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 716.8, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "9d1a5e0084238b29fd53", + "type": "line", + "x": 0.0, + "y": 76.0, + "width": 716.8, + "height": 40.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#96f2d7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 716.8, + 0.0 + ], + [ + 716.8, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "56403c4b45902e091c48", + "type": "text", + "x": 14.0, + "y": 86.0, + "width": 76.8, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Include/", + "originalText": "Include/", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "be42a99cf298a10c5224", + "type": "text", + "x": 205.2, + "y": 86.0, + "width": 28.799999999999997, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "752", + "originalText": "752", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "840d10818e9028e87ecd", + "type": "text", + "x": 367.6, + "y": 86.0, + "width": 9.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "1", + "originalText": "1", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "e3ae0b97edbded83b79c", + "type": "text", + "x": 549.2, + "y": 86.0, + "width": 19.2, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "17", + "originalText": "17", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "cca0ac7b7ae704026058", + "type": "line", + "x": 0.0, + "y": 116.0, + "width": 716.8, + "height": 0.0, + "angle": 0, + "strokeColor": "#ced4da", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 716.8, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "e1444ae2f02ccc300500", + "type": "line", + "x": 0.0, + "y": 116.0, + "width": 716.8, + "height": 40.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 716.8, + 0.0 + ], + [ + 716.8, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "c3ef93bce6dd334a6315", + "type": "text", + "x": 14.0, + "y": 126.0, + "width": 153.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Include/cpython/", + "originalText": "Include/cpython/", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "d33cf3b8c2c383fdcd1f", + "type": "text", + "x": 205.2, + "y": 126.0, + "width": 28.799999999999997, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "322", + "originalText": "322", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "d51a5fec8f5a78d86474", + "type": "text", + "x": 367.6, + "y": 126.0, + "width": 19.2, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "33", + "originalText": "33", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "0bcc45f0e081cd6b5938", + "type": "text", + "x": 549.2, + "y": 126.0, + "width": 19.2, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "90", + "originalText": "90", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "ce894ae891e849b745a7", + "type": "line", + "x": 0.0, + "y": 156.0, + "width": 716.8, + "height": 0.0, + "angle": 0, + "strokeColor": "#ced4da", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 716.8, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "865329ba7769f0c8ca3f", + "type": "line", + "x": 0.0, + "y": 156.0, + "width": 716.8, + "height": 40.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 716.8, + 0.0 + ], + [ + 716.8, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "da25ce2f3545db0958ac", + "type": "text", + "x": 14.0, + "y": 166.0, + "width": 163.2, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Include/internal/", + "originalText": "Include/internal/", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "0f8f7401b824718f7cb6", + "type": "text", + "x": 205.2, + "y": 166.0, + "width": 9.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "8", + "originalText": "8", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "f6f79573f79949747459", + "type": "text", + "x": 367.6, + "y": 166.0, + "width": 9.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "0", + "originalText": "0", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "11fe5b05a83be4ad21bc", + "type": "text", + "x": 549.2, + "y": 166.0, + "width": 28.799999999999997, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "522", + "originalText": "522", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "d98564a9e17adf88ff49", + "type": "line", + "x": 0.0, + "y": 196.0, + "width": 716.8, + "height": 0.0, + "angle": 0, + "strokeColor": "#ced4da", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 716.8, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "02c2b2574b58bb4f65a0", + "type": "text", + "x": 0.0, + "y": 216.0, + "width": 786.48, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Close, but not the same thing. The 17 in the top right are what public macros expand to.", + "originalText": "Close, but not the same thing. The 17 in the top right are what public macros expand to.", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + } + ], + "appState": { + "gridSize": 20, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} diff --git a/lessons/r06-the-c-api-tiers/diagrams/names-against-tiers.svg b/lessons/r06-the-c-api-tiers/diagrams/names-against-tiers.svg new file mode 100644 index 0000000..97aa399 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/names-against-tiers.svg @@ -0,0 +1 @@ +The naming convention against the directory it sits intierplain Py namesPyUnstable namesunderscore namesInclude/752117Include/cpython/3223390Include/internal/80522Close, but not the same thing. The 17 in the top right are what public macros expand to. diff --git a/lessons/r06-the-c-api-tiers/diagrams/the-gate.excalidraw b/lessons/r06-the-c-api-tiers/diagrams/the-gate.excalidraw new file mode 100644 index 0000000..c4b803c --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/the-gate.excalidraw @@ -0,0 +1,580 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "d10b66a346c041d8f960", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 510.84000000000003, + "height": 30.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "How the second door opens and closes", + "originalText": "How the second door opens and closes", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "9c952dffe6dd1caad3fb", + "type": "rectangle", + "x": 0.0, + "y": 50.0, + "width": 367.99999999999994, + "height": 70.0, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "6fed76ec4de6238ce5a4", + "type": "text" + }, + { + "id": "8780e0e032f46ff3911f", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "6fed76ec4de6238ce5a4", + "type": "text", + "x": 16.0, + "y": 72.5, + "width": 335.99999999999994, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "you include Python.h", + "originalText": "you include Python.h", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "9c952dffe6dd1caad3fb", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "5fe38681290218af8c9e", + "type": "rectangle", + "x": 0.0, + "y": 180.0, + "width": 367.99999999999994, + "height": 70.0, + "angle": 0, + "strokeColor": "#e8590c", + "backgroundColor": "#ffd8a8", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "75aee1a0e9c583b218a9", + "type": "text" + }, + { + "id": "8780e0e032f46ff3911f", + "type": "arrow" + }, + { + "id": "57d5c89db3d432ef5562", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "75aee1a0e9c583b218a9", + "type": "text", + "x": 16.0, + "y": 202.5, + "width": 335.99999999999994, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "is Py_LIMITED_API defined", + "originalText": "is Py_LIMITED_API defined", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "5fe38681290218af8c9e", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "f0f13179d555ca6d206a", + "type": "rectangle", + "x": 0.0, + "y": 310.0, + "width": 367.99999999999994, + "height": 70.0, + "angle": 0, + "strokeColor": "#099268", + "backgroundColor": "#96f2d7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "5e4c2a8977efa31a52c4", + "type": "text" + }, + { + "id": "57d5c89db3d432ef5562", + "type": "arrow" + }, + { + "id": "2b339a9232eee4eedd5d", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "5e4c2a8977efa31a52c4", + "type": "text", + "x": 16.0, + "y": 332.5, + "width": 335.99999999999994, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "no, so pull in cpython/object.h", + "originalText": "no, so pull in cpython/object.h", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "f0f13179d555ca6d206a", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "f2cad802c0f6db2f9a93", + "type": "rectangle", + "x": 0.0, + "y": 440.0, + "width": 367.99999999999994, + "height": 70.0, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "7610b6a0505b627c2be3", + "type": "text" + }, + { + "id": "2b339a9232eee4eedd5d", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "7610b6a0505b627c2be3", + "type": "text", + "x": 16.0, + "y": 462.5, + "width": 335.99999999999994, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "yes, so skip it", + "originalText": "yes, so skip it", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "f2cad802c0f6db2f9a93", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "8780e0e032f46ff3911f", + "type": "arrow", + "x": 183.99999999999997, + "y": 120.0, + "width": 0.0, + "height": 60.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0, + 60.0 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "9c952dffe6dd1caad3fb", + "focus": 0, + "gap": 4 + }, + "endBinding": { + "elementId": "5fe38681290218af8c9e", + "focus": 0, + "gap": 4 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "57d5c89db3d432ef5562", + "type": "arrow", + "x": 183.99999999999997, + "y": 250.0, + "width": 0.0, + "height": 60.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0, + 60.0 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "5fe38681290218af8c9e", + "focus": 0, + "gap": 4 + }, + "endBinding": { + "elementId": "f0f13179d555ca6d206a", + "focus": 0, + "gap": 4 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "be6c306d26a4088b3004", + "type": "text", + "x": 195.99999999999997, + "y": 270.0, + "width": 88.56, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "one macro", + "originalText": "one macro", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "2b339a9232eee4eedd5d", + "type": "arrow", + "x": 183.99999999999997, + "y": 380.0, + "width": 0.0, + "height": 60.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0, + 60.0 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "f0f13179d555ca6d206a", + "focus": 0, + "gap": 4 + }, + "endBinding": { + "elementId": "f2cad802c0f6db2f9a93", + "focus": 0, + "gap": 4 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "aecd437d8ab27163a96c", + "type": "text", + "x": 195.99999999999997, + "y": 400.0, + "width": 141.36, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "the whole struct", + "originalText": "the whole struct", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + } + ], + "appState": { + "gridSize": 20, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} diff --git a/lessons/r06-the-c-api-tiers/diagrams/the-gate.svg b/lessons/r06-the-c-api-tiers/diagrams/the-gate.svg new file mode 100644 index 0000000..7bcef0a --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/the-gate.svg @@ -0,0 +1 @@ +How the second door opens and closesyou include Python.his Py_LIMITED_API definedno, so pull in cpython/object.hyes, so skip itone macrothe whole struct diff --git a/lessons/r06-the-c-api-tiers/diagrams/three-doors.excalidraw b/lessons/r06-the-c-api-tiers/diagrams/three-doors.excalidraw new file mode 100644 index 0000000..cc1d6e1 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/three-doors.excalidraw @@ -0,0 +1,288 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "a485dbf7e873726462a8", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 658.8, + "height": 30.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "The C API is three directories, one inside the next", + "originalText": "The C API is three directories, one inside the next", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "9b36ce2fbdeb02b34fa9", + "type": "rectangle", + "x": 0.0, + "y": 50.0, + "width": 664.8, + "height": 146.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "c970e4081ad75912c780", + "type": "text", + "x": 16.0, + "y": 58.0, + "width": 460.79999999999995, + "height": 20.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Include/internal, 148 files, needs Py_BUILD_CORE", + "originalText": "Include/internal, 148 files, needs Py_BUILD_CORE", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "3d7af082c6fc4143ad62", + "type": "rectangle", + "x": 14.0, + "y": 88.0, + "width": 636.8, + "height": 94.0, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "89d355e1ed8deaed2f3d", + "type": "text", + "x": 30.0, + "y": 96.0, + "width": 604.8, + "height": 20.0, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Include/cpython, 63 files, needs Py_LIMITED_API to be undefined", + "originalText": "Include/cpython, 63 files, needs Py_LIMITED_API to be undefined", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "4649b82ca98333575041", + "type": "rectangle", + "x": 28.0, + "y": 126.0, + "width": 608.8, + "height": 52.0, + "angle": 0, + "strokeColor": "#6741d9", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "9ace609e03a8b4342f02", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "9ace609e03a8b4342f02", + "type": "text", + "x": 44.0, + "y": 142.0, + "width": 576.8, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Include, 79 files, anybody may use these", + "originalText": "Include, 79 files, anybody may use these", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "4649b82ca98333575041", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "fec92624f3f13b882d60", + "type": "text", + "x": 0.0, + "y": 216.0, + "width": 542.4, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "An extension gets the inner ring. CPython itself gets all three.", + "originalText": "An extension gets the inner ring. CPython itself gets all three.", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + } + ], + "appState": { + "gridSize": 20, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} diff --git a/lessons/r06-the-c-api-tiers/diagrams/three-doors.svg b/lessons/r06-the-c-api-tiers/diagrams/three-doors.svg new file mode 100644 index 0000000..1538b86 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/three-doors.svg @@ -0,0 +1 @@ +The C API is three directories, one inside the nextInclude/internal, 148 files, needs Py_BUILD_COREInclude/cpython, 63 files, needs Py_LIMITED_API to be undefinedInclude, 79 files, anybody may use theseAn extension gets the inner ring. CPython itself gets all three. diff --git a/lessons/r06-the-c-api-tiers/diagrams/what-leaves-the-binary.excalidraw b/lessons/r06-the-c-api-tiers/diagrams/what-leaves-the-binary.excalidraw new file mode 100644 index 0000000..3f70ea5 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/what-leaves-the-binary.excalidraw @@ -0,0 +1,336 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "dc947ac4ea9ed7efb00c", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 677.8799999999999, + "height": 30.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "How much of the private C API the linker hands out", + "originalText": "How much of the private C API the linker hands out", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "57cce7489c76962b7313", + "type": "text", + "x": 0.0, + "y": 57.0, + "width": 268.8, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "internal, spelled PyAPI_FUNC", + "originalText": "internal, spelled PyAPI_FUNC", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "d0c0c50fa156966a483f", + "type": "line", + "x": 308.0, + "y": 50.0, + "width": 460.0, + "height": 34.0, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0.0, + 0.0 + ], + [ + 460.0, + 0.0 + ], + [ + 460.0, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "7b2476d702421752ff7a", + "type": "text", + "x": 778.0, + "y": 57.0, + "width": 96.0, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "93 percent", + "originalText": "93 percent", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "dbc72d9b3656650f9bc4", + "type": "text", + "x": 0.0, + "y": 101.0, + "width": 288.0, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "internal, spelled plain extern", + "originalText": "internal, spelled plain extern", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "4060e063ee33db6d9661", + "type": "line", + "x": 308.0, + "y": 94.0, + "width": 3.0, + "height": 34.0, + "angle": 0, + "strokeColor": "#099268", + "backgroundColor": "#96f2d7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0.0, + 0.0 + ], + [ + 3.0, + 0.0 + ], + [ + 3.0, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "36054f96c1e62b0a0b54", + "type": "text", + "x": 321.0, + "y": 101.0, + "width": 105.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "0.4 percent", + "originalText": "0.4 percent", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "0d140814b7dddb48a866", + "type": "text", + "x": 0.0, + "y": 148.0, + "width": 541.92, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Both spellings sit in the same headers, often two lines apart.", + "originalText": "Both spellings sit in the same headers, often two lines apart.", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + } + ], + "appState": { + "gridSize": 20, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} diff --git a/lessons/r06-the-c-api-tiers/diagrams/what-leaves-the-binary.svg b/lessons/r06-the-c-api-tiers/diagrams/what-leaves-the-binary.svg new file mode 100644 index 0000000..356d6f7 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/what-leaves-the-binary.svg @@ -0,0 +1 @@ +How much of the private C API the linker hands outinternal, spelled PyAPI_FUNC93 percentinternal, spelled plain extern0.4 percentBoth spellings sit in the same headers, often two lines apart. diff --git a/lessons/r06-the-c-api-tiers/diagrams/what-limited-costs.excalidraw b/lessons/r06-the-c-api-tiers/diagrams/what-limited-costs.excalidraw new file mode 100644 index 0000000..4e379b6 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/what-limited-costs.excalidraw @@ -0,0 +1,753 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "fa1ba5f141bf2cade55e", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 611.28, + "height": 30.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "What defining Py_LIMITED_API actually changes", + "originalText": "What defining Py_LIMITED_API actually changes", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "2a069a09ca84ff6fbeea", + "type": "text", + "x": 123.25, + "y": 50.0, + "width": 181.5, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "an ordinary build", + "originalText": "an ordinary build", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "bb00c71c2a708631a2ac", + "type": "rectangle", + "x": 0.0, + "y": 87.0, + "width": 428.0, + "height": 57.0, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "c3bb8d74a5b22bbdb03f", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "c3bb8d74a5b22bbdb03f", + "type": "text", + "x": 16.0, + "y": 103.0, + "width": 396.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "PyObject has fields", + "originalText": "PyObject has fields", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "bb00c71c2a708631a2ac", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "394f67b586d42d9d52fd", + "type": "rectangle", + "x": 0.0, + "y": 147.0, + "width": 428.0, + "height": 57.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "6761b2fd6e3b31111fec", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "6761b2fd6e3b31111fec", + "type": "text", + "x": 16.0, + "y": 163.0, + "width": 396.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Py_DECREF is a decrement", + "originalText": "Py_DECREF is a decrement", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "394f67b586d42d9d52fd", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "08b3b08ff74cc922c23b", + "type": "rectangle", + "x": 0.0, + "y": 207.0, + "width": 428.0, + "height": 57.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "0e4694677aa915c81128", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "0e4694677aa915c81128", + "type": "text", + "x": 16.0, + "y": 223.0, + "width": 396.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Py_TYPE reads ob_type", + "originalText": "Py_TYPE reads ob_type", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "08b3b08ff74cc922c23b", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "ebd210980819782dacad", + "type": "rectangle", + "x": 0.0, + "y": 267.0, + "width": 428.0, + "height": 57.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "8c13ffff371749b5ef7e", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "8c13ffff371749b5ef7e", + "type": "text", + "x": 16.0, + "y": 283.0, + "width": 396.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "recompile for every minor release", + "originalText": "recompile for every minor release", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "ebd210980819782dacad", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "f8bb1bd78ca80b8c0f75", + "type": "text", + "x": 621.9, + "y": 50.0, + "width": 160.2, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "a limited build", + "originalText": "a limited build", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "758d10525e165688465b", + "type": "rectangle", + "x": 488.0, + "y": 87.0, + "width": 428.0, + "height": 57.0, + "angle": 0, + "strokeColor": "#099268", + "backgroundColor": "#96f2d7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "418fc2d7125b55e35800", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "418fc2d7125b55e35800", + "type": "text", + "x": 504.0, + "y": 103.0, + "width": 396.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "PyObject is an opaque type", + "originalText": "PyObject is an opaque type", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "758d10525e165688465b", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "62bb66d127441aac2006", + "type": "rectangle", + "x": 488.0, + "y": 147.0, + "width": 428.0, + "height": 57.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "36481949b9bce29a78b2", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "36481949b9bce29a78b2", + "type": "text", + "x": 504.0, + "y": 163.0, + "width": 396.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Py_DECREF calls _Py_DecRef", + "originalText": "Py_DECREF calls _Py_DecRef", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "62bb66d127441aac2006", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "927dca053f6434888b96", + "type": "rectangle", + "x": 488.0, + "y": 207.0, + "width": 428.0, + "height": 57.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "2816ec6496db492f28e3", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "2816ec6496db492f28e3", + "type": "text", + "x": 504.0, + "y": 223.0, + "width": 396.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "Py_TYPE calls Py_TYPE", + "originalText": "Py_TYPE calls Py_TYPE", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "927dca053f6434888b96", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "3698c6b2a32a2801eb97", + "type": "rectangle", + "x": 488.0, + "y": 267.0, + "width": 428.0, + "height": 57.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "023d31e7b1a4754a5252", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "023d31e7b1a4754a5252", + "type": "text", + "x": 504.0, + "y": 283.0, + "width": 396.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "one build runs on many releases", + "originalText": "one build runs on many releases", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "3698c6b2a32a2801eb97", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "491baa4879c013513a31", + "type": "rectangle", + "x": 0.0, + "y": 344.0, + "width": 916.0, + "height": 60.0, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "id": "18fb651773eefad7e691", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "18fb651773eefad7e691", + "type": "text", + "x": 16.0, + "y": 361.5, + "width": 884.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "text": "It takes away the layout, not the functions. Every field read becomes a call.", + "originalText": "It takes away the layout, not the functions. Every field read becomes a call.", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "491baa4879c013513a31", + "lineHeight": 1.25, + "autoResize": false + } + ], + "appState": { + "gridSize": 20, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} diff --git a/lessons/r06-the-c-api-tiers/diagrams/what-limited-costs.svg b/lessons/r06-the-c-api-tiers/diagrams/what-limited-costs.svg new file mode 100644 index 0000000..2a6ea0f --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/what-limited-costs.svg @@ -0,0 +1 @@ +What defining Py_LIMITED_API actually changesan ordinary buildPyObject has fieldsPy_DECREF is a decrementPy_TYPE reads ob_typerecompile for every minor releasea limited buildPyObject is an opaque typePy_DECREF calls _Py_DecRefPy_TYPE calls Py_TYPEone build runs on many releasesIt takes away the layout, not the functions. Every field read becomes a call. diff --git a/lessons/r06-the-c-api-tiers/diagrams/who-can-see-what.excalidraw b/lessons/r06-the-c-api-tiers/diagrams/who-can-see-what.excalidraw new file mode 100644 index 0000000..ab5a175 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/who-can-see-what.excalidraw @@ -0,0 +1,1131 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "58d6afc2fbdea4d4da91", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 627.84, + "height": 30.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "The three tiers, and what each one asks of you", + "originalText": "The three tiers, and what each one asks of you", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "347797761bad84af3985", + "type": "text", + "x": 14.0, + "y": 50.0, + "width": 27.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "tier", + "originalText": "tier", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "8c56aef28a91391782d4", + "type": "text", + "x": 157.2, + "y": 50.0, + "width": 72.0, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "directory", + "originalText": "directory", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "c2cc868d4b828da96e74", + "type": "text", + "x": 348.4, + "y": 50.0, + "width": 119.75999999999999, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "what opens it", + "originalText": "what opens it", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "6f0043ea90b6836c07ae", + "type": "text", + "x": 606.8, + "y": 50.0, + "width": 177.12, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "how the names look", + "originalText": "how the names look", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "05e669f93c266861acc4", + "type": "text", + "x": 817.1999999999999, + "y": 50.0, + "width": 150.95999999999998, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "how long it lasts", + "originalText": "how long it lasts", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "cf6cf5483ac09aa30c5e", + "type": "line", + "x": 0.0, + "y": 76.0, + "width": 994.4, + "height": 0.0, + "angle": 0, + "strokeColor": "#ced4da", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 994.4, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "b63415c07029f7ab3937", + "type": "line", + "x": 0.0, + "y": 76.0, + "width": 994.4, + "height": 40.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#96f2d7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 994.4, + 0.0 + ], + [ + 994.4, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "e7013aa140e9bf3f2c6f", + "type": "text", + "x": 14.0, + "y": 86.0, + "width": 57.599999999999994, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "public", + "originalText": "public", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "1cc366e43c8ac6f9e6f1", + "type": "text", + "x": 157.2, + "y": 86.0, + "width": 76.8, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Include/", + "originalText": "Include/", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "eca551e07e9f7d336fce", + "type": "text", + "x": 348.4, + "y": 86.0, + "width": 105.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "always open", + "originalText": "always open", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "bc4156a1659189359c0a", + "type": "text", + "x": 606.8, + "y": 86.0, + "width": 124.8, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "PyList_Append", + "originalText": "PyList_Append", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "a12bc874deccfee6d2c3", + "type": "text", + "x": 817.1999999999999, + "y": 86.0, + "width": 67.2, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "PEP 387", + "originalText": "PEP 387", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "955121d0a4ce795210bf", + "type": "line", + "x": 0.0, + "y": 116.0, + "width": 994.4, + "height": 0.0, + "angle": 0, + "strokeColor": "#ced4da", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 994.4, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "4fc8c20b002c7d4be5d0", + "type": "line", + "x": 0.0, + "y": 116.0, + "width": 994.4, + "height": 40.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 994.4, + 0.0 + ], + [ + 994.4, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "7bb18be3d47df8010e67", + "type": "text", + "x": 14.0, + "y": 126.0, + "width": 115.19999999999999, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "cpython only", + "originalText": "cpython only", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "50000c2260f209a77dce", + "type": "text", + "x": 157.2, + "y": 126.0, + "width": 153.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Include/cpython/", + "originalText": "Include/cpython/", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "318d81fe60a8816cdd45", + "type": "text", + "x": 348.4, + "y": 126.0, + "width": 230.39999999999998, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Py_LIMITED_API undefined", + "originalText": "Py_LIMITED_API undefined", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "77d1c387c2a86e9bf07e", + "type": "text", + "x": 606.8, + "y": 126.0, + "width": 182.4, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "PyUnstable_Code_New", + "originalText": "PyUnstable_Code_New", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "7a0fd886a15c1d2c7be6", + "type": "text", + "x": 817.1999999999999, + "y": 126.0, + "width": 105.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "one release", + "originalText": "one release", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "0ee1655becfca800d27e", + "type": "line", + "x": 0.0, + "y": 156.0, + "width": 994.4, + "height": 0.0, + "angle": 0, + "strokeColor": "#ced4da", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 994.4, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "0efca1f347b020b11016", + "type": "line", + "x": 0.0, + "y": 156.0, + "width": 994.4, + "height": 40.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 994.4, + 0.0 + ], + [ + 994.4, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "c42b2403c8d838471188", + "type": "text", + "x": 14.0, + "y": 166.0, + "width": 76.8, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "internal", + "originalText": "internal", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "76056bad3a7de8f347e4", + "type": "text", + "x": 157.2, + "y": 166.0, + "width": 163.2, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Include/internal/", + "originalText": "Include/internal/", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "020bdb7e2d87f88e1746", + "type": "text", + "x": 348.4, + "y": 166.0, + "width": 201.6, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "Py_BUILD_CORE defined", + "originalText": "Py_BUILD_CORE defined", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "5e1322037f02b59fa93a", + "type": "text", + "x": 606.8, + "y": 166.0, + "width": 134.4, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "_PyDict_SizeOf", + "originalText": "_PyDict_SizeOf", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "2394761700fde4b07049", + "type": "text", + "x": 817.1999999999999, + "y": 166.0, + "width": 96.0, + "height": 20.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "no promise", + "originalText": "no promise", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "15d4ffed6092b99e3194", + "type": "line", + "x": 0.0, + "y": 196.0, + "width": 994.4, + "height": 0.0, + "angle": 0, + "strokeColor": "#ced4da", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.0 + ], + [ + 994.4, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "dd872a1abb8b700c093d", + "type": "text", + "x": 0.0, + "y": 216.0, + "width": 677.04, + "height": 20.0, + "angle": 0, + "strokeColor": "#5c5f66", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [], + "updated": 1, + "link": null, + "locked": false, + "customData": null, + "text": "The middle row is the one most extensions actually live in without noticing.", + "originalText": "The middle row is the one most extensions actually live in without noticing.", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + } + ], + "appState": { + "gridSize": 20, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} diff --git a/lessons/r06-the-c-api-tiers/diagrams/who-can-see-what.svg b/lessons/r06-the-c-api-tiers/diagrams/who-can-see-what.svg new file mode 100644 index 0000000..fdd5220 --- /dev/null +++ b/lessons/r06-the-c-api-tiers/diagrams/who-can-see-what.svg @@ -0,0 +1 @@ +The three tiers, and what each one asks of youtierdirectorywhat opens ithow the names lookhow long it lastspublicInclude/always openPyList_AppendPEP 387cpython onlyInclude/cpython/Py_LIMITED_API undefinedPyUnstable_Code_Newone releaseinternalInclude/internal/Py_BUILD_CORE defined_PyDict_SizeOfno promiseThe middle row is the one most extensions actually live in without noticing. diff --git a/lessons/r06-the-c-api-tiers/r06.ipynb b/lessons/r06-the-c-api-tiers/r06.ipynb new file mode 100644 index 0000000..d6218dd --- /dev/null +++ b/lessons/r06-the-c-api-tiers/r06.ipynb @@ -0,0 +1,892 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "r06-01", + "metadata": {}, + "source": [ + "# R06. The C API tiers\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/r06-the-c-api-tiers/r06.ipynb)\n", + "\n", + "Everything so far has looked at CPython from inside Python. This one looks at it from the other side, the way a C extension does.\n", + "\n", + "An extension gets at the interpreter by including `Python.h` and calling functions. Which functions it is allowed to call is not one list. It is three, they live in three directories, and you pick which ones you get by defining a macro before the include.\n", + "\n", + "![three nested rings, the public headers inside the cpython only headers inside the internal headers](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r06-the-c-api-tiers/diagrams/three-doors.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "r06-02", + "metadata": {}, + "source": [ + "## About the source references\n", + "\n", + "Now and then this lesson points at CPython's own source, like this: `Include/exports.h:88-93@v3.15.0rc1`.\n", + "\n", + "Read it as three parts: the file, the lines, and the release those line numbers belong to. Sometimes there is a fourth part after a `#`, which is the name of the thing those lines are inside.\n", + "\n", + "Every reference is a link, and every one is checked against the pinned source on each change, so a stale reference fails the build instead of sending you somewhere wrong. You never have to read any of it. The references are there so you can go deeper when you want to, and so you can check that this lesson is not making things up.\n", + "\n", + "## Setup\n", + "\n", + "Colab does not come with the small package these lessons use, so the next cell installs it. If you are running this from a checkout of the repository it is already installed and the cell does nothing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-03", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "if sys.version_info < (3, 14):\n", + " print(\"This lesson needs CPython 3.14 or newer.\")\n", + " print(f\"This runtime is {sys.version.split()[0]}, and the cells below will not run on it.\")\n", + "else:\n", + " try:\n", + " import pyxray\n", + " except ImportError:\n", + " %pip install -q \"pyxray @ git+https://github.com/tamnd/cpython-internals@main#subdirectory=pyxray\"\n", + " import pyxray" + ] + }, + { + "cell_type": "markdown", + "id": "r06-04", + "metadata": {}, + "source": [ + "## Which Python is this\n", + "\n", + "About half the cells here read the header files that sit next to the interpreter. A normal install has them and a browser tab does not, so the first cell below works out whether they are there and the rest check that answer before they do anything. The other half only need `ctypes`, which works everywhere.\n", + "\n", + "## Which interpreter is this" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-05", + "metadata": { + "cpython_internals": { + "differs": "This prints the interpreter you are on, so it is different for everybody." + } + }, + "outputs": [], + "source": [ + "import pyxray\n", + "\n", + "pyxray.show()" + ] + }, + { + "cell_type": "markdown", + "id": "r06-06", + "metadata": {}, + "source": [ + "## Three directories\n", + "\n", + "[the set of C functions and macros an extension may call to talk to the interpreter](https://github.com/tamnd/cpython-internals/blob/main/GLOSSARY.md#c-api) is not one thing. `Include/` holds the headers any extension may use. `Include/cpython/` holds the ones that only make sense if you were compiled against this exact CPython. `Include/internal/` holds the ones CPython uses on itself.\n", + "\n", + "The first cell finds where they live and measures how much is in each. The interesting number is the third one.\n", + "\n", + "Most of CPython's header surface is in the directory extensions are not meant to open." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-07", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 the internal directory is a good deal smaller, 139 files and about thirty thousand lines rather than 148 and forty two thousand." + } + }, + "outputs": [], + "source": [ + "import pathlib\n", + "import sysconfig\n", + "\n", + "INCLUDE = pathlib.Path(sysconfig.get_paths()[\"include\"])\n", + "TIERS = ((\"public\", \"*.h\"), (\"cpython only\", \"cpython/*.h\"), (\"internal\", \"internal/*.h\"))\n", + "HEADERS = INCLUDE.is_dir() and any(INCLUDE.glob(\"*.h\"))\n", + "\n", + "\n", + "def headers(pattern):\n", + " \"\"\"Every header in one tier as text, or nothing when this runtime did not ship them.\"\"\"\n", + " return [path.read_text(errors=\"replace\") for path in sorted(INCLUDE.glob(pattern))]\n", + "\n", + "\n", + "if not HEADERS:\n", + " print(\" no headers next to this interpreter, looked in\", INCLUDE)\n", + " print(\" the cells that read them say so and skip themselves\")\n", + "else:\n", + " print(\" headers live in\", INCLUDE)\n", + " for tier, pattern in TIERS:\n", + " blobs = headers(pattern)\n", + " lines = sum(len(blob.splitlines()) for blob in blobs)\n", + " print(f\" {tier:13} {len(blobs):4} files {lines:7} lines\")" + ] + }, + { + "cell_type": "markdown", + "id": "r06-08", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 the internal directory is a good deal smaller, 139 files and about thirty thousand lines rather than 148 and forty two thousand." + ] + }, + { + "cell_type": "markdown", + "id": "r06-09", + "metadata": {}, + "source": [ + "More than half the lines are in the directory nobody outside CPython is supposed to read. That is not a mistake. The interpreter is written against its own headers, and those headers describe the parts that change from release to release.\n", + "\n", + "![a table of the three tiers, the directory each one lives in, the macro that opens it, how its names look and how long it lasts](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r06-the-c-api-tiers/diagrams/who-can-see-what.svg)\n", + "\n", + "The right hand column is the whole point. The public tier is covered by CPython's backwards compatibility policy. The middle one may change in any minor release. The internal one may change in a patch release, and the documentation says so plainly in [Doc/c-api/stable.rst:21-35@v3.15.0rc1](https://github.com/python/cpython/blob/v3.15.0rc1/Doc/c-api/stable.rst#L21-L35).\n", + "\n", + "## What one macro hides\n", + "\n", + "The gate is `Py_LIMITED_API`. Define it before you include `Python.h` and a large amount of the header tree stops existing for you.\n", + "\n", + "The mechanism is dull and worth seeing once. At the bottom of [Include/object.h:740-744@v3.15.0rc1#Py_CPYTHON_OBJECT_H](https://github.com/python/cpython/blob/v3.15.0rc1/Include/object.h#L740-L744) there is an `#ifndef Py_LIMITED_API` around an `#include \"cpython/object.h\"`. Almost every public header ends the same way. That is the entire second door.\n", + "\n", + "[the subset of the C API you get when you define Py_LIMITED_API before including Python.h](https://github.com/tamnd/cpython-internals/blob/main/GLOSSARY.md#limited-api) is not a separate set of files. It is the same files with a lot of them switched off.\n", + "\n", + "Defining Py_LIMITED_API removes about a quarter of the functions declared in the public headers, and all of the other two directories." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-10", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 the numbers are 575 and 162, and 824 in the other two directories. The four answers underneath are the same." + } + }, + "outputs": [], + "source": [ + "import re\n", + "\n", + "DECLARED = re.compile(r\"^PyAPI_FUNC\\([^)]*\\)\\s*\\**\\s*(\\w+)\", re.M)\n", + "OPENS = re.compile(r\"^\\s*#\\s*if\")\n", + "CLOSES = re.compile(r\"^\\s*#\\s*endif\")\n", + "ASKED = (\"PyList_Append\", \"PyErr_SetString\", \"PyBuffer_FillInfo\", \"PyThreadState_GetFrame\")\n", + "\n", + "\n", + "def split_by_the_guard(text):\n", + " \"\"\"Split one header's function names into what a limited build sees and what it does not.\"\"\"\n", + " shut = []\n", + " seen, hidden = set(), set()\n", + " for line in text.splitlines():\n", + " if OPENS.match(line):\n", + " shut.append(\"Py_LIMITED_API\" in line and (\"ifndef\" in line or \"!defined\" in line))\n", + " elif CLOSES.match(line) and shut:\n", + " shut.pop()\n", + " found = DECLARED.match(line)\n", + " if found:\n", + " (hidden if any(shut) else seen).add(found.group(1))\n", + " return seen, hidden\n", + "\n", + "\n", + "if HEADERS:\n", + " seen, hidden = set(), set()\n", + " for blob in headers(\"*.h\"):\n", + " one, other = split_by_the_guard(blob)\n", + " seen |= one\n", + " hidden |= other\n", + " away = set()\n", + " for pattern in (\"cpython/*.h\", \"internal/*.h\"):\n", + " for blob in headers(pattern):\n", + " away |= set(DECLARED.findall(blob))\n", + " print(\" in the public headers\")\n", + " print(\" functions a limited build may call:\", len(seen))\n", + " print(\" functions the guard takes away: \", len(hidden))\n", + " print(\" in the other two directories, which it never opens:\", len(away))\n", + " print()\n", + " for name in ASKED:\n", + " answer = \"yes\" if name in seen else \"no, the guard hides it\"\n", + " print(f\" can a limited build call {name:24} {answer}\")" + ] + }, + { + "cell_type": "markdown", + "id": "r06-11", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 the numbers are 575 and 162, and 824 in the other two directories. The four answers underneath are the same." + ] + }, + { + "cell_type": "markdown", + "id": "r06-12", + "metadata": {}, + "source": [ + "![a four step flow, including Python.h, checking whether Py_LIMITED_API is defined, and either pulling in cpython/object.h or skipping it](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r06-the-c-api-tiers/diagrams/the-gate.svg)\n", + "\n", + "Losing a quarter of the functions is the part people notice. It is not the part that hurts.\n", + "\n", + "What actually hurts is that the struct definitions go too. With `_Py_OPAQUE_PYOBJECT` set, [Include/object.h:124-126@v3.15.0rc1#_Py_OPAQUE_PYOBJECT](https://github.com/python/cpython/blob/v3.15.0rc1/Include/object.h#L124-L126) says only `/* PyObject is opaque */`, so there is no `ob_refcnt` field to read and no `ob_type` field to follow.\n", + "\n", + "![a comparison of an ordinary build against a limited one, showing that the fields disappear and the macros become calls](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r06-the-c-api-tiers/diagrams/what-limited-costs.svg)\n", + "\n", + "That is the trade. You give up knowing where anything is, and in return one built file keeps working across releases, because the thing that changes between releases is where everything is.\n", + "\n", + "## The lock on the third door\n", + "\n", + "The third door has a different kind of lock, and it is worth seeing because it is so much cruder than you would expect.\n", + "\n", + "[Include/internal/pycore_object.h:7-9@v3.15.0rc1#Py_BUILD_CORE](https://github.com/python/cpython/blob/v3.15.0rc1/Include/internal/pycore_object.h#L7-L9) is three lines: if `Py_BUILD_CORE` is not defined, stop the compiler with an error message. Nearly every file in that directory starts with the same three lines.\n", + "\n", + "The internal headers are protected by a compiler error and nothing else." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-13", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 it is 139 headers and 132 of them, because the directory grew in 3.15." + } + }, + "outputs": [], + "source": [ + "if HEADERS:\n", + " blobs = headers(\"internal/*.h\")\n", + " refuse = [blob for blob in blobs if \"#ifndef Py_BUILD_CORE\" in blob]\n", + " print(\" internal headers: \", len(blobs))\n", + " print(\" that stop the compiler without the macro:\", len(refuse))\n", + " print()\n", + " start = refuse[0].index(\"#ifndef Py_BUILD_CORE\")\n", + " for line in refuse[0][start:].splitlines()[:3]:\n", + " print(\" \", line)" + ] + }, + { + "cell_type": "markdown", + "id": "r06-14", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 it is 139 headers and 132 of them, because the directory grew in 3.15." + ] + }, + { + "cell_type": "markdown", + "id": "r06-15", + "metadata": {}, + "source": [ + "Nothing stops you defining `Py_BUILD_CORE` yourself. People do. It compiles, it links, and it breaks on the next patch release when a struct grows a field.\n", + "\n", + "## The names and the directories\n", + "\n", + "There is a second convention running alongside the directories, and it is the one you meet first, because it is in the names.\n", + "\n", + "A plain `Py` name is public. A `PyUnstable_` name is the middle tier saying so out loud, which [Doc/c-api/stable.rst:44-52@v3.15.0rc1](https://github.com/python/cpython/blob/v3.15.0rc1/Doc/c-api/stable.rst#L44-L52) describes as intended for debuggers and tools that follow CPython development. A leading underscore means private.\n", + "\n", + "You would expect the three conventions to line up exactly with the three directories. They nearly do.\n", + "\n", + "The naming convention and the directory disagree about a few dozen functions, in both directions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-16", + "metadata": { + "cpython_internals": { + "differs": "The three rows on 3.14 read 716, 1, 20 then 304, 30, 89 then 8, 0, 394. The shape is the same and the internal row is a lot shorter." + } + }, + "outputs": [], + "source": [ + "if HEADERS:\n", + "\n", + " def convention(name):\n", + " \"\"\"Which of the three naming conventions a name follows.\"\"\"\n", + " if name.startswith(\"PyUnstable\"):\n", + " return \"PyUnstable\"\n", + " return \"underscore\" if name.startswith(\"_\") else \"plain Py\"\n", + "\n", + " print(f\" {'tier':13} {'plain Py':>9} {'PyUnstable':>11} {'underscore':>11}\")\n", + " for tier, pattern in TIERS:\n", + " names = set()\n", + " for blob in headers(pattern):\n", + " names |= set(DECLARED.findall(blob))\n", + " counted = {\"plain Py\": 0, \"PyUnstable\": 0, \"underscore\": 0}\n", + " for name in names:\n", + " counted[convention(name)] += 1\n", + " print(\n", + " f\" {tier:13} {counted['plain Py']:>9} \"\n", + " f\"{counted['PyUnstable']:>11} {counted['underscore']:>11}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "r06-17", + "metadata": {}, + "source": [ + "> **Version note.** The three rows on 3.14 read 716, 1, 20 then 304, 30, 89 then 8, 0, 394. The shape is the same and the internal row is a lot shorter." + ] + }, + { + "cell_type": "markdown", + "id": "r06-18", + "metadata": {}, + "source": [ + "![a table counting plain Py names, PyUnstable names and underscore names in each of the three directories](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r06-the-c-api-tiers/diagrams/names-against-tiers.svg)\n", + "\n", + "The seventeen underscore names in the public row are the interesting ones, and they all have the same explanation. `Py_DECREF` is a macro, and at [Include/refcount.h:417-429@v3.15.0rc1#Py_DECREF](https://github.com/python/cpython/blob/v3.15.0rc1/Include/refcount.h#L417-L429) you can see what it expands to: a decrement, and then a call to `_Py_Dealloc` if the count reached zero.\n", + "\n", + "That call happens in your code, after the macro is pasted in. So `_Py_Dealloc` has to be a symbol your extension can link against, private name or not, and it is declared two hundred lines earlier at [Include/refcount.h:238-241@v3.15.0rc1#_Py_Dealloc](https://github.com/python/cpython/blob/v3.15.0rc1/Include/refcount.h#L238-L241).\n", + "\n", + "A private name has to be public when a public macro expands to it. That is the whole rule, and it accounts for nearly the entire list.\n", + "\n", + "## Ask the linker instead\n", + "\n", + "Everything up to here has been about compiling. Now the interesting bit, which is what happens once the compiling is over.\n", + "\n", + "Python can look up C symbols by name. `ctypes.pythonapi` is a handle on the running interpreter, and asking it for an attribute asks the dynamic linker for a symbol with that name.\n", + "\n", + "The tier a function belongs to makes no difference to whether you can find it at run time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-19", + "metadata": {}, + "outputs": [], + "source": [ + "import ctypes\n", + "\n", + "api = ctypes.pythonapi\n", + "SAMPLE = (\n", + " (\"public\", \"PyList_Append\"),\n", + " (\"public\", \"PyErr_SetString\"),\n", + " (\"cpython only\", \"PyUnstable_Code_New\"),\n", + " (\"cpython only\", \"PyFrame_GetLasti\"),\n", + " (\"internal\", \"_PyDict_SizeOf\"),\n", + " (\"internal\", \"_PyEval_EvalFrameDefault\"),\n", + " (\"internal\", \"_PyObject_GC_New\"),\n", + " (\"no such name\", \"PyNothingLikeThis\"),\n", + ")\n", + "for tier, name in SAMPLE:\n", + " found = \"the linker hands it over\" if hasattr(api, name) else \"not found\"\n", + " print(f\" {tier:13} {name:26} {found}\")" + ] + }, + { + "cell_type": "markdown", + "id": "r06-20", + "metadata": {}, + "source": [ + "Seven for seven, and the made up name is the only one that fails. The three tiers are a compile time arrangement. By the time there is a binary, they are gone.\n", + "\n", + "Which raises the obvious question: is the internal tier exported by accident, or on purpose? The answer is in the headers, and it is on purpose.\n", + "\n", + "[the macro that marks a declaration as a symbol other binaries may link against](https://github.com/tamnd/cpython-internals/blob/main/GLOSSARY.md#pyapi_func) expands, at [Include/exports.h:88-93@v3.15.0rc1#PyAPI_FUNC](https://github.com/python/cpython/blob/v3.15.0rc1/Include/exports.h#L88-L93), to `Py_EXPORTED_SYMBOL`, which is the compiler attribute for default visibility. A declaration that does not use it is not exported.\n", + "\n", + "The internal headers use both spellings, often two lines apart. `_PyDict_SizeOf` at [Include/internal/pycore_dict.h:50-54@v3.15.0rc1#_PyDict_SizeOf](https://github.com/python/cpython/blob/v3.15.0rc1/Include/internal/pycore_dict.h#L50-L54) is `PyAPI_FUNC`, with a comment above it saying which shipped extension needs it, and `_PyDict_SizeOf_LockHeld` right underneath is a plain `extern`.\n", + "\n", + "Inside the internal headers, the two spellings decide what leaves the binary, and almost nothing crosses over." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-21", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 it is 402 exported and 388 resolving, against 783 held back and 2 resolving, with 161 comments rather than 168. The gap between the two spellings is the same shape." + } + }, + "outputs": [], + "source": [ + "EXTERN = re.compile(r\"^extern\\s+[\\w *]+?\\**\\s*(\\w+)\\s*\\(\", re.M)\n", + "WHO = re.compile(r\"^//\\s*Export for (.+?)\\.?$\", re.M)\n", + "\n", + "if HEADERS:\n", + " loud, quiet, asked = set(), set(), []\n", + " for blob in headers(\"internal/*.h\"):\n", + " loud |= set(DECLARED.findall(blob))\n", + " quiet |= set(EXTERN.findall(blob))\n", + " asked += WHO.findall(blob)\n", + " quiet -= loud\n", + " out = sum(1 for name in loud if hasattr(api, name))\n", + " kept = sum(1 for name in quiet if hasattr(api, name))\n", + " print(f\" spelled PyAPI_FUNC: {len(loud):4}, and {out} of them resolve\")\n", + " print(f\" spelled plain extern: {len(quiet):4}, and {kept} of them resolve\")\n", + " print()\n", + " print(\" comments naming who needs the export:\", len(asked))\n", + " print(\" the first three:\", \", \".join(asked[:3]))" + ] + }, + { + "cell_type": "markdown", + "id": "r06-22", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 it is 402 exported and 388 resolving, against 783 held back and 2 resolving, with 161 comments rather than 168. The gap between the two spellings is the same shape." + ] + }, + { + "cell_type": "markdown", + "id": "r06-23", + "metadata": {}, + "source": [ + "So the internal tier is deliberately half exported. The reason is in those comments: CPython's own bundled extensions, `math` and `_asyncio` and `_pickle` and the rest, are compiled as separate shared libraries, and they need to reach back into the interpreter that loaded them.\n", + "\n", + "The handful of exported names that did not resolve are the ones behind `Py_GIL_DISABLED` and the Windows only ones, which is why the free threaded recording at the end finds a few more.\n", + "\n", + "## Calling one of them\n", + "\n", + "Finding a symbol is not the same as calling it. Calling it means telling `ctypes` what the arguments and the return value are, and getting that wrong crashes the process rather than raising.\n", + "\n", + "`_PyDict_SizeOf` is a safe one to try. It takes a dict and returns a count of bytes, and there is a familiar function sitting on top of it.\n", + "\n", + "The internal function, the dunder and sys.getsizeof are the same measurement with one thing added." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-24", + "metadata": {}, + "outputs": [], + "source": [ + "size_of = api._PyDict_SizeOf\n", + "size_of.argtypes = [ctypes.py_object]\n", + "size_of.restype = ctypes.c_ssize_t\n", + "\n", + "small = {\"a\": 1, \"b\": 2}\n", + "print(\" what the internal function says:\", size_of(small))\n", + "print(\" what the dunder says: \", small.__sizeof__())\n", + "print(\" what sys.getsizeof says: \", sys.getsizeof(small))\n", + "print(\" the difference: \", sys.getsizeof(small) - small.__sizeof__())\n", + "try:\n", + " import _testinternalcapi\n", + "\n", + " print(\" and the header the collector adds:\", _testinternalcapi.SIZEOF_PYGC_HEAD, \"bytes\")\n", + "except ImportError:\n", + " print(\" and _testinternalcapi is not here to say what that difference is\")" + ] + }, + { + "cell_type": "markdown", + "id": "r06-25", + "metadata": {}, + "source": [ + "`dict.__sizeof__` is a thin wrapper around the internal function, so those two agree exactly. `sys.getsizeof` adds the bytes the cycle collector keeps in front of the object, which you can see it doing at [Python/sysmodule.c:1970-1979@v3.15.0rc1#_PyType_PreHeaderSize](https://github.com/python/cpython/blob/v3.15.0rc1/Python/sysmodule.c#L1970-L1979).\n", + "\n", + "We went round the outside and got the same answer the front door gives. That is the honest summary of what the internal tier is: not hidden, just unsupported.\n", + "\n", + "## A macro and a function with the same name\n", + "\n", + "One more thing falls out of all this, and it explains the shape of a lot of the header tree.\n", + "\n", + "If a limited build cannot see the struct, it cannot read `ob_type` to implement `Py_TYPE`. So `Py_TYPE` has to exist twice: as a macro for everybody else, and as a real exported function for the limited build.\n", + "\n", + "[Include/object.h:185-198@v3.15.0rc1#Py_Is](https://github.com/python/cpython/blob/v3.15.0rc1/Include/object.h#L185-L198) does this in the open. A `PyAPI_FUNC` declaration, then a `#define` of the same name on the very next line. The macro wins when it is visible, and the function is there when it is not.\n", + "\n", + "Some C API names are a macro and an exported function at once, and which one you get depends on your build." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-26", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 Py_SIZE and Py_IS_TYPE are macros and nothing else. They got their function forms in 3.15, which the comment above them in object.h dates." + } + }, + "outputs": [], + "source": [ + "for name in (\"Py_TYPE\", \"Py_REFCNT\", \"Py_SIZE\", \"Py_IS_TYPE\", \"Py_Is\", \"Py_DECREF\", \"Py_CLEAR\"):\n", + " both = \"declared as well, so a limited build can call it\"\n", + " print(f\" {name:11} {both if hasattr(api, name) else 'a macro and nothing else'}\")" + ] + }, + { + "cell_type": "markdown", + "id": "r06-27", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 Py_SIZE and Py_IS_TYPE are macros and nothing else. They got their function forms in 3.15, which the comment above them in object.h dates." + ] + }, + { + "cell_type": "markdown", + "id": "r06-28", + "metadata": {}, + "source": [ + "`Py_DECREF` and `Py_CLEAR` never got function forms, which is why a limited build calls `_Py_DecRef` instead, as [Include/refcount.h:327-338@v3.15.0rc1#Py_DECREF](https://github.com/python/cpython/blob/v3.15.0rc1/Include/refcount.h#L327-L338) shows.\n", + "\n", + "That is the cost of the limited API in one line. The single most common operation in the whole C API stops being a decrement and becomes a function call.\n", + "\n", + "## What the file name says\n", + "\n", + "None of the tiers show up in a built extension. One thing does, and it is the file name.\n", + "\n", + "[a build of an extension that keeps working across releases, marked by abi3 in its file name](https://github.com/tamnd/cpython-internals/blob/main/GLOSSARY.md#stable-abi) extensions are named `something.abi3.so`. Version locked ones carry the exact version, like `something.cpython-315-darwin.so`. The interpreter decides what it is willing to load from that suffix alone.\n", + "\n", + "Which tier an installed extension was built against is written on the outside of the file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r06-29", + "metadata": { + "cpython_internals": { + "varies": "The counts underneath are whatever happens to be installed where you are running this, so they will not match. The suffix list is the part that depends on the version: 3.14 offers three and 3.15 offers six, because 3.15 added abi3t for free threaded builds." + } + }, + "outputs": [], + "source": [ + "import importlib.machinery\n", + "\n", + "print(\" suffixes this interpreter will load:\")\n", + "for one in importlib.machinery.EXTENSION_SUFFIXES:\n", + " print(\" \", one)\n", + "print()\n", + "counted = {\"built for one version\": 0, \"built for the stable abi\": 0, \"no tag at all\": 0}\n", + "example = {}\n", + "for entry in {p for p in sys.path if p and pathlib.Path(p).is_dir()}:\n", + " for found in pathlib.Path(entry).rglob(\"*.so\"):\n", + " tag = (\n", + " \"built for the stable abi\"\n", + " if \".abi3\" in found.name\n", + " else \"built for one version\"\n", + " if \".cpython-\" in found.name\n", + " else \"no tag at all\"\n", + " )\n", + " counted[tag] += 1\n", + " example.setdefault(tag, found.name)\n", + "for tag, total in counted.items():\n", + " print(f\" {tag:26} {total:4} {example.get(tag, '')}\")" + ] + }, + { + "cell_type": "markdown", + "id": "r06-30", + "metadata": {}, + "source": [ + "> **Version note.** The counts underneath are whatever happens to be installed where you are running this, so they will not match. The suffix list is the part that depends on the version: 3.14 offers three and 3.15 offers six, because 3.15 added abi3t for free threaded builds." + ] + }, + { + "cell_type": "markdown", + "id": "r06-31", + "metadata": {}, + "source": [ + "Three suffixes on 3.14 and six on 3.15. The new ones end in `abi3t`, and they belong to PEP 803, which gives free threaded builds their own stable ABI. R07 is about that list.\n", + "\n", + "## How much really leaves the binary\n", + "\n", + "Everything above was measured on whatever machine you are reading this on. The claim that the internal tier is half exported deserves better than that, so here is the same sweep run in a container, on a build made from the pinned source.\n", + "\n", + "How much of the C API that the headers call private is callable anyway?\n", + "\n", + "```python\n", + "\"\"\"Which of the C API's private declarations actually leave the binary.\n", + "\n", + "The headers put the C API in three directories. `Include/` is what any extension may use,\n", + "`Include/cpython/` is the part that only makes sense compiled against this exact CPython, and\n", + "`Include/internal/` says at the top of nearly every file that it will not compile unless you\n", + "claim to be CPython itself.\n", + "\n", + "That is all a compile time arrangement. This program asks what survives into the built\n", + "interpreter, by taking every name the headers declare and asking the dynamic linker for it\n", + "through `ctypes.pythonapi`. A name that resolves is a name any program can call, whatever the\n", + "header said about it.\n", + "\n", + "The interesting split is inside the internal headers, which use two different spellings.\n", + "`PyAPI_FUNC` means the symbol leaves the shared library. A plain `extern` means it does not.\n", + "Both spellings sit in the same file, often two lines apart.\n", + "\"\"\"\n", + "\n", + "import ctypes\n", + "import pathlib\n", + "import re\n", + "import sys\n", + "import sysconfig\n", + "from collections import Counter\n", + "\n", + "API = re.compile(r\"^PyAPI_FUNC\\([^)]*\\)\\s*\\**\\s*(\\w+)\", re.M)\n", + "EXTERN = re.compile(r\"^extern\\s+[\\w *]+?\\**\\s*(\\w+)\\s*\\(\", re.M)\n", + "NOTE = re.compile(r\"^//\\s*Export for (.+?)\\.?$\", re.M)\n", + "\n", + "TIERS = ((\"public\", \"*.h\"), (\"cpython only\", \"cpython/*.h\"), (\"internal\", \"internal/*.h\"))\n", + "\n", + "api = ctypes.pythonapi\n", + "include = pathlib.Path(sysconfig.get_paths()[\"include\"])\n", + "\n", + "\n", + "def resolves(name):\n", + " \"\"\"Ask the linker for a name, the way any program with a handle on the process can.\"\"\"\n", + " return hasattr(api, name)\n", + "\n", + "\n", + "def read(pattern):\n", + " \"\"\"Every header matching the pattern, as one blob of text per file.\"\"\"\n", + " return [path.read_text(errors=\"replace\") for path in sorted(include.glob(pattern))]\n", + "\n", + "\n", + "print(\"include directory:\", include)\n", + "print(\"build has the gil disabled:\", sysconfig.get_config_var(\"Py_GIL_DISABLED\"))\n", + "print(\"abi flags:\", repr(sys.abiflags))\n", + "print()\n", + "\n", + "for tier, pattern in TIERS:\n", + " blobs = read(pattern)\n", + " declared = set()\n", + " for blob in blobs:\n", + " declared |= set(API.findall(blob))\n", + " found = sum(1 for name in declared if resolves(name))\n", + " print(f\"{tier}: {len(blobs)} header files\")\n", + " print(f\" declared with PyAPI_FUNC: {len(declared)}\")\n", + " print(f\" of those, resolve in this process: {found}\")\n", + "\n", + "internal = read(\"internal/*.h\")\n", + "exported = set()\n", + "kept_in = set()\n", + "notes = []\n", + "for blob in internal:\n", + " exported |= set(API.findall(blob))\n", + " kept_in |= set(EXTERN.findall(blob))\n", + " notes += NOTE.findall(blob)\n", + "kept_in -= exported\n", + "\n", + "leaked = sum(1 for name in exported if resolves(name))\n", + "held = sum(1 for name in kept_in if resolves(name))\n", + "\n", + "print()\n", + "print(\"inside the internal headers\")\n", + "print(\" names spelled PyAPI_FUNC:\", len(exported))\n", + "print(\" names spelled plain extern:\", len(kept_in))\n", + "print(\" comments naming who needs the export:\", len(notes))\n", + "for who, count in Counter(notes).most_common(5):\n", + " print(f\" {count} for {who}\")\n", + "\n", + "print()\n", + "exported_share = leaked / len(exported) * 100\n", + "extern_share = held / len(kept_in) * 100\n", + "print(\"~ private names that leave the binary: {}\".format(leaked))\n", + "print(\"~ share of PyAPI_FUNC internal names that resolve: {:.1f} percent\".format(exported_share))\n", + "print(\"~ share of plain extern internal names that resolve: {:.1f} percent\".format(extern_share))\n", + "```\n", + "\n", + "```text\n", + "include directory: /opt/python/include/python3.15\n", + "build has the gil disabled: 0\n", + "abi flags: ''\n", + "\n", + "public: 79 header files\n", + " declared with PyAPI_FUNC: 770\n", + " of those, resolve in this process: 750\n", + "cpython only: 63 header files\n", + " declared with PyAPI_FUNC: 445\n", + " of those, resolve in this process: 439\n", + "internal: 148 header files\n", + " declared with PyAPI_FUNC: 530\n", + " of those, resolve in this process: 493\n", + "\n", + "inside the internal headers\n", + " names spelled PyAPI_FUNC: 530\n", + " names spelled plain extern: 757\n", + " comments naming who needs the export: 168\n", + " 25 for '_testinternalcapi' shared extension\n", + " 12 for test_peg_generator\n", + " 10 for '_datetime' shared extension\n", + " 9 for '_asyncio' shared extension\n", + " 7 for 'math' shared extension\n", + "\n", + "~ private names that leave the binary: 493\n", + "~ share of PyAPI_FUNC internal names that resolve: 93.0 percent\n", + "~ share of plain extern internal names that resolve: 0.4 percent\n", + "```\n", + "\n", + "That ran on Python 3.15.0rc1 in the release build this project publishes, which is `ghcr.io/tamnd/cpython-internals/cpython:release@sha256:fb55d6afcf053c974de6447fafbd2be6af20cdb9f596e25a0445607b8af981e3`. You do not need that build to read the numbers, and you do need it to produce them, which is why this is recorded rather than left as a cell you run. If you want to watch it happen yourself, `docker run --rm -i ghcr.io/tamnd/cpython-internals/cpython:release@sha256:fb55d6afcf053c974de6447fafbd2be6af20cdb9f596e25a0445607b8af981e3 python3 -` takes the program on standard input.\n", + "\n", + "Ninety three percent of the private declarations that are spelled `PyAPI_FUNC` can be found by name, and four in a thousand of the ones spelled `extern` can. Two spellings, two completely different outcomes, in the same files.\n", + "\n", + "![a bar chart comparing the share of internal PyAPI_FUNC names that resolve against the share of plain extern names](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r06-the-c-api-tiers/diagrams/what-leaves-the-binary.svg)\n", + "\n", + "The other run is the same program on a build made with `--disable-gil`, which compiles a different half of the headers.\n", + "\n", + "Does dropping the global interpreter lock change what the C API exports?\n", + "\n", + "```python\n", + "\"\"\"Which of the C API's private declarations actually leave the binary.\n", + "\n", + "The headers put the C API in three directories. `Include/` is what any extension may use,\n", + "`Include/cpython/` is the part that only makes sense compiled against this exact CPython, and\n", + "`Include/internal/` says at the top of nearly every file that it will not compile unless you\n", + "claim to be CPython itself.\n", + "\n", + "That is all a compile time arrangement. This program asks what survives into the built\n", + "interpreter, by taking every name the headers declare and asking the dynamic linker for it\n", + "through `ctypes.pythonapi`. A name that resolves is a name any program can call, whatever the\n", + "header said about it.\n", + "\n", + "The interesting split is inside the internal headers, which use two different spellings.\n", + "`PyAPI_FUNC` means the symbol leaves the shared library. A plain `extern` means it does not.\n", + "Both spellings sit in the same file, often two lines apart.\n", + "\"\"\"\n", + "\n", + "import ctypes\n", + "import pathlib\n", + "import re\n", + "import sys\n", + "import sysconfig\n", + "from collections import Counter\n", + "\n", + "API = re.compile(r\"^PyAPI_FUNC\\([^)]*\\)\\s*\\**\\s*(\\w+)\", re.M)\n", + "EXTERN = re.compile(r\"^extern\\s+[\\w *]+?\\**\\s*(\\w+)\\s*\\(\", re.M)\n", + "NOTE = re.compile(r\"^//\\s*Export for (.+?)\\.?$\", re.M)\n", + "\n", + "TIERS = ((\"public\", \"*.h\"), (\"cpython only\", \"cpython/*.h\"), (\"internal\", \"internal/*.h\"))\n", + "\n", + "api = ctypes.pythonapi\n", + "include = pathlib.Path(sysconfig.get_paths()[\"include\"])\n", + "\n", + "\n", + "def resolves(name):\n", + " \"\"\"Ask the linker for a name, the way any program with a handle on the process can.\"\"\"\n", + " return hasattr(api, name)\n", + "\n", + "\n", + "def read(pattern):\n", + " \"\"\"Every header matching the pattern, as one blob of text per file.\"\"\"\n", + " return [path.read_text(errors=\"replace\") for path in sorted(include.glob(pattern))]\n", + "\n", + "\n", + "print(\"include directory:\", include)\n", + "print(\"build has the gil disabled:\", sysconfig.get_config_var(\"Py_GIL_DISABLED\"))\n", + "print(\"abi flags:\", repr(sys.abiflags))\n", + "print()\n", + "\n", + "for tier, pattern in TIERS:\n", + " blobs = read(pattern)\n", + " declared = set()\n", + " for blob in blobs:\n", + " declared |= set(API.findall(blob))\n", + " found = sum(1 for name in declared if resolves(name))\n", + " print(f\"{tier}: {len(blobs)} header files\")\n", + " print(f\" declared with PyAPI_FUNC: {len(declared)}\")\n", + " print(f\" of those, resolve in this process: {found}\")\n", + "\n", + "internal = read(\"internal/*.h\")\n", + "exported = set()\n", + "kept_in = set()\n", + "notes = []\n", + "for blob in internal:\n", + " exported |= set(API.findall(blob))\n", + " kept_in |= set(EXTERN.findall(blob))\n", + " notes += NOTE.findall(blob)\n", + "kept_in -= exported\n", + "\n", + "leaked = sum(1 for name in exported if resolves(name))\n", + "held = sum(1 for name in kept_in if resolves(name))\n", + "\n", + "print()\n", + "print(\"inside the internal headers\")\n", + "print(\" names spelled PyAPI_FUNC:\", len(exported))\n", + "print(\" names spelled plain extern:\", len(kept_in))\n", + "print(\" comments naming who needs the export:\", len(notes))\n", + "for who, count in Counter(notes).most_common(5):\n", + " print(f\" {count} for {who}\")\n", + "\n", + "print()\n", + "exported_share = leaked / len(exported) * 100\n", + "extern_share = held / len(kept_in) * 100\n", + "print(\"~ private names that leave the binary: {}\".format(leaked))\n", + "print(\"~ share of PyAPI_FUNC internal names that resolve: {:.1f} percent\".format(exported_share))\n", + "print(\"~ share of plain extern internal names that resolve: {:.1f} percent\".format(extern_share))\n", + "```\n", + "\n", + "```text\n", + "include directory: /opt/python/include/python3.15t\n", + "build has the gil disabled: 1\n", + "abi flags: 't'\n", + "\n", + "public: 79 header files\n", + " declared with PyAPI_FUNC: 770\n", + " of those, resolve in this process: 754\n", + "cpython only: 63 header files\n", + " declared with PyAPI_FUNC: 445\n", + " of those, resolve in this process: 440\n", + "internal: 148 header files\n", + " declared with PyAPI_FUNC: 530\n", + " of those, resolve in this process: 499\n", + "\n", + "inside the internal headers\n", + " names spelled PyAPI_FUNC: 530\n", + " names spelled plain extern: 757\n", + " comments naming who needs the export: 168\n", + " 25 for '_testinternalcapi' shared extension\n", + " 12 for test_peg_generator\n", + " 10 for '_datetime' shared extension\n", + " 9 for '_asyncio' shared extension\n", + " 7 for 'math' shared extension\n", + "\n", + "~ private names that leave the binary: 499\n", + "~ share of PyAPI_FUNC internal names that resolve: 94.2 percent\n", + "~ share of plain extern internal names that resolve: 0.4 percent\n", + "```\n", + "\n", + "That ran on Python 3.15.0rc1 in the freethreaded build this project publishes, which is `ghcr.io/tamnd/cpython-internals/cpython:freethreaded@sha256:db72284e3a49f43c38b96bec2baed1380b8348e27ea6f54f6e8d0810b59c3144`. You do not need that build to read the numbers, and you do need it to produce them, which is why this is recorded rather than left as a cell you run. If you want to watch it happen yourself, `docker run --rm -i ghcr.io/tamnd/cpython-internals/cpython:freethreaded@sha256:db72284e3a49f43c38b96bec2baed1380b8348e27ea6f54f6e8d0810b59c3144 python3 -` takes the program on standard input.\n", + "\n", + "Six more names resolve there, and the numbers on the three tiers are otherwise identical. The extra six are the ones guarded by `Py_GIL_DISABLED`, which is the answer to whether the missing names were a mystery or just a build flag.\n", + "\n", + "## Try it yourself\n", + "\n", + "**One.** Take the sweep in the cell above and print the names in the internal headers that are spelled `PyAPI_FUNC` but do not resolve. Then go and look two of them up in the pinned source and work out what they have in common.\n", + "\n", + "**Two.** `PyUnstable_Code_New` resolved in the linker cell. Find its declaration in `Include/cpython/code.h` and work out from the comments around it why a name with `Unstable` in it is declared in a header at all rather than being kept private.\n", + "\n", + "**Three.** The cross tabulation cell counts eight plain `Py` names in the internal directory. Print them. They are all from the same area of the interpreter, and the reason is a piece of history rather than a design decision.\n", + "\n", + "**Four.** `ctypes.pythonapi` also finds data, not just functions. Look up `PyLong_Type` with `ctypes.cast` and confirm the address you get back is the same one `id(int)` reports.\n", + "\n", + "## What you now know\n", + "\n", + "The C API is three directories. `Include/` is for anybody, `Include/cpython/` needs you not to have defined `Py_LIMITED_API`, and `Include/internal/` needs you to define `Py_BUILD_CORE` and stops the compiler with an error if you do not.\n", + "\n", + "The limited API takes away about a quarter of the public functions, and all of the struct layouts. The second half is the expensive one, because it turns field reads into function calls, `Py_DECREF` included.\n", + "\n", + "The naming convention nearly matches the directories, and the exceptions have a reason. A private name has to be exported when a public macro expands to it.\n", + "\n", + "None of this survives the build. The linker exports ninety three percent of the internal declarations that ask for it, on purpose, because CPython's own bundled extensions are separate shared libraries that need to reach back in. What stops you using them is a comment and a compiler error, not the loader.\n", + "\n", + "## What is next\n", + "\n", + "R07 takes the last cell and expands it. The stable ABI is what `abi3` means, `abi3t` is what 3.15 added for free threaded builds, and `PyABIInfo` at [Include/modsupport.h:85-101@v3.15.0rc1#PyABIInfo](https://github.com/python/cpython/blob/v3.15.0rc1/Include/modsupport.h#L85-L101) is the struct an extension carries so the interpreter can check, at load time, that the two of you agree about which tier you compiled against." + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/probes/pyodide/lessons.json b/probes/pyodide/lessons.json index 85a6fb7..10c9c3e 100644 --- a/probes/pyodide/lessons.json +++ b/probes/pyodide/lessons.json @@ -70,7 +70,7 @@ { "name": "b03-07", "status": "ok", - "printed": "test_adding (__main__.Arithmetic.test_adding) ... ok\ntest_dividing (__main__.Arithmetic.test_dividing) ... ok\ntest_powers (__main__.Arithmetic.test_powers) ... FAIL\n\n======================================================================\nFAIL: test_powers (__main__.Arithmetic.test_powers)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"b03-07\", line 13, in test_powers\nAssertionError: 1024 != 1000\n\n----------------------------------------------------------------------\nRan 3 tests in 0.006s\n\nFAILED (failures=1)\n\n3 tests run, 1 failed, 0 errored\n" + "printed": "test_adding (__main__.Arithmetic.test_adding) ... ok\ntest_dividing (__main__.Arithmetic.test_dividing) ... ok\ntest_powers (__main__.Arithmetic.test_powers) ... FAIL\n\n======================================================================\nFAIL: test_powers (__main__.Arithmetic.test_powers)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"b03-07\", line 13, in test_powers\nAssertionError: 1024 != 1000\n\n----------------------------------------------------------------------\nRan 3 tests in 0.003s\n\nFAILED (failures=1)\n\n3 tests run, 1 failed, 0 errored\n" }, { "name": "b03-10", @@ -329,7 +329,7 @@ { "name": "c05-18", "status": "ok", - "printed": " the collector is scheduled when generation zero passes 2000\n json.loads built them in 179 ms, collector ran 1 times\n a list display did it in 348 ms, collector ran 89 times\n the two of them built the same number of dicts: True\n" + "printed": " the collector is scheduled when generation zero passes 2000\n json.loads built them in 121 ms, collector ran 1 times\n a list display did it in 72 ms, collector ran 89 times\n the two of them built the same number of dicts: True\n" }, { "name": "c05-22", @@ -622,7 +622,7 @@ { "name": "e04-09", "status": "ok", - "printed": " a plain object at 23755776 mod 4 0 mod 8 0\n a list at 23129824 mod 4 0 mod 8 0\n a dict at 23339592 mod 4 0 mod 8 0\n a string at 23871512 mod 4 0 mod 8 0\n a big integer at 25148352 mod 4 0 mod 8 0\n a float at 17322832 mod 4 0 mod 8 0\n a module at 14795336 mod 4 0 mod 8 0\n None at 3200984 mod 4 0 mod 8 0\n the number 7 at 3253660 mod 4 0 mod 8 4\n" + "printed": " a plain object at 24967648 mod 4 0 mod 8 0\n a list at 23618160 mod 4 0 mod 8 0\n a dict at 24928400 mod 4 0 mod 8 0\n a string at 23454448 mod 4 0 mod 8 0\n a big integer at 20594760 mod 4 0 mod 8 0\n a float at 17309680 mod 4 0 mod 8 0\n a module at 14795336 mod 4 0 mod 8 0\n None at 3200984 mod 4 0 mod 8 0\n the number 7 at 3253660 mod 4 0 mod 8 4\n" }, { "name": "e04-12", @@ -691,12 +691,12 @@ { "name": "e05-18", "status": "ok", - "printed": " no_try 395.3 ns per iteration\n with_try 467.9 ns per iteration\n checking 944.0 ns per iteration\n" + "printed": " no_try 114.5 ns per iteration\n with_try 135.5 ns per iteration\n checking 183.3 ns per iteration\n" }, { "name": "e05-21", "status": "ok", - "printed": " through 1 frames 4370.5 ns\n through 5 frames 6710.4 ns\n through 20 frames 25842.1 ns\n through 50 frames 72131.8 ns\n" + "printed": " through 1 frames 1113.1 ns\n through 5 frames 2197.0 ns\n through 20 frames 8518.7 ns\n through 50 frames 19675.2 ns\n" }, { "name": "e05-24", @@ -711,7 +711,7 @@ { "name": "e05-29", "status": "ok", - "printed": " for loop table 0 bytes 319.2 ns per item\n while and next table 12 bytes 581.6 ns per item\n" + "printed": " for loop table 0 bytes 68.3 ns per item\n while and next table 12 bytes 113.4 ns per item\n" }, { "name": "e05-32", @@ -780,7 +780,7 @@ { "name": "e06-31", "status": "ok", - "printed": " every value an int 276.1 ns per addition\n every value a float 40.5 ns per addition\n alternating between the two 238.0 ns per addition\n" + "printed": " every value an int 32.9 ns per addition\n every value a float 47.8 ns per addition\n alternating between the two 97.4 ns per addition\n" }, { "name": "e06-34", @@ -834,7 +834,7 @@ { "name": "e07-25", "status": "ok", - "printed": " with the jit on\n this build has no JIT in it, so there is nothing to switch on\n\n with the jit off, for comparison\n arithmetic loop 395.2 ns per iteration\n loop with a call 637.1 ns per iteration\n" + "printed": " with the jit on\n this build has no JIT in it, so there is nothing to switch on\n\n with the jit off, for comparison\n arithmetic loop 85.3 ns per iteration\n loop with a call 84.5 ns per iteration\n" } ] }, @@ -863,7 +863,7 @@ { "name": "e08-11", "status": "ok", - "printed": " nobody watching 377.7 ns per turn\n callback counts the line 2273.1 ns per turn, 2000015 calls\n callback returns DISABLE 467.0 ns per turn\n" + "printed": " nobody watching 44.9 ns per turn\n callback counts the line 351.7 ns per turn, 2000015 calls\n callback returns DISABLE 85.0 ns per turn\n" }, { "name": "e08-14", @@ -1020,17 +1020,17 @@ { "name": "e11-15", "status": "ok", - "printed": " extra lines instructions run nanoseconds cost of one more\n 0 7010 74819 \n 5 17010 382791 30.80\n 10 27010 699888 31.71\n 20 47010 1265219 28.27\n 40 87010 1946352 17.03\n" + "printed": " extra lines instructions run nanoseconds cost of one more\n 0 7010 33347 \n 5 17010 64437 3.11\n 10 27010 209706 14.53\n 20 47010 290590 4.04\n 40 87010 491474 5.02\n" }, { "name": "e11-18", "status": "ok", - "printed": " the same job instructions nanoseconds each\n a while loop 20012 311613 15.57\n a for loop 10008 162522 16.24\n a comprehension 6011 157577 26.21\n" + "printed": " the same job instructions nanoseconds each\n a while loop 20012 199249 9.96\n a for loop 10008 84038 8.40\n a comprehension 6011 63977 10.64\n" }, { "name": "e11-21", "status": "ok", - "printed": " one bytecode instruction 15.20 ns\n one call to an empty def 172.47 ns beyond the code that calls it\n so a call costs about 11 instructions\n" + "printed": " one bytecode instruction 4.55 ns\n one call to an empty def 30.32 ns beyond the code that calls it\n so a call costs about 7 instructions\n" } ] }, @@ -1489,7 +1489,7 @@ { "name": "f10-11", "status": "ok", - "printed": " -- MAKE_CELL 0 (a)\n MAKE_CELL 6 (n)\n\n 1 RESUME 0\n\n 2 LOAD_SMALL_INT 0\n STORE_DEREF 6 (n)\n\n 4 LOAD_FAST_BORROW 0 (a)\n LOAD_FAST_BORROW 6 (n)\n BUILD_TUPLE 2\n LOAD_CONST 1 ()\n MAKE_FUNCTION\n SET_FUNCTION_ATTRIBUTE 8 (closure)\n STORE_FAST 5 (inner)\n\n 7 LOAD_FAST_BORROW 5 (inner)\n RETURN_VALUE\n\nDisassembly of :\n -- COPY_FREE_VARS 2\n\n 4 RESUME 0\n\n 5 LOAD_DEREF 0 (a)\n LOAD_DEREF 1 (n)\n BINARY_OP 0 (+)\n RETURN_VALUE\n" + "printed": " -- MAKE_CELL 0 (a)\n MAKE_CELL 6 (n)\n\n 1 RESUME 0\n\n 2 LOAD_SMALL_INT 0\n STORE_DEREF 6 (n)\n\n 4 LOAD_FAST_BORROW 0 (a)\n LOAD_FAST_BORROW 6 (n)\n BUILD_TUPLE 2\n LOAD_CONST 1 ()\n MAKE_FUNCTION\n SET_FUNCTION_ATTRIBUTE 8 (closure)\n STORE_FAST 5 (inner)\n\n 7 LOAD_FAST_BORROW 5 (inner)\n RETURN_VALUE\n\nDisassembly of :\n -- COPY_FREE_VARS 2\n\n 4 RESUME 0\n\n 5 LOAD_DEREF 0 (a)\n LOAD_DEREF 1 (n)\n BINARY_OP 0 (+)\n RETURN_VALUE\n" }, { "name": "f10-13", @@ -1581,12 +1581,12 @@ { "name": "f12-07", "status": "ok", - "printed": " greet.py is 54 bytes of text\n greet.cpython-314.pyc is 251 bytes\n\n header 2b 0e 0d 0a 00 00 00 00 79 bc 9c 6a 36 00 00 00\n the rest 235 bytes, and marshal.loads gives back a\n code object\n" + "printed": " greet.py is 54 bytes of text\n greet.cpython-314.pyc is 251 bytes\n\n header 2b 0e 0d 0a 00 00 00 00 33 2b 9d 6a 36 00 00 00\n the rest 235 bytes, and marshal.loads gives back a\n code object\n" }, { "name": "f12-10", "status": "ok", - "printed": " magic 168627755\n flags 0\n mtime 1788656761 source 1788656761\n size 54 source 54\n" + "printed": " magic 168627755\n flags 0\n mtime 1788685107 source 1788685107\n size 54 source 54\n" }, { "name": "f12-13", @@ -1620,7 +1620,7 @@ { "name": "f12-29", "status": "ok", - "printed": " one bit flipped in the magic: bad magic number in 'broken': b'*\\x0e\\r\\n'\n\n put a wrong mtime in greet.cpython-314.pyc: 1788656774\n imported it with no complaint: 'hi world'\n and the header now says 1788656774\n the file was quietly rewritten: False\n" + "printed": " one bit flipped in the magic: bad magic number in 'broken': b'*\\x0e\\r\\n'\n\n put a wrong mtime in greet.cpython-314.pyc: 1788685260\n imported it with no complaint: 'hi world'\n and the header now says 1788685260\n the file was quietly rewritten: False\n" } ] }, @@ -1963,7 +1963,7 @@ { "name": "m07-29", "status": "ok", - "printed": " frozen objects before 0\n tracked in generation 2 24952\n frozen after gc.freeze() 24952\n left in generation 2 0\n frozen after gc.unfreeze() 0\n" + "printed": " frozen objects before 0\n tracked in generation 2 24956\n frozen after gc.freeze() 24956\n left in generation 2 0\n frozen after gc.unfreeze() 0\n" } ] }, @@ -1987,7 +1987,7 @@ { "name": "m08-09", "status": "ok", - "printed": " objects in each generation [3, 0, 24938]\n as soon as it exists generations [0]\n after a pass over gen 0 generations [2]\n after a pass over gen 1 generations [2]\n" + "printed": " objects in each generation [3, 0, 24942]\n as soon as it exists generations [0]\n after a pass over gen 0 generations [2]\n after a pass over gen 1 generations [2]\n" }, { "name": "m08-12", @@ -2002,12 +2002,12 @@ { "name": "m08-17", "status": "ok", - "printed": " cycles on the heap 200000\n one full pass over it 225 ms\n and over an empty one 29 ms\n" + "printed": " cycles on the heap 200000\n one full pass over it 58 ms\n and over an empty one 13 ms\n" }, { "name": "m08-20", "status": "ok", - "printed": " this version does not report candidates, so there is nothing to compare\n objects the collector is tracking 24954\n" + "printed": " this version does not report candidates, so there is nothing to compare\n objects the collector is tracking 24958\n" } ] }, @@ -2085,7 +2085,7 @@ { "name": "o01-07", "status": "ok", - "printed": " a pointer here is 4 bytes, so the header is 8\n it lives at 25257768\n\n first word, read as a count 1\n sys.getrefcount, less its own argument 1\n the same number: True\n\n second word 24598128\n id(Thing) is 24598128\n so the second word is the type pointer: True\n\n after a second name, the count reads 2\n after deleting that name, it reads 1\n" + "printed": " a pointer here is 4 bytes, so the header is 8\n it lives at 23375152\n\n first word, read as a count 1\n sys.getrefcount, less its own argument 1\n the same number: True\n\n second word 19428440\n id(Thing) is 19428440\n so the second word is the type pointer: True\n\n after a second name, the count reads 2\n after deleting that name, it reads 1\n" }, { "name": "o01-10", @@ -2958,7 +2958,7 @@ { "name": "r03-22", "status": "ok", - "printed": " first time, source file, no .pyc yet: 226.3 microseconds\n from a file, with a .pyc already there: 229.2 microseconds\n asking again for one already loaded: 335.0 nanoseconds\n the second of those is 684 times the third\n" + "printed": " first time, source file, no .pyc yet: 117.7 microseconds\n from a file, with a .pyc already there: 124.7 microseconds\n asking again for one already loaded: 232.7 nanoseconds\n the second of those is 536 times the third\n" } ] }, @@ -3007,7 +3007,7 @@ { "name": "r04-25", "status": "ok", - "printed": " asking FrozenImporter for os: 4.00 microseconds\n asking PathFinder for the same name: 18552.13 microseconds\n this runtime keeps no pyc for os, so the last two rows have nothing to measure\n" + "printed": " asking FrozenImporter for os: 2.32 microseconds\n asking PathFinder for the same name: 14061.83 microseconds\n this runtime keeps no pyc for os, so the last two rows have nothing to measure\n" }, { "name": "r04-29", @@ -3031,7 +3031,7 @@ { "name": "r05-07", "status": "ok", - "printed": " what that one import line cost here: 150.9 ms\n modules it needs, all told: 8\n which are: _elementtree, pyexpat, pyexpat.errors, pyexpat.model, xml, xml.etree, xml.etree.ElementPath, xml.etree.ElementTree\n how many of those this cell had to load: 8\n\n modules this process is now carrying: 242\n" + "printed": " what that one import line cost here: 75.3 ms\n modules it needs, all told: 8\n which are: _elementtree, pyexpat, pyexpat.errors, pyexpat.model, xml, xml.etree, xml.etree.ElementPath, xml.etree.ElementTree\n how many of those this cell had to load: 8\n\n modules this process is now carrying: 242\n" }, { "name": "r05-10", @@ -3085,6 +3085,61 @@ } ] }, + { + "slug": "r06-the-c-api-tiers", + "cells": [ + { + "name": "r06-03", + "status": "ok" + }, + { + "name": "r06-05", + "status": "ok", + "printed": "cpython 3.14.2 on Emscripten wasm32, WebAssembly\n note: this is 3.14, and everything here is written against 3.15, so bytecode and some object layouts will differ from the prose\n" + }, + { + "name": "r06-07", + "status": "ok", + "printed": " no headers next to this interpreter, looked in //include/python3.14\n the cells that read them say so and skip themselves\n" + }, + { + "name": "r06-10", + "status": "ok" + }, + { + "name": "r06-13", + "status": "ok" + }, + { + "name": "r06-16", + "status": "ok" + }, + { + "name": "r06-19", + "status": "ok", + "printed": " public PyList_Append the linker hands it over\n public PyErr_SetString the linker hands it over\n cpython only PyUnstable_Code_New the linker hands it over\n cpython only PyFrame_GetLasti the linker hands it over\n internal _PyDict_SizeOf the linker hands it over\n internal _PyEval_EvalFrameDefault the linker hands it over\n internal _PyObject_GC_New the linker hands it over\n no such name PyNothingLikeThis not found\n" + }, + { + "name": "r06-21", + "status": "ok" + }, + { + "name": "r06-24", + "status": "ok", + "printed": " what the internal function says: 100\n what the dunder says: 100\n what sys.getsizeof says: 108\n the difference: 8\n and the header the collector adds: 8 bytes\n" + }, + { + "name": "r06-26", + "status": "ok", + "printed": " Py_TYPE declared as well, so a limited build can call it\n Py_REFCNT declared as well, so a limited build can call it\n Py_SIZE a macro and nothing else\n Py_IS_TYPE a macro and nothing else\n Py_Is declared as well, so a limited build can call it\n Py_DECREF a macro and nothing else\n Py_CLEAR a macro and nothing else\n" + }, + { + "name": "r06-29", + "status": "ok", + "printed": " suffixes this interpreter will load:\n .cpython-314-wasm32-emscripten.so\n .abi3.so\n .so\n\n built for one version 0 \n built for the stable abi 0 \n no tag at all 0 \n" + } + ] + }, { "slug": "t01-one-line-seven-stages", "cells": [ @@ -3467,7 +3522,7 @@ { "name": "t04-30", "status": "ok", - "printed": "free variables: ('total',)\nthe cells: (,)\nfirst call: 1\nsecond call: 2\nthe cell now: 2\n" + "printed": "free variables: ('total',)\nthe cells: (,)\nfirst call: 1\nsecond call: 2\nthe cell now: 2\n" }, { "name": "t04-33", @@ -3744,7 +3799,7 @@ { "name": "t07-38", "status": "ok", - "printed": "asked twice, got the same object: True\nand it is still here after the call returned: \nits name: make_one\n" + "printed": "asked twice, got the same object: True\nand it is still here after the call returned: \nits name: make_one\n" }, { "name": "t07-40", @@ -3778,12 +3833,12 @@ { "name": "t08-11", "status": "ok", - "printed": "NoneType at 0x30d7d8, refcount is parked, this object is never freed, 8 bytes, not tracked\nint at 0x31a7cc, refcount is parked, this object is never freed, 16 bytes, not tracked\nstr at 0x14b6c00, 3 reference(s), 26 bytes, not tracked\nlist at 0x1516f30, 2 reference(s), 44 bytes, tracked by the cycle collector\ndict at 0x16735b0, 2 reference(s), 108 bytes, tracked by the cycle collector\nfunction at 0x11d5888, 4 reference(s), 84 bytes, tracked by the cycle collector\n" + "printed": "NoneType at 0x30d7d8, refcount is parked, this object is never freed, 8 bytes, not tracked\nint at 0x31a7cc, refcount is parked, this object is never freed, 16 bytes, not tracked\nstr at 0x15034d8, 3 reference(s), 26 bytes, not tracked\nlist at 0x1872ba0, 2 reference(s), 44 bytes, tracked by the cycle collector\ndict at 0x1802d28, 2 reference(s), 108 bytes, tracked by the cycle collector\nfunction at 0x18b3c38, 4 reference(s), 84 bytes, tracked by the cycle collector\n" }, { "name": "t08-14", "status": "ok", - "printed": "a == b True same contents\na is b False different objects\na is c True same object\n\nid(a) 0x15abfa8\nid(b) 0x164b700\nid(c) 0x15abfa8\n" + "printed": "a == b True same contents\na is b False different objects\na is c True same object\n\nid(a) 0x175f990\nid(b) 0x11b5970\nid(c) 0x175f990\n" }, { "name": "t08-17", @@ -3823,7 +3878,7 @@ { "name": "t08-35", "status": "ok", - "printed": "dict at 0x165ac08\ndict at 0xfc1440\nlist at 0x1788d68\n" + "printed": "dict at 0x11edd18\ndict at 0x1432f18\nlist at 0x18354a0\n" }, { "name": "t08-37", @@ -3882,7 +3937,7 @@ { "name": "t09-21", "status": "ok", - "printed": "Node at 0x18ad508 -> Node at 0x17b86d8 -> Node at 0x15631f8 -> Node at 0x18ad508\n" + "printed": "Node at 0x1858b98 -> Node at 0x1817e78 -> Node at 0x17f7290 -> Node at 0x1858b98\n" }, { "name": "t09-23", @@ -3912,7 +3967,7 @@ { "name": "t09-37", "status": "ok", - "printed": "first object was at 0xfb8a80\nsecond object is at 0xfb8a80\nsame address reused -> True\n" + "printed": "first object was at 0x161f130\nsecond object is at 0x161f130\nsame address reused -> True\n" } ] }, diff --git a/probes/pyodide/lessons.md b/probes/pyodide/lessons.md index 9cbac8b..a8bf27f 100644 --- a/probes/pyodide/lessons.md +++ b/probes/pyodide/lessons.md @@ -2,7 +2,7 @@ Generated by `just build-probe`. Do not edit by hand, the change will be overwritten. -76 lesson(s) on Pyodide 3.14.2: 76 ran end to end, 746 cell(s) in total. +77 lesson(s) on Pyodide 3.14.2: 77 ran end to end, 757 cell(s) in total. The checks in `report.md` next to this ask whether a surface exists. This runs the lessons themselves: every code cell of every notebook, in order, in one Pyodide runtime, with `pyxray` mounted off the disk rather than installed. The install cell is the one thing changed, and only its `%pip` line, which a reader in a browser does not need either. @@ -72,6 +72,7 @@ The checks in `report.md` next to this ask whether a surface exists. This runs t | r03-what-import-does | 10 | runs end to end | | r04-frozen-modules | 10 | runs end to end | | r05-lazy-imports | 13 | runs end to end | +| r06-the-c-api-tiers | 11 | runs end to end | | t01-one-line-seven-stages | 18 | runs end to end | | t02-text-becomes-tokens | 32 | runs end to end | | t03-tokens-become-a-tree | 13 | runs end to end | diff --git a/probes/pyodide/native.json b/probes/pyodide/native.json index b41a01c..f9c564b 100644 --- a/probes/pyodide/native.json +++ b/probes/pyodide/native.json @@ -9,7 +9,7 @@ "status": "ok", "value": { "python": "3.14.7", - "platform": "macosx-15.0-arm64", + "platform": "macosx-11.0-arm64", "pointer_bytes": 8, "free_threaded": false } diff --git a/probes/pyodide/pyodide.json b/probes/pyodide/pyodide.json index 6f2bba5..5068395 100644 --- a/probes/pyodide/pyodide.json +++ b/probes/pyodide/pyodide.json @@ -1,7 +1,7 @@ { "runtime": "pyodide", "python": "3.14.2", - "seconds": 2.52, + "seconds": 4.64, "payload_bytes": 13508566, "outcomes": [ { diff --git a/probes/pyodide/report.md b/probes/pyodide/report.md index 78ef0ef..8af88a1 100644 --- a/probes/pyodide/report.md +++ b/probes/pyodide/report.md @@ -1,6 +1,6 @@ # What works under Pyodide -Generated by `wasmprobe report`. Native run on CPython 3.14.7, WebAssembly run on Pyodide with CPython 3.14.2, which booted in 2.5 seconds off a local disk after 13.5 MB of runtime and standard library. +Generated by `wasmprobe report`. Native run on CPython 3.14.7, WebAssembly run on Pyodide with CPython 3.14.2, which booted in 4.6 seconds off a local disk after 13.5 MB of runtime and standard library. 15 checks: 12 works in both, 3 works natively, not in the browser. @@ -60,7 +60,7 @@ Both runtimes answered these, and answered them differently. | Check | Native | WebAssembly | | --- | --- | --- | -| Which CPython is this, and what was it built for | `{'python': '3.14.7', 'platform': 'macosx-15.0-arm64', 'pointer_bytes': 8, 'free_threaded': False}` | `{'python': '3.14.2', 'platform': 'emscripten-5.0.3-wasm32', 'pointer_bytes': 4, 'free_threaded': False}` | +| Which CPython is this, and what was it built for | `{'python': '3.14.7', 'platform': 'macosx-11.0-arm64', 'pointer_bytes': 8, 'free_threaded': False}` | `{'python': '3.14.2', 'platform': 'emscripten-5.0.3-wasm32', 'pointer_bytes': 4, 'free_threaded': False}` | | Does compiler_codegen turn a tree into an instruction sequence | `{'instructions': 8, 'first': 128, 'metadata_keys': ['argcount', 'consts', 'kwonlyargcount', 'posonlyargcount']}` | `{'instructions': 8, 'first': 128, 'metadata_keys': ['argcount', 'kwonlyargcount', 'posonlyargcount']}` | | Can ctypes read the two fields in front of every object | `{'refcount_field': 1, 'getrefcount': 2, 'type_pointer_matches': True, 'word_bytes': 8}` | `{'refcount_field': 1, 'getrefcount': 2, 'type_pointer_matches': True, 'word_bytes': 4}` | | Does the cycle collector behave the way T09 says it does | `{'cycle_freed': True, 'thresholds': [2000, 10, 10], 'enabled': True, 'generations': 3}` | `{'cycle_freed': True, 'thresholds': [2000, 10, 0], 'enabled': True, 'generations': 3}` | diff --git a/pyxray/src/pyxray/glossary.py b/pyxray/src/pyxray/glossary.py index efbb245..8b7ec3b 100644 --- a/pyxray/src/pyxray/glossary.py +++ b/pyxray/src/pyxray/glossary.py @@ -1968,6 +1968,40 @@ def anchor(name: str) -> str: see=("WebAssembly",), met="B01", ), + Term( + name="C API", + short="The C functions and macros an extension may call to talk to the interpreter.", + long="It is not one list but three, and which one you get is decided before you include anything. `Include/` is open to everybody, `Include/cpython/` needs `Py_LIMITED_API` to be undefined, and `Include/internal/` stops the compiler with an error unless you define `Py_BUILD_CORE`. More than half the header lines are in the third directory, the one no extension is meant to open.", + cite="Doc/c-api/stable.rst:21-35@v3.15.0rc1", + also=("`Python.h`",), + see=("limited API", "PyAPI_FUNC", "stable ABI"), + met="R06", + ), + Term( + name="limited API", + short="The subset of the C API you get when you define Py_LIMITED_API before Python.h.", + long="It takes away about a quarter of the public functions, which is the part people notice, and all of the struct layouts, which is the part that costs. With no fields to read, `Py_TYPE` stops being a pointer dereference and becomes a function call, and `Py_DECREF` becomes a call to `_Py_DecRef`. What you buy with that is one built file that keeps working across releases.", + cite="Include/object.h:124-126@v3.15.0rc1#_Py_OPAQUE_PYOBJECT", + also=("`Py_LIMITED_API`",), + see=("C API", "stable ABI"), + met="R06", + ), + Term( + name="PyAPI_FUNC", + short="The macro that marks a declaration as a symbol other binaries may link against.", + long="It expands to a compiler attribute asking for default visibility, so a declaration that uses it leaves the shared library and a plain `extern` does not. The internal headers use both spellings, often two lines apart, and the choice is deliberate: CPython's own bundled extensions are separate shared libraries that need to reach back in, and a comment above each export usually names which one.", + cite="Include/exports.h:88-93@v3.15.0rc1#PyAPI_FUNC", + see=("C API",), + met="R06", + ), + Term( + name="stable ABI", + short="An extension build that keeps working across releases, marked abi3 in its name.", + long="The tiers are a compile time idea and none of them survive into a built extension. The file name does. An interpreter decides what it is willing to load from the suffix alone, so `something.abi3.so` will load on many releases while `something.cpython-315-darwin.so` will load on exactly one. Python 3.15 adds `abi3t` for free threaded builds.", + also=("abi3", "abi3t"), + see=("limited API", "abi flags"), + met="R06", + ), ), ) diff --git a/tools/tier1/src/tier1/experiments.py b/tools/tier1/src/tier1/experiments.py index c86b6f7..0b8c57a 100644 --- a/tools/tier1/src/tier1/experiments.py +++ b/tools/tier1/src/tier1/experiments.py @@ -3020,6 +3020,123 @@ def one_thread(): ) +PROGRAM_TWENTYSIX = r'''"""Which of the C API's private declarations actually leave the binary. + +The headers put the C API in three directories. `Include/` is what any extension may use, +`Include/cpython/` is the part that only makes sense compiled against this exact CPython, and +`Include/internal/` says at the top of nearly every file that it will not compile unless you +claim to be CPython itself. + +That is all a compile time arrangement. This program asks what survives into the built +interpreter, by taking every name the headers declare and asking the dynamic linker for it +through `ctypes.pythonapi`. A name that resolves is a name any program can call, whatever the +header said about it. + +The interesting split is inside the internal headers, which use two different spellings. +`PyAPI_FUNC` means the symbol leaves the shared library. A plain `extern` means it does not. +Both spellings sit in the same file, often two lines apart. +""" + +import ctypes +import pathlib +import re +import sys +import sysconfig +from collections import Counter + +API = re.compile(r"^PyAPI_FUNC\([^)]*\)\s*\**\s*(\w+)", re.M) +EXTERN = re.compile(r"^extern\s+[\w *]+?\**\s*(\w+)\s*\(", re.M) +NOTE = re.compile(r"^//\s*Export for (.+?)\.?$", re.M) + +TIERS = (("public", "*.h"), ("cpython only", "cpython/*.h"), ("internal", "internal/*.h")) + +api = ctypes.pythonapi +include = pathlib.Path(sysconfig.get_paths()["include"]) + + +def resolves(name): + """Ask the linker for a name, the way any program with a handle on the process can.""" + return hasattr(api, name) + + +def read(pattern): + """Every header matching the pattern, as one blob of text per file.""" + return [path.read_text(errors="replace") for path in sorted(include.glob(pattern))] + + +print("include directory:", include) +print("build has the gil disabled:", sysconfig.get_config_var("Py_GIL_DISABLED")) +print("abi flags:", repr(sys.abiflags)) +print() + +for tier, pattern in TIERS: + blobs = read(pattern) + declared = set() + for blob in blobs: + declared |= set(API.findall(blob)) + found = sum(1 for name in declared if resolves(name)) + print(f"{tier}: {len(blobs)} header files") + print(f" declared with PyAPI_FUNC: {len(declared)}") + print(f" of those, resolve in this process: {found}") + +internal = read("internal/*.h") +exported = set() +kept_in = set() +notes = [] +for blob in internal: + exported |= set(API.findall(blob)) + kept_in |= set(EXTERN.findall(blob)) + notes += NOTE.findall(blob) +kept_in -= exported + +leaked = sum(1 for name in exported if resolves(name)) +held = sum(1 for name in kept_in if resolves(name)) + +print() +print("inside the internal headers") +print(" names spelled PyAPI_FUNC:", len(exported)) +print(" names spelled plain extern:", len(kept_in)) +print(" comments naming who needs the export:", len(notes)) +for who, count in Counter(notes).most_common(5): + print(f" {count} for {who}") + +print() +exported_share = leaked / len(exported) * 100 +extern_share = held / len(kept_in) * 100 +print("~ private names that leave the binary: {}".format(leaked)) +print("~ share of PyAPI_FUNC internal names that resolve: {:.1f} percent".format(exported_share)) +print("~ share of plain extern internal names that resolve: {:.1f} percent".format(extern_share)) +''' + + +WHAT_LEAVES_THE_BINARY = Experiment( + slug="r06-what-leaves-the-binary", + lesson="R06", + title="Every name the headers declare, handed to the linker one at a time", + asks="How much of the C API that the headers call private is callable anyway?", + needs=( + "it needs the headers next to the interpreter, which a normal install has and a " + "browser tab does not, and it needs ctypes to reach the process it is running in" + ), + build="release", + program=PROGRAM_TWENTYSIX, +) + + +WHAT_LEAVES_THE_BINARY_ON_A_FREE_THREADED_BUILD = Experiment( + slug="r06-what-leaves-the-binary-on-a-free-threaded-build", + lesson="R06", + title="The same sweep on the build that compiles a different half of the headers", + asks="Does dropping the global interpreter lock change what the C API exports?", + needs=( + "it needs a build configured with --disable-gil, because the question is whether the " + "names behind Py_GIL_DISABLED are the ones that were missing on the ordinary build" + ), + build="freethreaded", + program=PROGRAM_TWENTYSIX, +) + + EXPERIMENTS: tuple[Experiment, ...] = ( COMPILING_COSTS_NOTHING_THAT_LASTS, A_LEAK_YOU_CAN_SEE, @@ -3058,6 +3175,8 @@ def one_thread(): WHAT_DEFERRING_AN_IMPORT_IS_WORTH, HOW_MUCH_OF_A_WAKE_UP_IS_PARALLEL, HOW_MUCH_OF_A_WAKE_UP_IS_PARALLEL_WITHOUT_THE_LOCK, + WHAT_LEAVES_THE_BINARY, + WHAT_LEAVES_THE_BINARY_ON_A_FREE_THREADED_BUILD, )