diff --git a/GLOSSARY.md b/GLOSSARY.md index a67ab4c..32b22ef 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 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) +[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) | [PyABIInfo](#pyabiinfo) | [PyAPI_FUNC](#pyapi_func) | [PyVarObject](#pyvarobject) | [Pyodide](#pyodide) | [SOABI](#soabi) | [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) | [extension module](#extension-module) | [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 @@ -1800,7 +1800,31 @@ First met in R06. See also [C API](#c-api). In the source: [`Include/exports.h:8 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). +Also written abi3, abi3t. First met in R06. See also [limited API](#limited-api), [abi flags](#abi-flags), [extension module](#extension-module). + +### extension module + +**A module that arrives as a compiled shared library rather than as Python.** + +Everything about how it loads is decided in two steps. First the finder looks at the file name and decides whether the tag in it is one this build accepts, which costs one stat call and happens before anything is opened. Then, if the extension was built against 3.15 or later, the interpreter compares the ABI struct it carries against itself, by which point the extension's own code has already run. + +Also written `.so`, `.pyd`. First met in R07. See also [stable ABI](#stable-abi), [SOABI](#soabi), [PyABIInfo](#pyabiinfo). In the source: [`Python/dynload_shlib.c:39-60@v3.15.0rc1#_PyImport_DynLoadFiletab`](https://github.com/python/cpython/blob/v3.15.0rc1/Python/dynload_shlib.c#L39-L60). + +### SOABI + +**The tag a build stamps into the file names of the extensions it compiles.** + +It reads like `cpython-315t-darwin`, and it is three separate facts glued together: the implementation, the version with a `t` on the end if the build has no global interpreter lock, and the platform. Change any one of them and this interpreter will not even look at the file. The value is in `sysconfig`, and the suffix built from it is the first entry of `importlib.machinery.EXTENSION_SUFFIXES`. + +Also written `EXT_SUFFIX`. First met in R07. See also [extension module](#extension-module), [abi flags](#abi-flags). + +### PyABIInfo + +**Twelve bytes an extension carries saying which ABI it was built against.** + +Two version bytes for the struct itself, sixteen flag bits for stable, GIL, free threaded and internal, and two packed version numbers. `PyABIInfo_Check` compares it against the running interpreter and raises `ImportError` when they disagree, which is the only ABI check CPython does at all. It is new in 3.15, it is opt in, and the extension's own code has already run by the time it happens. + +Also written `PyABIInfo_Check`, `Py_mod_abi`. First met in R07. See also [stable ABI](#stable-abi), [extension module](#extension-module). In the source: [`Include/modsupport.h:86-101@v3.15.0rc1#PyABIInfo`](https://github.com/python/cpython/blob/v3.15.0rc1/Include/modsupport.h#L86-L101). ## Stopping a running interpreter diff --git a/README.md b/README.md index c95ac9c..a770e8a 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ A lesson can also end with a boss fight, which is a problem the text does not so | 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) | +| R07 | [The stable ABI](lessons/r07-the-stable-abi/r07.ipynb) | An extension has to get past two gates before it is a module, and both of them can be driven from Python. The first is the file name, read before anything is opened: the tag has to be one of the handful in a C array compiled into the interpreter, and a name that is wrong is not rejected but simply never looked at. Put empty files in a directory and ask the finder what it sees and the whole rule falls out, including that a free threaded build takes abi3t and refuses abi3, an ordinary 3.15 build takes both, and 3.14 has never heard of abi3t. The 3 in abi3 is not a Python version, it is PYTHON_ABI_VERSION, a counter that stopped moving in 2010, and sys.api_version has been 1013 since 2006. The second gate is PyABIInfo, twelve bytes new in 3.15, and because PyABIInfo_Check is a plain exported function the struct can be built in ctypes and handed to the real check, which refuses four of eight sample extensions and gives a different reason for each. The stable ABI itself is dated by the preprocessor gates around the declarations, 173 functions added since 3.2 with the busiest releases being the recent ones. Two recordings run both gates on a release build and on a build made with --disable-gil, where six tags become four and four refusals become six | 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/r07-the-stable-abi/r07.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 1c5df40..cd2181c 100644 --- a/citations.lock.json +++ b/citations.lock.json @@ -1,5 +1,10 @@ { "citations": { + "Doc/c-api/stable.rst:100-105@v3.15.0rc1": { + "digest": "1376594fb65566a6", + "first_line": "On some platforms, Python will look for and load shared library files named", + "lines": 6 + }, "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:", @@ -150,6 +155,11 @@ "first_line": "typedef struct _PyLongValue {", "lines": 9 }, + "Include/cpython/modsupport.h:41-45@v3.15.0rc1": { + "digest": "e20f48a944439662", + "first_line": "#ifdef Py_BUILD_CORE", + "lines": 5 + }, "Include/cpython/object.h:147-151@v3.15.0rc1": { "digest": "5c8d29cfecbeeaa4", "first_line": "// as well.", @@ -960,11 +970,21 @@ "first_line": "#define PyList_Check(op) \\", "lines": 3 }, + "Include/modsupport.h:135-143@v3.15.0rc1": { + "digest": "53f1e39873eb54fa", + "first_line": "#define _PyABIInfo_DEFAULT { \\", + "lines": 9 + }, "Include/modsupport.h:85-101@v3.15.0rc1": { "digest": "88ce91d467657676", "first_line": "/* ABI info & checking (new in 3.15) */", "lines": 17 }, + "Include/modsupport.h:86-101@v3.15.0rc1": { + "digest": "2f4ed0e32c8ed1fb", + "first_line": "#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15)", + "lines": 16 + }, "Include/moduleobject.h:78-89@v3.15.0rc1": { "digest": "391087226de90dc4", "first_line": "/* for Py_mod_multiple_interpreters: */", @@ -1070,6 +1090,11 @@ "first_line": "#define ANNOTATIONS_PLACEHOLDER 256", "lines": 11 }, + "Include/patchlevel.h:53-62@v3.15.0rc1": { + "digest": "3dae732b1e88bab7", + "first_line": "/* The API and ABI versions are left for backwards compatibility.", + "lines": 10 + }, "Include/refcount.h:105-117@v3.15.0rc1": { "digest": "38a06fa5fe8a79cd", "first_line": "static inline Py_ssize_t _Py_REFCNT(PyObject *ob) {", @@ -2165,6 +2190,11 @@ "first_line": "case Py_mod_gil:", "lines": 6 }, + "Objects/moduleobject.c:471-481@v3.15.0rc1": { + "digest": "051ece003855e3a8", + "first_line": "case Py_mod_gil:", + "lines": 11 + }, "Objects/object.c:1573-1595@v3.15.0rc1": { "digest": "c469a683fb938006", "first_line": "_PyObject_ComputedDictPointer(PyObject *obj)", @@ -4125,6 +4155,11 @@ "first_line": "// None", "lines": 34 }, + "Python/dynload_shlib.c:39-60@v3.15.0rc1": { + "digest": "a07dedafcbd988d4", + "first_line": "const char *_PyImport_DynLoadFiletab[] = {", + "lines": 22 + }, "Python/executor_cases.c.h:2729-2744@v3.15.0rc1": { "digest": "4430981ca716fced", "first_line": "case _UNARY_NOT_r01: {", @@ -4625,6 +4660,11 @@ "first_line": "// Check if the filter disables the lazy import.", "lines": 25 }, + "Python/import.c:5175-5204@v3.15.0rc1": { + "digest": "655604314e26eb8e", + "first_line": "static PyObject *", + "lines": 30 + }, "Python/initconfig.c:1193-1201@v3.15.0rc1": { "digest": "0cd6d8c3fdac2d2a", "first_line": "#ifdef MS_WINDOWS", @@ -4720,6 +4760,16 @@ "first_line": "#define TYPE_NULL '0'", "lines": 39 }, + "Python/modsupport.c:731-753@v3.15.0rc1": { + "digest": "e64c5435101ed32f", + "first_line": "if (info->flags & PyABIInfo_STABLE) {", + "lines": 23 + }, + "Python/modsupport.c:768-782@v3.15.0rc1": { + "digest": "209293eecfaefc64", + "first_line": "/* Free-threading/GIL */", + "lines": 15 + }, "Python/opcode_targets.h:1-12@v3.15.0rc1": { "digest": "1e469cc6f9bf653e", "first_line": "#if !_Py_TAIL_CALL_INTERP", diff --git a/experiments/README.md b/experiments/README.md index a2c908e..c93847e 100644 --- a/experiments/README.md +++ b/experiments/README.md @@ -49,6 +49,8 @@ So those programs run somewhere else. They run in the images this project publis | [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? | +| [r07-what-a-build-will-load](tier1/r07-what-a-build-will-load.md) | R07 | release | What does the interpreter check before it agrees to load an extension? | +| [r07-what-a-build-will-load-without-the-lock](tier1/r07-what-a-build-will-load-without-the-lock.md) | R07 | freethreaded | How much of the stable ABI does dropping the global interpreter lock rule out? | ## The commands diff --git a/experiments/tier1/r07-what-a-build-will-load-without-the-lock.md b/experiments/tier1/r07-what-a-build-will-load-without-the-lock.md new file mode 100644 index 0000000..cf17a32 --- /dev/null +++ b/experiments/tier1/r07-what-a-build-will-load-without-the-lock.md @@ -0,0 +1,230 @@ +# The same two gates on the build that abi3t was invented for + +Generated by `just build-tier1`. Do not edit by hand, the change will be overwritten. + +How much of the stable ABI does dropping the global interpreter lock rule out? + +- Lesson: R07 +- 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 whole question is which file names and which ABI flags that build is willing to accept. + +## The program + +```python +"""What a build agrees to load, and the two gates that decide it. + +An extension arrives as a file with a long name. The interpreter decides whether to consider +it at all from that name alone, before it opens it. If it does open it, and the extension was +built against 3.15 or later, there is a second gate: a five field `PyABIInfo` struct the +extension carries, which `PyABIInfo_Check` compares against the running interpreter. + +This program runs both gates by hand. It asks the finder which file names it is willing to +look at, by putting empty files in a temporary directory and asking for the module. Then it +builds `PyABIInfo` structs in ctypes for a row of hypothetical extensions and calls the real +check function on each one. + +The point of running it on two builds is that both gates move. A free threaded build will not +look at a name with `abi3` in it, and it refuses a struct that says the extension wants the +global interpreter lock. + +One thing is printed differently from how the interpreter spells it. The last part of a file +name tag is the machine the build was made for, which is `x86_64-linux-gnu` on one runner and +`aarch64-linux-gnu` on another. That part says nothing about the ABI question, so it is +replaced with the word `PLATFORM` everywhere below and the recording is the same either way. +""" + +import ctypes +import importlib.machinery +import pathlib +import re +import sys +import sysconfig +import tempfile + +VERSIONED = re.compile(r"(cpython-)(\d+)(t?)") + +STABLE = 0x0001 +GIL = 0x0002 +FREETHREADED = 0x0004 +INTERNAL = 0x0008 + + +class PyABIInfo(ctypes.Structure): + """The struct an extension built against 3.15 or later carries about itself.""" + + _fields_ = [ + ("abiinfo_major_version", ctypes.c_uint8), + ("abiinfo_minor_version", ctypes.c_uint8), + ("flags", ctypes.c_uint16), + ("build_version", ctypes.c_uint32), + ("abi_version", ctypes.c_uint32), + ] + + +def pack(major, minor): + """The version format the C API uses, which is the two numbers in the top two bytes.""" + return (major << 24) | (minor << 16) + + +def considered(name): + """Put one empty file in a directory of its own and ask whether the finder sees it.""" + room = pathlib.Path(tempfile.mkdtemp()) + (room / name).write_bytes(b"") + return importlib.machinery.PathFinder.find_spec("demo", [str(room)]) is not None + + +TAGS = importlib.machinery.EXTENSION_SUFFIXES +here = sysconfig.get_config_var("EXT_SUFFIX") +flipped = VERSIONED.sub(lambda m: m.group(1) + m.group(2) + ("" if m.group(3) else "t"), here) +future = VERSIONED.sub(lambda m: m.group(1) + "999" + m.group(3), here) +machine = re.sub(r"^\.cpython-\d+t?-?", "", here).removesuffix(".so") + + +def tidy(text): + """Take the machine out of a file name tag, since it says nothing about the ABI.""" + return text.replace(machine, "PLATFORM") if machine else text + + +print("version:", sys.version.split()[0]) +print("abiflags:", repr(sys.abiflags)) +print("gil disabled:", sysconfig.get_config_var("Py_GIL_DISABLED")) +print("soabi:", tidy(sysconfig.get_config_var("SOABI"))) +print("api version, unchanged since 2006:", sys.api_version) +print() + +print("file name tags this build will load:") +for suffix in TAGS: + print(" ", tidy(suffix)) +print() + +print("what the finder does with one file of each kind") +for name in ("demo" + here, "demo" + flipped, "demo" + future, "demo.abi3.so", "demo.abi3t.so"): + verdict = "considered" if considered(name) else "invisible, the name is wrong" + print(f" {tidy(name):34} {verdict}") +print() + +check = ctypes.pythonapi.PyABIInfo_Check +check.argtypes = [ctypes.POINTER(PyABIInfo), ctypes.c_char_p] +check.restype = ctypes.c_int + +CASES = ( + ("built for this exact version", GIL, pack(*sys.version_info[:2])), + ("built for 3.13 and nothing else", GIL, pack(3, 13)), + ("stable abi since 3.2", STABLE | GIL, pack(3, 2)), + ("stable abi since 3.14", STABLE | GIL, pack(3, 14)), + ("stable abi from the future", STABLE | GIL, pack(3, 99)), + ("free threaded only", STABLE | FREETHREADED, pack(3, 14)), + ("happy either way", STABLE | GIL | FREETHREADED, pack(3, 14)), + ("claims internal and stable", STABLE | INTERNAL | GIL, sys.hexversion), +) + +print("what PyABIInfo_Check makes of each kind of extension") +refused = 0 +for label, flags, version in CASES: + info = PyABIInfo(1, 0, flags, sys.hexversion, version) + try: + check(ctypes.byref(info), label.encode()) + except ImportError as problem: + refused += 1 + print(f" {label:32} refused: {str(problem).split(': ', 1)[1]}") + else: + print(f" {label:32} loads") +print() + +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") +OLD = re.compile(r"Py_LIMITED_API\+0\s*>=\s*0x03([0-9a-fA-F]{2})0000") +NEW = re.compile(r"Py_LIMITED_API\+0\s*>=\s*_Py_PACK_VERSION\((\d+),\s*(\d+)\)") + + +def gate(line): + """The stable ABI version a preprocessor line gates on, when it gates on one.""" + found = NEW.search(line) + if found: + return int(found.group(2)) + found = OLD.search(line) + return int(found.group(1), 16) if found else None + + +include = pathlib.Path(sysconfig.get_paths()["include"]) +arrived = {} +for path in sorted(include.glob("*.h")): + stack = [] + for line in path.read_text(errors="replace").splitlines(): + if OPENS.match(line): + stack.append(gate(line)) + elif CLOSES.match(line) and stack: + stack.pop() + found = DECLARED.match(line) + if found and any(v for v in stack): + arrived[found.group(1)] = max(v for v in stack if v) + +print("when the public headers say each gated function arrived") +counted = {} +for version in arrived.values(): + counted[version] = counted.get(version, 0) + 1 +for version in sorted(counted): + print(f" 3.{version:<4} {counted[version]:4}") +print() + +print(f"~ file name tags this build will load: {len(TAGS)}") +print(f"~ hypothetical extensions refused: {refused} of {len(CASES)}") +print(f"~ public functions behind a stable abi version gate: {len(arrived)}") +``` + +## What it printed + +```text +version: 3.15.0rc1 +abiflags: 't' +gil disabled: 1 +soabi: cpython-315t-PLATFORM +api version, unchanged since 2006: 1013 + +file name tags this build will load: + .cpython-315t-PLATFORM.so + .abi3t-PLATFORM.so + .abi3t.so + .so + +what the finder does with one file of each kind + demo.cpython-315t-PLATFORM.so considered + demo.cpython-315-PLATFORM.so invisible, the name is wrong + demo.cpython-999t-PLATFORM.so invisible, the name is wrong + demo.abi3.so invisible, the name is wrong + demo.abi3t.so considered + +what PyABIInfo_Check makes of each kind of extension + built for this exact version refused: incompatible with free-threaded CPython + built for 3.13 and nothing else refused: incompatible ABI version (3.13) + stable abi since 3.2 refused: incompatible with free-threaded CPython + stable abi since 3.14 refused: incompatible with free-threaded CPython + stable abi from the future refused: incompatible future stable ABI version (3.99) + free threaded only loads + happy either way loads + claims internal and stable refused: cannot use both internal and stable ABI + +when the public headers say each gated function arrived + 3.3 26 + 3.4 3 + 3.5 13 + 3.6 4 + 3.7 12 + 3.8 1 + 3.9 10 + 3.10 5 + 3.11 16 + 3.12 6 + 3.13 27 + 3.14 18 + 3.15 32 + +~ file name tags this build will load: 4 +~ hypothetical extensions refused: 6 of 8 +~ public functions behind a stable abi version gate: 173 +``` diff --git a/experiments/tier1/r07-what-a-build-will-load.md b/experiments/tier1/r07-what-a-build-will-load.md new file mode 100644 index 0000000..4678983 --- /dev/null +++ b/experiments/tier1/r07-what-a-build-will-load.md @@ -0,0 +1,232 @@ +# Both gates an extension has to pass, run by hand on an ordinary build + +Generated by `just build-tier1`. Do not edit by hand, the change will be overwritten. + +What does the interpreter check before it agrees to load an extension? + +- Lesson: R07 +- 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 3.15 for PyABIInfo_Check, the headers next to the interpreter for the version gate scan, and a real filesystem to put the empty files in. + +## The program + +```python +"""What a build agrees to load, and the two gates that decide it. + +An extension arrives as a file with a long name. The interpreter decides whether to consider +it at all from that name alone, before it opens it. If it does open it, and the extension was +built against 3.15 or later, there is a second gate: a five field `PyABIInfo` struct the +extension carries, which `PyABIInfo_Check` compares against the running interpreter. + +This program runs both gates by hand. It asks the finder which file names it is willing to +look at, by putting empty files in a temporary directory and asking for the module. Then it +builds `PyABIInfo` structs in ctypes for a row of hypothetical extensions and calls the real +check function on each one. + +The point of running it on two builds is that both gates move. A free threaded build will not +look at a name with `abi3` in it, and it refuses a struct that says the extension wants the +global interpreter lock. + +One thing is printed differently from how the interpreter spells it. The last part of a file +name tag is the machine the build was made for, which is `x86_64-linux-gnu` on one runner and +`aarch64-linux-gnu` on another. That part says nothing about the ABI question, so it is +replaced with the word `PLATFORM` everywhere below and the recording is the same either way. +""" + +import ctypes +import importlib.machinery +import pathlib +import re +import sys +import sysconfig +import tempfile + +VERSIONED = re.compile(r"(cpython-)(\d+)(t?)") + +STABLE = 0x0001 +GIL = 0x0002 +FREETHREADED = 0x0004 +INTERNAL = 0x0008 + + +class PyABIInfo(ctypes.Structure): + """The struct an extension built against 3.15 or later carries about itself.""" + + _fields_ = [ + ("abiinfo_major_version", ctypes.c_uint8), + ("abiinfo_minor_version", ctypes.c_uint8), + ("flags", ctypes.c_uint16), + ("build_version", ctypes.c_uint32), + ("abi_version", ctypes.c_uint32), + ] + + +def pack(major, minor): + """The version format the C API uses, which is the two numbers in the top two bytes.""" + return (major << 24) | (minor << 16) + + +def considered(name): + """Put one empty file in a directory of its own and ask whether the finder sees it.""" + room = pathlib.Path(tempfile.mkdtemp()) + (room / name).write_bytes(b"") + return importlib.machinery.PathFinder.find_spec("demo", [str(room)]) is not None + + +TAGS = importlib.machinery.EXTENSION_SUFFIXES +here = sysconfig.get_config_var("EXT_SUFFIX") +flipped = VERSIONED.sub(lambda m: m.group(1) + m.group(2) + ("" if m.group(3) else "t"), here) +future = VERSIONED.sub(lambda m: m.group(1) + "999" + m.group(3), here) +machine = re.sub(r"^\.cpython-\d+t?-?", "", here).removesuffix(".so") + + +def tidy(text): + """Take the machine out of a file name tag, since it says nothing about the ABI.""" + return text.replace(machine, "PLATFORM") if machine else text + + +print("version:", sys.version.split()[0]) +print("abiflags:", repr(sys.abiflags)) +print("gil disabled:", sysconfig.get_config_var("Py_GIL_DISABLED")) +print("soabi:", tidy(sysconfig.get_config_var("SOABI"))) +print("api version, unchanged since 2006:", sys.api_version) +print() + +print("file name tags this build will load:") +for suffix in TAGS: + print(" ", tidy(suffix)) +print() + +print("what the finder does with one file of each kind") +for name in ("demo" + here, "demo" + flipped, "demo" + future, "demo.abi3.so", "demo.abi3t.so"): + verdict = "considered" if considered(name) else "invisible, the name is wrong" + print(f" {tidy(name):34} {verdict}") +print() + +check = ctypes.pythonapi.PyABIInfo_Check +check.argtypes = [ctypes.POINTER(PyABIInfo), ctypes.c_char_p] +check.restype = ctypes.c_int + +CASES = ( + ("built for this exact version", GIL, pack(*sys.version_info[:2])), + ("built for 3.13 and nothing else", GIL, pack(3, 13)), + ("stable abi since 3.2", STABLE | GIL, pack(3, 2)), + ("stable abi since 3.14", STABLE | GIL, pack(3, 14)), + ("stable abi from the future", STABLE | GIL, pack(3, 99)), + ("free threaded only", STABLE | FREETHREADED, pack(3, 14)), + ("happy either way", STABLE | GIL | FREETHREADED, pack(3, 14)), + ("claims internal and stable", STABLE | INTERNAL | GIL, sys.hexversion), +) + +print("what PyABIInfo_Check makes of each kind of extension") +refused = 0 +for label, flags, version in CASES: + info = PyABIInfo(1, 0, flags, sys.hexversion, version) + try: + check(ctypes.byref(info), label.encode()) + except ImportError as problem: + refused += 1 + print(f" {label:32} refused: {str(problem).split(': ', 1)[1]}") + else: + print(f" {label:32} loads") +print() + +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") +OLD = re.compile(r"Py_LIMITED_API\+0\s*>=\s*0x03([0-9a-fA-F]{2})0000") +NEW = re.compile(r"Py_LIMITED_API\+0\s*>=\s*_Py_PACK_VERSION\((\d+),\s*(\d+)\)") + + +def gate(line): + """The stable ABI version a preprocessor line gates on, when it gates on one.""" + found = NEW.search(line) + if found: + return int(found.group(2)) + found = OLD.search(line) + return int(found.group(1), 16) if found else None + + +include = pathlib.Path(sysconfig.get_paths()["include"]) +arrived = {} +for path in sorted(include.glob("*.h")): + stack = [] + for line in path.read_text(errors="replace").splitlines(): + if OPENS.match(line): + stack.append(gate(line)) + elif CLOSES.match(line) and stack: + stack.pop() + found = DECLARED.match(line) + if found and any(v for v in stack): + arrived[found.group(1)] = max(v for v in stack if v) + +print("when the public headers say each gated function arrived") +counted = {} +for version in arrived.values(): + counted[version] = counted.get(version, 0) + 1 +for version in sorted(counted): + print(f" 3.{version:<4} {counted[version]:4}") +print() + +print(f"~ file name tags this build will load: {len(TAGS)}") +print(f"~ hypothetical extensions refused: {refused} of {len(CASES)}") +print(f"~ public functions behind a stable abi version gate: {len(arrived)}") +``` + +## What it printed + +```text +version: 3.15.0rc1 +abiflags: '' +gil disabled: 0 +soabi: cpython-315-PLATFORM +api version, unchanged since 2006: 1013 + +file name tags this build will load: + .cpython-315-PLATFORM.so + .abi3-PLATFORM.so + .abi3.so + .abi3t-PLATFORM.so + .abi3t.so + .so + +what the finder does with one file of each kind + demo.cpython-315-PLATFORM.so considered + demo.cpython-315t-PLATFORM.so invisible, the name is wrong + demo.cpython-999-PLATFORM.so invisible, the name is wrong + demo.abi3.so considered + demo.abi3t.so considered + +what PyABIInfo_Check makes of each kind of extension + built for this exact version loads + built for 3.13 and nothing else refused: incompatible ABI version (3.13) + stable abi since 3.2 loads + stable abi since 3.14 loads + stable abi from the future refused: incompatible future stable ABI version (3.99) + free threaded only refused: only compatible with free-threaded CPython + happy either way loads + claims internal and stable refused: cannot use both internal and stable ABI + +when the public headers say each gated function arrived + 3.3 26 + 3.4 3 + 3.5 13 + 3.6 4 + 3.7 12 + 3.8 1 + 3.9 10 + 3.10 5 + 3.11 16 + 3.12 6 + 3.13 27 + 3.14 18 + 3.15 32 + +~ file name tags this build will load: 6 +~ hypothetical extensions refused: 4 of 8 +~ public functions behind a stable abi version gate: 173 +``` diff --git a/lessons/CLAIMS.md b/lessons/CLAIMS.md index 8b99f80..a6375a8 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. -610 claims across 77 lessons, 59 of them not observable from Python. +618 claims across 78 lessons, 59 of them not observable from Python. ## B01. Building CPython, and whether you need to @@ -795,6 +795,19 @@ Without it the exception becomes the rule and this goes back to being a book. | 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) | +## R07. The stable ABI + +| Claim | Proved by | +| --- | --- | +| An interpreter will only look at extension files whose names carry a tag it recognises, and it has a short fixed list of those. | [`r07-07`](r07-the-stable-abi/r07.ipynb) | +| The version in the file name is the Python version, and the 3 in abi3 is not. | [`r07-10`](r07-the-stable-abi/r07.ipynb) | +| A file with the wrong version in its name is invisible to the finder, not an error. | [`r07-13`](r07-the-stable-abi/r07.ipynb) | +| When more than one acceptable file is present, the first suffix in the list wins, not the best match. | [`r07-16`](r07-the-stable-abi/r07.ipynb) | +| The struct an extension carries is twelve bytes, and Python can build one and hand it to the real check function. | [`r07-19`](r07-the-stable-abi/r07.ipynb) | +| The check refuses four of these eight, and no two of them for the same reason. | [`r07-22`](r07-the-stable-abi/r07.ipynb) | +| Every function added to the stable ABI since 3.2 is dated by the preprocessor gate around it, and you can read the dates straight out of the headers. | [`r07-25`](r07-the-stable-abi/r07.ipynb) | +| A function's stable ABI version is readable from the header line it is declared on. | [`r07-28`](r07-the-stable-abi/r07.ipynb) | + ## T01. One line, seven stages | Claim | Proved by | diff --git a/lessons/README.md b/lessons/README.md index e1b020b..64a9b47 100644 --- a/lessons/README.md +++ b/lessons/README.md @@ -81,6 +81,7 @@ Each lesson is a notebook you can run. There is nothing to install and nothing t | [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) | +| [R07. The stable ABI](r07-the-stable-abi/r07.ipynb) | The six file name tags this build will load an extension from and the one it stamps on its own output, the SOABI tag taken apart into implementation and version and threading and platform, five candidate file names each dropped into a directory of its own so the finder can say which it would even look at, all of them plus a plain demo.py in one directory to show which wins and that a compiled file shadows a Python one, the PyABIInfo struct rebuilt field by field in ctypes and measured at twelve bytes, eight hypothetical extensions handed to the real PyABIInfo_Check so the four refusals print with CPython's own wording, every public header walked as a preprocessor stack to date each of the 173 gated functions by the release that added it, four named functions with the file and line and stable ABI version read straight out of the headers, and both gates run again in containers on a release build and a free threaded one | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tamnd/cpython-internals/blob/main/lessons/r07-the-stable-abi/r07.ipynb) | ## The three programs diff --git a/lessons/r07-the-stable-abi/build.py b/lessons/r07-the-stable-abi/build.py new file mode 100644 index 0000000..46bcac4 --- /dev/null +++ b/lessons/r07-the-stable-abi/build.py @@ -0,0 +1,493 @@ +#!/usr/bin/env python +"""R07. The stable ABI. + +R06 was about which C functions you are allowed to call. This one is about what happens +afterwards, when the thing you compiled turns up on somebody else's machine as a file. + +There are two gates. The first is the file name, which the finder reads before it opens +anything. The second is a twelve byte struct the extension carries, which is new in 3.15 and +which the interpreter checks after the extension's own code has already run. + +Both gates are runnable from Python, which is the nice surprise here. The first one you can +drive by putting empty files in a temporary directory and asking the finder what it sees. The +second one you can drive by building the struct in ctypes and calling the real check function. + +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("r07-the-stable-abi", "r07") +badge = lesson.badge +cite = lesson.cite +term = lesson.term +figure = Diagrams("r07-the-stable-abi").figure + +LOADS = "r07-what-a-build-will-load" +NOLOCK = "r07-what-a-build-will-load-without-the-lock" + + +lesson.md(f""" +# R07. The stable ABI + +{badge} + +R06 sorted out which C functions you are allowed to call. This one picks up where that leaves off, when the thing you compiled turns up on somebody else's machine as a file with a long name. + +There are two gates between that file and a working module. The first is the name itself, which the interpreter reads before it opens anything. The second is a small struct the extension carries, which is new in 3.15 and which gets checked after the extension's own code has already run. + +Both of them can be driven from Python, which is the nice part. + +{figure("two-gates", "a four stage pipeline, a file turning up, the name being read, the file being opened and run, and the struct being checked")} +""") + + +lesson.md(""" +## About the source references + +Now and then this lesson points at CPython's own source, like this: `Include/patchlevel.h:53-62@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 + +Most of this lesson runs anywhere, because file names and `ctypes` work everywhere. Two things do not. The second gate arrived in 3.15, so on 3.14 those cells say what it would have answered. And two cells read the header files, which a normal install has and a browser tab does not. + +## Which interpreter is this +""") + + +lesson.code( + """ +import pyxray + +pyxray.show() +""", + differs=BANNER, + quiet=True, +) + + +lesson.md(f""" +## The name is the first gate + +An {term("extension module", "a module that arrives as a compiled shared library rather than as Python")} is a shared library with a long name. The long name is not decoration. It is the only thing the interpreter looks at before deciding whether the file is worth opening. + +Start with what this build accepts and what it stamps on its own output. + +{lesson.claim("An interpreter will only look at extension files whose names carry a tag it recognises, and it has a short fixed list of those.")} +""") + + +lesson.code( + """ +import importlib.machinery +import sysconfig + +TAGS = importlib.machinery.EXTENSION_SUFFIXES +HERE = sysconfig.get_config_var("EXT_SUFFIX") + +print(" file names this build is willing to load an extension from:") +for tag in TAGS: + print(" ", tag) +print() +print(" and the one it stamps on what it builds:", HERE) +for key in ("SOABI", "ABIFLAGS", "Py_GIL_DISABLED"): + print(f" {key:16} {sysconfig.get_config_var(key)!r}") +print(f" {'sys.abiflags':16} {sys.abiflags!r}") +""", + differs=( + "On 3.14 there are three tags rather than six. Both of the abi3t ones are missing " + "because that tag is new in 3.15, and so is the platform specific abi3 one." + ), +) + + +lesson.md(f""" +That list is not built at run time. It is a C array, and you can read the whole thing in {cite("Python/dynload_shlib.c:39-60@v3.15.0rc1#_PyImport_DynLoadFiletab")}. The `#ifndef Py_GIL_DISABLED` in the middle of it is why a free threaded build has fewer entries, and `_imp.extension_suffixes` at {cite("Python/import.c:5175-5204@v3.15.0rc1#_imp_extension_suffixes_impl")} does nothing but copy that array into a list. + +The tag is called {term("SOABI", "the tag a build stamps into the file names of the extensions it compiles")}, and it is three facts glued together. + +{figure("what-the-name-says", "the file name mymodule.cpython-315t-darwin.so broken into six labelled pieces")} + +{lesson.claim("The version in the file name is the Python version, and the 3 in abi3 is not.")} +""") + + +lesson.code( + """ +import re + +VERSIONED = re.compile(r"(cpython-)(\\d+)(t?)") + +found = VERSIONED.search(HERE) +if found: + print(" the tag this build writes:", HERE) + print(" which Python: ", found.group(1).rstrip("-")) + print(" which version: ", found.group(2)) + print(" threading: ", "no lock" if found.group(3) else "with the lock") + print(" the rest: ", HERE[found.end() :]) +else: + print(" this build puts no version in the name at all:", HERE) +print() +print(" the 3 in abi3 is not a Python version, it is PYTHON_ABI_VERSION") +print(" and the other number nobody has touched since 2006:", sys.api_version) +""", + differs=( + "On 3.14 the version in the tag reads 314 rather than 315. Everything else here is the " + "same, including the 1013 that nobody has touched since 2006." + ), +) + + +lesson.md(f""" +Those two numbers are worth a look, because they are a small piece of history sitting in a header nobody edits. {cite("Include/patchlevel.h:53-62@v3.15.0rc1#PYTHON_ABI_VERSION")} defines `PYTHON_API_VERSION 1013` and `PYTHON_ABI_VERSION 3` with a comment saying they have not been updated since 2006 and 2010, and that versioning is tied to the CPython version now. + +So the `3` in `abi3` is not "Python 3". It is a counter that stopped moving fifteen years ago, and the suffix table builds the string out of it. + +## What the finder will even look at + +Here is the first gate, run by hand. Put one empty file in a directory, ask for a module by that name, and see whether anything comes back. The file is empty, so nothing gets loaded; the finder only ever stats it. + +{lesson.claim("A file with the wrong version in its name is invisible to the finder, not an error.")} +""") + + +lesson.code( + """ +import pathlib +import tempfile + +flipped = VERSIONED.sub(lambda m: m.group(1) + m.group(2) + ("" if m.group(3) else "t"), HERE) +future = VERSIONED.sub(lambda m: m.group(1) + "999" + m.group(3), HERE) +CANDIDATES = ("demo" + HERE, "demo" + flipped, "demo" + future, "demo.abi3.so", "demo.abi3t.so") + + +def considered(name): + \"\"\"Put one empty file in a directory of its own and ask the finder whether it sees it.\"\"\" + room = pathlib.Path(tempfile.mkdtemp()) + (room / name).write_bytes(b"") + return importlib.machinery.PathFinder.find_spec("demo", [str(room)]) is not None + + +for name in CANDIDATES: + print(f" {name:42} {'considered' if considered(name) else 'invisible, wrong name'}") +""", + differs=( + "On 3.14 the abi3t file is invisible too, because that tag did not exist yet. The other " + "four answers are the same, with 314 in place of 315." + ), +) + + +lesson.md(f""" +{figure("which-names-load", "a table of six file names against three builds, saying which are considered and which are invisible")} + +Read the two `abi3` rows together and the design shows up. A free threaded build refuses `abi3` and takes `abi3t`. An ordinary 3.15 build takes both. A 3.14 build takes `abi3` and has never heard of `abi3t`. {cite("Doc/c-api/stable.rst:100-105@v3.15.0rc1")} states the rule and draws the conclusion for you: ship `abi3t` if you want one file that reaches both 3.15 builds. + +When several of them are sitting in the same directory, the order in that C array decides. + +{lesson.claim("When more than one acceptable file is present, the first suffix in the list wins, not the best match.")} +""") + + +lesson.code( + """ +room = pathlib.Path(tempfile.mkdtemp()) +for name in CANDIDATES: + (room / name).write_bytes(b"") +(room / "demo.py").write_text("") +picked = importlib.machinery.PathFinder.find_spec("demo", [str(room)]) +print(" all of them in one directory, and the finder picks:") +print(" ", pathlib.Path(picked.origin).name) +print(" loaded by", type(picked.loader).__name__) +print() +print(" the order is the suffix list, and the first match wins:") +for position, tag in enumerate(TAGS, 1): + print(f" {position}. {tag}") +""", + differs=( + "On 3.14 the name the finder picks has 314 in it, and the numbered list below has three " + "entries rather than six, for the same reason as the first cell." + ), +) + + +lesson.md(f""" +The exact build wins over the stable one, and both win over the plain `demo.py`. That last part is worth remembering, because it means a compiled file quietly shadows a Python file of the same name. + +## The second gate + +Everything so far cost one stat call. The second gate is much more expensive, because by the time it runs the file has been opened, the dynamic linker has resolved its symbols, and the extension's own initialisation code has started. + +{term("PyABIInfo", "twelve bytes an extension carries saying which ABI it was built against")} is the struct that gets checked. It is declared at {cite("Include/modsupport.h:86-101@v3.15.0rc1#PyABIInfo")}, and an extension that wants the check writes `PyABIInfo_VAR(abi_info)` and lists it as a slot, which {cite("Include/modsupport.h:135-143@v3.15.0rc1#PyABIInfo_VAR")} turns into a static struct filled in from whatever macros were set when it was compiled. + +{figure("the-five-fields", "the five fields of PyABIInfo stacked, with the version fields at the bottom")} + +`PyABIInfo_Check` is a plain function in the public headers, so `ctypes` can call it, and the struct is five integers, so `ctypes` can build one. That is the whole trick of this lesson. + +{lesson.claim("The struct an extension carries is twelve bytes, and Python can build one and hand it to the real check function.")} +""") + + +lesson.code( + """ +import ctypes + +api = ctypes.pythonapi +ABICHECK = hasattr(api, "PyABIInfo_Check") + +STABLE = 0x0001 +GIL = 0x0002 +FREETHREADED = 0x0004 +INTERNAL = 0x0008 + + +class PyABIInfo(ctypes.Structure): + \"\"\"The five fields an extension built against 3.15 or later carries about itself.\"\"\" + + _fields_ = [ + ("abiinfo_major_version", ctypes.c_uint8), + ("abiinfo_minor_version", ctypes.c_uint8), + ("flags", ctypes.c_uint16), + ("build_version", ctypes.c_uint32), + ("abi_version", ctypes.c_uint32), + ] + + +def pack(major, minor): + \"\"\"The version format the C API uses, two numbers packed into the top two bytes.\"\"\" + return (major << 24) | (minor << 16) + + +if not ABICHECK: + print(" PyABIInfo_Check arrived in 3.15 and this is", sys.version.split()[0]) + print(" so the next cell has nothing to run") +else: + print(" the struct is", ctypes.sizeof(PyABIInfo), "bytes, laid out as") + for field, kind in PyABIInfo._fields_: + print(f" {field:24} {kind.__name__}") +""", + differs=( + "On 3.14 there is no PyABIInfo_Check to find, so this cell says so and the next one " + "skips itself. The struct did not exist before 3.15." + ), +) + + +lesson.md(f""" +## Who gets turned away + +Now the interesting part. Build one struct per kind of extension somebody might ship, hand each one to the real check function, and print what comes back. Nothing is being loaded here; the function is only comparing numbers. + +{lesson.claim("The check refuses four of these eight, and no two of them for the same reason.")} +""") + + +lesson.code( + """ +CASES = ( + ("built for this exact version", GIL, pack(*sys.version_info[:2])), + ("built for 3.13 and nothing else", GIL, pack(3, 13)), + ("stable abi since 3.2", STABLE | GIL, pack(3, 2)), + ("stable abi since 3.14", STABLE | GIL, pack(3, 14)), + ("stable abi from the future", STABLE | GIL, pack(3, 99)), + ("free threaded only", STABLE | FREETHREADED, pack(3, 14)), + ("happy either way", STABLE | GIL | FREETHREADED, pack(3, 14)), + ("claims internal and stable", STABLE | INTERNAL, sys.hexversion), +) + +if ABICHECK: + check = api.PyABIInfo_Check + check.argtypes = [ctypes.POINTER(PyABIInfo), ctypes.c_char_p] + check.restype = ctypes.c_int + for label, flags, version in CASES: + info = PyABIInfo(1, 0, flags, sys.hexversion, version) + try: + check(ctypes.byref(info), label.encode()) + except ImportError as problem: + print(f" {label:32} refused: {str(problem).split(': ', 1)[1]}") + else: + print(f" {label:32} loads") +else: + print(" nothing to run here, this interpreter has no PyABIInfo_Check") +""", + differs="On 3.14 this prints the one line saying there is nothing to run.", +) + + +lesson.md(f""" +{figure("who-gets-refused", "a table of eight hypothetical extensions against two builds, with the verdict in each cell")} + +Four different refusals from one function. The version comparisons are at {cite("Python/modsupport.c:731-753@v3.15.0rc1#PyABIInfo_STABLE")}, where a stable extension is allowed to be older than the interpreter but not newer, while a version locked one has to match exactly. The threading check is fifteen lines further down at {cite("Python/modsupport.c:768-782@v3.15.0rc1#Py_GIL_DISABLED")}, and it is symmetrical: a GIL only extension is refused on a free threaded build and a free threaded only one is refused here. + +The row that loads on both is the one that claims both flags, which is the same conclusion the file name gave. `abi3t`, and no promise about the lock. + +Worth being honest about the limits. This check is opt in, an extension that does not carry the struct is not checked at all, and {cite("Doc/c-api/stable.rst:100-105@v3.15.0rc1")} says plainly that Python does not necessarily check that extensions it loads have a compatible ABI. CPython's own bundled modules opt in through a macro at {cite("Include/cpython/modsupport.h:41-45@v3.15.0rc1#_Py_ABI_SLOT")}, and the slot is read at {cite("Objects/moduleobject.c:471-481@v3.15.0rc1#Py_mod_abi")}, one case in the loop that walks a module's slots. + +## How the stable ABI grew + +The last thing worth measuring is the stable ABI as a list that changes. It is not fixed. Functions get added to it, one release at a time, and the headers record when each one arrived, in the form of the version gate around its declaration. + +{lesson.claim("Every function added to the stable ABI since 3.2 is dated by the preprocessor gate around it, and you can read the dates straight out of the headers.")} +""") + + +lesson.code( + """ +INCLUDE = pathlib.Path(sysconfig.get_paths()["include"]) +HEADERS = INCLUDE.is_dir() and any(INCLUDE.glob("*.h")) + +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") +OLD = re.compile(r"Py_LIMITED_API\\+0\\s*>=\\s*0x03([0-9a-fA-F]{2})0000") +NEW = re.compile(r"Py_LIMITED_API\\+0\\s*>=\\s*_Py_PACK_VERSION\\((\\d+),\\s*(\\d+)\\)") + + +def gate(line): + \"\"\"The stable ABI version a preprocessor line gates on, when it gates on one at all.\"\"\" + hit = NEW.search(line) + if hit: + return int(hit.group(2)) + hit = OLD.search(line) + return int(hit.group(1), 16) if hit else None + + +if not HEADERS: + print(" no headers next to this interpreter, looked in", INCLUDE) + print(" so this cell and the next one have nothing to read") +else: + arrived = {} + for path in sorted(INCLUDE.glob("*.h")): + stack = [] + for line in path.read_text(errors="replace").splitlines(): + if OPENS.match(line): + stack.append(gate(line)) + elif CLOSES.match(line) and stack: + stack.pop() + hit = DECLARED.match(line) + if hit and any(v for v in stack): + arrived[hit.group(1)] = max(v for v in stack if v) + counted = {} + for version in arrived.values(): + counted[version] = counted.get(version, 0) + 1 + print(" public functions the headers put behind a stable abi version gate:", len(arrived)) + for version in sorted(counted): + print(f" 3.{version:<4} {counted[version]:4} {'#' * counted[version]}") +""", + differs=( + "On 3.14 the total is 140 rather than 173. There is no 3.15 row, and the 3.14 row is 17 " + "rather than 18, because one more function was added to it after 3.14 shipped." + ), +) + + +lesson.md(f""" +{figure("when-it-arrived", "a bar chart of how many gated functions each release from 3.3 to 3.15 added")} + +Two hundred and change over thirteen years, and the recent releases are the busy ones. The 528 functions that were already in the stable ABI when it was defined in 3.2 are not on this chart at all; they have no gate, because there is no version to gate on. + +You can point the same scan at one name and get its gate and the line it sits on. + +{lesson.claim("A function's stable ABI version is readable from the header line it is declared on.")} +""") + + +lesson.code( + """ +WANTED = ("PyDict_GetItemRef", "PyLong_AsInt64", "PyABIInfo_Check", "PyList_Append") + +if HEADERS: + for path in sorted(INCLUDE.glob("*.h")): + lines = path.read_text(errors="replace").splitlines() + stack = [] + for number, line in enumerate(lines, 1): + if OPENS.match(line): + stack.append(gate(line)) + elif CLOSES.match(line) and stack: + stack.pop() + hit = DECLARED.match(line) + if hit and hit.group(1) in WANTED: + live = [v for v in stack if v] + answer = f"3.{max(live)} and later" if live else "every version since 3.2" + print(f" {hit.group(1):20} {path.name}:{number:<5} {answer}") +""", + differs=( + "On 3.14 PyABIInfo_Check is missing from the output entirely, because the declaration " + "is not in those headers yet. The other three are in the same files at the same lines." + ), +) + + +lesson.md(f""" +`PyList_Append` has no gate, so it has been callable from a limited build since the beginning. `PyABIInfo_Check` is gated on 3.15, which is a slightly funny situation: the function that checks whether your ABI is old enough is itself too new for most of the ABIs it would check. + +## Both gates on two builds + +Everything above ran on one interpreter. The claim that the two gates move together deserves two, so here is the same work run in a container against a build made from the pinned source. + +{recording(LOADS)} + +Six file name tags, four of the eight sample extensions refused. Now the same program on a build made with `--disable-gil`. + +{recording(NOLOCK)} + +Four tags rather than six, and six refusals rather than four. Both gates tightened, and they tightened in the same direction: the `abi3` name is gone from the list, and every struct that says `PyABIInfo_GIL` without also saying `PyABIInfo_FREETHREADED` is turned away. + +That is the whole argument for `abi3t` in two recordings. An extension shipped as `abi3` in 2024 does not reach a free threaded interpreter, by name or by struct, and neither refusal is a bug. + +## Try it yourself + +**One.** The finder picked the exact version file over `demo.abi3.so`. Delete that one from the directory and run it again, then keep deleting until only `demo.py` is left. The order you see is the C array in `dynload_shlib.c`, read top to bottom. + +**Two.** Set `abiinfo_major_version` to 0 in one of the cases and call the check again. Find the line in `Python/modsupport.c` that explains what you see, and work out why that escape hatch exists. + +**Three.** The gate scan only looks at `Include/*.h`. Point it at `Include/cpython/*.h` as well and see how many gated functions turn up there. Then work out why any function in that directory would carry a `Py_LIMITED_API` gate at all. + +**Four.** `sys.api_version` is 1013 and has not moved since 2006. Find the two places in the source that still read it, and decide for yourself whether the check they do is worth anything. + +## What you now know + +An extension has to get past two gates. The first is its file name, and it is cheap, mechanical and unforgiving: the tag has to be one of the handful in a C array compiled into the interpreter, and anything else is not rejected but simply never seen. + +The tag is the implementation, the version, a `t` when the build has no global interpreter lock, and the platform. The `abi3` and `abi3t` tags say the file was built against the stable ABI instead. A free threaded build takes only `abi3t`, an ordinary 3.15 build takes both and prefers `abi3`, and 3.14 has never heard of `abi3t`. + +The second gate is twelve bytes of struct, new in 3.15 and opt in. It compares four things: whether the extension claims the stable ABI, which version it was built against, whether that version is allowed to be older than yours, and which side of the free threading split it is on. Four of the seven sample extensions in this lesson were refused, each for a different reason. + +Neither gate is a real safety net. The name check happens before anything is read, so it catches the common mistake and nothing else. The struct check happens after the extension's code has already run, and only if the extension asked for it. + +## What is next + +R08 turns the last three lessons around. Instead of asking what an extension may call and whether it will load, it asks what the interpreter is doing while all of this is happening, and what happens when it stops. Shutdown is the part of the runtime with the fewest promises, and it is where every assumption the earlier lessons made about lifetime finally gets tested. +""") + + +raise SystemExit(lesson.save()) diff --git a/lessons/r07-the-stable-abi/diagrams.py b/lessons/r07-the-stable-abi/diagrams.py new file mode 100644 index 0000000..5e92337 --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +"""The diagrams for R07, the two gates an extension has to get past. + +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 file name first, since that is the gate an extension meets +before anything opens it, then the table of which names each build will look at, then the +struct that is the second gate, then who it turns away, and last the stable ABI as a thing +that grew one release at a time. +""" + +from nbdiagram import Gallery, figures + +gallery = Gallery("r07-the-stable-abi") + +gallery.add( + figures.pipeline( + "two-gates", + [ + ("a file turns up", "mod.abi3t.so"), + ("read the name", "a tag I take?"), + ("open and run it", "dlopen, then init"), + ("check the struct", "numbers agree?"), + ], + highlight=1, + title="What an extension has to get past before it is a module", + caption="The first gate costs one stat call. The second one has already run the extension's code.", + ) +) + + +gallery.add( + figures.spans( + "what-the-name-says", + "mymodule.cpython-315t-darwin.so", + [ + (0, 8, "the module"), + (9, 16, "which Python"), + (17, 20, "3.15"), + (20, 21, "no GIL"), + (22, 28, "the platform"), + (29, 31, "shared library"), + ], + title="Everything the interpreter knows before it opens the file", + caption="Change any one of the middle four and this build will not even look at it.", + ) +) + + +gallery.add( + figures.table( + "which-names-load", + ["the file is called", "3.15 with the GIL", "3.15 free threaded", "3.14"], + [ + ["mod.cpython-315-darwin.so", "considered", "invisible", "invisible"], + ["mod.cpython-315t-darwin.so", "invisible", "considered", "invisible"], + ["mod.cpython-314-darwin.so", "invisible", "invisible", "considered"], + ["mod.abi3.so", "considered", "invisible", "considered"], + ["mod.abi3t.so", "considered", "considered", "invisible"], + ["mod.so", "considered", "considered", "considered"], + ], + title="Which file names each build is willing to look at", + caption="abi3t is the only tag both 3.15 builds accept, which is why it is the one to ship.", + tones=["intermediate", "intermediate", "intermediate", "durable", "durable", "quiet"], + ) +) + + +gallery.add( + figures.stack( + "the-five-fields", + [ + "abi_version, the version the extension was built for", + "build_version, the version its headers came from", + "flags, stable, GIL, free threaded, internal", + "abiinfo_minor_version, 0 so far", + "abiinfo_major_version, 1, or 0 to skip the check", + ], + title="PyABIInfo, the twelve bytes an extension carries about itself", + note="Twelve bytes of struct decide whether an ImportError or a module comes back.", + ) +) + + +gallery.add( + figures.table( + "who-gets-refused", + ["what the extension claims", "3.15 with the GIL", "3.15 free threaded"], + [ + ["built for this exact version", "loads", "incompatible with free threaded"], + [ + "built for 3.13 and nothing else", + "incompatible ABI version", + "incompatible ABI version", + ], + ["stable abi since 3.2", "loads", "incompatible with free threaded"], + ["stable abi since 3.14", "loads", "incompatible with free threaded"], + [ + "stable abi from the future", + "incompatible future version", + "incompatible future version", + ], + ["free threaded only", "only compatible with free threaded", "loads"], + ["happy either way", "loads", "loads"], + ["claims internal and stable", "cannot use both", "cannot use both"], + ], + title="What PyABIInfo_Check says to each kind of extension", + caption="One row loads on both builds, and it is the row that promises nothing about the lock.", + tones=[ + "intermediate", + "warning", + "intermediate", + "intermediate", + "warning", + "intermediate", + "durable", + "warning", + ], + ) +) + + +gallery.add( + figures.bars( + "when-it-arrived", + [ + ["3.3", 26], + ["3.4", 3], + ["3.5", 13], + ["3.6", 4], + ["3.7", 12], + ["3.8", 1], + ["3.9", 10], + ["3.10", 5], + ["3.11", 16], + ["3.12", 6], + ["3.13", 27], + ["3.14", 18], + ["3.15", 32], + ], + unit="functions", + title="When each of the 173 gated functions joined the stable ABI", + caption="The 528 that were there in 3.2 are not on this chart. These are the ones added since.", + tones=[ + "quiet", + "quiet", + "quiet", + "quiet", + "quiet", + "quiet", + "quiet", + "quiet", + "quiet", + "quiet", + "focus", + "focus", + "focus", + ], + width=560, + ) +) + + +raise SystemExit(gallery.save()) diff --git a/lessons/r07-the-stable-abi/diagrams/the-five-fields.excalidraw b/lessons/r07-the-stable-abi/diagrams/the-five-fields.excalidraw new file mode 100644 index 0000000..5334d10 --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/the-five-fields.excalidraw @@ -0,0 +1,473 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "741c6b50cf627e1ead74", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 789.8399999999999, + "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": "PyABIInfo, the twelve bytes an extension carries about itself", + "originalText": "PyABIInfo, the twelve bytes an extension carries about itself", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "75d30cedd83fdd90f8b3", + "type": "rectangle", + "x": 0.0, + "y": 50.0, + "width": 656.0, + "height": 57.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": "f703f39ce12444e17f5f", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "f703f39ce12444e17f5f", + "type": "text", + "x": 16.0, + "y": 66.0, + "width": 624.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": "abiinfo_major_version, 1, or 0 to skip the check", + "originalText": "abiinfo_major_version, 1, or 0 to skip the check", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "75d30cedd83fdd90f8b3", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "25765585c258a87a9198", + "type": "text", + "x": 672.0, + "y": 66.0, + "width": 25.2, + "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": "top", + "originalText": "top", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "eeb425a61b1dbb8d603c", + "type": "rectangle", + "x": 0.0, + "y": 102.0, + "width": 656.0, + "height": 57.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": "b9f11dea244c256c65bb", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "b9f11dea244c256c65bb", + "type": "text", + "x": 16.0, + "y": 118.0, + "width": 624.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": "abiinfo_minor_version, 0 so far", + "originalText": "abiinfo_minor_version, 0 so far", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "eeb425a61b1dbb8d603c", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "e2072056a04f7d9653c1", + "type": "rectangle", + "x": 0.0, + "y": 154.0, + "width": 656.0, + "height": 57.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": "3b43483886744a51a335", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "3b43483886744a51a335", + "type": "text", + "x": 16.0, + "y": 170.0, + "width": 624.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": "flags, stable, GIL, free threaded, internal", + "originalText": "flags, stable, GIL, free threaded, internal", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "e2072056a04f7d9653c1", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "0d755fdd980d2209acc8", + "type": "rectangle", + "x": 0.0, + "y": 206.0, + "width": 656.0, + "height": 57.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": "239bde7334802f35524c", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "239bde7334802f35524c", + "type": "text", + "x": 16.0, + "y": 222.0, + "width": 624.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": "build_version, the version its headers came from", + "originalText": "build_version, the version its headers came from", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "0d755fdd980d2209acc8", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "d69d7da5207df6a1f025", + "type": "rectangle", + "x": 0.0, + "y": 258.0, + "width": 656.0, + "height": 57.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": "310e2c266a3c4efe288f", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "310e2c266a3c4efe288f", + "type": "text", + "x": 16.0, + "y": 274.0, + "width": 624.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": "abi_version, the version the extension was built for", + "originalText": "abi_version, the version the extension was built for", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "d69d7da5207df6a1f025", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "d66b488a4d23a9e2f90d", + "type": "text", + "x": 0.0, + "y": 330.0, + "width": 693.5999999999999, + "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": "Twelve bytes of struct decide whether an ImportError or a module comes back.", + "originalText": "Twelve bytes of struct decide whether an ImportError or a module comes back.", + "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/r07-the-stable-abi/diagrams/the-five-fields.svg b/lessons/r07-the-stable-abi/diagrams/the-five-fields.svg new file mode 100644 index 0000000..9e6906d --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/the-five-fields.svg @@ -0,0 +1 @@ +PyABIInfo, the twelve bytes an extension carries about itselfabiinfo_major_version, 1, or 0 to skip the checktopabiinfo_minor_version, 0 so farflags, stable, GIL, free threaded, internalbuild_version, the version its headers came fromabi_version, the version the extension was built forTwelve bytes of struct decide whether an ImportError or a module comes back. diff --git a/lessons/r07-the-stable-abi/diagrams/two-gates.excalidraw b/lessons/r07-the-stable-abi/diagrams/two-gates.excalidraw new file mode 100644 index 0000000..91638d1 --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/two-gates.excalidraw @@ -0,0 +1,691 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "f22d35ce26285962e616", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 744.4799999999999, + "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 an extension has to get past before it is a module", + "originalText": "What an extension has to get past before it is a module", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "57e16bc8caa0d756ce3e", + "type": "rectangle", + "x": 0.0, + "y": 50.0, + "width": 206.0, + "height": 80.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": "6cd7b40f686f7ddaf404", + "type": "text" + }, + { + "id": "4aa1ea7e14d57132b02d", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "6cd7b40f686f7ddaf404", + "type": "text", + "x": 16.0, + "y": 77.5, + "width": 174.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": "a file turns up", + "originalText": "a file turns up", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "57e16bc8caa0d756ce3e", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "b4f2fe53dc2389ae7646", + "type": "text", + "x": 45.400000000000006, + "y": 140.0, + "width": 115.19999999999999, + "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": "mod.abi3t.so", + "originalText": "mod.abi3t.so", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "7a6d097c690953cecbfe", + "type": "rectangle", + "x": 266.0, + "y": 50.0, + "width": 206.0, + "height": 80.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": "6b2d865075dbbfbcf2f0", + "type": "text" + }, + { + "id": "4aa1ea7e14d57132b02d", + "type": "arrow" + }, + { + "id": "39dbd9c5699f97cc0247", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "6b2d865075dbbfbcf2f0", + "type": "text", + "x": 282.0, + "y": 77.5, + "width": 174.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": "read the name", + "originalText": "read the name", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "7a6d097c690953cecbfe", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "f70a4f9b7b15035cf564", + "type": "text", + "x": 306.6, + "y": 140.0, + "width": 124.8, + "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": "a tag I take?", + "originalText": "a tag I take?", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "ace4cf026203c4eaee91", + "type": "rectangle", + "x": 532.0, + "y": 50.0, + "width": 206.0, + "height": 80.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": "137682c41b019a1edb80", + "type": "text" + }, + { + "id": "39dbd9c5699f97cc0247", + "type": "arrow" + }, + { + "id": "e54591893d34f5629a41", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "137682c41b019a1edb80", + "type": "text", + "x": 548.0, + "y": 77.5, + "width": 174.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": "open and run it", + "originalText": "open and run it", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ace4cf026203c4eaee91", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "4217d8400827c0135387", + "type": "text", + "x": 553.4, + "y": 140.0, + "width": 163.2, + "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": "dlopen, then init", + "originalText": "dlopen, then init", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "13898dc253d5f16722eb", + "type": "rectangle", + "x": 798.0, + "y": 50.0, + "width": 206.0, + "height": 80.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": "537be139d43e63cac0fa", + "type": "text" + }, + { + "id": "e54591893d34f5629a41", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "537be139d43e63cac0fa", + "type": "text", + "x": 814.0, + "y": 77.5, + "width": 174.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": "check the struct", + "originalText": "check the struct", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "13898dc253d5f16722eb", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "928b54543812157067af", + "type": "text", + "x": 833.8, + "y": 140.0, + "width": 134.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": "numbers agree?", + "originalText": "numbers agree?", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "4aa1ea7e14d57132b02d", + "type": "arrow", + "x": 206.0, + "y": 90.0, + "width": 60.0, + "height": 0.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 + ], + [ + 60.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "57e16bc8caa0d756ce3e", + "focus": 0, + "gap": 4 + }, + "endBinding": { + "elementId": "7a6d097c690953cecbfe", + "focus": 0, + "gap": 4 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "39dbd9c5699f97cc0247", + "type": "arrow", + "x": 472.0, + "y": 90.0, + "width": 60.0, + "height": 0.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 + ], + [ + 60.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "7a6d097c690953cecbfe", + "focus": 0, + "gap": 4 + }, + "endBinding": { + "elementId": "ace4cf026203c4eaee91", + "focus": 0, + "gap": 4 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "e54591893d34f5629a41", + "type": "arrow", + "x": 738.0, + "y": 90.0, + "width": 60.0, + "height": 0.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 + ], + [ + 60.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "ace4cf026203c4eaee91", + "focus": 0, + "gap": 4 + }, + "endBinding": { + "elementId": "13898dc253d5f16722eb", + "focus": 0, + "gap": 4 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "8e09d939dd8c57d14d8b", + "type": "text", + "x": 0.0, + "y": 180.0, + "width": 776.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": "The first gate costs one stat call. The second one has already run the extension's code.", + "originalText": "The first gate costs one stat call. The second one has already run the extension's code.", + "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/r07-the-stable-abi/diagrams/two-gates.svg b/lessons/r07-the-stable-abi/diagrams/two-gates.svg new file mode 100644 index 0000000..9e2eb96 --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/two-gates.svg @@ -0,0 +1 @@ +What an extension has to get past before it is a modulea file turns upmod.abi3t.soread the namea tag I take?open and run itdlopen, then initcheck the structnumbers agree?The first gate costs one stat call. The second one has already run the extension's code. diff --git a/lessons/r07-the-stable-abi/diagrams/what-the-name-says.excalidraw b/lessons/r07-the-stable-abi/diagrams/what-the-name-says.excalidraw new file mode 100644 index 0000000..135117b --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/what-the-name-says.excalidraw @@ -0,0 +1,1079 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "274ff220ee35f271f5c9", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 732.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": "Everything the interpreter knows before it opens the file", + "originalText": "Everything the interpreter knows before it opens the file", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "a8fcce68cfad4b55a380", + "type": "text", + "x": 0.0, + "y": 50.0, + "width": 372.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, + "customData": { + "textLength": 372.0 + }, + "text": "mymodule.cpython-315t-darwin.so", + "originalText": "mymodule.cpython-315t-darwin.so", + "fontSize": 20, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "e81da773b4c6bc54afc2", + "type": "line", + "x": 0.0, + "y": 81.0, + "width": 126.56, + "height": 72.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 40, + "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 + ], + [ + 96.0, + 0.0 + ], + [ + 126.56, + 72.0 + ], + [ + 0.0, + 72.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "8d4234ea4a9d0caeb9c8", + "type": "line", + "x": 108.0, + "y": 81.0, + "width": 172.32, + "height": 72.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 40, + "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 + ], + [ + 84.0, + 0.0 + ], + [ + 172.32, + 72.0 + ], + [ + 26.560000000000002, + 72.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "3f13e93cfd2d9a820ff3", + "type": "line", + "x": 204.0, + "y": 81.0, + "width": 151.12, + "height": 72.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#96f2d7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 40, + "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 + ], + [ + 36.0, + 0.0 + ], + [ + 151.12, + 72.0 + ], + [ + 84.32, + 72.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "abcbaf2dafd3edead142", + "type": "line", + "x": 240.0, + "y": 81.0, + "width": 209.12, + "height": 72.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#ffd8a8", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 40, + "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 + ], + [ + 12.0, + 0.0 + ], + [ + 209.12, + 72.0 + ], + [ + 123.12, + 72.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "36d5c05fd884f446515c", + "type": "line", + "x": 264.0, + "y": 81.0, + "width": 328.0799999999999, + "height": 72.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 40, + "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 + ], + [ + 72.0, + 0.0 + ], + [ + 328.0799999999999, + 72.0 + ], + [ + 193.12, + 72.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "47072ef7c928701736c6", + "type": "line", + "x": 348.0, + "y": 81.0, + "width": 400.4799999999999, + "height": 72.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 40, + "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 + ], + [ + 24.0, + 0.0 + ], + [ + 400.4799999999999, + 72.0 + ], + [ + 252.07999999999993, + 72.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "caf74883495f63029f31", + "type": "line", + "x": 0.0, + "y": 81.0, + "width": 96.0, + "height": 0.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, + "points": [ + [ + 0.0, + 0.0 + ], + [ + 96.0, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "de2e647e67aaab63015c", + "type": "line", + "x": 108.0, + "y": 81.0, + "width": 84.0, + "height": 0.0, + "angle": 0, + "strokeColor": "#6741d9", + "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 + ], + [ + 84.0, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "558ca27cbc534d0c84bb", + "type": "line", + "x": 204.0, + "y": 81.0, + "width": 36.0, + "height": 0.0, + "angle": 0, + "strokeColor": "#099268", + "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 + ], + [ + 36.0, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "5a80d50f9288047ba5fd", + "type": "line", + "x": 240.0, + "y": 81.0, + "width": 12.0, + "height": 0.0, + "angle": 0, + "strokeColor": "#e8590c", + "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 + ], + [ + 12.0, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "318fa9515698670def1d", + "type": "line", + "x": 264.0, + "y": 81.0, + "width": 72.0, + "height": 0.0, + "angle": 0, + "strokeColor": "#e03131", + "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 + ], + [ + 72.0, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "93f057b95047b198a921", + "type": "line", + "x": 348.0, + "y": 81.0, + "width": 24.0, + "height": 0.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, + "points": [ + [ + 0.0, + 0.0 + ], + [ + 24.0, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "fc88701c0db584fe5923", + "type": "rectangle", + "x": 0.0, + "y": 153.0, + "width": 126.56, + "height": 52.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": "0a92cbf2fdcb68681d52", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "0a92cbf2fdcb68681d52", + "type": "text", + "x": 16.0, + "y": 169.0, + "width": 94.56, + "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": "the module", + "originalText": "the module", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "fc88701c0db584fe5923", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "38af278cb0f5a4f6f842", + "type": "rectangle", + "x": 134.56, + "y": 153.0, + "width": 145.76, + "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": "7ba71dde7bebe2cd4293", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "7ba71dde7bebe2cd4293", + "type": "text", + "x": 150.56, + "y": 169.0, + "width": 113.75999999999999, + "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": "which Python", + "originalText": "which Python", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "38af278cb0f5a4f6f842", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "8594e483f2ab3b5ff8a8", + "type": "rectangle", + "x": 288.32, + "y": 153.0, + "width": 66.8, + "height": 52.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": "0a33ba358bee1c7c6b8a", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "0a33ba358bee1c7c6b8a", + "type": "text", + "x": 304.32, + "y": 169.0, + "width": 34.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": "3.15", + "originalText": "3.15", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "8594e483f2ab3b5ff8a8", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "2ed9fc9e8df03e69813e", + "type": "rectangle", + "x": 363.12, + "y": 153.0, + "width": 86.0, + "height": 52.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": "d33fa42e2117ee24cb04", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "d33fa42e2117ee24cb04", + "type": "text", + "x": 379.12, + "y": 169.0, + "width": 54.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, + "text": "no GIL", + "originalText": "no GIL", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "2ed9fc9e8df03e69813e", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "ad5a11694b9a74330dbb", + "type": "rectangle", + "x": 457.12, + "y": 153.0, + "width": 134.95999999999998, + "height": 72.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": "914b5f9438e37f3af7b9", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "914b5f9438e37f3af7b9", + "type": "text", + "x": 473.12, + "y": 169.0, + "width": 102.95999999999998, + "height": 40.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": "the\nplatform", + "originalText": "the\nplatform", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ad5a11694b9a74330dbb", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "debd9313c21c618a270d", + "type": "rectangle", + "x": 600.0799999999999, + "y": 153.0, + "width": 148.39999999999998, + "height": 72.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": "1b785bd949785605258d", + "type": "text" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "1b785bd949785605258d", + "type": "text", + "x": 616.0799999999999, + "y": 169.0, + "width": 116.39999999999998, + "height": 40.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": "shared\nlibrary", + "originalText": "shared\nlibrary", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "debd9313c21c618a270d", + "lineHeight": 1.25, + "autoResize": false + }, + { + "id": "d6b98ecf647d262a253e", + "type": "text", + "x": 0.0, + "y": 245.0, + "width": 653.52, + "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": "Change any one of the middle four and this build will not even look at it.", + "originalText": "Change any one of the middle four and this build will not even look at it.", + "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/r07-the-stable-abi/diagrams/what-the-name-says.svg b/lessons/r07-the-stable-abi/diagrams/what-the-name-says.svg new file mode 100644 index 0000000..227375c --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/what-the-name-says.svg @@ -0,0 +1 @@ +Everything the interpreter knows before it opens the filemymodule.cpython-315t-darwin.sothe modulewhich Python3.15no GILtheplatformsharedlibraryChange any one of the middle four and this build will not even look at it. diff --git a/lessons/r07-the-stable-abi/diagrams/when-it-arrived.excalidraw b/lessons/r07-the-stable-abi/diagrams/when-it-arrived.excalidraw new file mode 100644 index 0000000..602445d --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/when-it-arrived.excalidraw @@ -0,0 +1,1711 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "03c77c03be143194c3d7", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 779.04, + "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": "When each of the 173 gated functions joined the stable ABI", + "originalText": "When each of the 173 gated functions joined the stable ABI", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "e867395781d0f08d66ae", + "type": "text", + "x": 0.0, + "y": 57.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": "3.3", + "originalText": "3.3", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "2474f640f8bc03970222", + "type": "line", + "x": 58.4, + "y": 50.0, + "width": 455.0, + "height": 34.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 455.0, + 0.0 + ], + [ + 455.0, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "f6710434cca2df68fd4d", + "type": "text", + "x": 523.4, + "y": 57.0, + "width": 115.19999999999999, + "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": "26 functions", + "originalText": "26 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "80fa91f6ee300a9bc208", + "type": "text", + "x": 0.0, + "y": 101.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": "3.4", + "originalText": "3.4", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "c7a641e68a1b14a56e1c", + "type": "line", + "x": 58.4, + "y": 94.0, + "width": 52.50000000000001, + "height": 34.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 52.50000000000001, + 0.0 + ], + [ + 52.50000000000001, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "096c115e58474077e599", + "type": "text", + "x": 120.9, + "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": "3 functions", + "originalText": "3 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "02b6eb61b873ccc57516", + "type": "text", + "x": 0.0, + "y": 145.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": "3.5", + "originalText": "3.5", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "4fdd61837cfb6f878de5", + "type": "line", + "x": 58.4, + "y": 138.0, + "width": 227.49999999999997, + "height": 34.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 227.49999999999997, + 0.0 + ], + [ + 227.49999999999997, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "3c09d69e7a9f4f1ac88a", + "type": "text", + "x": 295.9, + "y": 145.0, + "width": 115.19999999999999, + "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": "13 functions", + "originalText": "13 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "ea38cffdc0ac01c1bdc6", + "type": "text", + "x": 0.0, + "y": 189.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": "3.6", + "originalText": "3.6", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "4875f333ed2112abbfd8", + "type": "line", + "x": 58.4, + "y": 182.0, + "width": 70.0, + "height": 34.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 70.0, + 0.0 + ], + [ + 70.0, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "b4952fa36b28e8409226", + "type": "text", + "x": 138.4, + "y": 189.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": "4 functions", + "originalText": "4 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "4c2f26f79211671f0bd6", + "type": "text", + "x": 0.0, + "y": 233.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": "3.7", + "originalText": "3.7", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "37b8ab31208727f3cd35", + "type": "line", + "x": 58.4, + "y": 226.0, + "width": 209.99999999999997, + "height": 34.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 209.99999999999997, + 0.0 + ], + [ + 209.99999999999997, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "25c0b071923b65ab43ca", + "type": "text", + "x": 278.4, + "y": 233.0, + "width": 115.19999999999999, + "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": "12 functions", + "originalText": "12 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "f4eb96c94572e6aa1239", + "type": "text", + "x": 0.0, + "y": 277.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": "3.8", + "originalText": "3.8", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "41b1a750df4975bde5ad", + "type": "line", + "x": 58.4, + "y": 270.0, + "width": 17.500000000000007, + "height": 34.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 17.500000000000007, + 0.0 + ], + [ + 17.500000000000007, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "85dc8b81afd02591e612", + "type": "text", + "x": 85.9, + "y": 277.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": "1 functions", + "originalText": "1 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "6156c6b7cd77ffb33c3c", + "type": "text", + "x": 0.0, + "y": 321.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": "3.9", + "originalText": "3.9", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "f377935b83dd2879174e", + "type": "line", + "x": 58.4, + "y": 314.0, + "width": 175.0, + "height": 34.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 175.0, + 0.0 + ], + [ + 175.0, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "9dc39c9a3d6fde5068c1", + "type": "text", + "x": 243.4, + "y": 321.0, + "width": 115.19999999999999, + "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": "10 functions", + "originalText": "10 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "1875c7109ad78b01a255", + "type": "text", + "x": 0.0, + "y": 365.0, + "width": 38.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": "3.10", + "originalText": "3.10", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "ea722ba617c3fb8793d1", + "type": "line", + "x": 58.4, + "y": 358.0, + "width": 87.5, + "height": 34.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 87.5, + 0.0 + ], + [ + 87.5, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "d03fd2acbfd4c4723679", + "type": "text", + "x": 155.9, + "y": 365.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": "5 functions", + "originalText": "5 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "c9fbf1298c42c36ec3bf", + "type": "text", + "x": 0.0, + "y": 409.0, + "width": 38.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": "3.11", + "originalText": "3.11", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "7c95053ac3bd13df3ebe", + "type": "line", + "x": 58.4, + "y": 402.0, + "width": 280.0, + "height": 34.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 280.0, + 0.0 + ], + [ + 280.0, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "f400e6194779ca3c0cbb", + "type": "text", + "x": 348.4, + "y": 409.0, + "width": 115.19999999999999, + "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": "16 functions", + "originalText": "16 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "34d4f91570bad435219c", + "type": "text", + "x": 0.0, + "y": 453.0, + "width": 38.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": "3.12", + "originalText": "3.12", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "910e9d1a484a2e359293", + "type": "line", + "x": 58.4, + "y": 446.0, + "width": 105.0, + "height": 34.0, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 105.0, + 0.0 + ], + [ + 105.0, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "b07ad96cdb5aa12ba4c4", + "type": "text", + "x": 173.4, + "y": 453.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": "6 functions", + "originalText": "6 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "cc263c55ee13b729a561", + "type": "text", + "x": 0.0, + "y": 497.0, + "width": 38.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": "3.13", + "originalText": "3.13", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "173803f268b4a51ab76c", + "type": "line", + "x": 58.4, + "y": 490.0, + "width": 472.5, + "height": 34.0, + "angle": 0, + "strokeColor": "#e8590c", + "backgroundColor": "#ffd8a8", + "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 + ], + [ + 472.5, + 0.0 + ], + [ + 472.5, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "3b240f429b128aa211d2", + "type": "text", + "x": 540.9, + "y": 497.0, + "width": 115.19999999999999, + "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": "27 functions", + "originalText": "27 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "5e015ae264b535b6571a", + "type": "text", + "x": 0.0, + "y": 541.0, + "width": 38.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": "3.14", + "originalText": "3.14", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "54157d078c4b2c39a4fa", + "type": "line", + "x": 58.4, + "y": 534.0, + "width": 315.0, + "height": 34.0, + "angle": 0, + "strokeColor": "#e8590c", + "backgroundColor": "#ffd8a8", + "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 + ], + [ + 315.0, + 0.0 + ], + [ + 315.0, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "0219f0eec80d254c3bc4", + "type": "text", + "x": 383.4, + "y": 541.0, + "width": 115.19999999999999, + "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": "18 functions", + "originalText": "18 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "252972d5130709acdcff", + "type": "text", + "x": 0.0, + "y": 585.0, + "width": 38.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": "3.15", + "originalText": "3.15", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "f73feb8a92a712981ada", + "type": "line", + "x": 58.4, + "y": 578.0, + "width": 560.0, + "height": 34.0, + "angle": 0, + "strokeColor": "#e8590c", + "backgroundColor": "#ffd8a8", + "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 + ], + [ + 560.0, + 0.0 + ], + [ + 560.0, + 34.0 + ], + [ + 0.0, + 34.0 + ], + [ + 0.0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "12d27785253e759fedc9", + "type": "text", + "x": 628.4, + "y": 585.0, + "width": 115.19999999999999, + "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": "32 functions", + "originalText": "32 functions", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "c8480cb9977c065509b2", + "type": "text", + "x": 0.0, + "y": 632.0, + "width": 756.96, + "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 528 that were there in 3.2 are not on this chart. These are the ones added since.", + "originalText": "The 528 that were there in 3.2 are not on this chart. These are the ones added since.", + "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/r07-the-stable-abi/diagrams/when-it-arrived.svg b/lessons/r07-the-stable-abi/diagrams/when-it-arrived.svg new file mode 100644 index 0000000..87a6acc --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/when-it-arrived.svg @@ -0,0 +1 @@ +When each of the 173 gated functions joined the stable ABI3.326 functions3.43 functions3.513 functions3.64 functions3.712 functions3.81 functions3.910 functions3.105 functions3.1116 functions3.126 functions3.1327 functions3.1418 functions3.1532 functionsThe 528 that were there in 3.2 are not on this chart. These are the ones added since. diff --git a/lessons/r07-the-stable-abi/diagrams/which-names-load.excalidraw b/lessons/r07-the-stable-abi/diagrams/which-names-load.excalidraw new file mode 100644 index 0000000..c19045b --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/which-names-load.excalidraw @@ -0,0 +1,1694 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "74f516504c18b5c2c7cc", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 655.92, + "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": "Which file names each build is willing to look at", + "originalText": "Which file names each build is willing to look at", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "93b6f638cf2afa585b91", + "type": "text", + "x": 14.0, + "y": 50.0, + "width": 147.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": "the file is called", + "originalText": "the file is called", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "2363a8236a260c833853", + "type": "text", + "x": 291.6, + "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": "3.15 with the GIL", + "originalText": "3.15 with the GIL", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "63f9a3a1b488149403ac", + "type": "text", + "x": 482.8, + "y": 50.0, + "width": 154.8, + "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": "3.15 free threaded", + "originalText": "3.15 free threaded", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "815f11689e23943c0219", + "type": "text", + "x": 683.6, + "y": 50.0, + "width": 34.8, + "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": "3.14", + "originalText": "3.14", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "86d9d50182a90d95fefb", + "type": "line", + "x": 0.0, + "y": 76.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "fc8bb1d0f986cab00fb4", + "type": "line", + "x": 0.0, + "y": 76.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ], + [ + 793.6, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "2e2900c3c932212b6e3c", + "type": "text", + "x": 14.0, + "y": 86.0, + "width": 240.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": "mod.cpython-315-darwin.so", + "originalText": "mod.cpython-315-darwin.so", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "16944b9aaffc40194a44", + "type": "text", + "x": 291.6, + "y": 86.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": "considered", + "originalText": "considered", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "bc9fbb2562ccde4c8863", + "type": "text", + "x": 482.8, + "y": 86.0, + "width": 86.39999999999999, + "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": "invisible", + "originalText": "invisible", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "93a2e6851b669b4ab1f2", + "type": "text", + "x": 683.6, + "y": 86.0, + "width": 86.39999999999999, + "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": "invisible", + "originalText": "invisible", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "0f0d1aff6c1bc324bf9d", + "type": "line", + "x": 0.0, + "y": 116.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "c63e51022099b2e9d93c", + "type": "line", + "x": 0.0, + "y": 116.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ], + [ + 793.6, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "2fd94016c3453778c070", + "type": "text", + "x": 14.0, + "y": 126.0, + "width": 249.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": "mod.cpython-315t-darwin.so", + "originalText": "mod.cpython-315t-darwin.so", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "9a12f7038d0948b933ac", + "type": "text", + "x": 291.6, + "y": 126.0, + "width": 86.39999999999999, + "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": "invisible", + "originalText": "invisible", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "b782dc8f7f326c3efece", + "type": "text", + "x": 482.8, + "y": 126.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": "considered", + "originalText": "considered", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "360f975b4ffc64f20f02", + "type": "text", + "x": 683.6, + "y": 126.0, + "width": 86.39999999999999, + "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": "invisible", + "originalText": "invisible", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "9fcb204dcbe709fc9346", + "type": "line", + "x": 0.0, + "y": 156.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "d18ea695fbf7d840a6c6", + "type": "line", + "x": 0.0, + "y": 156.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ], + [ + 793.6, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "e37b902f45eb2adf987d", + "type": "text", + "x": 14.0, + "y": 166.0, + "width": 240.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": "mod.cpython-314-darwin.so", + "originalText": "mod.cpython-314-darwin.so", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "060000799d96d5774b6e", + "type": "text", + "x": 291.6, + "y": 166.0, + "width": 86.39999999999999, + "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": "invisible", + "originalText": "invisible", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "0526398f3b1fa9adb90e", + "type": "text", + "x": 482.8, + "y": 166.0, + "width": 86.39999999999999, + "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": "invisible", + "originalText": "invisible", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "43641c97948c60c650c2", + "type": "text", + "x": 683.6, + "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": "considered", + "originalText": "considered", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "95f5b4a3b72984cd3c18", + "type": "line", + "x": 0.0, + "y": 196.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "b945719c04446461e291", + "type": "line", + "x": 0.0, + "y": 196.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ], + [ + 793.6, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "b392fe81fc65274fec64", + "type": "text", + "x": 14.0, + "y": 206.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": "mod.abi3.so", + "originalText": "mod.abi3.so", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "9069385fd16e81598ee7", + "type": "text", + "x": 291.6, + "y": 206.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": "considered", + "originalText": "considered", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "f055378b5598a881627b", + "type": "text", + "x": 482.8, + "y": 206.0, + "width": 86.39999999999999, + "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": "invisible", + "originalText": "invisible", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "25a1cec1abe19176bc34", + "type": "text", + "x": 683.6, + "y": 206.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": "considered", + "originalText": "considered", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "5d8cd6b00d44f043c101", + "type": "line", + "x": 0.0, + "y": 236.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "a0dede252580ab170ae1", + "type": "line", + "x": 0.0, + "y": 236.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ], + [ + 793.6, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "052377021eb681348945", + "type": "text", + "x": 14.0, + "y": 246.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": "mod.abi3t.so", + "originalText": "mod.abi3t.so", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "714c21860a2668df15f7", + "type": "text", + "x": 291.6, + "y": 246.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": "considered", + "originalText": "considered", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "e65ce02715d9499d5018", + "type": "text", + "x": 482.8, + "y": 246.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": "considered", + "originalText": "considered", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "51ba15ca8e5411b1bdfe", + "type": "text", + "x": 683.6, + "y": 246.0, + "width": 86.39999999999999, + "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": "invisible", + "originalText": "invisible", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "5550fc1707270eb5c80e", + "type": "line", + "x": 0.0, + "y": 276.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "b2afb69abf8b688f69b1", + "type": "line", + "x": 0.0, + "y": 276.0, + "width": 793.6, + "height": 40.0, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9ecef", + "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 + ], + [ + 793.6, + 0.0 + ], + [ + 793.6, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "adbdbee8a5dbb75f4f35", + "type": "text", + "x": 14.0, + "y": 286.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": "mod.so", + "originalText": "mod.so", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "6445d7e7259ac874fedc", + "type": "text", + "x": 291.6, + "y": 286.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": "considered", + "originalText": "considered", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "fdff74bd5b0dc0494818", + "type": "text", + "x": 482.8, + "y": 286.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": "considered", + "originalText": "considered", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "95739af393b2512c51ec", + "type": "text", + "x": 683.6, + "y": 286.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": "considered", + "originalText": "considered", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "4c09b9fa613aac1b296f", + "type": "line", + "x": 0.0, + "y": 316.0, + "width": 793.6, + "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 + ], + [ + 793.6, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "662242a79ccc918f3d01", + "type": "text", + "x": 0.0, + "y": 336.0, + "width": 723.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": "abi3t is the only tag both 3.15 builds accept, which is why it is the one to ship.", + "originalText": "abi3t is the only tag both 3.15 builds accept, which is why it is the one to ship.", + "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/r07-the-stable-abi/diagrams/which-names-load.svg b/lessons/r07-the-stable-abi/diagrams/which-names-load.svg new file mode 100644 index 0000000..9535480 --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/which-names-load.svg @@ -0,0 +1 @@ +Which file names each build is willing to look atthe file is called3.15 with the GIL3.15 free threaded3.14mod.cpython-315-darwin.soconsideredinvisibleinvisiblemod.cpython-315t-darwin.soinvisibleconsideredinvisiblemod.cpython-314-darwin.soinvisibleinvisibleconsideredmod.abi3.soconsideredinvisibleconsideredmod.abi3t.soconsideredconsideredinvisiblemod.soconsideredconsideredconsideredabi3t is the only tag both 3.15 builds accept, which is why it is the one to ship. diff --git a/lessons/r07-the-stable-abi/diagrams/who-gets-refused.excalidraw b/lessons/r07-the-stable-abi/diagrams/who-gets-refused.excalidraw new file mode 100644 index 0000000..9c3175e --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/who-gets-refused.excalidraw @@ -0,0 +1,1835 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://github.com/tamnd/cpython-internals", + "elements": [ + { + "id": "287580084be2b45a7f29", + "type": "text", + "x": 0.0, + "y": 0.0, + "width": 694.4399999999999, + "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 PyABIInfo_Check says to each kind of extension", + "originalText": "What PyABIInfo_Check says to each kind of extension", + "fontSize": 24, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "ea8154fe7917fabcc33d", + "type": "text", + "x": 14.0, + "y": 50.0, + "width": 229.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": "what the extension claims", + "originalText": "what the extension claims", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "29d4926dcfffda04335c", + "type": "text", + "x": 339.59999999999997, + "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": "3.15 with the GIL", + "originalText": "3.15 with the GIL", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "2feca2ffebdf72bc6567", + "type": "text", + "x": 694.0, + "y": 50.0, + "width": 154.8, + "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": "3.15 free threaded", + "originalText": "3.15 free threaded", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "46cf5d4161aa87fe35a1", + "type": "line", + "x": 0.0, + "y": 76.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "5b203115de48911cb23b", + "type": "line", + "x": 0.0, + "y": 76.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ], + [ + 1005.5999999999999, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "fa45329d74ebbfb489dd", + "type": "text", + "x": 14.0, + "y": 86.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": "built for this exact version", + "originalText": "built for this exact version", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "4b8e45e2d6f7ca587d69", + "type": "text", + "x": 339.59999999999997, + "y": 86.0, + "width": 48.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": "loads", + "originalText": "loads", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "af5af47638f5bf31cace", + "type": "text", + "x": 694.0, + "y": 86.0, + "width": 297.59999999999997, + "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": "incompatible with free threaded", + "originalText": "incompatible with free threaded", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "678c29b40aa7cace80cf", + "type": "line", + "x": 0.0, + "y": 116.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "01186802d1e5c032380d", + "type": "line", + "x": 0.0, + "y": 116.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ], + [ + 1005.5999999999999, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "18fa850348a275507040", + "type": "text", + "x": 14.0, + "y": 126.0, + "width": 297.59999999999997, + "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": "built for 3.13 and nothing else", + "originalText": "built for 3.13 and nothing else", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "da5be543a2a025db5d37", + "type": "text", + "x": 339.59999999999997, + "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": "incompatible ABI version", + "originalText": "incompatible ABI version", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "ce74723080cfe3df9734", + "type": "text", + "x": 694.0, + "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": "incompatible ABI version", + "originalText": "incompatible ABI version", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "4a73a1d01a44821d7fdb", + "type": "line", + "x": 0.0, + "y": 156.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "d40a77b17ffcc7be2b65", + "type": "line", + "x": 0.0, + "y": 156.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ], + [ + 1005.5999999999999, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "38babc73fa05fb6f63f5", + "type": "text", + "x": 14.0, + "y": 166.0, + "width": 192.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": "stable abi since 3.2", + "originalText": "stable abi since 3.2", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "29bd22f62e41585036c2", + "type": "text", + "x": 339.59999999999997, + "y": 166.0, + "width": 48.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": "loads", + "originalText": "loads", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "5c4170f8c3fe47dae618", + "type": "text", + "x": 694.0, + "y": 166.0, + "width": 297.59999999999997, + "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": "incompatible with free threaded", + "originalText": "incompatible with free threaded", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "74c76b0aa8b8bd00bda8", + "type": "line", + "x": 0.0, + "y": 196.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "5a7a0ac96935f48abb67", + "type": "line", + "x": 0.0, + "y": 196.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ], + [ + 1005.5999999999999, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "680d072bd9233c696ee9", + "type": "text", + "x": 14.0, + "y": 206.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": "stable abi since 3.14", + "originalText": "stable abi since 3.14", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "bd86e0e270c949d25194", + "type": "text", + "x": 339.59999999999997, + "y": 206.0, + "width": 48.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": "loads", + "originalText": "loads", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "f66c03a730c2da9adfe2", + "type": "text", + "x": 694.0, + "y": 206.0, + "width": 297.59999999999997, + "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": "incompatible with free threaded", + "originalText": "incompatible with free threaded", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "9172afe343286910c4a7", + "type": "line", + "x": 0.0, + "y": 236.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "880090bc5dc24e54ce94", + "type": "line", + "x": 0.0, + "y": 236.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ], + [ + 1005.5999999999999, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "fd8021d8c6be51d07dcf", + "type": "text", + "x": 14.0, + "y": 246.0, + "width": 249.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": "stable abi from the future", + "originalText": "stable abi from the future", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "3010bdfea3bf56e85d22", + "type": "text", + "x": 339.59999999999997, + "y": 246.0, + "width": 259.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": "incompatible future version", + "originalText": "incompatible future version", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "6513347da3d09e2f6af8", + "type": "text", + "x": 694.0, + "y": 246.0, + "width": 259.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": "incompatible future version", + "originalText": "incompatible future version", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "b9d7e5d7f84db6f84675", + "type": "line", + "x": 0.0, + "y": 276.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "78562cef534aabb406ad", + "type": "line", + "x": 0.0, + "y": 276.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ], + [ + 1005.5999999999999, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "b76285b758c22d805b09", + "type": "text", + "x": 14.0, + "y": 286.0, + "width": 172.79999999999998, + "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": "free threaded only", + "originalText": "free threaded only", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "55922791c3b1249578b4", + "type": "text", + "x": 339.59999999999997, + "y": 286.0, + "width": 326.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": "only compatible with free threaded", + "originalText": "only compatible with free threaded", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "ccd0e75597dd7bb1bddc", + "type": "text", + "x": 694.0, + "y": 286.0, + "width": 48.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": "loads", + "originalText": "loads", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "c26b51cf7797a99c63e4", + "type": "line", + "x": 0.0, + "y": 316.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "f774425d1d40d04fff7e", + "type": "line", + "x": 0.0, + "y": 316.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ], + [ + 1005.5999999999999, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "c5960798ff3156ce4c54", + "type": "text", + "x": 14.0, + "y": 326.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": "happy either way", + "originalText": "happy either way", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "44edafa19fcf19fca181", + "type": "text", + "x": 339.59999999999997, + "y": 326.0, + "width": 48.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": "loads", + "originalText": "loads", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "71497c00c52c857d6c82", + "type": "text", + "x": 694.0, + "y": 326.0, + "width": 48.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": "loads", + "originalText": "loads", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "0398527a070919ed5eaf", + "type": "line", + "x": 0.0, + "y": 356.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "986985c319e0b84125f2", + "type": "line", + "x": 0.0, + "y": 356.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ], + [ + 1005.5999999999999, + 40.0 + ], + [ + 0, + 40.0 + ], + [ + 0, + 0.0 + ] + ], + "lastCommittedPoint": null, + "polygon": true + }, + { + "id": "62e0030befa3b936f78a", + "type": "text", + "x": 14.0, + "y": 366.0, + "width": 249.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": "claims internal and stable", + "originalText": "claims internal and stable", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "dd377cf029f06ec4a410", + "type": "text", + "x": 339.59999999999997, + "y": 366.0, + "width": 144.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": "cannot use both", + "originalText": "cannot use both", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "2102f722b79de15b8558", + "type": "text", + "x": 694.0, + "y": 366.0, + "width": 144.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": "cannot use both", + "originalText": "cannot use both", + "fontSize": 16, + "fontFamily": 3, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "lineHeight": 1.25, + "autoResize": true + }, + { + "id": "1264ac2ea2b37b874538", + "type": "line", + "x": 0.0, + "y": 396.0, + "width": 1005.5999999999999, + "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 + ], + [ + 1005.5999999999999, + 0.0 + ] + ], + "lastCommittedPoint": null + }, + { + "id": "04128b17f7835e89479e", + "type": "text", + "x": 0.0, + "y": 416.0, + "width": 757.6800000000001, + "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 row loads on both builds, and it is the row that promises nothing about the lock.", + "originalText": "One row loads on both builds, and it is the row that promises nothing about the lock.", + "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/r07-the-stable-abi/diagrams/who-gets-refused.svg b/lessons/r07-the-stable-abi/diagrams/who-gets-refused.svg new file mode 100644 index 0000000..bc0f1a2 --- /dev/null +++ b/lessons/r07-the-stable-abi/diagrams/who-gets-refused.svg @@ -0,0 +1 @@ +What PyABIInfo_Check says to each kind of extensionwhat the extension claims3.15 with the GIL3.15 free threadedbuilt for this exact versionloadsincompatible with free threadedbuilt for 3.13 and nothing elseincompatible ABI versionincompatible ABI versionstable abi since 3.2loadsincompatible with free threadedstable abi since 3.14loadsincompatible with free threadedstable abi from the futureincompatible future versionincompatible future versionfree threaded onlyonly compatible with free threadedloadshappy either wayloadsloadsclaims internal and stablecannot use bothcannot use bothOne row loads on both builds, and it is the row that promises nothing about the lock. diff --git a/lessons/r07-the-stable-abi/r07.ipynb b/lessons/r07-the-stable-abi/r07.ipynb new file mode 100644 index 0000000..07f3e71 --- /dev/null +++ b/lessons/r07-the-stable-abi/r07.ipynb @@ -0,0 +1,1048 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "r07-01", + "metadata": {}, + "source": [ + "# R07. The stable ABI\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/r07-the-stable-abi/r07.ipynb)\n", + "\n", + "R06 sorted out which C functions you are allowed to call. This one picks up where that leaves off, when the thing you compiled turns up on somebody else's machine as a file with a long name.\n", + "\n", + "There are two gates between that file and a working module. The first is the name itself, which the interpreter reads before it opens anything. The second is a small struct the extension carries, which is new in 3.15 and which gets checked after the extension's own code has already run.\n", + "\n", + "Both of them can be driven from Python, which is the nice part.\n", + "\n", + "![a four stage pipeline, a file turning up, the name being read, the file being opened and run, and the struct being checked](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r07-the-stable-abi/diagrams/two-gates.svg)" + ] + }, + { + "cell_type": "markdown", + "id": "r07-02", + "metadata": {}, + "source": [ + "## About the source references\n", + "\n", + "Now and then this lesson points at CPython's own source, like this: `Include/patchlevel.h:53-62@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": "r07-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": "r07-04", + "metadata": {}, + "source": [ + "## Which Python is this\n", + "\n", + "Most of this lesson runs anywhere, because file names and `ctypes` work everywhere. Two things do not. The second gate arrived in 3.15, so on 3.14 those cells say what it would have answered. And two cells read the header files, which a normal install has and a browser tab does not.\n", + "\n", + "## Which interpreter is this" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r07-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": "r07-06", + "metadata": {}, + "source": [ + "## The name is the first gate\n", + "\n", + "An [a module that arrives as a compiled shared library rather than as Python](https://github.com/tamnd/cpython-internals/blob/main/GLOSSARY.md#extension-module) is a shared library with a long name. The long name is not decoration. It is the only thing the interpreter looks at before deciding whether the file is worth opening.\n", + "\n", + "Start with what this build accepts and what it stamps on its own output.\n", + "\n", + "An interpreter will only look at extension files whose names carry a tag it recognises, and it has a short fixed list of those." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r07-07", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 there are three tags rather than six. Both of the abi3t ones are missing because that tag is new in 3.15, and so is the platform specific abi3 one." + } + }, + "outputs": [], + "source": [ + "import importlib.machinery\n", + "import sysconfig\n", + "\n", + "TAGS = importlib.machinery.EXTENSION_SUFFIXES\n", + "HERE = sysconfig.get_config_var(\"EXT_SUFFIX\")\n", + "\n", + "print(\" file names this build is willing to load an extension from:\")\n", + "for tag in TAGS:\n", + " print(\" \", tag)\n", + "print()\n", + "print(\" and the one it stamps on what it builds:\", HERE)\n", + "for key in (\"SOABI\", \"ABIFLAGS\", \"Py_GIL_DISABLED\"):\n", + " print(f\" {key:16} {sysconfig.get_config_var(key)!r}\")\n", + "print(f\" {'sys.abiflags':16} {sys.abiflags!r}\")" + ] + }, + { + "cell_type": "markdown", + "id": "r07-08", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 there are three tags rather than six. Both of the abi3t ones are missing because that tag is new in 3.15, and so is the platform specific abi3 one." + ] + }, + { + "cell_type": "markdown", + "id": "r07-09", + "metadata": {}, + "source": [ + "That list is not built at run time. It is a C array, and you can read the whole thing in [Python/dynload_shlib.c:39-60@v3.15.0rc1#_PyImport_DynLoadFiletab](https://github.com/python/cpython/blob/v3.15.0rc1/Python/dynload_shlib.c#L39-L60). The `#ifndef Py_GIL_DISABLED` in the middle of it is why a free threaded build has fewer entries, and `_imp.extension_suffixes` at [Python/import.c:5175-5204@v3.15.0rc1#_imp_extension_suffixes_impl](https://github.com/python/cpython/blob/v3.15.0rc1/Python/import.c#L5175-L5204) does nothing but copy that array into a list.\n", + "\n", + "The tag is called [the tag a build stamps into the file names of the extensions it compiles](https://github.com/tamnd/cpython-internals/blob/main/GLOSSARY.md#soabi), and it is three facts glued together.\n", + "\n", + "![the file name mymodule.cpython-315t-darwin.so broken into six labelled pieces](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r07-the-stable-abi/diagrams/what-the-name-says.svg)\n", + "\n", + "The version in the file name is the Python version, and the 3 in abi3 is not." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r07-10", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 the version in the tag reads 314 rather than 315. Everything else here is the same, including the 1013 that nobody has touched since 2006." + } + }, + "outputs": [], + "source": [ + "import re\n", + "\n", + "VERSIONED = re.compile(r\"(cpython-)(\\d+)(t?)\")\n", + "\n", + "found = VERSIONED.search(HERE)\n", + "if found:\n", + " print(\" the tag this build writes:\", HERE)\n", + " print(\" which Python: \", found.group(1).rstrip(\"-\"))\n", + " print(\" which version: \", found.group(2))\n", + " print(\" threading: \", \"no lock\" if found.group(3) else \"with the lock\")\n", + " print(\" the rest: \", HERE[found.end() :])\n", + "else:\n", + " print(\" this build puts no version in the name at all:\", HERE)\n", + "print()\n", + "print(\" the 3 in abi3 is not a Python version, it is PYTHON_ABI_VERSION\")\n", + "print(\" and the other number nobody has touched since 2006:\", sys.api_version)" + ] + }, + { + "cell_type": "markdown", + "id": "r07-11", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 the version in the tag reads 314 rather than 315. Everything else here is the same, including the 1013 that nobody has touched since 2006." + ] + }, + { + "cell_type": "markdown", + "id": "r07-12", + "metadata": {}, + "source": [ + "Those two numbers are worth a look, because they are a small piece of history sitting in a header nobody edits. [Include/patchlevel.h:53-62@v3.15.0rc1#PYTHON_ABI_VERSION](https://github.com/python/cpython/blob/v3.15.0rc1/Include/patchlevel.h#L53-L62) defines `PYTHON_API_VERSION 1013` and `PYTHON_ABI_VERSION 3` with a comment saying they have not been updated since 2006 and 2010, and that versioning is tied to the CPython version now.\n", + "\n", + "So the `3` in `abi3` is not \"Python 3\". It is a counter that stopped moving fifteen years ago, and the suffix table builds the string out of it.\n", + "\n", + "## What the finder will even look at\n", + "\n", + "Here is the first gate, run by hand. Put one empty file in a directory, ask for a module by that name, and see whether anything comes back. The file is empty, so nothing gets loaded; the finder only ever stats it.\n", + "\n", + "A file with the wrong version in its name is invisible to the finder, not an error." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r07-13", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 the abi3t file is invisible too, because that tag did not exist yet. The other four answers are the same, with 314 in place of 315." + } + }, + "outputs": [], + "source": [ + "import pathlib\n", + "import tempfile\n", + "\n", + "flipped = VERSIONED.sub(lambda m: m.group(1) + m.group(2) + (\"\" if m.group(3) else \"t\"), HERE)\n", + "future = VERSIONED.sub(lambda m: m.group(1) + \"999\" + m.group(3), HERE)\n", + "CANDIDATES = (\"demo\" + HERE, \"demo\" + flipped, \"demo\" + future, \"demo.abi3.so\", \"demo.abi3t.so\")\n", + "\n", + "\n", + "def considered(name):\n", + " \"\"\"Put one empty file in a directory of its own and ask the finder whether it sees it.\"\"\"\n", + " room = pathlib.Path(tempfile.mkdtemp())\n", + " (room / name).write_bytes(b\"\")\n", + " return importlib.machinery.PathFinder.find_spec(\"demo\", [str(room)]) is not None\n", + "\n", + "\n", + "for name in CANDIDATES:\n", + " print(f\" {name:42} {'considered' if considered(name) else 'invisible, wrong name'}\")" + ] + }, + { + "cell_type": "markdown", + "id": "r07-14", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 the abi3t file is invisible too, because that tag did not exist yet. The other four answers are the same, with 314 in place of 315." + ] + }, + { + "cell_type": "markdown", + "id": "r07-15", + "metadata": {}, + "source": [ + "![a table of six file names against three builds, saying which are considered and which are invisible](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r07-the-stable-abi/diagrams/which-names-load.svg)\n", + "\n", + "Read the two `abi3` rows together and the design shows up. A free threaded build refuses `abi3` and takes `abi3t`. An ordinary 3.15 build takes both. A 3.14 build takes `abi3` and has never heard of `abi3t`. [Doc/c-api/stable.rst:100-105@v3.15.0rc1](https://github.com/python/cpython/blob/v3.15.0rc1/Doc/c-api/stable.rst#L100-L105) states the rule and draws the conclusion for you: ship `abi3t` if you want one file that reaches both 3.15 builds.\n", + "\n", + "When several of them are sitting in the same directory, the order in that C array decides.\n", + "\n", + "When more than one acceptable file is present, the first suffix in the list wins, not the best match." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r07-16", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 the name the finder picks has 314 in it, and the numbered list below has three entries rather than six, for the same reason as the first cell." + } + }, + "outputs": [], + "source": [ + "room = pathlib.Path(tempfile.mkdtemp())\n", + "for name in CANDIDATES:\n", + " (room / name).write_bytes(b\"\")\n", + "(room / \"demo.py\").write_text(\"\")\n", + "picked = importlib.machinery.PathFinder.find_spec(\"demo\", [str(room)])\n", + "print(\" all of them in one directory, and the finder picks:\")\n", + "print(\" \", pathlib.Path(picked.origin).name)\n", + "print(\" loaded by\", type(picked.loader).__name__)\n", + "print()\n", + "print(\" the order is the suffix list, and the first match wins:\")\n", + "for position, tag in enumerate(TAGS, 1):\n", + " print(f\" {position}. {tag}\")" + ] + }, + { + "cell_type": "markdown", + "id": "r07-17", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 the name the finder picks has 314 in it, and the numbered list below has three entries rather than six, for the same reason as the first cell." + ] + }, + { + "cell_type": "markdown", + "id": "r07-18", + "metadata": {}, + "source": [ + "The exact build wins over the stable one, and both win over the plain `demo.py`. That last part is worth remembering, because it means a compiled file quietly shadows a Python file of the same name.\n", + "\n", + "## The second gate\n", + "\n", + "Everything so far cost one stat call. The second gate is much more expensive, because by the time it runs the file has been opened, the dynamic linker has resolved its symbols, and the extension's own initialisation code has started.\n", + "\n", + "[twelve bytes an extension carries saying which ABI it was built against](https://github.com/tamnd/cpython-internals/blob/main/GLOSSARY.md#pyabiinfo) is the struct that gets checked. It is declared at [Include/modsupport.h:86-101@v3.15.0rc1#PyABIInfo](https://github.com/python/cpython/blob/v3.15.0rc1/Include/modsupport.h#L86-L101), and an extension that wants the check writes `PyABIInfo_VAR(abi_info)` and lists it as a slot, which [Include/modsupport.h:135-143@v3.15.0rc1#PyABIInfo_VAR](https://github.com/python/cpython/blob/v3.15.0rc1/Include/modsupport.h#L135-L143) turns into a static struct filled in from whatever macros were set when it was compiled.\n", + "\n", + "![the five fields of PyABIInfo stacked, with the version fields at the bottom](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r07-the-stable-abi/diagrams/the-five-fields.svg)\n", + "\n", + "`PyABIInfo_Check` is a plain function in the public headers, so `ctypes` can call it, and the struct is five integers, so `ctypes` can build one. That is the whole trick of this lesson.\n", + "\n", + "The struct an extension carries is twelve bytes, and Python can build one and hand it to the real check function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r07-19", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 there is no PyABIInfo_Check to find, so this cell says so and the next one skips itself. The struct did not exist before 3.15." + } + }, + "outputs": [], + "source": [ + "import ctypes\n", + "\n", + "api = ctypes.pythonapi\n", + "ABICHECK = hasattr(api, \"PyABIInfo_Check\")\n", + "\n", + "STABLE = 0x0001\n", + "GIL = 0x0002\n", + "FREETHREADED = 0x0004\n", + "INTERNAL = 0x0008\n", + "\n", + "\n", + "class PyABIInfo(ctypes.Structure):\n", + " \"\"\"The five fields an extension built against 3.15 or later carries about itself.\"\"\"\n", + "\n", + " _fields_ = [\n", + " (\"abiinfo_major_version\", ctypes.c_uint8),\n", + " (\"abiinfo_minor_version\", ctypes.c_uint8),\n", + " (\"flags\", ctypes.c_uint16),\n", + " (\"build_version\", ctypes.c_uint32),\n", + " (\"abi_version\", ctypes.c_uint32),\n", + " ]\n", + "\n", + "\n", + "def pack(major, minor):\n", + " \"\"\"The version format the C API uses, two numbers packed into the top two bytes.\"\"\"\n", + " return (major << 24) | (minor << 16)\n", + "\n", + "\n", + "if not ABICHECK:\n", + " print(\" PyABIInfo_Check arrived in 3.15 and this is\", sys.version.split()[0])\n", + " print(\" so the next cell has nothing to run\")\n", + "else:\n", + " print(\" the struct is\", ctypes.sizeof(PyABIInfo), \"bytes, laid out as\")\n", + " for field, kind in PyABIInfo._fields_:\n", + " print(f\" {field:24} {kind.__name__}\")" + ] + }, + { + "cell_type": "markdown", + "id": "r07-20", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 there is no PyABIInfo_Check to find, so this cell says so and the next one skips itself. The struct did not exist before 3.15." + ] + }, + { + "cell_type": "markdown", + "id": "r07-21", + "metadata": {}, + "source": [ + "## Who gets turned away\n", + "\n", + "Now the interesting part. Build one struct per kind of extension somebody might ship, hand each one to the real check function, and print what comes back. Nothing is being loaded here; the function is only comparing numbers.\n", + "\n", + "The check refuses four of these eight, and no two of them for the same reason." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r07-22", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 this prints the one line saying there is nothing to run." + } + }, + "outputs": [], + "source": [ + "CASES = (\n", + " (\"built for this exact version\", GIL, pack(*sys.version_info[:2])),\n", + " (\"built for 3.13 and nothing else\", GIL, pack(3, 13)),\n", + " (\"stable abi since 3.2\", STABLE | GIL, pack(3, 2)),\n", + " (\"stable abi since 3.14\", STABLE | GIL, pack(3, 14)),\n", + " (\"stable abi from the future\", STABLE | GIL, pack(3, 99)),\n", + " (\"free threaded only\", STABLE | FREETHREADED, pack(3, 14)),\n", + " (\"happy either way\", STABLE | GIL | FREETHREADED, pack(3, 14)),\n", + " (\"claims internal and stable\", STABLE | INTERNAL, sys.hexversion),\n", + ")\n", + "\n", + "if ABICHECK:\n", + " check = api.PyABIInfo_Check\n", + " check.argtypes = [ctypes.POINTER(PyABIInfo), ctypes.c_char_p]\n", + " check.restype = ctypes.c_int\n", + " for label, flags, version in CASES:\n", + " info = PyABIInfo(1, 0, flags, sys.hexversion, version)\n", + " try:\n", + " check(ctypes.byref(info), label.encode())\n", + " except ImportError as problem:\n", + " print(f\" {label:32} refused: {str(problem).split(': ', 1)[1]}\")\n", + " else:\n", + " print(f\" {label:32} loads\")\n", + "else:\n", + " print(\" nothing to run here, this interpreter has no PyABIInfo_Check\")" + ] + }, + { + "cell_type": "markdown", + "id": "r07-23", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 this prints the one line saying there is nothing to run." + ] + }, + { + "cell_type": "markdown", + "id": "r07-24", + "metadata": {}, + "source": [ + "![a table of eight hypothetical extensions against two builds, with the verdict in each cell](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r07-the-stable-abi/diagrams/who-gets-refused.svg)\n", + "\n", + "Four different refusals from one function. The version comparisons are at [Python/modsupport.c:731-753@v3.15.0rc1#PyABIInfo_STABLE](https://github.com/python/cpython/blob/v3.15.0rc1/Python/modsupport.c#L731-L753), where a stable extension is allowed to be older than the interpreter but not newer, while a version locked one has to match exactly. The threading check is fifteen lines further down at [Python/modsupport.c:768-782@v3.15.0rc1#Py_GIL_DISABLED](https://github.com/python/cpython/blob/v3.15.0rc1/Python/modsupport.c#L768-L782), and it is symmetrical: a GIL only extension is refused on a free threaded build and a free threaded only one is refused here.\n", + "\n", + "The row that loads on both is the one that claims both flags, which is the same conclusion the file name gave. `abi3t`, and no promise about the lock.\n", + "\n", + "Worth being honest about the limits. This check is opt in, an extension that does not carry the struct is not checked at all, and [Doc/c-api/stable.rst:100-105@v3.15.0rc1](https://github.com/python/cpython/blob/v3.15.0rc1/Doc/c-api/stable.rst#L100-L105) says plainly that Python does not necessarily check that extensions it loads have a compatible ABI. CPython's own bundled modules opt in through a macro at [Include/cpython/modsupport.h:41-45@v3.15.0rc1#_Py_ABI_SLOT](https://github.com/python/cpython/blob/v3.15.0rc1/Include/cpython/modsupport.h#L41-L45), and the slot is read at [Objects/moduleobject.c:471-481@v3.15.0rc1#Py_mod_abi](https://github.com/python/cpython/blob/v3.15.0rc1/Objects/moduleobject.c#L471-L481), one case in the loop that walks a module's slots.\n", + "\n", + "## How the stable ABI grew\n", + "\n", + "The last thing worth measuring is the stable ABI as a list that changes. It is not fixed. Functions get added to it, one release at a time, and the headers record when each one arrived, in the form of the version gate around its declaration.\n", + "\n", + "Every function added to the stable ABI since 3.2 is dated by the preprocessor gate around it, and you can read the dates straight out of the headers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r07-25", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 the total is 140 rather than 173. There is no 3.15 row, and the 3.14 row is 17 rather than 18, because one more function was added to it after 3.14 shipped." + } + }, + "outputs": [], + "source": [ + "INCLUDE = pathlib.Path(sysconfig.get_paths()[\"include\"])\n", + "HEADERS = INCLUDE.is_dir() and any(INCLUDE.glob(\"*.h\"))\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", + "OLD = re.compile(r\"Py_LIMITED_API\\+0\\s*>=\\s*0x03([0-9a-fA-F]{2})0000\")\n", + "NEW = re.compile(r\"Py_LIMITED_API\\+0\\s*>=\\s*_Py_PACK_VERSION\\((\\d+),\\s*(\\d+)\\)\")\n", + "\n", + "\n", + "def gate(line):\n", + " \"\"\"The stable ABI version a preprocessor line gates on, when it gates on one at all.\"\"\"\n", + " hit = NEW.search(line)\n", + " if hit:\n", + " return int(hit.group(2))\n", + " hit = OLD.search(line)\n", + " return int(hit.group(1), 16) if hit else None\n", + "\n", + "\n", + "if not HEADERS:\n", + " print(\" no headers next to this interpreter, looked in\", INCLUDE)\n", + " print(\" so this cell and the next one have nothing to read\")\n", + "else:\n", + " arrived = {}\n", + " for path in sorted(INCLUDE.glob(\"*.h\")):\n", + " stack = []\n", + " for line in path.read_text(errors=\"replace\").splitlines():\n", + " if OPENS.match(line):\n", + " stack.append(gate(line))\n", + " elif CLOSES.match(line) and stack:\n", + " stack.pop()\n", + " hit = DECLARED.match(line)\n", + " if hit and any(v for v in stack):\n", + " arrived[hit.group(1)] = max(v for v in stack if v)\n", + " counted = {}\n", + " for version in arrived.values():\n", + " counted[version] = counted.get(version, 0) + 1\n", + " print(\" public functions the headers put behind a stable abi version gate:\", len(arrived))\n", + " for version in sorted(counted):\n", + " print(f\" 3.{version:<4} {counted[version]:4} {'#' * counted[version]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "r07-26", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 the total is 140 rather than 173. There is no 3.15 row, and the 3.14 row is 17 rather than 18, because one more function was added to it after 3.14 shipped." + ] + }, + { + "cell_type": "markdown", + "id": "r07-27", + "metadata": {}, + "source": [ + "![a bar chart of how many gated functions each release from 3.3 to 3.15 added](https://raw.githubusercontent.com/tamnd/cpython-internals/main/lessons/r07-the-stable-abi/diagrams/when-it-arrived.svg)\n", + "\n", + "Two hundred and change over thirteen years, and the recent releases are the busy ones. The 528 functions that were already in the stable ABI when it was defined in 3.2 are not on this chart at all; they have no gate, because there is no version to gate on.\n", + "\n", + "You can point the same scan at one name and get its gate and the line it sits on.\n", + "\n", + "A function's stable ABI version is readable from the header line it is declared on." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r07-28", + "metadata": { + "cpython_internals": { + "differs": "On 3.14 PyABIInfo_Check is missing from the output entirely, because the declaration is not in those headers yet. The other three are in the same files at the same lines." + } + }, + "outputs": [], + "source": [ + "WANTED = (\"PyDict_GetItemRef\", \"PyLong_AsInt64\", \"PyABIInfo_Check\", \"PyList_Append\")\n", + "\n", + "if HEADERS:\n", + " for path in sorted(INCLUDE.glob(\"*.h\")):\n", + " lines = path.read_text(errors=\"replace\").splitlines()\n", + " stack = []\n", + " for number, line in enumerate(lines, 1):\n", + " if OPENS.match(line):\n", + " stack.append(gate(line))\n", + " elif CLOSES.match(line) and stack:\n", + " stack.pop()\n", + " hit = DECLARED.match(line)\n", + " if hit and hit.group(1) in WANTED:\n", + " live = [v for v in stack if v]\n", + " answer = f\"3.{max(live)} and later\" if live else \"every version since 3.2\"\n", + " print(f\" {hit.group(1):20} {path.name}:{number:<5} {answer}\")" + ] + }, + { + "cell_type": "markdown", + "id": "r07-29", + "metadata": {}, + "source": [ + "> **Version note.** On 3.14 PyABIInfo_Check is missing from the output entirely, because the declaration is not in those headers yet. The other three are in the same files at the same lines." + ] + }, + { + "cell_type": "markdown", + "id": "r07-30", + "metadata": {}, + "source": [ + "`PyList_Append` has no gate, so it has been callable from a limited build since the beginning. `PyABIInfo_Check` is gated on 3.15, which is a slightly funny situation: the function that checks whether your ABI is old enough is itself too new for most of the ABIs it would check.\n", + "\n", + "## Both gates on two builds\n", + "\n", + "Everything above ran on one interpreter. The claim that the two gates move together deserves two, so here is the same work run in a container against a build made from the pinned source.\n", + "\n", + "What does the interpreter check before it agrees to load an extension?\n", + "\n", + "```python\n", + "\"\"\"What a build agrees to load, and the two gates that decide it.\n", + "\n", + "An extension arrives as a file with a long name. The interpreter decides whether to consider\n", + "it at all from that name alone, before it opens it. If it does open it, and the extension was\n", + "built against 3.15 or later, there is a second gate: a five field `PyABIInfo` struct the\n", + "extension carries, which `PyABIInfo_Check` compares against the running interpreter.\n", + "\n", + "This program runs both gates by hand. It asks the finder which file names it is willing to\n", + "look at, by putting empty files in a temporary directory and asking for the module. Then it\n", + "builds `PyABIInfo` structs in ctypes for a row of hypothetical extensions and calls the real\n", + "check function on each one.\n", + "\n", + "The point of running it on two builds is that both gates move. A free threaded build will not\n", + "look at a name with `abi3` in it, and it refuses a struct that says the extension wants the\n", + "global interpreter lock.\n", + "\n", + "One thing is printed differently from how the interpreter spells it. The last part of a file\n", + "name tag is the machine the build was made for, which is `x86_64-linux-gnu` on one runner and\n", + "`aarch64-linux-gnu` on another. That part says nothing about the ABI question, so it is\n", + "replaced with the word `PLATFORM` everywhere below and the recording is the same either way.\n", + "\"\"\"\n", + "\n", + "import ctypes\n", + "import importlib.machinery\n", + "import pathlib\n", + "import re\n", + "import sys\n", + "import sysconfig\n", + "import tempfile\n", + "\n", + "VERSIONED = re.compile(r\"(cpython-)(\\d+)(t?)\")\n", + "\n", + "STABLE = 0x0001\n", + "GIL = 0x0002\n", + "FREETHREADED = 0x0004\n", + "INTERNAL = 0x0008\n", + "\n", + "\n", + "class PyABIInfo(ctypes.Structure):\n", + " \"\"\"The struct an extension built against 3.15 or later carries about itself.\"\"\"\n", + "\n", + " _fields_ = [\n", + " (\"abiinfo_major_version\", ctypes.c_uint8),\n", + " (\"abiinfo_minor_version\", ctypes.c_uint8),\n", + " (\"flags\", ctypes.c_uint16),\n", + " (\"build_version\", ctypes.c_uint32),\n", + " (\"abi_version\", ctypes.c_uint32),\n", + " ]\n", + "\n", + "\n", + "def pack(major, minor):\n", + " \"\"\"The version format the C API uses, which is the two numbers in the top two bytes.\"\"\"\n", + " return (major << 24) | (minor << 16)\n", + "\n", + "\n", + "def considered(name):\n", + " \"\"\"Put one empty file in a directory of its own and ask whether the finder sees it.\"\"\"\n", + " room = pathlib.Path(tempfile.mkdtemp())\n", + " (room / name).write_bytes(b\"\")\n", + " return importlib.machinery.PathFinder.find_spec(\"demo\", [str(room)]) is not None\n", + "\n", + "\n", + "TAGS = importlib.machinery.EXTENSION_SUFFIXES\n", + "here = sysconfig.get_config_var(\"EXT_SUFFIX\")\n", + "flipped = VERSIONED.sub(lambda m: m.group(1) + m.group(2) + (\"\" if m.group(3) else \"t\"), here)\n", + "future = VERSIONED.sub(lambda m: m.group(1) + \"999\" + m.group(3), here)\n", + "machine = re.sub(r\"^\\.cpython-\\d+t?-?\", \"\", here).removesuffix(\".so\")\n", + "\n", + "\n", + "def tidy(text):\n", + " \"\"\"Take the machine out of a file name tag, since it says nothing about the ABI.\"\"\"\n", + " return text.replace(machine, \"PLATFORM\") if machine else text\n", + "\n", + "\n", + "print(\"version:\", sys.version.split()[0])\n", + "print(\"abiflags:\", repr(sys.abiflags))\n", + "print(\"gil disabled:\", sysconfig.get_config_var(\"Py_GIL_DISABLED\"))\n", + "print(\"soabi:\", tidy(sysconfig.get_config_var(\"SOABI\")))\n", + "print(\"api version, unchanged since 2006:\", sys.api_version)\n", + "print()\n", + "\n", + "print(\"file name tags this build will load:\")\n", + "for suffix in TAGS:\n", + " print(\" \", tidy(suffix))\n", + "print()\n", + "\n", + "print(\"what the finder does with one file of each kind\")\n", + "for name in (\"demo\" + here, \"demo\" + flipped, \"demo\" + future, \"demo.abi3.so\", \"demo.abi3t.so\"):\n", + " verdict = \"considered\" if considered(name) else \"invisible, the name is wrong\"\n", + " print(f\" {tidy(name):34} {verdict}\")\n", + "print()\n", + "\n", + "check = ctypes.pythonapi.PyABIInfo_Check\n", + "check.argtypes = [ctypes.POINTER(PyABIInfo), ctypes.c_char_p]\n", + "check.restype = ctypes.c_int\n", + "\n", + "CASES = (\n", + " (\"built for this exact version\", GIL, pack(*sys.version_info[:2])),\n", + " (\"built for 3.13 and nothing else\", GIL, pack(3, 13)),\n", + " (\"stable abi since 3.2\", STABLE | GIL, pack(3, 2)),\n", + " (\"stable abi since 3.14\", STABLE | GIL, pack(3, 14)),\n", + " (\"stable abi from the future\", STABLE | GIL, pack(3, 99)),\n", + " (\"free threaded only\", STABLE | FREETHREADED, pack(3, 14)),\n", + " (\"happy either way\", STABLE | GIL | FREETHREADED, pack(3, 14)),\n", + " (\"claims internal and stable\", STABLE | INTERNAL | GIL, sys.hexversion),\n", + ")\n", + "\n", + "print(\"what PyABIInfo_Check makes of each kind of extension\")\n", + "refused = 0\n", + "for label, flags, version in CASES:\n", + " info = PyABIInfo(1, 0, flags, sys.hexversion, version)\n", + " try:\n", + " check(ctypes.byref(info), label.encode())\n", + " except ImportError as problem:\n", + " refused += 1\n", + " print(f\" {label:32} refused: {str(problem).split(': ', 1)[1]}\")\n", + " else:\n", + " print(f\" {label:32} loads\")\n", + "print()\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", + "OLD = re.compile(r\"Py_LIMITED_API\\+0\\s*>=\\s*0x03([0-9a-fA-F]{2})0000\")\n", + "NEW = re.compile(r\"Py_LIMITED_API\\+0\\s*>=\\s*_Py_PACK_VERSION\\((\\d+),\\s*(\\d+)\\)\")\n", + "\n", + "\n", + "def gate(line):\n", + " \"\"\"The stable ABI version a preprocessor line gates on, when it gates on one.\"\"\"\n", + " found = NEW.search(line)\n", + " if found:\n", + " return int(found.group(2))\n", + " found = OLD.search(line)\n", + " return int(found.group(1), 16) if found else None\n", + "\n", + "\n", + "include = pathlib.Path(sysconfig.get_paths()[\"include\"])\n", + "arrived = {}\n", + "for path in sorted(include.glob(\"*.h\")):\n", + " stack = []\n", + " for line in path.read_text(errors=\"replace\").splitlines():\n", + " if OPENS.match(line):\n", + " stack.append(gate(line))\n", + " elif CLOSES.match(line) and stack:\n", + " stack.pop()\n", + " found = DECLARED.match(line)\n", + " if found and any(v for v in stack):\n", + " arrived[found.group(1)] = max(v for v in stack if v)\n", + "\n", + "print(\"when the public headers say each gated function arrived\")\n", + "counted = {}\n", + "for version in arrived.values():\n", + " counted[version] = counted.get(version, 0) + 1\n", + "for version in sorted(counted):\n", + " print(f\" 3.{version:<4} {counted[version]:4}\")\n", + "print()\n", + "\n", + "print(f\"~ file name tags this build will load: {len(TAGS)}\")\n", + "print(f\"~ hypothetical extensions refused: {refused} of {len(CASES)}\")\n", + "print(f\"~ public functions behind a stable abi version gate: {len(arrived)}\")\n", + "```\n", + "\n", + "```text\n", + "version: 3.15.0rc1\n", + "abiflags: ''\n", + "gil disabled: 0\n", + "soabi: cpython-315-PLATFORM\n", + "api version, unchanged since 2006: 1013\n", + "\n", + "file name tags this build will load:\n", + " .cpython-315-PLATFORM.so\n", + " .abi3-PLATFORM.so\n", + " .abi3.so\n", + " .abi3t-PLATFORM.so\n", + " .abi3t.so\n", + " .so\n", + "\n", + "what the finder does with one file of each kind\n", + " demo.cpython-315-PLATFORM.so considered\n", + " demo.cpython-315t-PLATFORM.so invisible, the name is wrong\n", + " demo.cpython-999-PLATFORM.so invisible, the name is wrong\n", + " demo.abi3.so considered\n", + " demo.abi3t.so considered\n", + "\n", + "what PyABIInfo_Check makes of each kind of extension\n", + " built for this exact version loads\n", + " built for 3.13 and nothing else refused: incompatible ABI version (3.13)\n", + " stable abi since 3.2 loads\n", + " stable abi since 3.14 loads\n", + " stable abi from the future refused: incompatible future stable ABI version (3.99)\n", + " free threaded only refused: only compatible with free-threaded CPython\n", + " happy either way loads\n", + " claims internal and stable refused: cannot use both internal and stable ABI\n", + "\n", + "when the public headers say each gated function arrived\n", + " 3.3 26\n", + " 3.4 3\n", + " 3.5 13\n", + " 3.6 4\n", + " 3.7 12\n", + " 3.8 1\n", + " 3.9 10\n", + " 3.10 5\n", + " 3.11 16\n", + " 3.12 6\n", + " 3.13 27\n", + " 3.14 18\n", + " 3.15 32\n", + "\n", + "~ file name tags this build will load: 6\n", + "~ hypothetical extensions refused: 4 of 8\n", + "~ public functions behind a stable abi version gate: 173\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", + "Six file name tags, four of the eight sample extensions refused. Now the same program on a build made with `--disable-gil`.\n", + "\n", + "How much of the stable ABI does dropping the global interpreter lock rule out?\n", + "\n", + "```python\n", + "\"\"\"What a build agrees to load, and the two gates that decide it.\n", + "\n", + "An extension arrives as a file with a long name. The interpreter decides whether to consider\n", + "it at all from that name alone, before it opens it. If it does open it, and the extension was\n", + "built against 3.15 or later, there is a second gate: a five field `PyABIInfo` struct the\n", + "extension carries, which `PyABIInfo_Check` compares against the running interpreter.\n", + "\n", + "This program runs both gates by hand. It asks the finder which file names it is willing to\n", + "look at, by putting empty files in a temporary directory and asking for the module. Then it\n", + "builds `PyABIInfo` structs in ctypes for a row of hypothetical extensions and calls the real\n", + "check function on each one.\n", + "\n", + "The point of running it on two builds is that both gates move. A free threaded build will not\n", + "look at a name with `abi3` in it, and it refuses a struct that says the extension wants the\n", + "global interpreter lock.\n", + "\n", + "One thing is printed differently from how the interpreter spells it. The last part of a file\n", + "name tag is the machine the build was made for, which is `x86_64-linux-gnu` on one runner and\n", + "`aarch64-linux-gnu` on another. That part says nothing about the ABI question, so it is\n", + "replaced with the word `PLATFORM` everywhere below and the recording is the same either way.\n", + "\"\"\"\n", + "\n", + "import ctypes\n", + "import importlib.machinery\n", + "import pathlib\n", + "import re\n", + "import sys\n", + "import sysconfig\n", + "import tempfile\n", + "\n", + "VERSIONED = re.compile(r\"(cpython-)(\\d+)(t?)\")\n", + "\n", + "STABLE = 0x0001\n", + "GIL = 0x0002\n", + "FREETHREADED = 0x0004\n", + "INTERNAL = 0x0008\n", + "\n", + "\n", + "class PyABIInfo(ctypes.Structure):\n", + " \"\"\"The struct an extension built against 3.15 or later carries about itself.\"\"\"\n", + "\n", + " _fields_ = [\n", + " (\"abiinfo_major_version\", ctypes.c_uint8),\n", + " (\"abiinfo_minor_version\", ctypes.c_uint8),\n", + " (\"flags\", ctypes.c_uint16),\n", + " (\"build_version\", ctypes.c_uint32),\n", + " (\"abi_version\", ctypes.c_uint32),\n", + " ]\n", + "\n", + "\n", + "def pack(major, minor):\n", + " \"\"\"The version format the C API uses, which is the two numbers in the top two bytes.\"\"\"\n", + " return (major << 24) | (minor << 16)\n", + "\n", + "\n", + "def considered(name):\n", + " \"\"\"Put one empty file in a directory of its own and ask whether the finder sees it.\"\"\"\n", + " room = pathlib.Path(tempfile.mkdtemp())\n", + " (room / name).write_bytes(b\"\")\n", + " return importlib.machinery.PathFinder.find_spec(\"demo\", [str(room)]) is not None\n", + "\n", + "\n", + "TAGS = importlib.machinery.EXTENSION_SUFFIXES\n", + "here = sysconfig.get_config_var(\"EXT_SUFFIX\")\n", + "flipped = VERSIONED.sub(lambda m: m.group(1) + m.group(2) + (\"\" if m.group(3) else \"t\"), here)\n", + "future = VERSIONED.sub(lambda m: m.group(1) + \"999\" + m.group(3), here)\n", + "machine = re.sub(r\"^\\.cpython-\\d+t?-?\", \"\", here).removesuffix(\".so\")\n", + "\n", + "\n", + "def tidy(text):\n", + " \"\"\"Take the machine out of a file name tag, since it says nothing about the ABI.\"\"\"\n", + " return text.replace(machine, \"PLATFORM\") if machine else text\n", + "\n", + "\n", + "print(\"version:\", sys.version.split()[0])\n", + "print(\"abiflags:\", repr(sys.abiflags))\n", + "print(\"gil disabled:\", sysconfig.get_config_var(\"Py_GIL_DISABLED\"))\n", + "print(\"soabi:\", tidy(sysconfig.get_config_var(\"SOABI\")))\n", + "print(\"api version, unchanged since 2006:\", sys.api_version)\n", + "print()\n", + "\n", + "print(\"file name tags this build will load:\")\n", + "for suffix in TAGS:\n", + " print(\" \", tidy(suffix))\n", + "print()\n", + "\n", + "print(\"what the finder does with one file of each kind\")\n", + "for name in (\"demo\" + here, \"demo\" + flipped, \"demo\" + future, \"demo.abi3.so\", \"demo.abi3t.so\"):\n", + " verdict = \"considered\" if considered(name) else \"invisible, the name is wrong\"\n", + " print(f\" {tidy(name):34} {verdict}\")\n", + "print()\n", + "\n", + "check = ctypes.pythonapi.PyABIInfo_Check\n", + "check.argtypes = [ctypes.POINTER(PyABIInfo), ctypes.c_char_p]\n", + "check.restype = ctypes.c_int\n", + "\n", + "CASES = (\n", + " (\"built for this exact version\", GIL, pack(*sys.version_info[:2])),\n", + " (\"built for 3.13 and nothing else\", GIL, pack(3, 13)),\n", + " (\"stable abi since 3.2\", STABLE | GIL, pack(3, 2)),\n", + " (\"stable abi since 3.14\", STABLE | GIL, pack(3, 14)),\n", + " (\"stable abi from the future\", STABLE | GIL, pack(3, 99)),\n", + " (\"free threaded only\", STABLE | FREETHREADED, pack(3, 14)),\n", + " (\"happy either way\", STABLE | GIL | FREETHREADED, pack(3, 14)),\n", + " (\"claims internal and stable\", STABLE | INTERNAL | GIL, sys.hexversion),\n", + ")\n", + "\n", + "print(\"what PyABIInfo_Check makes of each kind of extension\")\n", + "refused = 0\n", + "for label, flags, version in CASES:\n", + " info = PyABIInfo(1, 0, flags, sys.hexversion, version)\n", + " try:\n", + " check(ctypes.byref(info), label.encode())\n", + " except ImportError as problem:\n", + " refused += 1\n", + " print(f\" {label:32} refused: {str(problem).split(': ', 1)[1]}\")\n", + " else:\n", + " print(f\" {label:32} loads\")\n", + "print()\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", + "OLD = re.compile(r\"Py_LIMITED_API\\+0\\s*>=\\s*0x03([0-9a-fA-F]{2})0000\")\n", + "NEW = re.compile(r\"Py_LIMITED_API\\+0\\s*>=\\s*_Py_PACK_VERSION\\((\\d+),\\s*(\\d+)\\)\")\n", + "\n", + "\n", + "def gate(line):\n", + " \"\"\"The stable ABI version a preprocessor line gates on, when it gates on one.\"\"\"\n", + " found = NEW.search(line)\n", + " if found:\n", + " return int(found.group(2))\n", + " found = OLD.search(line)\n", + " return int(found.group(1), 16) if found else None\n", + "\n", + "\n", + "include = pathlib.Path(sysconfig.get_paths()[\"include\"])\n", + "arrived = {}\n", + "for path in sorted(include.glob(\"*.h\")):\n", + " stack = []\n", + " for line in path.read_text(errors=\"replace\").splitlines():\n", + " if OPENS.match(line):\n", + " stack.append(gate(line))\n", + " elif CLOSES.match(line) and stack:\n", + " stack.pop()\n", + " found = DECLARED.match(line)\n", + " if found and any(v for v in stack):\n", + " arrived[found.group(1)] = max(v for v in stack if v)\n", + "\n", + "print(\"when the public headers say each gated function arrived\")\n", + "counted = {}\n", + "for version in arrived.values():\n", + " counted[version] = counted.get(version, 0) + 1\n", + "for version in sorted(counted):\n", + " print(f\" 3.{version:<4} {counted[version]:4}\")\n", + "print()\n", + "\n", + "print(f\"~ file name tags this build will load: {len(TAGS)}\")\n", + "print(f\"~ hypothetical extensions refused: {refused} of {len(CASES)}\")\n", + "print(f\"~ public functions behind a stable abi version gate: {len(arrived)}\")\n", + "```\n", + "\n", + "```text\n", + "version: 3.15.0rc1\n", + "abiflags: 't'\n", + "gil disabled: 1\n", + "soabi: cpython-315t-PLATFORM\n", + "api version, unchanged since 2006: 1013\n", + "\n", + "file name tags this build will load:\n", + " .cpython-315t-PLATFORM.so\n", + " .abi3t-PLATFORM.so\n", + " .abi3t.so\n", + " .so\n", + "\n", + "what the finder does with one file of each kind\n", + " demo.cpython-315t-PLATFORM.so considered\n", + " demo.cpython-315-PLATFORM.so invisible, the name is wrong\n", + " demo.cpython-999t-PLATFORM.so invisible, the name is wrong\n", + " demo.abi3.so invisible, the name is wrong\n", + " demo.abi3t.so considered\n", + "\n", + "what PyABIInfo_Check makes of each kind of extension\n", + " built for this exact version refused: incompatible with free-threaded CPython\n", + " built for 3.13 and nothing else refused: incompatible ABI version (3.13)\n", + " stable abi since 3.2 refused: incompatible with free-threaded CPython\n", + " stable abi since 3.14 refused: incompatible with free-threaded CPython\n", + " stable abi from the future refused: incompatible future stable ABI version (3.99)\n", + " free threaded only loads\n", + " happy either way loads\n", + " claims internal and stable refused: cannot use both internal and stable ABI\n", + "\n", + "when the public headers say each gated function arrived\n", + " 3.3 26\n", + " 3.4 3\n", + " 3.5 13\n", + " 3.6 4\n", + " 3.7 12\n", + " 3.8 1\n", + " 3.9 10\n", + " 3.10 5\n", + " 3.11 16\n", + " 3.12 6\n", + " 3.13 27\n", + " 3.14 18\n", + " 3.15 32\n", + "\n", + "~ file name tags this build will load: 4\n", + "~ hypothetical extensions refused: 6 of 8\n", + "~ public functions behind a stable abi version gate: 173\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", + "Four tags rather than six, and six refusals rather than four. Both gates tightened, and they tightened in the same direction: the `abi3` name is gone from the list, and every struct that says `PyABIInfo_GIL` without also saying `PyABIInfo_FREETHREADED` is turned away.\n", + "\n", + "That is the whole argument for `abi3t` in two recordings. An extension shipped as `abi3` in 2024 does not reach a free threaded interpreter, by name or by struct, and neither refusal is a bug.\n", + "\n", + "## Try it yourself\n", + "\n", + "**One.** The finder picked the exact version file over `demo.abi3.so`. Delete that one from the directory and run it again, then keep deleting until only `demo.py` is left. The order you see is the C array in `dynload_shlib.c`, read top to bottom.\n", + "\n", + "**Two.** Set `abiinfo_major_version` to 0 in one of the cases and call the check again. Find the line in `Python/modsupport.c` that explains what you see, and work out why that escape hatch exists.\n", + "\n", + "**Three.** The gate scan only looks at `Include/*.h`. Point it at `Include/cpython/*.h` as well and see how many gated functions turn up there. Then work out why any function in that directory would carry a `Py_LIMITED_API` gate at all.\n", + "\n", + "**Four.** `sys.api_version` is 1013 and has not moved since 2006. Find the two places in the source that still read it, and decide for yourself whether the check they do is worth anything.\n", + "\n", + "## What you now know\n", + "\n", + "An extension has to get past two gates. The first is its file name, and it is cheap, mechanical and unforgiving: the tag has to be one of the handful in a C array compiled into the interpreter, and anything else is not rejected but simply never seen.\n", + "\n", + "The tag is the implementation, the version, a `t` when the build has no global interpreter lock, and the platform. The `abi3` and `abi3t` tags say the file was built against the stable ABI instead. A free threaded build takes only `abi3t`, an ordinary 3.15 build takes both and prefers `abi3`, and 3.14 has never heard of `abi3t`.\n", + "\n", + "The second gate is twelve bytes of struct, new in 3.15 and opt in. It compares four things: whether the extension claims the stable ABI, which version it was built against, whether that version is allowed to be older than yours, and which side of the free threading split it is on. Four of the seven sample extensions in this lesson were refused, each for a different reason.\n", + "\n", + "Neither gate is a real safety net. The name check happens before anything is read, so it catches the common mistake and nothing else. The struct check happens after the extension's code has already run, and only if the extension asked for it.\n", + "\n", + "## What is next\n", + "\n", + "R08 turns the last three lessons around. Instead of asking what an extension may call and whether it will load, it asks what the interpreter is doing while all of this is happening, and what happens when it stops. Shutdown is the part of the runtime with the fewest promises, and it is where every assumption the earlier lessons made about lifetime finally gets tested." + ] + } + ], + "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 10c9c3e..b54b878 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.003s\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.001s\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 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" + "printed": " the collector is scheduled when generation zero passes 2000\n json.loads built them in 46 ms, collector ran 1 times\n a list display did it in 42 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 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" + "printed": " a plain object at 23460448 mod 4 0 mod 8 0\n a list at 24421088 mod 4 0 mod 8 0\n a dict at 21399552 mod 4 0 mod 8 0\n a string at 21709584 mod 4 0 mod 8 0\n a big integer at 20909352 mod 4 0 mod 8 0\n a float at 20724176 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 114.5 ns per iteration\n with_try 135.5 ns per iteration\n checking 183.3 ns per iteration\n" + "printed": " no_try 60.3 ns per iteration\n with_try 59.5 ns per iteration\n checking 143.1 ns per iteration\n" }, { "name": "e05-21", "status": "ok", - "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" + "printed": " through 1 frames 737.0 ns\n through 5 frames 1614.5 ns\n through 20 frames 4035.6 ns\n through 50 frames 9865.0 ns\n" }, { "name": "e05-24", @@ -711,7 +711,7 @@ { "name": "e05-29", "status": "ok", - "printed": " for loop table 0 bytes 68.3 ns per item\n while and next table 12 bytes 113.4 ns per item\n" + "printed": " for loop table 0 bytes 43.1 ns per item\n while and next table 12 bytes 57.3 ns per item\n" }, { "name": "e05-32", @@ -780,7 +780,7 @@ { "name": "e06-31", "status": "ok", - "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" + "printed": " every value an int 49.2 ns per addition\n every value a float 47.1 ns per addition\n alternating between the two 78.6 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 85.3 ns per iteration\n loop with a call 84.5 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 74.7 ns per iteration\n loop with a call 90.9 ns per iteration\n" } ] }, @@ -863,7 +863,7 @@ { "name": "e08-11", "status": "ok", - "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" + "printed": " nobody watching 44.9 ns per turn\n callback counts the line 329.0 ns per turn, 2000015 calls\n callback returns DISABLE 48.8 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 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" + "printed": " extra lines instructions run nanoseconds cost of one more\n 0 7010 30577 \n 5 17010 49950 1.94\n 10 27010 166124 11.62\n 20 47010 270209 5.20\n 40 87010 455119 4.62\n" }, { "name": "e11-18", "status": "ok", - "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" + "printed": " the same job instructions nanoseconds each\n a while loop 20012 179457 8.97\n a for loop 10008 76772 7.67\n a comprehension 6011 59398 9.88\n" }, { "name": "e11-21", "status": "ok", - "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" + "printed": " one bytecode instruction 4.26 ns\n one call to an empty def 24.62 ns beyond the code that calls it\n so a call costs about 6 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 33 2b 9d 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 b5 3f 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 1788685107 source 1788685107\n size 54 source 54\n" + "printed": " magic 168627755\n flags 0\n mtime 1788690357 source 1788690357\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: 1788685260\n imported it with no complaint: 'hi world'\n and the header now says 1788685260\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: 1788690250\n imported it with no complaint: 'hi world'\n and the header now says 1788690250\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 24956\n frozen after gc.freeze() 24956\n left in generation 2 0\n frozen after gc.unfreeze() 0\n" + "printed": " frozen objects before 0\n tracked in generation 2 24959\n frozen after gc.freeze() 24959\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, 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" + "printed": " objects in each generation [3, 0, 24945]\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 58 ms\n and over an empty one 13 ms\n" + "printed": " cycles on the heap 200000\n one full pass over it 44 ms\n and over an empty one 16 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 24958\n" + "printed": " this version does not report candidates, so there is nothing to compare\n objects the collector is tracking 24961\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 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" + "printed": " a pointer here is 4 bytes, so the header is 8\n it lives at 24939192\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 20309280\n id(Thing) is 20309280\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", @@ -2693,7 +2693,7 @@ { "name": "o12-19", "status": "ok", - "printed": " dropped floats an int can reach lists a tuple can reach\n 40 0 0\n 80 0 0\n 120 20 40\n 200 100 120\n 300 200 220\n" + "printed": " dropped floats an int can reach lists a tuple can reach\n 40 0 0\n 80 0 0\n 120 20 40\n 200 99 120\n 300 199 220\n" }, { "name": "o12-22", @@ -2958,7 +2958,7 @@ { "name": "r03-22", "status": "ok", - "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" + "printed": " first time, source file, no .pyc yet: 156.2 microseconds\n from a file, with a .pyc already there: 105.3 microseconds\n asking again for one already loaded: 193.6 nanoseconds\n the second of those is 544 times the third\n" } ] }, @@ -3007,7 +3007,7 @@ { "name": "r04-25", "status": "ok", - "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" + "printed": " asking FrozenImporter for os: 1.95 microseconds\n asking PathFinder for the same name: 9231.20 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: 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" + "printed": " what that one import line cost here: 80.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" }, { "name": "r05-10", @@ -3140,6 +3140,59 @@ } ] }, + { + "slug": "r07-the-stable-abi", + "cells": [ + { + "name": "r07-03", + "status": "ok" + }, + { + "name": "r07-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": "r07-07", + "status": "ok", + "printed": " file names this build is willing to load an extension from:\n .cpython-314-wasm32-emscripten.so\n .abi3.so\n .so\n\n and the one it stamps on what it builds: .cpython-314-wasm32-emscripten.so\n SOABI 'cpython-314-wasm32-emscripten'\n ABIFLAGS ''\n Py_GIL_DISABLED 0\n sys.abiflags ''\n" + }, + { + "name": "r07-10", + "status": "ok", + "printed": " the tag this build writes: .cpython-314-wasm32-emscripten.so\n which Python: cpython\n which version: 314\n threading: with the lock\n the rest: -wasm32-emscripten.so\n\n the 3 in abi3 is not a Python version, it is PYTHON_ABI_VERSION\n and the other number nobody has touched since 2006: 1013\n" + }, + { + "name": "r07-13", + "status": "ok", + "printed": " demo.cpython-314-wasm32-emscripten.so considered\n demo.cpython-314t-wasm32-emscripten.so invisible, wrong name\n demo.cpython-999-wasm32-emscripten.so invisible, wrong name\n demo.abi3.so considered\n demo.abi3t.so invisible, wrong name\n" + }, + { + "name": "r07-16", + "status": "ok", + "printed": " all of them in one directory, and the finder picks:\n demo.cpython-314-wasm32-emscripten.so\n loaded by ExtensionFileLoader\n\n the order is the suffix list, and the first match wins:\n 1. .cpython-314-wasm32-emscripten.so\n 2. .abi3.so\n 3. .so\n" + }, + { + "name": "r07-19", + "status": "ok", + "printed": " PyABIInfo_Check arrived in 3.15 and this is 3.14.2\n so the next cell has nothing to run\n" + }, + { + "name": "r07-22", + "status": "ok", + "printed": " nothing to run here, this interpreter has no PyABIInfo_Check\n" + }, + { + "name": "r07-25", + "status": "ok", + "printed": " no headers next to this interpreter, looked in //include/python3.14\n so this cell and the next one have nothing to read\n" + }, + { + "name": "r07-28", + "status": "ok" + } + ] + }, { "slug": "t01-one-line-seven-stages", "cells": [ @@ -3522,7 +3575,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", @@ -3799,7 +3852,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", @@ -3833,12 +3886,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 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" + "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 0x150ac08, 3 reference(s), 26 bytes, not tracked\nlist at 0x154e0a8, 2 reference(s), 44 bytes, tracked by the cycle collector\ndict at 0x184c7c0, 2 reference(s), 108 bytes, tracked by the cycle collector\nfunction at 0x18a2988, 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) 0x175f990\nid(b) 0x11b5970\nid(c) 0x175f990\n" + "printed": "a == b True same contents\na is b False different objects\na is c True same object\n\nid(a) 0x1447f50\nid(b) 0x17fd738\nid(c) 0x1447f50\n" }, { "name": "t08-17", @@ -3878,7 +3931,7 @@ { "name": "t08-35", "status": "ok", - "printed": "dict at 0x11edd18\ndict at 0x1432f18\nlist at 0x18354a0\n" + "printed": "dict at 0x1342e70\ndict at 0x1605e88\nlist at 0x17fd698\n" }, { "name": "t08-37", @@ -3937,7 +3990,7 @@ { "name": "t09-21", "status": "ok", - "printed": "Node at 0x1858b98 -> Node at 0x1817e78 -> Node at 0x17f7290 -> Node at 0x1858b98\n" + "printed": "Node at 0x14b9120 -> Node at 0x1753e98 -> Node at 0x16ba1a0 -> Node at 0x14b9120\n" }, { "name": "t09-23", @@ -3967,7 +4020,7 @@ { "name": "t09-37", "status": "ok", - "printed": "first object was at 0x161f130\nsecond object is at 0x161f130\nsame address reused -> True\n" + "printed": "first object was at 0x14cb3c0\nsecond object is at 0x14cb3c0\nsame address reused -> True\n" } ] }, diff --git a/probes/pyodide/lessons.md b/probes/pyodide/lessons.md index a8bf27f..9064dd0 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. -77 lesson(s) on Pyodide 3.14.2: 77 ran end to end, 757 cell(s) in total. +78 lesson(s) on Pyodide 3.14.2: 78 ran end to end, 767 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. @@ -73,6 +73,7 @@ The checks in `report.md` next to this ask whether a surface exists. This runs t | 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 | +| r07-the-stable-abi | 10 | 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/pyodide.json b/probes/pyodide/pyodide.json index 5068395..8a6ecb3 100644 --- a/probes/pyodide/pyodide.json +++ b/probes/pyodide/pyodide.json @@ -1,7 +1,7 @@ { "runtime": "pyodide", "python": "3.14.2", - "seconds": 4.64, + "seconds": 1.626, "payload_bytes": 13508566, "outcomes": [ { diff --git a/probes/pyodide/report.md b/probes/pyodide/report.md index 8af88a1..381658a 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 4.6 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 1.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. diff --git a/pyxray/src/pyxray/glossary.py b/pyxray/src/pyxray/glossary.py index 8b7ec3b..35f0ed1 100644 --- a/pyxray/src/pyxray/glossary.py +++ b/pyxray/src/pyxray/glossary.py @@ -1999,9 +1999,35 @@ def anchor(name: str) -> str: 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"), + see=("limited API", "abi flags", "extension module"), met="R06", ), + Term( + name="extension module", + short="A module that arrives as a compiled shared library rather than as Python.", + long="Everything about how it loads is decided in two steps. First the finder looks at the file name and decides whether the tag in it is one this build accepts, which costs one stat call and happens before anything is opened. Then, if the extension was built against 3.15 or later, the interpreter compares the ABI struct it carries against itself, by which point the extension's own code has already run.", + cite="Python/dynload_shlib.c:39-60@v3.15.0rc1#_PyImport_DynLoadFiletab", + also=("`.so`", "`.pyd`"), + see=("stable ABI", "SOABI", "PyABIInfo"), + met="R07", + ), + Term( + name="SOABI", + short="The tag a build stamps into the file names of the extensions it compiles.", + long="It reads like `cpython-315t-darwin`, and it is three separate facts glued together: the implementation, the version with a `t` on the end if the build has no global interpreter lock, and the platform. Change any one of them and this interpreter will not even look at the file. The value is in `sysconfig`, and the suffix built from it is the first entry of `importlib.machinery.EXTENSION_SUFFIXES`.", + also=("`EXT_SUFFIX`",), + see=("extension module", "abi flags"), + met="R07", + ), + Term( + name="PyABIInfo", + short="Twelve bytes an extension carries saying which ABI it was built against.", + long="Two version bytes for the struct itself, sixteen flag bits for stable, GIL, free threaded and internal, and two packed version numbers. `PyABIInfo_Check` compares it against the running interpreter and raises `ImportError` when they disagree, which is the only ABI check CPython does at all. It is new in 3.15, it is opt in, and the extension's own code has already run by the time it happens.", + cite="Include/modsupport.h:86-101@v3.15.0rc1#PyABIInfo", + also=("`PyABIInfo_Check`", "`Py_mod_abi`"), + see=("stable ABI", "extension module"), + met="R07", + ), ), ) diff --git a/tools/tier1/src/tier1/experiments.py b/tools/tier1/src/tier1/experiments.py index 0b8c57a..a254f29 100644 --- a/tools/tier1/src/tier1/experiments.py +++ b/tools/tier1/src/tier1/experiments.py @@ -3137,6 +3137,197 @@ def read(pattern): ) +PROGRAM_TWENTYSEVEN = r'''"""What a build agrees to load, and the two gates that decide it. + +An extension arrives as a file with a long name. The interpreter decides whether to consider +it at all from that name alone, before it opens it. If it does open it, and the extension was +built against 3.15 or later, there is a second gate: a five field `PyABIInfo` struct the +extension carries, which `PyABIInfo_Check` compares against the running interpreter. + +This program runs both gates by hand. It asks the finder which file names it is willing to +look at, by putting empty files in a temporary directory and asking for the module. Then it +builds `PyABIInfo` structs in ctypes for a row of hypothetical extensions and calls the real +check function on each one. + +The point of running it on two builds is that both gates move. A free threaded build will not +look at a name with `abi3` in it, and it refuses a struct that says the extension wants the +global interpreter lock. + +One thing is printed differently from how the interpreter spells it. The last part of a file +name tag is the machine the build was made for, which is `x86_64-linux-gnu` on one runner and +`aarch64-linux-gnu` on another. That part says nothing about the ABI question, so it is +replaced with the word `PLATFORM` everywhere below and the recording is the same either way. +""" + +import ctypes +import importlib.machinery +import pathlib +import re +import sys +import sysconfig +import tempfile + +VERSIONED = re.compile(r"(cpython-)(\d+)(t?)") + +STABLE = 0x0001 +GIL = 0x0002 +FREETHREADED = 0x0004 +INTERNAL = 0x0008 + + +class PyABIInfo(ctypes.Structure): + """The struct an extension built against 3.15 or later carries about itself.""" + + _fields_ = [ + ("abiinfo_major_version", ctypes.c_uint8), + ("abiinfo_minor_version", ctypes.c_uint8), + ("flags", ctypes.c_uint16), + ("build_version", ctypes.c_uint32), + ("abi_version", ctypes.c_uint32), + ] + + +def pack(major, minor): + """The version format the C API uses, which is the two numbers in the top two bytes.""" + return (major << 24) | (minor << 16) + + +def considered(name): + """Put one empty file in a directory of its own and ask whether the finder sees it.""" + room = pathlib.Path(tempfile.mkdtemp()) + (room / name).write_bytes(b"") + return importlib.machinery.PathFinder.find_spec("demo", [str(room)]) is not None + + +TAGS = importlib.machinery.EXTENSION_SUFFIXES +here = sysconfig.get_config_var("EXT_SUFFIX") +flipped = VERSIONED.sub(lambda m: m.group(1) + m.group(2) + ("" if m.group(3) else "t"), here) +future = VERSIONED.sub(lambda m: m.group(1) + "999" + m.group(3), here) +machine = re.sub(r"^\.cpython-\d+t?-?", "", here).removesuffix(".so") + + +def tidy(text): + """Take the machine out of a file name tag, since it says nothing about the ABI.""" + return text.replace(machine, "PLATFORM") if machine else text + + +print("version:", sys.version.split()[0]) +print("abiflags:", repr(sys.abiflags)) +print("gil disabled:", sysconfig.get_config_var("Py_GIL_DISABLED")) +print("soabi:", tidy(sysconfig.get_config_var("SOABI"))) +print("api version, unchanged since 2006:", sys.api_version) +print() + +print("file name tags this build will load:") +for suffix in TAGS: + print(" ", tidy(suffix)) +print() + +print("what the finder does with one file of each kind") +for name in ("demo" + here, "demo" + flipped, "demo" + future, "demo.abi3.so", "demo.abi3t.so"): + verdict = "considered" if considered(name) else "invisible, the name is wrong" + print(f" {tidy(name):34} {verdict}") +print() + +check = ctypes.pythonapi.PyABIInfo_Check +check.argtypes = [ctypes.POINTER(PyABIInfo), ctypes.c_char_p] +check.restype = ctypes.c_int + +CASES = ( + ("built for this exact version", GIL, pack(*sys.version_info[:2])), + ("built for 3.13 and nothing else", GIL, pack(3, 13)), + ("stable abi since 3.2", STABLE | GIL, pack(3, 2)), + ("stable abi since 3.14", STABLE | GIL, pack(3, 14)), + ("stable abi from the future", STABLE | GIL, pack(3, 99)), + ("free threaded only", STABLE | FREETHREADED, pack(3, 14)), + ("happy either way", STABLE | GIL | FREETHREADED, pack(3, 14)), + ("claims internal and stable", STABLE | INTERNAL | GIL, sys.hexversion), +) + +print("what PyABIInfo_Check makes of each kind of extension") +refused = 0 +for label, flags, version in CASES: + info = PyABIInfo(1, 0, flags, sys.hexversion, version) + try: + check(ctypes.byref(info), label.encode()) + except ImportError as problem: + refused += 1 + print(f" {label:32} refused: {str(problem).split(': ', 1)[1]}") + else: + print(f" {label:32} loads") +print() + +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") +OLD = re.compile(r"Py_LIMITED_API\+0\s*>=\s*0x03([0-9a-fA-F]{2})0000") +NEW = re.compile(r"Py_LIMITED_API\+0\s*>=\s*_Py_PACK_VERSION\((\d+),\s*(\d+)\)") + + +def gate(line): + """The stable ABI version a preprocessor line gates on, when it gates on one.""" + found = NEW.search(line) + if found: + return int(found.group(2)) + found = OLD.search(line) + return int(found.group(1), 16) if found else None + + +include = pathlib.Path(sysconfig.get_paths()["include"]) +arrived = {} +for path in sorted(include.glob("*.h")): + stack = [] + for line in path.read_text(errors="replace").splitlines(): + if OPENS.match(line): + stack.append(gate(line)) + elif CLOSES.match(line) and stack: + stack.pop() + found = DECLARED.match(line) + if found and any(v for v in stack): + arrived[found.group(1)] = max(v for v in stack if v) + +print("when the public headers say each gated function arrived") +counted = {} +for version in arrived.values(): + counted[version] = counted.get(version, 0) + 1 +for version in sorted(counted): + print(f" 3.{version:<4} {counted[version]:4}") +print() + +print(f"~ file name tags this build will load: {len(TAGS)}") +print(f"~ hypothetical extensions refused: {refused} of {len(CASES)}") +print(f"~ public functions behind a stable abi version gate: {len(arrived)}") +''' + + +WHAT_A_BUILD_WILL_LOAD = Experiment( + slug="r07-what-a-build-will-load", + lesson="R07", + title="Both gates an extension has to pass, run by hand on an ordinary build", + asks="What does the interpreter check before it agrees to load an extension?", + needs=( + "it needs 3.15 for PyABIInfo_Check, the headers next to the interpreter for the version " + "gate scan, and a real filesystem to put the empty files in" + ), + build="release", + program=PROGRAM_TWENTYSEVEN, +) + + +WHAT_A_BUILD_WILL_LOAD_WITHOUT_THE_LOCK = Experiment( + slug="r07-what-a-build-will-load-without-the-lock", + lesson="R07", + title="The same two gates on the build that abi3t was invented for", + asks="How much of the stable ABI does dropping the global interpreter lock rule out?", + needs=( + "it needs a build configured with --disable-gil, because the whole question is which " + "file names and which ABI flags that build is willing to accept" + ), + build="freethreaded", + program=PROGRAM_TWENTYSEVEN, +) + + EXPERIMENTS: tuple[Experiment, ...] = ( COMPILING_COSTS_NOTHING_THAT_LASTS, A_LEAK_YOU_CAN_SEE, @@ -3177,6 +3368,8 @@ def read(pattern): HOW_MUCH_OF_A_WAKE_UP_IS_PARALLEL_WITHOUT_THE_LOCK, WHAT_LEAVES_THE_BINARY, WHAT_LEAVES_THE_BINARY_ON_A_FREE_THREADED_BUILD, + WHAT_A_BUILD_WILL_LOAD, + WHAT_A_BUILD_WILL_LOAD_WITHOUT_THE_LOCK, )