Globale Variable in DLL



  • Hallo zusammen,

    ich habe den Compiler vom klassischen Borland-Compiler auf den neueren clang-enhanced Compiler umgestellt. Nun beobachte ich ein Verhalten, das ich mir nicht ganz erklären kann. Ich hoffe auf eure Hilfe.

    Ausgangspunkt ist eine DLL mit einer Main.c in der ein std::vector als globale Variable deklariert ist. In diversen Funktionen der DLL wird auf diese Variable zugegriffen.

    Im Kern sieht die Main.c ungefähr so aus:

    std::vector<Foo> fooList;
    
    int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
    {
    ...
    }
    
    void __stdcall Add(void)
    {
        Foo foo;
        fooList.push_back(foo);
    }
    
    void __stdcall Finalize(void)
    {
        fooList.clear();
    }
    

    Die DLL wird per LoadLibrary() dynamisch geladen, linkt gegen die dynamische RTL und verwendet die Laufzeit-Packages.

    Beim Beenden der Anwendung wird der Destruktor von fooList nun sofort aufgerufen, sodass beim Aufruf von Finalize() bzw. von DllEntryPoint mit DLL_PROCESS_DETACH dieser schon futsch ist. Bei Verwendung des klassischen Compilers war das nicht der Fall...

    Ich hatte erwartet, dass die globale Variablen einer DLL erst bei Übergabe von DLL_PROCESS_DETACH gelöscht werden. Damit lag ich offensichtlich falsch, was bedeutet, dass es bisher schon undefiniertes Verhalten war und nur zufällig funktioniert hat.

    Liege ich mit meiner Vermutung richtig? Wenn ja, hat jemand eine Idee, wie man das lösen kann? Auf die schnelle habe ich den Vektor als Pointer deklariert, möchte das aber nicht so beibehalten.

    Edit
    Die SuFu des "neuen" Forums ist brauchbar 🙂
    statische-variable-in-dll

    Vollständig erhellt bin ich noch nicht, aber mal sehen was sich im Netz findet.



  • Das kommt alles drauf an wie die Toolchain Initialisierung/Finalisierung von statischen Objekten implementiert.
    Keine Ahnung wie deine "clang-enhanced" Toolchain das macht.

    Schreib dir mal ne kleine Klasse die im Konstruktor und Destruktor jeweils irgendwas macht was nicht wegoptimiert wird (z.B. einfach OutputDebugString() aufrufen). Von der macht du ein globales Objekt, und dann setzt du in den Konstruktor und den Destruktor jeweils einen Breakpoint.

    Und dann postest du die Callstacks hier.



  • Beim Kopieren der Call-Stacks ist mir eingefallen, dass ich das Thema schon einmal hatte. Damals habe ich bei stackoverflow (difference between bcc32 and bcc32c object lifetime) so ziemlich die gleiche Frage gestellt. Der Unterschied ist hauptsächlich, dass hier noch eine DLL im Spiel ist.

    Die Erkenntnis von damals war, dass statische Objekte, egal ob global oder Member einer Klasse, unabhängig von der Lebensdauer anderer Objekte sind. Mir war bisher nicht klar, dass statische Objekte, die in einer DLL erzeugt werden, im Speicher der Hauptanwendung hängen.

    Bedeutet also, ich muss fooList aus dem obigen Beispiel als Pointer deklarieren und die Lebensdauer selber managen. Oder?

    Call-Stacks (clang)

    Aufruf des Konstruktors nach LoadLibrary:

    :0342C2BD Foo::Foo(this=:0343CE24)
    :0342C158 __cxx_global_var_init.22()
    :0342C2AC _GLOBAL__sub_I_RecorderPlugin.cpp()

    Aufruf des Destruktors nach Beenden des Programms (vor FreeLibrary):

    :0342D275 Foo::~Foo(this=:0343CE24)
    :0342C2E3 Foo::~Foo(this=:0343CE24)
    :0342c188 ; construct<double, {723}...
    :32237d88 CC32C270MT.___call_atexit_procs + 0x5c

    Call-Stacks (Borland)

    Aufruf des Konstruktors nach LoadLibrary:
    :03798DA2 Foo::Foo(this=:037A5544)
    :03798C5C STCON0()
    :32209025 ; C:\WINDOWS\SysWOW64\CC32230MT.DLL
    :32209554 CC32230MT.__wstartupd + 0x90

    Aufruf des Destruktors nach Beenden des Programms (nach FreeLibrary):

    :03798DC2 Foo::~Foo(this=:037A5544)
    :03798C8D STDES0()
    :32209043 ; C:\WINDOWS\SysWOW64\CC32230MT.DLL
    :32209586 CC32230MT.__wstartupd + 0xc2
    :037822a9 ; Teegdiplus
    :77905608 ntdll.RtlGetNtSystemRoot + 0x68
    :779354c2 ; ntdll.dll
    :77935084 ; ntdll.dll
    :77915919 ntdll.LdrUnloadDll + 0xe9
    :779158b5 ntdll.LdrUnloadDll + 0x85
    :76ba05e6 KERNELBASE.FreeLibrary + 0x16



  • Da fehlen wohl einige Debug-Symbole.
    Es fehlen interessante Frames, z.B. was bevor CC32C270MT.___call_atexit_procs kommt, und einige Symbole sind Schrott (Teegdiplus, C:\WINDOWS\SysWOW64\CC32230MT.DLL).

    Naja, egal.
    Was du probieren könntest wäre folgendes:

    1. Du machst eine Wrapper-Klasse für fooList die sich um die Initialisierung und Cleanup kümmert.
    2. Du verschiebst fooList in eine Hilfsfunktion (Meyers' Singleton)
    3. Du rufst die Hilfsfunktion in DllMain auf

    Also quasi

    struct FooStatics {
        std::vector<Foo> fooList;
        FooStatics() { // Replaces the Add() function
            fooList.push_back(...);
        }
        ~FooStatics() { // Replaces the Finalize() function
            // ...
        }
    };
    
    FooStatics& fooStatics() {
        static FooStatics fs;
        return fs;
    }
    
    int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
    {
    //...
        case DLL_PROCESS_ATTACH:
            fooStatics();
    //...
    }
    

    Wenn DllEntryPoint(DLL_PROCESS_ATTACH) zu früh läuft kann sein dass dir das um die Ohren fliegt, aber einen Versuch wäre es wert.

    Und nochwas...
    DllEntryPoint ist ein unüblicher Name für diese Funktion, normalerweise nennt man das Ding DllMain. Verwendest du irgendwelche Linker-Switches damit DllEntryPoint zum "Entry Point" wird? Falls ja könntest du versuchen den Switch wegzulassen und statt dessen den Namen DllMain zu verwenden. Was die Implementierungen normalerweise machen ist nämlich der DLL eine Entry Point Funktion zu verpassen die von der Implementierung bereitgestellt wird, und diese ruft dann erst - zum passenden Zeitpunkt - deine DllMain auf. Wenn du das umgehst indem du den Linker zwingst direkt deine Funktion als Entry Point zu verwenden, kann das zu Problemen dieser Art führen.



  • @hustbaer sagte in Globale Variable in DLL:

    Da fehlen wohl einige Debug-Symbole.
    Es fehlen interessante Frames, z.B. was bevor CC32C270MT.___call_atexit_procs kommt, und einige Symbole sind Schrott (Teegdiplus, C:\WINDOWS\SysWOW64\CC32230MT.DLL).

    Verflucht seist du, E.........!

    FooStatics& fooStatics() {
        static FooStatics fs;
        return fs;
    }
    

    Leider hat das nicht geholfen. Der Destruktor von FooStatics wird für fs beim Beenden des Programs aufgerufen.

    DllEntryPoint ist ein unüblicher Name für diese Funktion, normalerweise nennt man das Ding DllMain.

    Der Name wird von der IDE automatisch vergeben. Umbenennen hilft aber auch nicht.

    Das Verhalten scheint auch mit dem VCL-Framwork zusammenzuhängen:
    Auto-created TForm objects are owned by the global TApplication object. That object is destroyed (thus destroying its owned Forms) after the application's main()/wmain()/WinMain() entry point function has exited. Globals are destroyed during application cleanup.

    Etwas doof ist das schon, da es bedeutet, dass ich beim "Aufräumen" der Anwendung, nicht auf globale oder statische Variablen zugreifen darf...



  • @Kerem sagte in Globale Variable in DLL:

    Leider hat das nicht geholfen. Der Destruktor von FooStatics wird für fs beim Beenden des Programs aufgerufen.

    Natürlich wird er das. Die Frage ist wann während des Beendens er aufgerufen wird. Wenn es spät genug passiert, ist das ja kein Problem.



  • @hustbaer sagte in Globale Variable in DLL:

    Die Frage ist wann während des Beendens er aufgerufen wird.

    Der Aufruf erfolgt sobald die Anwendung geschlossen wird, noch bevor der Destruktor des Hauptfensters aufgerufen wird. Leider ist im Code des Hauptfensters der ganze DLL-Lade- und Entlade-Mechanismus implementiert (Konstrukor: LoadLibrary, Destruktor FreeLibrary).

    @Kerem sagte in Globale Variable in DLL:

    Auto-created TForm objects are owned by the global TApplication object. That object is destroyed (thus destroying its owned Forms) after the application's main()/wmain()/WinMain() entry point function has exited. Globals are destroyed during application cleanup.

    Bei VCL-Anwendungen wird das Hauptfenster standardmäßig automatisch erzeugt. Die restliche Anwendungslogik wird dann von dort aus angesprochen. Dies führt in meinem Fall dazu, dass beim Aufruf von FreeLibrary im Destruktor des Hauptfensters das "application cleanup" bereits stattgefunden hat und alle statischen Objekte gelöscht wurden.


Anmelden zum Antworten