PEP 697 – Begrenzte C-API für die Erweiterung von Opaque-Typen
- Autor:
- Petr Viktorin <encukou at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 23. August 2022
- Python-Version:
- 3.12
- Post-History:
- 24. Mai 2022, 06. Okt. 2022
- Resolution:
- Discourse-Nachricht
Zusammenfassung
Fügen Sie der begrenzten C-API Unterstützung für die Erweiterung einiger Typen mit Opaque-Daten hinzu, indem Sie es dem Code ermöglichen, nur mit Daten zu arbeiten, die für eine bestimmte (Unter-)Klasse spezifisch sind.
Dieser Mechanismus muss mit PyHeapTypeObject verwendbar sein.
Diese PEP schlägt nicht vor, nicht dynamisch dimensionierte variable Objekte wie tuple oder int zu erweitern, da diese ein anderes Speicherlayout haben und die Nachfrage dafür als gering eingeschätzt wird. Diese PEP lässt Raum, dies zukünftig über denselben Mechanismus zu tun, falls gewünscht.
Motivation
Das motivierende Problem, das diese PEP löst, ist das Anhängen von C-Level-Zuständen an benutzerdefinierte Typen – d. h. Metaklassen (Unterklassen von type).
Dies ist oft in „Wrappern“ erforderlich, die andere Typsysteme (z. B. C++, Java, Rust) als Python-Klassen exponieren. Diese müssen typischerweise Informationen über die „eingewickelte“ Nicht-Python-Klasse am Python-Typobjekt anhängen.
Dies sollte in der begrenzten API möglich sein, damit die Sprach-Wrapper oder Code-Generatoren zum Erstellen von Stable ABI-Erweiterungen verwendet werden können. (Siehe PEP 652 für die Vorteile der Bereitstellung einer stabilen ABI.)
Die Erweiterung von type ist ein Beispiel für ein allgemeineres Problem: das Erweitern einer Klasse bei gleichzeitiger loser Kopplung – d. h. ohne Abhängigkeit vom Speicherlayout der Oberklasse. (Das ist viel Jargon; siehe Rationale für ein konkretes Beispiel für die Erweiterung von list.)
Begründung
Erweiterung von Opaque-Typen
In der begrenzten API sind die meisten structs opak: ihre Größe und ihr Speicherlayout werden nicht offengelegt, sodass sie in neuen Versionen von CPython (oder alternativen Implementierungen der C-API) geändert werden können.
Das bedeutet, dass das übliche Muster der Unterklassifizierung – die struct, die für Instanzen des Basis-Typs verwendet wird, als erstes Element der struct, die für Instanzen des abgeleiteten Typs verwendet wird – nicht funktioniert. Zur Veranschaulichung mit Code erweitert das Beispiel aus dem Tutorial PyListObject (PyListObject, list) unter Verwendung der folgenden struct
typedef struct {
PyListObject list;
int state;
} SubListObject;
Dies wird in der begrenzten API nicht kompiliert, da PyListObject opak ist (um Änderungen bei der Implementierung von Funktionen und Optimierungen zu ermöglichen).
Stattdessen schlägt diese PEP die Verwendung einer struct mit nur dem in der Unterklasse benötigten Zustand vor, nämlich
typedef struct {
int state;
} SubListState;
// (or just `typedef int SubListState;` in this case)
Die Unterklasse kann nun vollständig vom Speicherlayout (und der Größe) der Oberklasse entkoppelt werden.
Dies ist heute möglich. Um eine solche struct zu verwenden
- bei der Erstellung der Klasse, verwenden Sie
PyListObject->tp_basicsize + sizeof(SubListState)alsPyType_Spec.basicsize; - beim Zugriff auf die Daten, verwenden Sie
PyListObject->tp_basicsizeals Offset in der Instanz (PyObject*).
Dies hat jedoch Nachteile
- Die
basicsizeder Basis kann nicht richtig ausgerichtet sein, was auf einigen Architekturen zu Problemen führen kann, wenn sie nicht behoben werden. (Diese Probleme können besonders knifflig sein, wenn sich die Ausrichtung in einer neuen Version ändert.) PyTypeObject.tp_basicsizewird in der begrenzten API nicht exponiert, sodass Erweiterungen, die die begrenzte API unterstützen,PyObject_GetAttrString(obj, "__basicsize__")verwenden müssen. Dies ist umständlich und in Randfällen unsicher (das Python-Attribut kann überschrieben werden).- Variable Objekte werden nicht behandelt (siehe Erweiterung von variablen Objekten unten).
Um dies einfach (und sogar zur *Best Practice* für Projekte, die lose Kopplung gegenüber maximaler Leistung wählen) zu machen, schlägt diese PEP eine API vor, um
- Geben Sie während der Klassenerstellung an, dass
SubListStateanPyListObject„angehängt“ werden soll, ohne zusätzliche Details überlistzu übergeben. (Der Interpreter selbst erhält alle notwendigen Informationen, wie z. B.tp_basicsize, von der Basis.)Dies wird durch eine negative
PyType_Spec.basicsizeangegeben:-sizeof(SubListState). - Erhalten Sie aus einer Instanz und dem
PyTypeObject*der Unterklasse einen Zeiger auf dieSubListState. Eine neue Funktion,PyObject_GetTypeData, wird dafür hinzugefügt.
Die Basisklasse ist natürlich nicht auf PyListObject beschränkt: Sie kann verwendet werden, um jede Basisklasse zu erweitern, deren Instanz struct opak, über Releases hinweg instabil oder überhaupt nicht exponiert ist – einschließlich type (PyHeapTypeObject) oder Erweiterungen von Drittanbietern (zum Beispiel NumPy-Arrays [1]).
Für Fälle, in denen kein zusätzlicher Zustand benötigt wird, wird eine basicsize von Null zugelassen: In diesem Fall wird die tp_basicsize der Basis geerbt. (Dies funktioniert derzeit, es fehlen jedoch explizite Dokumentation und Tests.)
Die tp_basicsize der neuen Klasse wird auf die berechnete Gesamtgröße gesetzt, sodass Code, der Klassen inspiziert, wie zuvor weiter funktioniert.
Erweiterung von variablen Objekten
Zusätzliche Überlegungen sind erforderlich, um variable-sized objects unter Beibehaltung der losen Kopplung zu unterklassifizieren: die variable Daten können mit den Unterklassen-Daten kollidieren (SubListState im obigen Beispiel).
Derzeit bietet CPython keine Möglichkeit, solche Kollisionen zu verhindern. Daher wird der vorgeschlagene Mechanismus zur Erweiterung von Opaque-Klassen (negativer base->tp_itemsize) standardmäßig fehlschlagen.
Wir könnten hier aufhören, aber da der motivierende Typ – PyHeapTypeObject – variable Größe hat, benötigen wir einen sicheren Weg, um die Unterklassifizierung zu ermöglichen. Ein kurzer Hintergrund zuerst
Variable Layouts
Es gibt zwei Hauptspeicherlayouts für variable Objekte.
Bei Typen wie int oder tuple werden die variablen Daten an einem festen Offset gespeichert. Wenn Unterklassen zusätzlichen Speicher benötigen, muss dieser nach allen variablen Daten hinzugefügt werden
PyTupleObject:
┌───────────────────┬───┬───┬╌╌╌╌┐
│ PyObject_VAR_HEAD │var. data │
└───────────────────┴───┴───┴╌╌╌╌┘
tuple subclass:
┌───────────────────┬───┬───┬╌╌╌╌┬─────────────┐
│ PyObject_VAR_HEAD │var. data │subclass data│
└───────────────────┴───┴───┴╌╌╌╌┴─────────────┘
Bei anderen Typen, wie z. B. PyHeapTypeObject, befinden sich variable Daten immer am Ende des Speicherbereichs der Instanz
heap type:
┌───────────────────┬──────────────┬───┬───┬╌╌╌╌┐
│ PyObject_VAR_HEAD │Heap type data│var. data │
└───────────────────┴──────────────┴───┴───┴╌╌╌╌┘
type subclass:
┌───────────────────┬──────────────┬─────────────┬───┬───┬╌╌╌╌┐
│ PyObject_VAR_HEAD │Heap type data│subclass data│var. data │
└───────────────────┴──────────────┴─────────────┴───┴───┴╌╌╌╌┘
Das erste Layout ermöglicht einen schnellen Zugriff auf das Item-Array. Das zweite ermöglicht es Unterklassen, das variable Array zu ignorieren (vorausgesetzt, sie verwenden Offsets vom Beginn des Objekts, um auf ihre Daten zuzugreifen).
Da sich diese PEP auf PyHeapTypeObject konzentriert, schlägt sie eine API vor, um die Unterklassifizierung für die zweite Variante zu ermöglichen. Die Unterstützung für die erste kann später hinzugefügt werden, als API-kompatible Änderung (obwohl der PEP-Autor bezweifelt, dass sich der Aufwand lohnt).
Erweiterung von Klassen mit dem PyHeapTypeObject-ähnlichen Layout
Diese PEP schlägt ein Typ-Flag vor, Py_TPFLAGS_ITEMS_AT_END, das das PyHeapTypeObject-ähnliche Layout anzeigt. Dies kann auf zwei Arten gesetzt werden:
- die Oberklasse kann das Flag setzen und damit Autoren von Unterklassen ermöglichen, sich nicht darum kümmern zu müssen, dass
itemsizeinvolviert ist, oder - die neue Unterklasse setzt das Flag und behauptet, dass der Autor weiß, dass die Oberklasse geeignet ist (aber vielleicht noch nicht aktualisiert wurde, um das Flag zu verwenden).
Dieses Flag ist erforderlich, um variable Typen sicher zu erweitern.
Eine Alternative zu einem Flag wäre, von den Autoren von Unterklassen zu verlangen, dass sie wissen, dass die Basis ein kompatibles Layout verwendet (z. B. aus der Dokumentation). Eine frühere Version dieser PEP schlug dafür ein neues PyType_Slot vor. Dies erwies sich als schwer zu erklären und widerspricht der Idee, die Unterklasse vom Basis-Layout zu entkoppeln.
Das neue Flag wird verwendet, um die sichere Erweiterung von variablen Typen zu ermöglichen: die Erstellung eines Typs mit spec->basicsize < 0 und base->tp_itemsize > 0 erfordert das Flag.
Zusätzlich schlägt diese PEP eine Hilfsfunktion vor, um die variablen Daten einer gegebenen Instanz abzurufen, wenn diese das neue Flag Py_TPFLAGS_ITEMS_AT_END verwendet. Dies verbirgt die notwendige Zeigerarithmetik hinter einer API, die potenziell an andere Layouts in der Zukunft angepasst werden kann (einschließlich potenziell einem VM-verwalteten Layout).
Gesamtbild
Um die Überprüfung aller Fälle zu erleichtern, hier ein beunruhigend aussehender Entscheidungsbaum:
Hinweis
Die einzelnen Fälle sind isoliert leichter zu erklären (siehe die Referenzimplementierung für Entwurfsdokumente).
spec->basicsize > 0: Keine Änderung am Status quo. (Das Basisklassenlayout ist bekannt.)spec->basicsize == 0: (Erben der basicsize)base->tp_itemsize == 0: Die Item-Größe wird aufspec->tp_itemsizegesetzt. (Keine Änderung am Status quo.)base->tp_itemsize > 0: (Erweiterung einer Klasse mit variabler Größe)spec->itemsize == 0: Die Item-Größe wird geerbt. (Keine Änderung am Status quo.)spec->itemsize > 0: Die Item-Größe wird gesetzt. (Dies ist schwer sicher zu verwenden, aber es ist das aktuelle Verhalten von CPython.)
spec->basicsize < 0: (Erweiterung der basicsize)base->tp_itemsize == 0: (Erweiterung einer Klasse mit fester Größe)spec->itemsize == 0: Die Item-Größe wird auf 0 gesetzt.spec->itemsize > 0: Fehler. (Wir müssten eineob_sizehinzufügen, was nur für triviale Typen möglich ist – und das triviale Layout muss bekannt sein.)
base->tp_itemsize > 0: (Erweiterung einer Klasse mit variabler Größe)spec->itemsize == 0: (Erben der itemsize)Py_TPFLAGS_ITEMS_AT_ENDverwendet: itemsize wird geerbt.Py_TPFLAGS_ITEMS_AT_ENDnicht verwendet: Fehler. (Mögliche Kollision.)
spec->itemsize > 0: Fehler. (Die Änderung/Erweiterung der Item-Größe kann nicht sicher erfolgen.)
Das Setzen von spec->itemsize < 0 ist immer ein Fehler. Diese PEP schlägt keinen Mechanismus vor, um tp->itemsize zu erweitern, anstatt es nur zu erben.
Relative Member-Offsets
Ein weiteres Puzzleteil ist PyMemberDef.offset. Erweiterungen, die eine unterklassenspezifische struct (SubListState oben) verwenden, erhalten eine Möglichkeit, „relative“ Offsets (Offsets, die von dieser struct ausgehen) anstelle von „absoluten“ (basierend auf der PyObject struct) anzugeben.
Eine Möglichkeit wäre, „relative“ Offsets automatisch anzunehmen, wenn eine Klasse mit der neuen API erstellt wird. Diese implizite Annahme wäre jedoch zu überraschend.
Um expliziter zu sein, schlägt diese PEP ein neues Flag für „relative“ Offsets vor. Zumindest anfangs dient dieses Flag nur als Prüfung gegen Missbrauch (und als Hinweis für Gutachter). Es muss vorhanden sein, wenn es mit der neuen API verwendet wird, und darf sonst nicht verwendet werden.
Spezifikation
In den folgenden Codeblöcken sind nur die Funktionsköpfe Teil der Spezifikation. Anderer Code (die Größen-/Offsetberechnungen) sind Details der anfänglichen CPython-Implementierung und können geändert werden.
Relative basicsize
Das Mitglied basicsize von PyType_Spec darf null oder negativ sein. In diesem Fall gibt sein absoluter Wert an, wie viel *zusätzlicher* Speicherplatz Instanzen der neuen Klasse benötigen, zusätzlich zur basicsize der Basisklasse. Das heißt, die basicsize der resultierenden Klasse wird sein:
type->tp_basicsize = _align(base->tp_basicsize) + _align(-spec->basicsize);
wobei _align auf ein Vielfaches von alignof(max_align_t) aufrundet.
Wenn spec->basicsize Null ist, wird die basicsize direkt geerbt, d. h. auf base->tp_basicsize gesetzt, ohne Ausrichtung. (Dies funktioniert bereits; explizite Tests und Dokumentation werden hinzugefügt.)
Auf einer Instanz ist der spezifische Speicherbereich der Unterklasse – d. h. der „zusätzliche Speicher“, den die Unterklasse zusätzlich zu ihrer Basis reserviert – über eine neue Funktion, PyObject_GetTypeData, verfügbar. In CPython wird diese Funktion wie folgt definiert:
void *
PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls) {
return (char *)obj + _align(cls->tp_base->tp_basicsize);
}
Eine weitere Funktion wird hinzugefügt, um die Größe dieses Speicherbereichs abzurufen:
Py_ssize_t
PyType_GetTypeDataSize(PyTypeObject *cls) {
return cls->tp_basicsize - _align(cls->tp_base->tp_basicsize);
}
Das Ergebnis kann höher sein als durch -basicsize angefordert. Es ist sicher, alles zu verwenden (z. B. mit memset).
Die neuen *Get*-Funktionen haben einen wichtigen Vorbehalt, der in der Dokumentation darauf hingewiesen wird: Sie dürfen nur für Klassen verwendet werden, die mit negativer PyType_Spec.basicsize erstellt wurden. Für andere Klassen ist ihr Verhalten undefiniert. (Beachten Sie, dass dies ermöglicht, dass der obige Code annimmt, dass cls->tp_base nicht NULL ist.)
itemsize erben
Wenn spec->itemsize Null ist, wird tp_itemsize von der Basis geerbt. (Dies funktioniert bereits; explizite Tests und Dokumentation werden hinzugefügt.)
Ein neues Typ-Flag, Py_TPFLAGS_ITEMS_AT_END, wird hinzugefügt. Dieses Flag kann nur für Typen mit einer tp_itemsize ungleich Null gesetzt werden. Es zeigt an, dass der variable Teil einer Instanz am Ende des Speichers der Instanz gespeichert wird.
Der Standard-Metatyp (PyType_Type) setzt dieses Flag.
Eine neue Funktion, PyObject_GetItemData, wird hinzugefügt, um auf den für variable Inhalte von Typen mit dem neuen Flag reservierten Speicher zuzugreifen. In CPython wird sie wie folgt definiert:
void *
PyObject_GetItemData(PyObject *obj) {
if (!PyType_HasFeature(Py_TYPE(obj), Py_TPFLAGS_ITEMS_AT_END) {
<fail with TypeError>
}
return (char *)obj + Py_TYPE(obj)->tp_basicsize;
}
Diese Funktion wird zunächst nicht in die begrenzte API aufgenommen.
Das Erweitern einer Klasse mit positiver base->itemsize unter Verwendung einer negativen spec->basicsize schlägt fehl, es sei denn, Py_TPFLAGS_ITEMS_AT_END ist gesetzt, entweder auf der Basis oder in spec->flags. (Siehe Erweiterung von variablen Objekten für eine vollständige Erklärung.)
Das Erweitern einer Klasse mit positiver spec->itemsize unter Verwendung einer negativen spec->basicsize schlägt fehl.
Relative Member-Offsets
Bei Typen, die mit negativer PyType_Spec.basicsize definiert sind, müssen die Offsets der über Py_tp_members definierten Member relativ zu den zusätzlichen Unterklassendaten und nicht zur vollständigen PyObject struct sein. Dies wird durch ein neues Flag in PyMemberDef.flags angezeigt: Py_RELATIVE_OFFSET.
In der anfänglichen Implementierung ist das neue Flag redundant. Es dient nur dazu, die geänderte Bedeutung des Offsets klarzustellen und Fehler zu vermeiden. Es ist ein Fehler, Py_RELATIVE_OFFSET nicht mit negativer basicsize zu verwenden, und es ist ein Fehler, es in einem anderen Kontext zu verwenden (d. h. direkte oder indirekte Aufrufe von PyDescr_NewMember, PyMember_GetOne, PyMember_SetOne).
CPython passt den Offset an und löscht das Flag Py_RELATIVE_OFFSET bei der Initialisierung eines Typs. Das bedeutet, dass
- die
tp_membersdes erstellten Typs nicht mit demPy_tp_members-Slot der Eingabedefinition übereinstimmen und - jeglicher Code, der
tp_membersliest, das Flag nicht behandeln muss.
Liste der neuen API
Die folgenden neuen Funktionen/Werte werden vorgeschlagen.
Diese werden zur begrenzten API/Stable ABI hinzugefügt
void * PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls)Py_ssize_t PyType_GetTypeDataSize(PyTypeObject *cls)- Flag
Py_TPFLAGS_ITEMS_AT_ENDfürPyTypeObject.tp_flags - Flag
Py_RELATIVE_OFFSETfürPyMemberDef.flags
Diese werden nur zur öffentlichen C-API hinzugefügt
void *PyObject_GetItemData(PyObject *obj)
Abwärtskompatibilität
Es sind keine Rückwärtskompatibilitätsprobleme bekannt.
Annahmen
Die Implementierung geht davon aus, dass der Speicher einer Instanz zwischen type->tp_base->tp_basicsize und type->tp_basicsize-Offsets „gehört“ (außer bei variablen Typen). Dies ist nicht explizit dokumentiert, aber CPython bis Version 3.11 hat sich darauf verlassen, wenn __dict__ zu Unterklassen hinzugefügt wurde, daher sollte es sicher sein.
Sicherheitsimplikationen
Keine bekannt.
Zustimmungen
Der Autor von pybind11 hat ursprünglich darum gebeten, das Problem zu lösen (siehe Punkt 2 in dieser Liste) und verifiziert die Implementierung.
Florian vom HPy-Projekt sagte, dass die API im Allgemeinen gut aussieht. (Siehe unten für eine mögliche Lösung für Leistungsprobleme.)
Wie man das lehrt
Die anfängliche Implementierung wird Referenzdokumentation und einen „Was ist neu“-Eintrag enthalten, was für die Zielgruppe – Autoren von C-Erweiterungsbibliotheken – ausreichend sein sollte.
Referenzimplementierung
Eine Referenzimplementierung befindet sich im extend-opaque-Branch im encukou/cpython GitHub-Repository.
Mögliche zukünftige Verbesserungen
Ausrichtung & Leistung
Die vorgeschlagene Implementierung kann etwas Speicher verschwenden, wenn Instanz-Structs eine geringere Ausrichtung als alignof(max_align_t) benötigen. Auch die Behandlung von Ausrichtungen macht die Berechnung langsamer, als sie sein könnte, wenn wir uns auf die korrekt für den Subtyp ausgerichtete base->tp_basicsize verlassen könnten.
Mit anderen Worten, die vorgeschlagene Implementierung konzentriert sich auf Sicherheit und Benutzerfreundlichkeit und tauscht dafür Platz und Zeit. Wenn sich herausstellt, dass dies ein Problem ist, kann die Implementierung angepasst werden, ohne die API zu brechen.
- Der Offset zum typspezifischen Puffer kann gespeichert werden, sodass
PyObject_GetTypeDataeffektiv(char *)obj + cls->ht_typedataoffsetist, was die Leistung auf Kosten eines zusätzlichen Zeigers in der Klasse potenziell verbessert. - Dann kann ein neues
PyType_Slotdie gewünschte Ausrichtung angeben, um den Speicherbedarf für Instanzen zu reduzieren.
Andere Layouts für variable Typen
Ein Flag wie Py_TPFLAGS_ITEMS_AT_END könnte hinzugefügt werden, um das in Erweiterung von variablen Objekten beschriebene „tuple-ähnliche“ Layout anzuzeigen, und alle von dieser PEP vorgeschlagenen Mechanismen könnten angepasst werden, um es zu unterstützen. Andere Layouts könnten ebenfalls hinzugefügt werden. Es scheint jedoch nur sehr wenig praktischen Nutzen zu geben, daher ist es nur eine theoretische Möglichkeit.
Abgelehnte Ideen
Anstelle einer negativen spec->basicsize hätte ein neues PyType_Spec-Flag hinzugefügt werden können. Die Auswirkung wäre für bestehenden Code, der auf diese Interna ohne aktuelle Kenntnis der Änderung zugreift, dieselbe, da sich die Bedeutung des Feldwerts in dieser Situation ändert.
Fußnoten
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0697.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT