Erweiterungsmodule isolieren¶
Für wen das gedacht ist¶
Diese Anleitung richtet sich an Betreuer von C-API-Erweiterungen, die ihre Erweiterung sicherer machen wollen für Anwendungen, in denen Python selbst als Bibliothek verwendet wird.
Hintergrund¶
Ein Interpreter ist der Zusammenhang, in dem Python-Code läuft. Er umfasst Konfiguration (etwa den Importpfad) und Laufzeitzustand (etwa die Menge der importierten Module).
Python kann mehrere Interpreter in einem Prozess laufen lassen. Dabei sind zwei Fälle zu bedenken – Benutzer können Interpreter laufen lassen:
hintereinander, mit mehreren
Py_InitializeEx()/Py_FinalizeEx()-Durchläufen, undparallel, indem sie mit
Py_NewInterpreter()/Py_EndInterpreter()„Unterinterpreter“ verwalten.
Beide Fälle (und Verbindungen davon) sind am nützlichsten, wenn Python in eine Bibliothek eingebettet wird. Bibliotheken sollten im Allgemeinen keine Annahmen über die Anwendung treffen, die sie verwendet – dazu gehört die Annahme eines prozessweiten „Haupt-Python-Interpreters“.
Historisch gehen Python-Erweiterungsmodule mit diesem Anwendungsfall nicht gut um. Viele Erweiterungsmodule (und sogar einige Module der Standardbibliothek) verwenden globalen Zustand je Prozess, weil C-static-Variablen ausgesprochen leicht zu verwenden sind. So landen Daten, die einem Interpreter eigen sein sollten, gemeinsam genutzt bei allen Interpretern. Sofern der Entwickler der Erweiterung nicht achtgibt, ist es sehr leicht, Grenzfälle einzuschleppen, die zu Abstürzen führen, wenn ein Modul in mehr als einem Interpreter im selben Prozess geladen wird.
Zustand je Interpreter ist leider nicht leicht zu erreichen. Autoren von Erweiterungen denken beim Entwickeln meist nicht an mehrere Interpreter, und das Verhalten zu testen ist derzeit umständlich.
Auftritt: Zustand je Modul¶
Anstatt sich auf Zustand je Interpreter zu stützen, entwickelt sich die C-API von Python so, dass sie den feinkörnigeren Zustand je Modul besser unterstützt. Das heißt, dass Daten auf C-Ebene an ein Modulobjekt gebunden werden sollten. Jeder Interpreter erzeugt sein eigenes Modulobjekt und hält die Daten damit getrennt. Um die Isolation zu testen, lassen sich sogar mehrere Modulobjekte derselben Erweiterung in einem einzigen Interpreter laden.
Zustand je Modul bietet einen einfachen Weg, über Lebensdauer und Besitz von Ressourcen nachzudenken: Das Erweiterungsmodul initialisiert sich, wenn ein Modulobjekt erzeugt wird, und räumt auf, wenn es freigegeben wird. In dieser Hinsicht ist ein Modul wie jedes andere PyObject*. Es gibt keine Haken für „beim Herunterfahren des Interpreters“, an die man denken – oder die man vergessen – muss.
Beachte, dass es Anwendungsfälle für verschiedene Arten von „Globalen“ gibt: Zustand je Prozess, je Interpreter, je Thread oder je Aufgabe. Mit Zustand je Modul als Vorgabe sind diese weiter möglich, du solltest sie aber als Ausnahmefälle behandeln: Brauchst du sie, solltest du ihnen zusätzliche Aufmerksamkeit und Tests widmen. (Beachte, dass diese Anleitung sie nicht behandelt.)
Isolated Module Objects¶
Der wichtigste Punkt, den du beim Entwickeln eines Erweiterungsmoduls im Kopf behalten solltest, ist: Aus einer einzigen dynamischen Bibliothek können mehrere Modulobjekte erzeugt werden. Zum Beispiel:
>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii # create a new module object
>>> old_binascii == binascii
False
Als Daumenregel sollten die beiden Module vollständig unabhängig sein. Alle Objekte und aller Zustand, die dem Modul eigen sind, sollten im Modulobjekt eingekapselt sein, nicht mit anderen Modulobjekten geteilt werden und aufgeräumt werden, wenn das Modulobjekt freigegeben wird. Weil das nur eine Daumenregel ist, sind Ausnahmen möglich (siehe Managing Global State), sie brauchen aber mehr Überlegung und Aufmerksamkeit für Grenzfälle.
Manche Module kämen mit weniger strengen Einschränkungen aus, aber isolierte Module machen es leichter, klare Erwartungen und Richtlinien aufzustellen, die über eine Vielzahl von Anwendungsfällen hinweg funktionieren.
Überraschende Grenzfälle¶
Beachte, dass isolierte Module einige überraschende Grenzfälle mit sich bringen. Am auffälligsten ist, dass jedes Modulobjekt seine Klassen und Ausnahmen üblicherweise nicht mit anderen ähnlichen Modulen teilt. Im Anschluss an das Beispiel oben ist zu beachten, dass old_binascii.Error und binascii.Error getrennte Objekte sind. Im folgenden Code wird die Ausnahme nicht gefangen:
>>> old_binascii.Error == binascii.Error
False
>>> try:
... old_binascii.unhexlify(b'qwertyuiop')
... except binascii.Error:
... print('boo')
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
binascii.Error: Non-hexadecimal digit found
Das ist zu erwarten. Beachte, dass reine Python-Module sich genauso verhalten: Es gehört dazu, wie Python arbeitet.
Das Ziel ist, Erweiterungsmodule auf C-Ebene sicher zu machen, nicht, Tricks naheliegend zu machen. sys.modules „von Hand“ zu verändern, zählt als Trick.
Module mit mehreren Interpretern sicher machen¶
Managing Global State¶
Manchmal ist der Zustand, der zu einem Python-Modul gehört, nicht diesem Modul eigen, sondern dem ganzen Prozess (oder etwas anderem, das „globaler“ ist als ein Modul). Zum Beispiel:
Das Modul
readlineverwaltet das Terminal.Ein Modul, das auf einer Platine läuft, will die LED auf der Platine steuern.
In diesen Fällen sollte das Python-Modul Zugriff auf den globalen Zustand bieten, anstatt ihn zu besitzen. Schreibe das Modul, wenn möglich, so, dass mehrere Kopien davon unabhängig voneinander auf den Zustand zugreifen können (ebenso wie andere Bibliotheken, ob für Python oder für andere Sprachen). Ist das nicht möglich, erwäge ausdrückliches Sperren.
Wenn prozessglobaler Zustand nötig ist, lassen sich Probleme mit mehreren Interpretern am einfachsten vermeiden, indem du ausdrücklich verhinderst, dass ein Modul mehr als einmal je Prozess geladen wird – siehe Verzicht: Auf ein Modulobjekt je Prozess beschränken.
Zustand je Modul verwalten¶
Um Zustand je Modul zu verwenden, nimm die mehrphasige Initialisierung von Erweiterungsmodulen. Damit zeigst du an, dass dein Modul mehrere Interpreter korrekt unterstützt.
Setze PyModuleDef.m_size auf eine positive Zahl, um so viele Bytes modullokalen Speicher anzufordern. Üblicherweise wird das auf die Größe einer modulspezifischen struct gesetzt, die den gesamten Zustand des Moduls auf C-Ebene aufnehmen kann. Dort solltest du insbesondere Zeiger auf Klassen (einschließlich Ausnahmen, aber ohne statische Typen) und Einstellungen ablegen (etwa field_size_limit von csv), die der C-Code zum Arbeiten braucht.
Bemerkung
Eine andere Möglichkeit ist, den Zustand im __dict__ des Moduls zu halten, aber du musst dann Abstürze vermeiden, wenn Benutzer __dict__ aus Python-Code verändern. Das bedeutet üblicherweise Fehler- und Typprüfungen auf C-Ebene, die man leicht falsch macht und schwer ausreichend testen kann.
Wird der Modulzustand im C-Code allerdings nicht gebraucht, ist es eine gute Idee, ihn nur im __dict__ zu halten.
Enthält der Modulzustand PyObject-Zeiger, muss das Modulobjekt Referenzen auf diese Objekte halten und die Haken auf Modulebene m_traverse, m_clear und m_free umsetzen. Diese arbeiten wie tp_traverse, tp_clear und tp_free einer Klasse. Sie zu ergänzen bedeutet etwas Arbeit und macht den Code länger. Das ist der Preis für Module, die sich sauber entladen lassen.
Ein Beispiel für ein Modul mit Zustand je Modul ist derzeit als xxlimited verfügbar. Die Beispiel-Initialisierung des Moduls steht am Ende der Datei.
Verzicht: Auf ein Modulobjekt je Prozess beschränken¶
Ein nicht negatives PyModuleDef.m_size zeigt an, dass ein Modul mehrere Interpreter korrekt unterstützt. Ist das bei deinem Modul noch nicht der Fall, kannst du es ausdrücklich nur einmal je Prozess ladbar machen. Zum Beispiel:
// A process-wide flag
static int loaded = 0;
// Mutex to provide thread safety (only needed for free-threaded Python)
static PyMutex modinit_mutex = {0};
static int
exec_module(PyObject* module)
{
PyMutex_Lock(&modinit_mutex);
if (loaded) {
PyMutex_Unlock(&modinit_mutex);
PyErr_SetString(PyExc_ImportError,
"cannot load module more than once per process");
return -1;
}
loaded = 1;
PyMutex_Unlock(&modinit_mutex);
// ... rest of initialization
}
Wenn die Funktion PyModuleDef.m_clear deines Moduls auf eine künftige erneute Initialisierung vorbereiten kann, sollte sie das Flag loaded zurücksetzen. In diesem Fall unterstützt dein Modul keine mehreren Instanzen, die gleichzeitig bestehen, aber es unterstützt zum Beispiel, nach dem Herunterfahren der Python-Laufzeit (Py_FinalizeEx()) und erneuter Initialisierung (Py_Initialize()) geladen zu werden.
Zugriff auf den Modulzustand aus Funktionen¶
Auf den Zustand aus Funktionen auf Modulebene zuzugreifen, ist einfach. Funktionen bekommen das Modulobjekt als erstes Argument. Um den Zustand herauszuholen, kannst du PyModule_GetState verwenden:
static PyObject *
func(PyObject *module, PyObject *args)
{
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
// ... rest of logic
}
Bemerkung
PyModule_GetState kann NULL zurückgeben, ohne eine Ausnahme zu setzen, wenn es keinen Modulzustand gibt, also PyModuleDef.m_size null war. In deinem eigenen Modul hast du m_size in der Hand, das lässt sich also leicht verhindern.
Halden-Typen¶
Herkömmlich sind in C-Code definierte Typen statisch, also static PyTypeObject-Strukturen, die unmittelbar im Code definiert und mit PyType_Ready() initialisiert werden.
Solche Typen werden notwendigerweise prozessweit geteilt. Sie zwischen Modulobjekten zu teilen bedeutet, auf jeden Zustand achtzugeben, den sie besitzen oder auf den sie zugreifen. Um die möglichen Probleme zu begrenzen, sind statische Typen auf Python-Ebene unveränderlich: Du kannst zum Beispiel nicht str.myattribute = 123 setzen.
Wirklich unveränderliche Objekte zwischen Interpretern zu teilen ist in Ordnung, solange sie keinen Zugriff auf veränderliche Objekte bieten. In CPython hat jedes Python-Objekt jedoch ein veränderliches Implementierungsdetail: den Referenzzähler. Änderungen am Referenzzähler sind durch den GIL abgesichert. Code, der irgendwelche Python-Objekte über Interpreter hinweg teilt, hängt damit implizit vom aktuellen, prozessweiten GIL von CPython ab.
Weil sie unveränderlich und prozessglobal sind, können statische Typen nicht auf „ihren“ Modulzustand zugreifen. Wenn irgendeine Methode eines solchen Typs Zugriff auf den Modulzustand braucht, muss der Typ in einen auf der Halde angelegten Typ umgewandelt werden, kurz Halden-Typ. Diese entsprechen Klassen, die mit der class-Anweisung von Python erzeugt werden, deutlich genauer.
Für neue Module ist es eine gute Daumenregel, standardmäßig Halden-Typen zu verwenden.
Statische Typen in Halden-Typen umwandeln¶
Statische Typen lassen sich in Halden-Typen umwandeln, aber beachte, dass die API für Halden-Typen nicht für eine „verlustfreie“ Umwandlung aus statischen Typen entworfen wurde – also dafür, einen Typ zu erzeugen, der sich genau wie ein gegebener statischer Typ verhält. Wenn du die Klassendefinition in einer neuen API neu schreibst, änderst du daher wahrscheinlich unbeabsichtigt ein paar Details (etwa die Pickle-Fähigkeit oder geerbte Slots). Teste immer die Details, die dir wichtig sind.
Achte besonders auf die folgenden zwei Punkte (aber beachte, dass dies keine vollständige Liste ist):
Anders als statische Typen sind Halden-Typobjekte standardmäßig veränderlich. Verwende das Flag
Py_TPFLAGS_IMMUTABLETYPE, um die Veränderlichkeit zu verhindern.Halden-Typen erben standardmäßig
tp_new, es kann also möglich werden, sie aus Python-Code zu erzeugen. Das kannst du mit dem FlagPy_TPFLAGS_DISALLOW_INSTANTIATIONverhindern.
Halden-Typen definieren¶
Halden-Typen lassen sich erzeugen, indem du eine PyType_Spec-Struktur füllst – eine Beschreibung oder „Blaupause“ einer Klasse – und PyType_FromModuleAndSpec() aufrufst, um ein neues Klassenobjekt zu bauen.
Bemerkung
Andere Funktionen wie PyType_FromSpec() können ebenfalls Halden-Typen erzeugen, aber PyType_FromModuleAndSpec() verknüpft das Modul mit der Klasse und erlaubt damit den Zugriff auf den Modulzustand aus Methoden.
Die Klasse sollte im Allgemeinen sowohl im Modulzustand (für den sicheren Zugriff aus C) als auch im __dict__ des Moduls (für den Zugriff aus Python-Code) abgelegt werden.
Das Protokoll der Speicherbereinigung¶
Instanzen von Halden-Typen halten eine Referenz auf ihren Typ. Das stellt sicher, dass der Typ nicht vor allen seinen Instanzen zerstört wird, kann aber zu Referenzzyklen führen, die die Speicherbereinigung auflösen muss.
Um Speicherlecks zu vermeiden, müssen Instanzen von Halden-Typen das Protokoll der Speicherbereinigung umsetzen. Halden-Typen sollten also:
das Flag
Py_TPFLAGS_HAVE_GChaben.eine traverse-Funktion über
Py_tp_traversedefinieren, die den Typ besucht (etwa überPy_VISIT(Py_TYPE(self))).
Weitere Überlegungen findest du in der Dokumentation zu Py_TPFLAGS_HAVE_GC und tp_traverse.
Die API zum Definieren von Halden-Typen ist organisch gewachsen, wodurch sie in ihrem heutigen Zustand etwas unhandlich ist. Die folgenden Abschnitte führen dich durch die verbreiteten Probleme.
tp_traverse in Python 3.8 und älter¶
Die Anforderung, den Typ aus tp_traverse zu besuchen, wurde in Python 3.9 ergänzt. Wenn du Python 3.8 und älter unterstützt, darf die traverse-Funktion den Typ nicht besuchen, sie muss also komplizierter sein:
static int my_traverse(PyObject *self, visitproc visit, void *arg)
{
if (Py_Version >= 0x03090000) {
Py_VISIT(Py_TYPE(self));
}
return 0;
}
Leider wurde Py_Version erst in Python 3.11 ergänzt. Verwende als Ersatz:
PY_VERSION_HEX, sofern du nicht das stabile ABI verwendest, odersys.version_info(überPySys_GetObject()undPyArg_ParseTuple()).
tp_traverse weitergeben¶
Wenn deine traverse-Funktion an tp_traverse ihrer Basisklasse (oder eines anderen Typs) weitergibt, stelle sicher, dass Py_TYPE(self) nur einmal besucht wird. Beachte, dass nur von Halden-Typen erwartet wird, den Typ in tp_traverse zu besuchen.
Wenn deine traverse-Funktion zum Beispiel Folgendes enthält:
base->tp_traverse(self, visit, arg)
… und base ein statischer Typ sein kann, dann sollte sie außerdem Folgendes enthalten:
if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) {
// a heap type's tp_traverse already visited Py_TYPE(self)
} else {
if (Py_Version >= 0x03090000) {
Py_VISIT(Py_TYPE(self));
}
}
Den Referenzzähler des Typs in tp_new und tp_clear zu behandeln, ist nicht nötig.
tp_dealloc definieren¶
Wenn dein Typ eine eigene tp_dealloc-Funktion hat, muss sie:
PyObject_GC_UnTrack()aufrufen, bevor irgendwelche Felder ungültig werden, undden Referenzzähler des Typs verringern.
Damit der Typ gültig bleibt, während tp_free aufgerufen wird, muss der Referenzzähler des Typs nach der Freigabe der Instanz verringert werden. Zum Beispiel:
static void my_dealloc(PyObject *self)
{
PyObject_GC_UnTrack(self);
...
PyTypeObject *type = Py_TYPE(self);
type->tp_free(self);
Py_DECREF(type);
}
Die Standardfunktion tp_dealloc tut das, wenn dein Typ tp_dealloc also nicht überschreibt, brauchst du es nicht zu ergänzen.
tp_free nicht überschreiben¶
Der Slot tp_free eines Halden-Typs muss auf PyObject_GC_Del() gesetzt sein. Das ist die Vorgabe. Überschreibe sie nicht.
PyObject_New vermeiden¶
Objekte, die von der Speicherbereinigung verfolgt werden, müssen über Funktionen angefordert werden, die die Speicherbereinigung kennen.
Wenn du PyObject_New() oder PyObject_NewVar() verwendest:
Hole und rufe, wenn möglich, den Slot
tp_allocdes Typs auf. Ersetze alsoTYPE *o = PyObject_New(TYPE, typeobj)durch:TYPE *o = typeobj->tp_alloc(typeobj, 0);
Ersetze
o = PyObject_NewVar(TYPE, typeobj, size)durch dasselbe, verwende aber size anstelle der 0.Ist das oben Genannte nicht möglich (etwa innerhalb eines eigenen
tp_alloc), rufePyObject_GC_New()oderPyObject_GC_NewVar()auf:TYPE *o = PyObject_GC_New(TYPE, typeobj); TYPE *o = PyObject_GC_NewVar(TYPE, typeobj, size);
Zugriff auf den Modulzustand aus Klassen¶
Wenn du ein Typobjekt hast, das mit PyType_FromModuleAndSpec() definiert wurde, kannst du PyType_GetModule() aufrufen, um das zugehörige Modul zu bekommen, und dann PyModule_GetState(), um den Zustand des Moduls zu bekommen.
Um ein paar Zeilen mühsamen Fehlerbehandlungscodes zu sparen, kannst du diese zwei Schritte mit PyType_GetModuleState() verbinden. Das ergibt:
my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (state == NULL) {
return NULL;
}
Zugriff auf den Modulzustand aus gewöhnlichen Methoden¶
Auf den Zustand auf Modulebene aus Methoden einer Klasse zuzugreifen, ist etwas komplizierter, aber dank einer in Python 3.9 eingeführten API möglich. Um an den Zustand zu kommen, musst du zuerst die definierende Klasse holen und daraus dann den Modulzustand.
Die größte Hürde ist, die Klasse zu bekommen, in der eine Methode definiert wurde, kurz die „definierende Klasse“ dieser Methode. Die definierende Klasse kann eine Referenz auf das Modul haben, zu dem sie gehört.
Verwechsle die definierende Klasse nicht mit Py_TYPE(self). Wird die Methode auf einer Unterklasse deines Typs aufgerufen, verweist Py_TYPE(self) auf diese Unterklasse, die in einem anderen Modul als deinem definiert sein kann.
Bemerkung
Der folgende Python-Code kann das Konzept veranschaulichen. Base.get_defining_class gibt Base zurück, selbst wenn type(self) == Sub ist:
class Base:
def get_type_of_self(self):
return type(self)
def get_defining_class(self):
return __class__
class Sub(Base):
pass
Damit eine Methode ihre „definierende Klasse“ bekommt, muss sie die Aufrufkonvention METH_METHOD | METH_FASTCALL | METH_KEYWORDS und die entsprechende PyCMethod-Signatur verwenden:
PyObject *PyCMethod(
PyObject *self, // object the method was called on
PyTypeObject *defining_class, // defining class
PyObject *const *args, // C array of arguments
Py_ssize_t nargs, // length of "args"
PyObject *kwnames) // NULL, or dict of keyword arguments
Sobald du die definierende Klasse hast, rufe PyType_GetModuleState() auf, um den Zustand des zugehörigen Moduls zu bekommen.
Zum Beispiel:
static PyObject *
example_method(PyObject *self,
PyTypeObject *defining_class,
PyObject *const *args,
Py_ssize_t nargs,
PyObject *kwnames)
{
my_struct *state = (my_struct*)PyType_GetModuleState(defining_class);
if (state == NULL) {
return NULL;
}
... // rest of logic
}
PyDoc_STRVAR(example_method_doc, "...");
static PyMethodDef my_methods[] = {
{"example_method",
(PyCFunction)(void(*)(void))example_method,
METH_METHOD|METH_FASTCALL|METH_KEYWORDS,
example_method_doc}
{NULL},
}
Zugriff auf den Modulzustand aus Slot-Methoden, Lese- und Schreibfunktionen¶
Bemerkung
Das ist neu in Python 3.11.
Slot-Methoden – die schnellen C-Gegenstücke zu den besonderen Methoden, etwa nb_add für __add__ oder tp_new für die Initialisierung – haben eine sehr einfache API, die es anders als bei PyCMethod nicht erlaubt, die definierende Klasse hineinzugeben. Dasselbe gilt für Lese- und Schreibfunktionen, die mit PyGetSetDef definiert werden.
Um in diesen Fällen auf den Modulzustand zuzugreifen, verwende die Funktion PyType_GetModuleByDef() und gib die Moduldefinition hinein. Sobald du das Modul hast, rufe PyModule_GetState() auf, um an den Zustand zu kommen:
PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &module_def);
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
PyType_GetModuleByDef() arbeitet, indem es die Methodenauflösungsreihenfolge (also alle Oberklassen) nach der ersten Oberklasse durchsucht, die ein zugehöriges Modul hat.
Bemerkung
In sehr exotischen Fällen (Vererbungsketten über mehrere Module hinweg, die aus derselben Definition erzeugt wurden) gibt PyType_GetModuleByDef() möglicherweise nicht das Modul der wirklich definierenden Klasse zurück. Es gibt aber immer ein Modul mit derselben Definition zurück und stellt damit eine kompatible C-Speicheranordnung sicher.
Lebensdauer des Modulzustands¶
Wird ein Modulobjekt von der Speicherbereinigung eingesammelt, wird sein Modulzustand freigegeben. Für jeden Zeiger auf (einen Teil des) Modulzustand musst du eine Referenz auf das Modulobjekt halten.
Üblicherweise ist das kein Problem, weil Typen, die mit PyType_FromModuleAndSpec() erzeugt wurden, und ihre Instanzen eine Referenz auf das Modul halten. Du musst bei der Referenzzählung aber achtgeben, wenn du den Modulzustand von anderen Stellen aus referenzierst, etwa aus Rückrufen für externe Bibliotheken.
Offene Fragen¶
Rund um Zustand je Modul und Halden-Typen sind noch mehrere Fragen offen.
Erörterungen darüber, wie sich die Lage verbessern lässt, finden am besten im Diskussionsforum unter dem Schlagwort c-api statt.
Gültigkeitsbereich je Klasse¶
Derzeit (Stand Python 3.11) ist es nicht möglich, Zustand an einzelne Typen zu binden, ohne sich auf Implementierungsdetails von CPython zu stützen (die sich künftig ändern können – vielleicht, ironischerweise, um eine saubere Lösung für den Gültigkeitsbereich je Klasse zu erlauben).
Verlustfreie Umwandlung in Halden-Typen¶
Die API für Halden-Typen wurde nicht für eine „verlustfreie“ Umwandlung aus statischen Typen entworfen, also dafür, einen Typ zu erzeugen, der sich genau wie ein gegebener statischer Typ verhält.