PEP 793 – PyModExport: Ein neuer Einstiegspunkt für C-Erweiterungsmodule
- Autor:
- Petr Viktorin <encukou at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Entwurf
- Typ:
- Standards Track
- Erstellt:
- 23-Mai-2025
- Python-Version:
- 3.15
- Post-History:
- 14-Mrz-2025, 27-Mai-2025
Zusammenfassung
In diesem PEP schlagen wir einen neuen Einstiegspunkt für C-Erweiterungsmodule vor, mit dem man ein Modul mithilfe eines Arrays von PyModuleDef_Slot Strukturen definieren kann, ohne eine umschließende PyModuleDef Struktur. Dies ermöglicht es Erweiterungsautoren, die Verwendung eines statisch allozierten PyObject zu vermeiden, und hebt das häufigste Hindernis auf, um eine kompilierte Bibliotheksdatei sowohl für reguläre als auch für freie Thread-Builds von CPython verwendbar zu machen.
Um dies zu ermöglichen, spezifizieren wir auch neue Modul-Slot-Typen, um die Felder von PyModuleDef zu ersetzen und das Hinzufügen eines Tokens zu ermöglichen, ähnlich dem Py_tp_token, das für Typobjekte verwendet wird.
Wir fügen auch eine API hinzu, um Module dynamisch aus Slots zu definieren.
Die bestehende API (PyInit_*) ist soft-deprecated. (Das heißt: sie wird weiterhin ohne Warnungen funktionieren, sie wird vollständig dokumentiert und unterstützt, aber wir planen nicht, ihr neue Funktionen hinzuzufügen.)
Hintergrund & Motivation
Das Speicherlayout von Python-Objekten unterscheidet sich zwischen regulären und freien Thread-Builds. Eine ABI, die sowohl reguläre als auch freie Thread-Builds unterstützt, kann daher nicht das aktuelle Speicherlayout von PyObject enthalten. Um mit der bestehenden ABI (und API) kompatibel zu bleiben, kann sie keine statisch allozierten Python-Objekte unterstützen.
Es gibt einen Typ von Objekt, der in den meisten Erweiterungsmodulen benötigt wird und in praktisch allen Fällen statisch alloziert wird: die PyModuleDef, die von den Modul-Export-Hooks (d. h. PyInit_*-Funktionen) zurückgegeben wird.
Modul-Export-Hooks (PyInit_*-Funktionen) können zwei Arten von Objekten zurückgeben
- Ein vollständig initialisiertes Modulobjekt (für sogenannte Single-Phase-Initialisierung). Dies war die einzige Option in 3.4 und darunter. Module, die auf diese Weise erstellt werden, haben überraschendes (aber abwärtskompatibles) Verhalten bezüglich mehrerer Interpreter oder wiederholtem Laden. (Insbesondere werden die Inhalte des
__dict__eines solchen Moduls über alle Instanzen des Modulobjekts hinweg geteilt.)Das zurückgegebene Modul wird typischerweise mit der Funktion
PyModule_Createerstellt, die eine statisch allozierte (oder zumindest langlebige)PyModuleDefStruktur benötigt.Es ist möglich, dies mit der Low-Level-API
PyModule_New*zu umgehen. Dies vermeidet die Notwendigkeit vonPyModuleDef, bietet aber deutlich weniger Funktionalität. - Ein
PyModuleDefObjekt, das eine Beschreibung zur Erstellung eines Modulobjekts enthält. Diese Option, die Multi-Phase-Initialisierung, wurde in PEP 489 eingeführt; siehe dessen Motivation, warum sie existiert.
Der Interpreter kann vor dem Aufruf des Export-Hooks nicht zwischen diesen Fällen unterscheiden.
Der Interpreter-Schalter
Python 3.12 fügte eine Möglichkeit für Module hinzu, zu kennzeichnen, ob sie in einem Subinterpreter geladen werden dürfen: der Slot Py_mod_multiple_interpreters. Das Setzen auf den Wert „nicht unterstützt“ signalisiert, dass eine Erweiterung nur im Hauptinterpreter geladen werden kann.
Leider kann Python diese Information nur erhalten, indem der Modul-Export-Hook aufgerufen wird. Für Single-Phase-Module erstellt dies das Modulobjekt und führt beliebigen Initialisierungscode aus. Für Module, die Py_mod_multiple_interpreters auf „nicht unterstützt“ setzen, muss diese Initialisierung im Hauptinterpreter stattfinden.
Damit dies funktioniert, wechselt Python, wenn ein neues Modul in einem Subinterpreter geladen wird, vorübergehend zum Hauptinterpreter, ruft dort den Export-Hook auf und wechselt dann entweder zurück und wiederholt den Import, oder schlägt fehl.
Diese unnötige und fragile Zusatzarbeit unterstreicht das zugrundeliegende Designproblem: Python hat keine Möglichkeit, Informationen über eine Erweiterung zu erhalten, bevor die Erweiterung sich möglicherweise vollständig initialisieren kann.
Begründung
Um zu vermeiden, dass der Modul-Export-Hook ein statisch alloziertes PyObject* benötigt, fallen zwei Optionen ein
- Zurückgeben eines dynamisch allozierten Objekts, dessen Eigentümerschaft an den Interpreter übertragen wird. Diese Struktur könnte der bestehenden
PyModuleDefsehr ähnlich sein, da sie die gleichen Daten enthalten muss. Im Gegensatz zur bestehendenPyModuleDefmüsste diese referenzgezählt sein, damit sie sowohl „ihr“ Modul überlebt als auch nicht leckt. - Hinzufügen eines neuen Export-Hooks, der kein
PyObject*zurückgibt.Dies wurde bereits für Python 3.5 in PEP 489 in Erwägung gezogen, aber abgelehnt.
Die Beibehaltung nur desPyInit-Hook-Namens, auch wenn er für den Export einer Definition nicht ganz passend ist, ergab eine viel einfachere Lösung.Leider ist die Lösung nach einem Jahrzehnt der Behebung der Auswirkungen dieser Wahl nicht mehr einfach.
Ein neuer Hook wird auch Python ermöglichen, das zweite in der Motivation erwähnte Problem zu vermeiden – den Interpreter-Schalter. Effektiv wird er eine neue Phase zur Multi-Phase-Initialisierung hinzufügen, in der Python prüfen kann, ob das Modul kompatibel ist.
Slots ohne Wrapper-Struktur verwenden
Die bestehende PyModuleDef ist eine Struktur mit einigen festen Feldern und einem „Slots“-Array. Im Gegensatz zu Slots können die festen Felder nicht einzeln als veraltet markiert und ersetzt werden. Dieser Vorschlag verzichtet auf feste Felder und schlägt die direkte Verwendung eines Slots-Arrays vor, ohne eine Wrapper-Struktur.
Die Struktur PyModuleDef_Slot hat im Vergleich zu festen Feldern einige Nachteile. Wir glauben, dass diese behoben werden können, belassen dies aber außerhalb des Umfangs dieses PEP (siehe „Verbesserung von Slots im Allgemeinen“ im Abschnitt „Mögliche zukünftige Richtungen“).
Token
Eine statische PyModuleDef hat neben der Beschreibung, wie ein Modul erstellt werden soll, einen weiteren Zweck. Als statisch alloziiertes Singleton, das an das Modulobjekt angehängt bleibt, ermöglicht es Erweiterungsautoren zu prüfen, ob ein bestimmtes Python-Modul „ihres“ ist: Wenn ein Modulobjekt eine bekannte PyModuleDef hat, hat sein Modulstatus ein bekanntes Speicherlayout.
Ein analoges Problem wurde für Typen durch Hinzufügen von Py_tp_token gelöst. Dieser Vorschlag fügt denselben Mechanismus zu Modulen hinzu.
Im Gegensatz zu Typen hat der Importmechanismus oft einen Zeiger, von dem bekannt ist, dass er als Token-Wert geeignet ist; in diesen Fällen kann er ein Standard-Token bereitstellen. Daher benötigen Modul-Token keine Variante des uneleganten Py_TP_USE_SPEC.
Um Erweiterungen zu helfen, die Python-Versionen überspannen, werden PyModuleDef-Adressen als Standard-Token verwendet, und wo es sinnvoll ist, werden sie mit Token austauschbar gemacht.
Soft-Deprecating des bestehenden Export-Hooks
Der einzige Grund für Autoren bestehender Erweiterungen, auf die hier vorgeschlagene API umzusteigen, ist, dass sie ein einziges Modul sowohl für freie als auch für nicht-freie Thread-Builds ermöglicht. Es ist wichtig, dass Python dies ermöglicht, aber für viele bestehende Module ist es bei weitem nicht wert, die Kompatibilität mit 3.14 und älteren Versionen zu verlieren.
Es ist noch viel zu früh, um die Deprecierung der alten API zu planen.
Stattdessen schlägt dieser PEP vor, keine neuen Funktionen mehr zum PyInit_*-Schema hinzuzufügen. Der perfekte Zeitpunkt für Erweiterungsautoren zum Wechsel ist schließlich, wenn sie die Modulinitialisierung sowieso ändern möchten.
Spezifikation
Der Export-Hook
Beim Importieren eines Erweiterungsmoduls sucht Python nun zuerst nach einem Export-Hook wie diesem
PyModuleDef_Slot *PyModExport_<NAME>(void);
wobei <NAME> der Name des Moduls ist. Für Nicht-ASCII-Namen sucht es stattdessen nach PyModExportU_<NAME>, wobei <NAME> wie bei bestehenden PyInitU_*-Hooks kodiert ist (d. h. punycode-kodiert mit Bindestrichen, die durch Unterstriche ersetzt sind).
Wenn nicht gefunden, wird der Import wie in früheren Python-Versionen fortgesetzt (d. h. durch Nachschlagen einer PyInit_*- oder PyInitU_*-Funktion).
Wenn gefunden, ruft Python den Hook ohne Argumente auf.
Bei einem Fehler muss der Export-Hook NULL zurückgeben und eine Ausnahme setzen. Dies führt zum Fehlschlag des Imports. (Python wird bei Fehlern nicht auf PyInit_* zurückgreifen.)
Bei Erfolg muss der Hook einen Zeiger auf ein Array von PyModuleDef_Slot Strukturen zurückgeben. Python erstellt dann ein Modul basierend auf den gegebenen Slots, indem es die unten vorgeschlagenen Funktionen aufruft: PyModule_FromSlotsAndSpec und PyModule_Exec. Ihre Beschreibung enthält Anforderungen an das Slot-Array.
Das zurückgegebene Array und alle von ihm referenzierten Daten (rekursiv) müssen bis zur Laufzeitabschaltung gültig und konstant bleiben. (Wir erwarten, dass Funktionen eine statische Konstante exportieren, oder eine von mehreren Konstanten, die z. B. von Py_Version abhängen. Dynamisches Verhalten sollte im Allgemeinen in den Funktionen Py_mod_create und Py_mod_exec erfolgen.)
Dynamische Erstellung
Eine neue Funktion wird hinzugefügt, um ein Modul aus einem Array von Slots zu erstellen
PyObject *PyModule_FromSlotsAndSpec(const PyModuleDef_Slot *slots, PyObject *spec)
Das Argument slots muss auf ein Array von PyModuleDef_Slot Strukturen zeigen, das durch einen Slot mit slot=0 (typischerweise als {0} in C geschrieben) terminiert wird. Es gibt keine erforderlichen Slots, obwohl slots nicht NULL sein dürfen. Daraus folgt, dass die minimale Eingabe nur den Terminator-Slot enthält.
Hinweis
Wenn PEP 803 akzeptiert wird, ist der Py_mod_abi Slot obligatorisch.
Das Argument spec ist ein duck-typisiertes ModuleSpec-ähnliches Objekt, d. h. alle Attribute, die für importlib.machinery.ModuleSpec definiert sind, haben eine übereinstimmende Semantik. Das Attribut name ist erforderlich, diese Einschränkung kann jedoch in Zukunft aufgehoben werden. Der name wird anstelle des Py_mod_name Slots verwendet (genau wie PyModule_FromDefAndSpec PyModuleDef.m_name ignoriert).
Die Slot-Arrays sowohl für PyModule_FromSlotsAndSpec als auch für den neuen Export-Hook erlauben nur bis zu einem Py_mod_exec Slot. Arrays in PyModuleDef.m_slots können mehr enthalten; dies wird sich nicht ändern. Diese Einschränkung ist leicht zu umgehen und mehrere exec Slots werden selten verwendet [1].
Für Module, die ohne eine PyModuleDef erstellt werden, wird die Funktion Py_mod_create mit NULL für das zweite Argument (def) aufgerufen. (In Zukunft, wenn wir einen Anwendungsfall für die Übergabe des Eingabe-Slots-Arrays finden, kann ein neuer Slot mit einer aktualisierten Signatur hinzugefügt werden.)
Im Gegensatz zum PyModExport_* Hook kann das slots-Array nach dem Aufruf von PyModule_FromSlotsAndSpec geändert oder zerstört werden. (Das heißt, Python muss eine Kopie aller Eingabedaten nehmen.) Eine Ausnahme bilden PyMethodDef-Arrays, die von Py_mod_methods bereitgestellt werden; diese müssen statisch alloziert sein (oder anderweitig garantiert so lange existieren, wie die daraus erstellten Objekte existieren). Diese Einschränkung kann in Zukunft aufgehoben werden.
Eine neue Funktion, PyModule_Exec, wird hinzugefügt, um die exec Slot(s) für ein Modul auszuführen. Dies verhält sich wie PyModule_ExecDef, unterstützt aber Module, die mithilfe von Slots erstellt wurden, und nimmt keine explizite def entgegen.
int PyModule_Exec(PyObject *module)
Der Aufruf hiervon ist erforderlich, um ein Modul vollständig zu initialisieren. PyModule_FromSlotsAndSpec wird ihn nicht ausführen (genau wie PyModule_FromDefAndSpec PyModule_ExecDef nicht aufruft).
Für Module, die aus einer def erstellt wurden, ist der Aufruf hiervon äquivalent zum Aufruf von PyModule_ExecDef(module, PyModule_GetDef(module)).
Token
Modulobjekte speichern optional ein „Token“: einen void*-Zeiger ähnlich dem Py_tp_token für Typen.
Hinweis
Dies ist eine spezialisierte Funktionalität, die die Funktion PyType_GetModuleByDef ersetzen soll; Benutzer, die PyType_GetModuleByDef nicht benötigen, werden wahrscheinlich auch keine Token benötigen.
Dieser Abschnitt enthält die technische Spezifikation; ein Beispiel für die beabsichtigte Verwendung finden Sie in exampletype_repr im Beispielabschnitt.
Wenn spezifiziert, mit einem neuen Py_mod_token Slot, muss das Modul-Token
- das Modul überleben, sodass es nicht wieder für etwas anderes verwendet wird, solange das Modul existiert; und
- zum Erweiterungsmodul „gehören“, in dem das Modul lebt, sodass es nicht mit anderen Erweiterungsmodulen kollidiert.
(Typischerweise sollte es das Slot-Array oder die PyModuleDef sein, aus der ein Modul erstellt wird, oder eine andere statische Konstante für dynamisch erstellte Module.)
Wenn die Adresse einer PyModuleDef als Token eines Moduls verwendet wird, sollte sich das Modul so verhalten, als wäre es aus dieser PyModuleDef erstellt worden. Insbesondere muss der Modulstatus übereinstimmendes Layout und Semantik aufweisen.
Module, die mit PyModule_FromSlotsAndSpec oder dem PyModExport_<NAME> Export-Hook erstellt wurden, können einen neuen Py_mod_token Slot verwenden, um das Token zu setzen.
Module, die aus einer PyModuleDef erstellt werden, erhalten das Token auf diese Definition gesetzt. Ein expliziter Py_mod_token Slot wird für diese zurückgewiesen. (Dies ermöglicht es Implementierungen, Speicher für das Token und die Def zu teilen.)
Für Module, die über den neuen Export-Hook erstellt werden, wird das Token standardmäßig auf die Adresse des Slot-Arrays gesetzt. (Dies gilt nicht für Module, die von PyModule_FromSlotsAndSpec erstellt werden, da die Eingabe dieser Funktion das Modul möglicherweise nicht überlebt.)
Das Token wird für keine Nicht-PyModuleType-Instanzen gesetzt.
Eine Funktion PyModule_GetToken wird hinzugefügt, um das Token abzurufen. Da das Ergebnis NULL sein kann, wird es über einen Zeiger übergeben; die Funktion gibt bei Erfolg 0 und bei Misserfolg -1 zurück.
int PyModule_GetToken(PyObject *, void **token_p)
Eine neue Funktion PyType_GetModuleByToken wird hinzugefügt, mit einer Signatur wie die bestehende PyType_GetModuleByDef, aber einem const void *token Argument und demselben Verhalten, außer dass sie Token anstelle von nur Defs abgleicht und eine starke Referenz zurückgibt.
Zur einfacheren Abwärtskompatibilität wird die bestehende Funktion PyType_GetModuleByDef so geändert, dass sie auch ein Modul-Token (als PyModuleDef *-Zeiger gecastet) als def-Argument akzeptiert. Das heißt, PyType_GetModuleByToken und PyType_GetModuleByDef unterscheiden sich nur in der formalen Signatur des zweiten Arguments und durch die Rückgabe einer geliehenen vs. starken Referenz. (Die Funktion PyModule_GetDef wird keine ähnliche Änderung erfahren, da Benutzer auf Mitglieder ihres Ergebnisses zugreifen können.)
Neue Slots
Für jedes Feld der Struktur PyModuleDef, außer denen von PyModuleDef_HEAD_INIT, wird eine neue Slot-ID bereitgestellt: Py_mod_name, Py_mod_doc, Py_mod_clear usw. Slots, die sich auf den Modulstatus und nicht auf das Modulobjekt beziehen, verwenden ein Präfix Py_mod_state_. Eine vollständige Liste finden Sie in Zusammenfassung der neuen API.
Alle neuen Slots – diese und der oben erwähnte Py_tp_token – dürfen nicht wiederholt im Slot-Array vorkommen und dürfen nicht in einem PyModuleDef.m_slots-Array verwendet werden. Sie dürfen keinen NULL-Wert haben (stattdessen kann der Slot ganz weggelassen werden).
Beachten Sie, dass derzeit für Module, die aus einem Spec erstellt werden (d. h. mit PyModule_FromDefAndSpec), das Feld PyModuleDef.m_name ignoriert und stattdessen der Name aus dem Spec verwendet wird. Alle in diesem Dokument vorgeschlagenen APIs erstellen Module aus einem Spec, und sie werden Py_mod_name auf die gleiche Weise ignorieren. Der Slot wird optional sein, aber Erweiterungsautoren wird dringend empfohlen, ihn für zukünftige APIs, externe Tools, Debugging und Introspektion einzuschließen.
Bits & Pieces
Ein Makro PyMODEXPORT_FUNC wird hinzugefügt, ähnlich dem Makro PyMODINIT_FUNC, aber mit PyModuleDef_Slot * als Rückgabetyp.
Eine Funktion PyModule_GetStateSize wird hinzugefügt, um die von Py_mod_state_size oder PyModuleDef.m_size gesetzte Größe abzurufen. Da das Ergebnis -1 sein kann (für Single-Phase-Init-Module), wird es über einen Zeiger ausgegeben; die Funktion gibt bei Erfolg 0 und bei Misserfolg -1 zurück.
int PyModule_GetStateSize(PyObject *, Py_ssize_t *result);
Soft-Deprecating des bestehenden Export-Hooks
Der PyInit_* Export-Hook wird soft-deprecated.
Zusammenfassung der neuen API
Python wird einen neuen Modul-Export-Hook laden, mit zwei Varianten
PyModuleDef_Slot *PyModExport_<NAME>(void);
PyModuleDef_Slot *PyModExportU_<ENCODED_NAME>(void);
Die folgenden Funktionen werden hinzugefügt
PyObject *PyModule_FromSlotsAndSpec(const PyModuleDef_Slot *, PyObject *spec)
int PyModule_Exec(PyObject *)
int PyModule_GetToken(PyObject *, void**)
PyObject *PyType_GetModuleByToken(PyTypeObject *type, const void *token)
int PyModule_GetStateSize(PyObject *, Py_ssize_t *result);
Ein neues Makro wird hinzugefügt
PyMODEXPORT_FUNC
Und neue Slot-Typen (#define-Namen für kleine Ganzzahlen)
Py_mod_name(äquivalent zuPyModuleDef.m_name)Py_mod_doc(äquivalent zuPyModuleDef.m_doc)Py_mod_state_size(äquivalent zuPyModuleDef.m_size)Py_mod_methods(äquivalent zuPyModuleDef.m_methods)Py_mod_state_traverse(äquivalent zuPyModuleDef.m_traverse)Py_mod_state_clear(äquivalent zuPyModuleDef.m_clear)Py_mod_state_free(äquivalent zuPyModuleDef.m_free)Py_mod_token(siehe oben)
All dies wird zur Limited API hinzugefügt.
Abwärtskompatibilität
Wenn ein bestehendes Modul für den neuen Mechanismus portiert wird, gibt PyModule_GetDef für dieses Modul NULL zurück. (Dies entspricht der aktuellen Dokumentation von PyModule_GetDef.) Wir behaupten, dass die Art und Weise, wie ein Modul definiert wurde, ein Implementierungsdetail dieses Moduls ist, daher sollte dies nicht als Breaking Change betrachtet werden.
Ähnlich kann die Funktion PyType_GetModuleByDef aufhören, Module abzugleichen, deren Definition sich geändert hat. Modulautoren können dies vermeiden, indem sie explizit ein def als token setzen.
PyType_GetModuleByDef akzeptiert nun ein Modul-Token als def-Argument. Wir legen eine geeignete Einschränkung für die Verwendung von PyModuleDef-Adressen als Token fest, und Nicht-PyModuleDef-Zeiger waren zuvor ungültige Eingaben, daher ist dies kein Rückwärtskompatibilitätsproblem.
Die Funktion Py_mod_create kann nun mit NULL für das zweite Argument aufgerufen werden. Dies könnte Personen, die von def zu slots portieren, in die Irre führen, daher muss dies in den Portierungshinweisen erwähnt werden.
Vorwärtskompatibilität
Wenn ein Modul den neuen Export-Hook definiert, ignorieren CPython-Versionen, die diesen PEP implementieren, den traditionellen PyInit_* Hook.
Erweiterungen, die Python-Versionen überspannen, sollen beide Hooks definieren; jeder Build von CPython „wählt“ den neuesten, den er unterstützt.
Portierungsleitfaden
Hier ist ein Leitfaden zur Konvertierung eines bestehenden Moduls in die neue API, einschließlich einiger kniffliger Randfälle. Er sollte in ein HOWTO in der Dokumentation verschoben werden.
Dieser Leitfaden richtet sich an handgeschriebene Module. Für Codegeneratoren und Sprach-Wrapper ist der Abwärtskompatibilitäts-Shim unten möglicherweise nützlicher.
- Scannen Sie Ihren Code nach Verwendungen von
PyModule_GetDef. Diese Funktion gibtNULLfür Module zurück, die den neuen Mechanismus verwenden. Stattdessen- Um den Inhalt von
PyModuleDefdes Moduls zu erhalten, verwenden Sie die C-Struktur direkt. Alternativ können Sie Attribute aus dem Modul abrufen, indem Sie z. B.PyModule_GetNameObject, das Attribut__doc__undPyModule_GetStateSizeverwenden. (Beachten Sie, dass Python-Code die Attribute eines Moduls mutieren kann.) - Um zu testen, ob ein Modulobjekt „Ihr eigenes“ ist, verwenden Sie stattdessen
PyModule_GetToken. Später in diesem Leitfaden setzen Sie das Token so, dass es die bestehendePyModuleDef-Struktur ist.
- Um den Inhalt von
- Optional können Sie Ihren Code nach Verwendungen von
PyType_GetModuleByDefdurchsuchen und diese durchPyType_GetModuleByTokenersetzen. Später in diesem Leitfaden setzen Sie das Token so, dass es die bestehendePyModuleDef-Struktur ist.(Sie können diesen Schritt überspringen, wenn Sie auf Python-Versionen abzielen, die
PyType_GetModuleByTokennicht bereitstellen, daPyType_GetModuleByDefabwärtskompatibel ist.) - Schauen Sie sich die Funktion an, die von
Py_mod_createidentifiziert wird, falls vorhanden. Stellen Sie sicher, dass sie ihr zweites Argument (PyModuleDef) nicht verwendet, da sie mitNULLaufgerufen wird. Verwenden Sie anstelle des Arguments die bestehendePyModuleDef-Struktur direkt. - Wenn Sie mehrere
Py_mod_execSlots verwenden, konsolidieren Sie diese: Wählen Sie eine der Funktionen oder schreiben Sie eine neue und rufen Sie die anderen von dort aus auf. Entfernen Sie alle außer einemPy_mod_execSlot. - Erstellen Sie eine Kopie des bestehenden
PyModuleDef_Slot-Arrays, auf das durch das Feldm_slotsIhrerPyModuleDefverwiesen wird. Wenn Sie kein bestehendes Slot-Array haben, erstellen Sie eines wie folgt:static PyModuleDef_Slot module_slots[] = { {0} };
Geben Sie diesem Array einen eindeutigen Namen. Weitere Beispiele gehen davon aus, dass Sie es
module_slotsgenannt haben. - Fügen Sie Slots für alle Mitglieder der bestehenden
PyModuleDef-Struktur hinzu. Eine Liste der neuen Slots finden Sie in Zusammenfassung der neuen API. Zum Beispiel, um einen Namen und eine Docstring hinzuzufügen:static PyModuleDef_Slot module_slots[] = { {Py_mod_name, "mymodule"}, {Py_mod_doc, (char*)PyDoc_STR("my docstring")}, // ... (keep existing slots here) {0} };
- Wenn Sie von
PyModule_GetDefzuPyModule_GetTokengewechselt haben und/oderPyType_GetModuleByDefoderPyType_GetModuleByTokenverwenden, fügen Sie einenPy_mod_tokenSlot hinzu, der auf die bestehendePyModuleDef-Struktur verweist.static PyModuleDef_Slot module_slots[] = { // ... (keep existing slots here) {Py_mod_token, &your_module_def}, {0} };
- Fügen Sie einen neuen Export-Hook hinzu.
PyMODEXPORT_FUNC PyModExport_examplemodule(PyObject); PyMODEXPORT_FUNC PyModExport_examplemodule(void) { return module_slots; }
Der neue Export-Hook wird unter Python 3.15 und höher verwendet. Sobald Ihr Modul keine älteren Versionen mehr unterstützt:
- Löschen Sie die
PyInit_-Funktion. - Wenn die bestehende
PyModuleDef-Struktur nur fürPy_mod_tokenund/oderPyType_GetModuleByTokenverwendet wird, können Sie die ZeilePy_mod_tokenentfernen und&your_module_defüberall anders durchmodule_slotsersetzen. - Löschen Sie ungenutzte Daten. Die
PyModuleDef-Struktur und das ursprüngliche Slot-Array sind wahrscheinlich ungenutzt.
Abwärtskompatibilitäts-Shim
Es ist möglich, eine generische Funktion zu schreiben, die den „alten“ Export-Hook (PyInit_) mithilfe der hier vorgeschlagenen API implementiert.
Die folgende Implementierung kann kopiert und in ein Projekt eingefügt werden; nur die Namen PyInit_examplemodule (zweimal) und PyModExport_examplemodule müssen angepasst werden.
Wenn es zu dem unten stehenden Beispiel hinzugefügt und mit einer nicht-free-threaded Build dieser PEP-Referenzimplementierung kompiliert wird, ist die resultierende Erweiterung kompatibel mit Nicht-Free-Threading 3.9+-Builds, zusätzlich zu einem Free-Threading-Build der Referenzimplementierung. (Das Modul muss ohne Versionskennzeichnung benannt werden, z. B. examplemodule.so, und auf sys.path liegen.)
Vollständige Unterstützung für die Erstellung solcher Module erfordert Backports einiger neuer APIs und Unterstützung in Build-/Installationswerkzeugen. Dies liegt außerhalb des Rahmens dieser PEP. (Insbesondere "schummeln" die Demos, indem sie eine Teilmenge der Limited API 3.15 verwenden, die zufällig unter 3.9 funktioniert; eine ordnungsgemäße Implementierung würde Limited API 3.9 mit Backport-Shims für neue APIs wie Py_mod_name verwenden.)
Diese Implementierung stellt einige zusätzliche Anforderungen an das Slot-Array.
- Slots, die
PyModuleDef-Mitgliedern entsprechen, müssen zuerst kommen. - Ein
Py_mod_name-Slot ist erforderlich. - Jeder
Py_mod_tokenmuss auf&module_def_and_tokengesetzt werden, das hier definiert ist.
#include <string.h> // memset
PyMODINIT_FUNC PyInit_examplemodule(void);
static PyModuleDef module_def_and_token;
PyMODINIT_FUNC
PyInit_examplemodule(void)
{
PyModuleDef_Slot *slot = PyModExport_examplemodule();
if (module_def_and_token.m_name) {
// Take care to only set up the static PyModuleDef once.
// (PyModExport might theoretically return different data each time.)
return PyModuleDef_Init(&module_def_and_token);
}
int copying_slots = 1;
for (/* slot set above */; slot->slot; slot++) {
switch (slot->slot) {
// Set PyModuleDef members from slots. These slots must come first.
# define COPYSLOT_CASE(SLOT, MEMBER, TYPE) \
case SLOT: \
if (!copying_slots) { \
PyErr_SetString(PyExc_SystemError, \
#SLOT " must be specified earlier"); \
goto error; \
} \
module_def_and_token.MEMBER = (TYPE)(slot->value); \
break; \
/////////////////////////////////////////////////////////////////
COPYSLOT_CASE(Py_mod_name, m_name, char*)
COPYSLOT_CASE(Py_mod_doc, m_doc, char*)
COPYSLOT_CASE(Py_mod_state_size, m_size, Py_ssize_t)
COPYSLOT_CASE(Py_mod_methods, m_methods, PyMethodDef*)
COPYSLOT_CASE(Py_mod_state_traverse, m_traverse, traverseproc)
COPYSLOT_CASE(Py_mod_state_clear, m_clear, inquiry)
COPYSLOT_CASE(Py_mod_state_free, m_free, freefunc)
case Py_mod_token:
// With PyInit_, the PyModuleDef is used as the token.
if (slot->value != &module_def_and_token) {
PyErr_SetString(PyExc_SystemError,
"Py_mod_token must be set to "
"&module_def_and_token");
goto error;
}
break;
default:
// The remaining slots become m_slots in the def.
// (`slot` now points to the "rest" of the original
// zero-terminated array.)
if (copying_slots) {
module_def_and_token.m_slots = slot;
}
copying_slots = 0;
break;
}
}
if (!module_def_and_token.m_name) {
// This function needs m_name as the "is initialized" marker.
PyErr_SetString(PyExc_SystemError, "Py_mod_name slot is required");
goto error;
}
return PyModuleDef_Init(&module_def_and_token);
error:
memset(&module_def_and_token, 0, sizeof(module_def_and_token));
return NULL;
}
Sicherheitsimplikationen
Keine bekannt
Wie man das lehrt
Zusätzlich zu den regulären Referenzdokumenten sollte der Porting-Leitfaden als neues HOWTO hinzugefügt werden.
Beispiel
/*
Example module with C-level module-global state, and
- a simple function that updates and queries the state
- a class wihose repr() queries the same module state (as an example of
PyType_GetModuleByToken)
Once compiled and renamed to not include a version tag (for example
examplemodule.so on Linux), this will run succesfully on both regular
and free-threaded builds.
Python usage:
import examplemodule
print(examplemodule.increment_value()) # 0
print(examplemodule.increment_value()) # 1
print(examplemodule.increment_value()) # 2
print(examplemodule.increment_value()) # 3
class Subclass(examplemodule.ExampleType):
pass
instance = Subclass()
print(instance) # <Subclass object; module value = 3>
*/
// Avoid CPython-version-specific ABI (inline functions & macros):
#define Py_LIMITED_API 0x030f0000 // 3.15
#include <Python.h>
typedef struct {
int value;
} examplemodule_state;
static PyModuleDef_Slot examplemodule_slots[];
// increment_value function
static PyObject *
increment_value(PyObject *module, PyObject *_ignored)
{
examplemodule_state *state = PyModule_GetState(module);
int result = ++(state->value);
return PyLong_FromLong(result);
}
static PyMethodDef examplemodule_methods[] = {
{"increment_value", increment_value, METH_NOARGS},
{NULL}
};
// ExampleType
static PyObject *
exampletype_repr(PyObject *self)
{
/* To get module state, we cannot use PyModule_GetState(Py_TYPE(self)),
* since Py_TYPE(self) might be a subclass defined in an unrelated module.
* So, use PyType_GetModuleByToken.
*/
PyObject *module = PyType_GetModuleByToken(
Py_TYPE(self), examplemodule_slots);
if (!module) {
return NULL;
}
examplemodule_state *state = PyModule_GetState(module);
Py_DECREF(module);
if (!state) {
return NULL;
}
return PyUnicode_FromFormat("<%T object; module value = %d>",
self, state->value);
}
static PyType_Spec exampletype_spec = {
.name = "examplemodule.ExampleType",
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.slots = (PyType_Slot[]) {
{Py_tp_repr, exampletype_repr},
{0},
},
};
// Module
static int
examplemodule_exec(PyObject *module) {
examplemodule_state *state = PyModule_GetState(module);
state->value = -1;
PyTypeObject *type = (PyTypeObject*)PyType_FromModuleAndSpec(
module, &exampletype_spec, NULL);
if (!type) {
return -1;
}
if (PyModule_AddType(module, type) < 0) {
Py_DECREF(type);
return -1;
}
Py_DECREF(type);
return 0;
}
PyDoc_STRVAR(examplemodule_doc, "Example extension.");
static PyModuleDef_Slot examplemodule_slots[] = {
{Py_mod_name, "examplemodule"},
{Py_mod_doc, (char*)examplemodule_doc},
{Py_mod_methods, examplemodule_methods},
{Py_mod_state_size, (void*)sizeof(examplemodule_state)},
{Py_mod_exec, (void*)examplemodule_exec},
{0}
};
// Avoid "implicit declaration of function" warning:
PyMODEXPORT_FUNC PyModExport_examplemodule(void);
PyMODEXPORT_FUNC
PyModExport_examplemodule(void)
{
return examplemodule_slots;
}
Referenzimplementierung
Eine Entwurfsimplementierung ist in einem GitHub-Branch verfügbar.
Offene Fragen
(Fügen Sie Ihre hinzu!)
Abgelehnte Ideen
Exportieren eines Datenzeigers anstelle einer Funktion
Dies schlägt eine neue Modul-Export-*Funktion* vor, die statische konstante Daten zurückgeben soll. Diese Daten könnten direkt als Datenzeiger exportiert werden.
Mit einer Funktion vermeiden wir den Umgang mit einer neuen Art von exportiertem Symbol.
Eine Funktion ermöglicht es der Erweiterung auch, ihre Umgebung auf begrenzte Weise zu introspektieren – zum Beispiel, um die zurückgegebenen Daten an die aktuelle Python-Version anzupassen.
Ändern von PyModuleDef, sodass es kein PyObject mehr ist
Es ist möglich, PyModuleDef so zu ändern, dass es nicht mehr den PyObject-Header enthält und den aktuellen PyInit_*-Hook weiter verwendet. Es gibt mehrere Probleme mit diesem Ansatz.
- Die Import-Maschinerie müsste Bit-Muster in den Objekten untersuchen, um zwischen verschiedenen Speicherlayouts zu unterscheiden.
- das "alte"
PyObject-basiertePyModuleDef, das von aktuellenabi3-Erweiterungen zurückgegeben wird, - das neue
PyModuleDef, PyObject-basierte Modulobjekte für die Einphaseninitialisierung.
Dies ist fehleranfällig und schränkt zukünftige Änderungen an
PyObjectein: Die Speicherlayouts müssen unterscheidbar bleiben, bis sowohl die Einphaseninitialisierung als auch die aktuelle Stable ABI nicht mehr unterstützt werden. - das "alte"
PyModuleDef_Initist dokumentiert, um "sicherzustellen, dass eine Moduldefinition ein ordnungsgemäß initialisiertes Python-Objekt ist, das seinen Typ und eine Referenzanzahl korrekt meldet." Dies müsste sich ohne Vorwarnung ändern und würde jeglichen Benutzercode brechen, derPyModuleDefs als Python-Objekte behandelt.
Mögliche zukünftige Richtungen
Diese Ideen liegen außerhalb des Rahmens dieses Vorschlags.
Verbesserung von Slots im Allgemeinen
Slots – und insbesondere der bestehende PyModuleDef_Slot – haben einige Mängel. Die wichtigsten sind:
- Typsicherheit:
void *wird für Datenzeiger, Funktionszeiger und kleine Ganzzahlen verwendet, was Casts erfordert, die technisch undefiniertes Verhalten in C darstellen – aber in der Praxis auf allen relevanten Architekturen funktionieren. (Zum Beispiel:Py_tp_docmarkiert einen String;Py_mod_gileine Ganzzahl.) - Begrenzte Vorwärtskompatibilität: Wenn eine Erweiterung eine Slot-ID bereitstellt, die dem aktuellen Interpreter unbekannt ist, schlägt die Modulerstellung fehl. Dies macht die Verwendung von "optionalen" Funktionen – solchen, die nur dann wirksam werden sollen, wenn der Interpreter sie unterstützt – umständlich. (Die kürzlich hinzugefügten Slots
Py_mod_gilundPy_mod_multiple_interpreterssind gute Beispiele.)Eine Abhilfemaßnahme ist,
Py_Versionin der Exportfunktion zu überprüfen und ein für den aktuellen Interpreter geeignetes Slot-Array zurückzugeben.
Aktualisierung von Standardwerten
Mit einer neuen API könnten wir Standardwerte für die Slots Py_mod_multiple_interpreters und Py_mod_gil aktualisieren.
Der Inittab
Wir müssen PyModuleDef-lose Slots im inittab zulassen – das heißt, eine neue Variante von PyImport_ExtendInittab hinzufügen. Sollte das Teil dieser PEP sein?
Der inittab wird für das Embedding verwendet, wo eine gemeinsame/stabile ABI nicht sehr wichtig ist. Es könnte also in Ordnung sein, dies einer späteren Änderung zu überlassen.
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-0793.rst
Zuletzt geändert: 2025-10-16 14:03:05 GMT