Isolering av tilläggsmoduler

Vem bör läsa detta

Den här guiden är skriven för underhållare av C-API-tillägg som vill göra det tillägget säkrare att använda i applikationer där Python självt används som ett bibliotek.

Bakgrund

En tolk är det sammanhang i vilket Python-koden körs. Den innehåller konfiguration (t.ex. importsökvägen) och runtime-tillstånd (t.ex. uppsättningen importerade moduler).

Python stöder körning av flera tolkar i en process. Det finns två fall att tänka på - användare kan köra tolkar:

Båda fallen (och kombinationer av dem) skulle vara mest användbara när Python bäddas in i ett bibliotek. Bibliotek bör i allmänhet inte göra antaganden om den applikation som använder dem, vilket inkluderar att anta en processomfattande ”huvud-Python-tolk”.

Historiskt sett har Pythons tilläggsmoduler inte hanterat detta användningsfall väl. Många tilläggsmoduler (och till och med vissa stdlib-moduler) använder per-process globalt tillstånd, eftersom C static variabler är extremt lätta att använda. Således delas data som borde vara specifika för en tolk i slutändan mellan tolkarna. Om inte tilläggsutvecklaren är försiktig är det mycket lätt att införa kantfall som leder till krascher när en modul laddas i mer än en tolk i samma process.

Tyvärr är det inte lätt att uppnå per-tolk-tillstånd. Författarna till tillägg tenderar att inte ha flera tolkar i åtanke när de utvecklar, och det är för närvarande besvärligt att testa beteendet.

Ange tillstånd per modul

Istället för att fokusera på tillstånd per tolk utvecklas Pythons C API för att bättre stödja det mer granulerade per-modul-tillståndet. Detta innebär att data på C-nivå bör kopplas till ett modulobjekt. Varje tolk skapar sitt eget modulobjekt och håller data åtskilda. För att testa isoleringen kan flera modulobjekt som motsvarar ett enda tillägg till och med laddas i en enda tolk.

Per-modulstatus ger ett enkelt sätt att tänka på livstid och resursägande: tilläggsmodulen initieras när ett modulobjekt skapas och städas upp när det frigörs. I det här avseendet är en modul precis som alla andra PyObject*; det finns inga ”on interpreter shutdown”-hooks att tänka på - eller glömma bort.

Observera att det finns användningsområden för olika typer av ”globaler”: per-process-, per-tolk-, per-tråd- eller per-task-tillstånd. Med per-modul-tillstånd som standard är dessa fortfarande möjliga, men du bör behandla dem som undantagsfall: om du behöver dem bör du ge dem extra omsorg och testning. (Observera att den här guiden inte täcker dem)

Isolerade modulobjekt

Det som är viktigt att tänka på när man utvecklar en tilläggsmodul är att flera modulobjekt kan skapas från ett enda delat bibliotek. Till exempel

>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii  # create a new module object
>>> old_binascii == binascii
False

En tumregel är att de två modulerna ska vara helt oberoende av varandra. Alla objekt och tillstånd som är specifika för modulen ska vara inkapslade i modulobjektet, inte delas med andra modulobjekt och rensas upp när modulobjektet avallokeras. Eftersom detta bara är en tumregel är undantag möjliga (se Managing Global State), men de kommer att kräva mer eftertanke och uppmärksamhet på kantfall.

Även om vissa moduler skulle kunna klara sig med mindre stränga restriktioner, gör isolerade moduler det lättare att fastställa tydliga förväntningar och riktlinjer som fungerar i en mängd olika användningsfall.

Överraskande specialfall

Observera att isolerade moduler skapar en del överraskande marginalfall. Framför allt kommer varje modulobjekt vanligtvis inte att dela sina klasser och undantag med andra liknande moduler. Fortsätt från exemplet ovan, notera att old_binascii.Error och binascii.Error är separata objekt. I följande kod fångas undantaget inte upp:

>>> 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

Detta är förväntat. Lägg märke till att rena Python-moduler beter sig på samma sätt: det är en del av hur Python fungerar.

Målet är att göra tilläggsmoduler säkra på C-nivå, inte att få hack att bete sig intuitivt. Att mutera sys.modules ”manuellt” räknas som ett hack.

Säkra moduler med flera tolkar

Att hantera den globala staten

Ibland är det tillstånd som är kopplat till en Python-modul inte specifikt för den modulen, utan för hela processen (eller något annat ”mer globalt” än en modul). Ett exempel:

  • Modulen readline hanterar terminalen.

  • En modul som körs på ett kretskort vill styra den inbyggda LED-lampan.

I dessa fall bör Python-modulen ge åtkomst till det globala tillståndet, snarare än att äga det. Om möjligt, skriv modulen så att flera kopior av den kan komma åt tillståndet oberoende av varandra (tillsammans med andra bibliotek, oavsett om det är för Python eller andra språk). Om det inte är möjligt bör man överväga explicit låsning.

Om det är nödvändigt att använda processglobalt tillstånd är det enklaste sättet att undvika problem med flera tolkar att uttryckligen förhindra att en modul laddas mer än en gång per process - se Välj bort: Begränsning till ett modulobjekt per process.

Hantering av tillstånd per modul

Om du vill använda tillstånd per modul använder du multiphase extension module initialization. Detta signalerar att din modul stöder flera tolkar på rätt sätt.

Sätt PyModuleDef.m_size till ett positivt tal för att begära så många byte lagringsutrymme lokalt för modulen. Vanligtvis kommer detta att sättas till storleken på någon modulspecifik struct, som kan lagra allt modulens C-nivåtillstånd. I synnerhet är det där du bör lägga pekare till klasser (inklusive undantag, men exklusive statiska typer) och inställningar (t.ex. csv field_size_limit) som C-koden behöver för att fungera.

Anteckning

Ett annat alternativ är att lagra tillståndet i modulens __dict__, men du måste undvika att krascha när användare ändrar __dict__ från Python-kod. Detta innebär vanligtvis fel- och typkontroll på C-nivå, vilket är lätt att göra fel och svårt att testa tillräckligt.

Om modulstatus inte behövs i C-kod är det dock en bra idé att lagra den i __dict__.

Om modultillståndet innehåller pekare av typen PyObject måste modulobjektet innehålla referenser till dessa objekt och implementera modulnivåkrokarna m_traverse, m_clear och m_free. Dessa fungerar som tp_traverse, tp_clear och tp_free för en klass. Att lägga till dem kommer att kräva en del arbete och göra koden längre; detta är priset för moduler som kan avlastas på ett rent sätt.

Ett exempel på en modul med tillstånd per modul finns för närvarande tillgängligt som xxlimited; exempel på modulinitialisering visas längst ner i filen.

Välj bort: Begränsning till ett modulobjekt per process

En icke-negativ PyModuleDef.m_size signalerar att en modul har korrekt stöd för flera tolkar. Om detta ännu inte är fallet för din modul, kan du uttryckligen göra din modul laddningsbar endast en gång per process. Till exempel:

// 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
}

Om din moduls PyModuleDef.m_clear-funktion kan förbereda för framtida återinitialisering, bör den rensa loaded-flaggan. I det här fallet kommer din modul inte att stödja flera instanser som existerar samtidigt, men den kommer till exempel att stödja laddning efter Python-körtidsavstängning (Py_FinalizeEx()) och ominitialisering (Py_Initialize()).

Modulstatusåtkomst från funktioner

Det är enkelt att komma åt tillståndet från funktioner på modulnivå. Funktioner får modulobjektet som sitt första argument; för att extrahera tillståndet kan du använda PyModule_GetState:

statiskt PyObjekt *
func(PyObject *modul, PyObject *args)
{
    my_struct *state = (my_struct*)PyModule_GetState(module);
    if (tillstånd == NULL) {
        returnera NULL;
    }
    // ... resten av logiken
}

Anteckning

PyModule_GetState kan returnera NULL utan att sätta ett undantag om det inte finns något modultillstånd, dvs. PyModuleDef.m_size var noll. I din egen modul har du kontroll över m_size, så det här är lätt att förhindra.

Typer av högar

Traditionellt är typer som definieras i C-kod statiska, det vill säga static PyTypeObject -strukturer som definieras direkt i koden och initialiseras med PyType_Ready().

Sådana typer delas nödvändigtvis över hela processen. Att dela dem mellan modulobjekt kräver att man är uppmärksam på alla tillstånd som de äger eller har åtkomst till. För att begränsa de möjliga problemen är statiska typer oföränderliga på Python-nivå: du kan till exempel inte ställa in str.myattribute = 123.

Att dela verkligt oföränderliga objekt mellan tolkar är bra, så länge de inte ger åtkomst till föränderliga objekt. I CPython har dock varje Python-objekt en föränderlig implementeringsdetalj: referensantalet. Ändringar av referensantalet skyddas av GIL. Kod som delar Python-objekt mellan olika tolkar är således implicit beroende av CPythons aktuella, processomfattande GIL.

Eftersom de är oföränderliga och processglobala kan statiska typer inte komma åt ”sitt” modultillstånd. Om någon metod av en sådan typ kräver åtkomst till modultillståndet måste typen konverteras till en heap-allokerad typ, eller heap-typ förkortat. Dessa motsvarar mer klasser som skapas av Pythons class statement.

För nya moduler är det en bra tumregel att använda heap-typer som standard.

Ändra statiska typer till heap-typer

Statiska typer kan konverteras till heap-typer, men observera att API:et för heap-typer inte utformades för ”förlustfri” konvertering från statiska typer - det vill säga att skapa en typ som fungerar exakt som en given statisk typ. Så när du skriver om klassdefinitionen i ett nytt API kommer du sannolikt att oavsiktligt ändra några detaljer (t.ex. pickleability eller ärvda slots). Testa alltid de detaljer som är viktiga för dig.

Var särskilt uppmärksam på följande två punkter (men observera att detta inte är en uttömmande lista):

  • Till skillnad från statiska typer är objekt av typen heap muterbara som standard. Använd flaggan Py_TPFLAGS_IMMUTABLETYPE för att förhindra föränderlighet.

  • Heap-typer ärver tp_new som standard, så det kan bli möjligt att instansiera dem från Python-kod. Du kan förhindra detta med flaggan Py_TPFLAGS_DISALLOW_INSTANTIATION.

Definiera heap-typer

Heap-typer kan skapas genom att fylla en PyType_Spec-struktur, en beskrivning eller ”blueprint” av en klass, och anropa PyType_FromModuleAndSpec() för att konstruera ett nytt klassobjekt.

Anteckning

Andra funktioner, som PyType_FromSpec(), kan också skapa heap-typer, men PyType_FromModuleAndSpec() associerar modulen med klassen, vilket ger tillgång till modultillståndet från metoder.

Klassen bör i allmänhet lagras i både modultillståndet (för säker åtkomst från C) och modulens __dict__ (för åtkomst från Python-kod).

Protokoll för insamling av sopor

Instanser av heap-typer håller en referens till sin typ. Detta säkerställer att typen inte förstörs innan alla dess instanser har förstörts, men kan resultera i referenscykler som måste brytas av skräpsamlaren.

För att undvika minnesläckage måste instanser av heap-typer implementera garbage collection-protokollet. Det vill säga, heap-typer bör:

  • Har Py_TPFLAGS_HAVE_GC flaggan.

  • Definiera en traversefunktion med hjälp av Py_tp_traverse, som besöker typen (t.ex. med Py_VISIT(Py_TYPE(self))).

Se dokumentationen för Py_TPFLAGS_HAVE_GC och tp_traverse för ytterligare information.

API:et för att definiera heap-typer växte organiskt, vilket gör att det är lite besvärligt att använda i sitt nuvarande tillstånd. Följande avsnitt kommer att vägleda dig genom vanliga problem.

tp_traverse i Python 3.8 och lägre

Kravet på att besöka typen från tp_traverse lades till i Python 3.9. Om du stöder Python 3.8 och lägre får traverse-funktionen inte besöka typen, så det måste vara mer komplicerat:

static int my_traverse(PyObject *self, visitproc visit, void *arg)
{
    if (Py_Version >= 0x03090000) {
        Py_VISIT(Py_TYPE(self));
    }
    return 0;
}

Tyvärr lades Py_Version till först i Python 3.11. Som ersättning kan du använda:

Delegering av tp_traverse

Om din traversefunktion delegerar till tp_traverse i sin basklass (eller en annan typ), se till att Py_TYPE(self) bara besöks en gång. Observera att endast heap-typer förväntas besöka typen i tp_traverse.

Till exempel, om din traversefunktion inkluderar:

base->tp_traverse(self, besök, arg)

…och base kan vara en statisk typ, då bör den också innehålla:

if (bas->tp_flags & Py_TPFLAGS_HEAPTYPE) {
    // en heap-typs tp_traverse har redan besökt Py_TYPE(self)
} else {
    if (Py_Version >= 0x03090000) {
        Py_VISIT(Py_TYPE(self));
    }
}

Det är inte nödvändigt att hantera typens referensantal i tp_new och tp_clear.

Definiera tp_dealloc

Om din typ har en anpassad tp_dealloc-funktion måste den göra det:

  • anropa PyObject_GC_UnTrack() innan några fält ogiltigförklaras, och

  • decimera referensantalet för typen.

För att hålla typen giltig medan tp_free anropas måste typens refcount minskas efter att instansen har deallokerats. Till exempel:

statiskt void my_dealloc(PyObject *self)
{
    PyObject_GC_UnTrack(self);
    ...
    PyTypeObject *type = Py_TYPE(self);
    type->tp_free(self);
    Py_DECREF(typ);
}

Standardfunktionen tp_dealloc gör detta, så om din typ inte åsidosätter tp_dealloc behöver du inte lägga till den.

Inte åsidosätta tp_free

Slot tp_free för en heap-typ måste vara inställd på PyObject_GC_Del(). Detta är standardvärdet; åsidosätt det inte.

Undvika PyObject_New

GC-spårade objekt måste allokeras med hjälp av GC-medvetna funktioner.

Om du använder PyObject_New() eller PyObject_NewVar():

  • Hämta och anropa typens tp_alloc slot, om möjligt. Det vill säga, ersätt TYPE *o = PyObject_New(TYPE, typeobj) med:

    TYPE *o = typeobj->tp_alloc(typeobj, 0);
    

    Ersätt o = PyObject_NewVar(TYPE, typeobj, size) med samma sak, men använd size istället för 0.

  • Om ovanstående inte är möjligt (t.ex. inuti en anpassad tp_alloc), anropa PyObject_GC_New() eller PyObject_GC_NewVar():

    TYPE *o = PyObject_GC_New(TYPE, typeobj);
    
    TYPE *o = PyObject_GC_NewVar(TYPE, typeobj, size);
    

Modul Tillståndsåtkomst från klasser

Om du har ett typobjekt som definierats med PyType_FromModuleAndSpec() kan du anropa PyType_GetModule() för att hämta den associerade modulen och sedan PyModule_GetState() för att hämta modulens tillstånd.

För att spara lite tråkig boilerplate-kod för felhantering kan du kombinera dessa två steg med PyType_GetModuleState(), vilket resulterar i:

my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (tillstånd == NULL) {
    returnera NULL;
}

Modul Tillgång till tillstånd från reguljära metoder

Att komma åt tillståndet på modulnivå från metoder i en klass är något mer komplicerat, men det är möjligt tack vare API som introducerades i Python 3.9. För att få tillståndet måste du först hämta den definierande klassen och sedan hämta modultillståndet från den.

Det största hindret är att få fram den klass som en metod definierades i, eller kort och gott metodens ”definierande klass”. Den definierande klassen kan ha en referens till den modul den är en del av.

Förväxla inte den definierande klassen med Py_TYPE(self). Om metoden anropas på en subklass av din typ, kommer Py_TYPE(self) att referera till den subklassen, som kan vara definierad i en annan modul än din.

Anteckning

Följande Python-kod kan illustrera konceptet. Base.get_defining_class returnerar Base även om type(self) == Sub:

klass Bas:
    def get_type_of_self(self):
        return typ(själv)

    def get_defining_class(self):
        return __class__

klass Sub(Bas):
    pass

För att en metod ska få sin ”definierande klass” måste den använda METH_METHOD | METH_FASTCALL | METH_KEYWORDS calling convention och motsvarande PyCMethod-signatur:

PyObject *PyCMetod(
    PyObject *self, // objektet som metoden anropades på
    PyTypeObject *defining_class, // definierande klass
    PyObject *const *args, // C-array med argument
    Py_ssize_t nargs, // längd på "args"
    PyObject *kwnames) // NULL, eller dikt av nyckelordsargument

När du har den definierande klassen kan du anropa PyType_GetModuleState() för att få fram tillståndet för den associerade modulen.

Till exempel:

statiskt PyObject *
example_method(PyObject *self,
        PyTypeObject *definierande_klass,
        PyObject *konst *args,
        Py_ssize_t nargs,
        PyObject *kwnamn)
{
    my_struct *state = (my_struct*)PyType_GetModuleState(defining_class);
    if (state == NULL) {
        returnera NULL;
    }
    ... // resten av logiken
}

PyDoc_STRVAR(example_method_doc, "...");

statiska PyMethodDef my_methods[] = {
    {"example_method",
      (PyCFunction)(void(*)(void))exempel_method,
      METH_METHOD|METH_FASTCALL|METH_KEYWORDS,
      exempel_metod_dokument}
    {NULL},
}

Modul State Access från Slot Methods, Getters och Setters

Anteckning

Detta är nytt i Python 3.11.

Slot methods - de snabba C-ekvivalenterna för specialmetoder, som nb_add för __add__ eller tp_new för initialisering - har ett mycket enkelt API som inte tillåter att den definierande klassen skickas in, till skillnad från PyCMethod. Detsamma gäller för getters och setters som definieras med PyGetSetDef.

För att komma åt modultillståndet i dessa fall använder du funktionen PyType_GetModuleByDef() och skickar in moduldefinitionen. När du har modulen, anropa PyModule_GetState() för att hämta tillståndet:

PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &module_def);
my_struct *state = (my_struct*)PyModule_GetState(module);
if (tillstånd == NULL) {
    return NULL;
}

PyType_GetModuleByDef() fungerar genom att söka i method resolution order (dvs. alla superklasser) efter den första superklassen som har en motsvarande modul.

Anteckning

I mycket exotiska fall (arvskedjor som sträcker sig över flera moduler som skapats från samma definition) kanske PyType_GetModuleByDef() inte returnerar modulen för den verkliga definierande klassen. Den kommer dock alltid att returnera en modul med samma definition, vilket garanterar en kompatibel C-minneslayout.

Modulens livslängd Status

När ett modulobjekt garbage-collected frigörs dess modultillstånd. För varje pekare till (en del av) modultillståndet måste du hålla en referens till modulobjektet.

Vanligtvis är detta inte ett problem, eftersom typer som skapats med PyType_FromModuleAndSpec(), och deras instanser, innehåller en referens till modulen. Du måste dock vara försiktig med referensräkningen när du refererar till modulstatus från andra platser, t.ex. callbacks för externa bibliotek.

Öppna problem

Flera problem kring tillstånd per modul och heap-typer är fortfarande öppna.

Diskussioner om hur man kan förbättra situationen förs bäst på discuss forum under c-api tag.

Scope per klass

Det är för närvarande (från och med Python 3.11) inte möjligt att koppla tillstånd till enskilda typer utan att förlita sig på CPython-implementeringsdetaljer (vilket kan ändras i framtiden - kanske, ironiskt nog, för att möjliggöra en korrekt lösning för per-class scope).

Förlustfri konvertering till Heap-typer

API:et för heap-typer är inte utformat för ”förlustfri” konvertering från statiska typer, det vill säga att skapa en typ som fungerar exakt som en given statisk typ.