Globale Variablen in statischen Bibliotheken



  • Guten Abend,

    ich schreibe an einer Bibliothek, die, wenn sie mit einem Programm gelinkt wird, vor bzw. nach dem Aufruf von main() Code ausführen können muss (Initialisierer/Finalisierer).

    Dazu habe ich mich eines Tricks bedient, der die Konstruktoren von globalen Objekten ausnutzt:

    class Initializer
    {
    	Initializer() { puts("initialisiert!"); }
    } initializer;
    

    Unter Linux funktioniert das bis jetzt gut, meine Bibliothek wird dynamisch gebunden und die Initialisierer initialisieren.
    Unter Windows siehts anders aus: Klassen per DLL importieren ist ein Graus, und da es eh keine Paketverwaltung o.ä. gibt, ist es die beste Methode, das statisch zu linken.

    Jetzt ist es nur leider so, dass das Objekt irgendwie nicht erstellt wird, wenn es sich in der statischen Bibliothek befindet. Wenn ich stattdessen die Objektdateien direkt mit dem Programm linke, funktioniert alles problemlos.

    Ich verwende MinGW (und darauf bin ich angewiesen, um Cross-kompilieren zu können). Beim Erstellen der Bibliothek wird anscheinend eine Symboltabelle generiert, die das Initialisierer-Objekt nicht enthält (Spekulation).

    Kann man den Linker dazu bringen, die .o-Dateien einzeln mit dem Programm zu binden anstatt die Tabellen zu verwenden, oder gibt es vielleicht eine ganz andere Lösung für das Problem?

    Danke.



  • Laut Standard funktioniert das nur wenn sich alle Teile in einer "Übersetzungseinheit" befinden.
    Dass es unter Linux funktioniert ist also reiner Zufall.

    Ich habe mal vor sehr vielen Jahren etwas Ähnliches (automatische Registrierung für eine Factory) versucht und bin auch darauf hereingefallen. Beim Aufteilen des Projektes in unterschiedliche LIBs hat nichts mehr funktioniert 😃



  • Nimm DLLMain als Einstiegspunkt für die .dll
    http://msdn.microsoft.com/en-us/library/ms682583(v=vs.85).aspx

    Ansonsten spricht auch nichts gegen eine normale init-funktion.



  • .trion schrieb:

    Kann man den Linker dazu bringen, die .o-Dateien einzeln mit dem Programm zu binden anstatt die Tabellen zu verwenden, oder gibt es vielleicht eine ganz andere Lösung für das Problem?

    Ich kenne folgenden Workaround:

    // DeineLibrary.h --------------------------------------------
    
    namespace Detail {
    
    class Initializer;
    Initializer const& GetInitializer();
    
    namespace {
    
    Initializer const& g_initializer = GetInitializer();
    
    } // namespace
    
    } // namespace Detail
    
    // DeineLibrary.cpp ------------------------------------------
    
    namespace Detail {
    
    class Initializer {
        // ...
    };
    
    Initializer const& GetInitializer()
    {
        static Initializer s_initializer;
        return s_initializer;
    }
    

    So wie ich das verstehe, ist dann auch laut Standard garantiert, dass s_initializer in GetInitializer() immer konstruiert wird, auch wenn g_initializer nirgends referenziert wird.
    Wobei ... dass es laut Standard garantiert ist, heisst natürlich nicht, dass es auch überall wirklich funktioniert 😉



  • Scorcher24 schrieb:

    Ansonsten spricht auch nichts gegen eine normale init-funktion.

    Hihi, doch. Dass man sie aufrufen muss 🙂



  • hustbaer schrieb:

    Ich kenne folgenden Workaround.....

    Du bist ein Gott! Das hat mich 2 Tage gekostet 😉 Läuft einwandfrei.

    Für Copy&Paste-freudige, das sind meine Initialisierermakros:

    #define declare_initializer(identifier) \
    	class identifier { \
    		public: \
    			identifier(); \
    	}; \
    	identifier const & get##identifier(); \
    	namespace { identifier const & ref##identifier  = get##identifier(); } 
    
    #define implement_initializer(identifier) \
    	identifier const & get##identifier() { \
    		static identifier instance; \
    		return instance; \
    	} \
    	identifier::identifier()
    

    Für einen neuen Initialisierer:

    // In einem Header, der irgendwo im Zielprojekt eingebunden wird
    declare_initializer(MyInitializer);
    
    // In der Library
    implement_initializer(MyInitializer)
    {
    	// Code
    }
    

    Finalisierer funktionieren analog, nur muss man dabei den Destruktor statt den Konstruktor definieren.

    Edit: Kommando zurück, geht doch ein bisschen anders



  • Reicht eigentlich auch eine globale Funktion, oder brauchts die Klasse (abgesehen von evtl. nützlichem Initialisierungscode im Konstruktor)?

    const Initializer& GetInitializer(); // wie gehabt
    
    namespace
    { 
        const Initializer& ref = GetInitializer();
    }
    

    Und muss GetInitializer() ausserhalb des anonymen Namensraums stehen? Besteht damit nicht nur die Gefahr eines potentiellen Namenskonflikts?



  • @.trion:
    Wenn ich mich richtig erinnere sind sowohl die Header als auch der anonyme Namespace nötig.

    Die Header deswegen, damit man in jeder Übersetzungseinheit wo die Library XYZ benutzt wird, ein eigenes Ding (statische Variable) hat, dessen Initialisierung dazu führt, dass die GetInitializer() Funktion ausgeführt wird. (D.h. natürlich diese Header muss dann von allen Headern der Library XYZ includiert werden, sonst funktiert der Trick ja nichtmehr)

    Ohne diesen Trick könnte (dürfte, laut Standard) der Compilier/Linker wieder die ganze Initializer-Instanz weglassen, so lange keine Funktionen aus der Übersetzungseinheit der Library in der die Initializer-Instanz definiert ist aufgerufen wird.

    Also konkret, wenn du dein "refFoo" in foo.cpp definierst, der Anwender aber keine einzige Funktion aus foo.cpp aufruft, dann kann (darf) der Compiler foo.cpp einfach ganz weglassen, inklusive "refFoo".
    Wenn das OK ist, dann ... ist es OK 🙂

    Allerdings gibt es Fälle wo es nicht OK ist, und dann kann man mit eben diesem Trick arbeiten.

    Achja... die Notwendigkeit des anonymen Namespace ergibt sich aus der Notwendigkeit der Header: ohne den hätte man nämlich ein mehrfach definiertes Symbol. (Man könnte natürlich "static" statt einem anonymen Namespace verwenden, aber ich verwende halt in solchen Fällen immer anonyme Namespaces)



  • Nexus schrieb:

    Reicht eigentlich auch eine globale Funktion?

    Die läuft wie gesagt Gefahr, wegoptimiert zu werden.

    hustbaer schrieb:

    @.trion:
    Wenn ich mich richtig erinnere sind sowohl die Header als auch der anonyme Namespace nötig.

    Fehler meinerseits, hat wegen einer falschen Linkereinstellung so ausgesehen als würde es funktionieren. Ist oben gefixt und geht jetzt.
    Danke nochmal.



  • .trion schrieb:

    Die läuft wie gesagt Gefahr, wegoptimiert zu werden.

    Nein, ich meine eine Funktion (halt wie die static -Memberfunktion), keine Variable. Warum sollte die eher wegoptimiert werden als die Memberfunktion?



  • @Nexus:
    Für Initialisierung braucht man eigentlich keine Klasse, da tut's eine freie Funktion die einen Dummy-Wert zurückgibt, den man dann zur Initialisierung einer Hilfsvariable verwendet. Allerdings wird die Funktion ja mehrfach aufgerufen, d.h. man müsste dann selbst checken ob es der erste Aufruf ist oder ein "redundanter".

    Und für Cleanup braucht man sowieso ne Klasse.



  • Okay, danke.

    Zur Initialisierung globaler Variablen habe ich im C++-Standard bisher nur das gefunden:

    §3.6.2/1 Initialization of non-local objects schrieb:

    Objects with static storage duration (3.7.1) shall be zero-initialized (8.5) before any other initializationtakes place. Zero-initialization and initialization with a constant expression are collectively called static initialization; all other initialization is dynamic initialization. Objects of POD types (3.9) with static storage duration initialized with constant expressions (5.19) shall be initialized before any dynamic initialization takes place. Objects with static storage duration defined in namespace scope in the same translation unit and dynamically initialized shall be initialized in the order in which their definition appears in the translation unit.

    Heisst das nicht, dass globale Objekte ("static storage duration") mit dynamischer Initialisierung innerhalb einer ÜE initialisiert werden müssen, und zwar in der Deklarationsreihenfolge? Oder ist das eher so zu verstehen, dass wenn sie initialisiert werden, die Reihenfolge eingehalten werden muss? Oder hat das überhaupt nichts damit zu tun?

    Ich habe nämlich ebenfalls so eine Initialisierung vor. Die SFML-Bibliothek hat auch sowas mit dem klassischen Ansatz (alles in .cpp-Datei) gelöst und es scheint nicht wirklich Probleme zu geben...



  • Nexus schrieb:

    Okay, danke.

    Zur Initialisierung globaler Variablen habe ich im C++-Standard bisher nur das gefunden:

    §3.6.2/1 Initialization of non-local objects schrieb:

    Objects with static storage duration (3.7.1) shall be zero-initialized (8.5) before any other initializationtakes place. Zero-initialization and initialization with a constant expression are collectively called static initialization; all other initialization is dynamic initialization. Objects of POD types (3.9) with static storage duration initialized with constant expressions (5.19) shall be initialized before any dynamic initialization takes place. Objects with static storage duration defined in namespace scope in the same translation unit and dynamically initialized shall be initialized in the order in which their definition appears in the translation unit.

    Heisst das nicht, dass globale Objekte ("static storage duration") mit dynamischer Initialisierung innerhalb einer ÜE initialisiert werden müssen, und zwar in der Deklarationsreihenfolge? Oder ist das eher so zu verstehen, dass wenn sie initialisiert werden, die Reihenfolge eingehalten werden muss? Oder hat das überhaupt nichts damit zu tun?

    Also so, wie ich das sehe kommt da der erste Satz (fett) zum tragen. Das heisst, dass sie immer 0 Initialisiert werden. Dass es so ist merkt man ja auch, dass die statischen Sachen im .bss Segment liegen, welches ja 0 Initialisiert ist. Oder verstehe ich dich mal wieder falsch? 😉



  • @Nexus:
    Das von dir Zitierte ist ja soweit schön und auch kein Problem. Um die Reihenfolge geht es hier ja nicht.

    Oder ist das eher so zu verstehen, dass wenn sie initialisiert werden, die Reihenfolge eingehalten werden muss?

    Ja, genau das ist das Problem.

    Lies mal zwei Punkte weiter unten:

    §3.6.2/3 schrieb:

    It is implementation-defined whether or not the dynamic initialization (8.5, 9.4, 12.1, 12.6.1) of an object of
    namespace scope is done before the first statement of main. If the initialization is deferred to some point
    in time after the first statement of main, it shall occur before the first use of any function or object defined
    in the same translation unit as the object to be initialized.

    Das impliziert, dass das Objekt gar nicht initialisiert werden muss, wenn nie Funktionen oder Objekte aus der Translation Unit aufgerufen/verwendet werden.

    D.h. ...

    Unproblematisch:

    // in einem .cpp File:
    
    namespace {
    
    Foo g_somethingThatBarNeeds = MakeFoo();
    
    } // namespace
    
    Bar::Bar() // "use of a function" -> g_somethingThatBarNeeds muss initialisiert worden sein bevor Bar::Bar() läuft (*)
    {
        m_magicValue = g_somethingThatBarNeeds.GetMagicValue(); // ... d.h. wir können es hier Gefahrlos verwenden
    }
    
    //*: ausgenommen Bar::Bar wird selbst während der Initialisierung eines "object with static storage duration defined in namespace scope" aufgerufen,
    //   dann haben wir das allseits beliebte "initialization order fiasco" und alles geht den Bach runter :^)
    

    Unproblematisch deswegen, weil g_somethingThatBarNeeds ja nicht gerbaucht wird, so lange keine "Bar" erzeugt werden. Und wenn "Bar" erzeugt werden ist auch garantiert dass g_somethingThatBarNeeds initialisiert wird.

    Problematisch:

    // in einem .cpp File:
    
    namespace {
    
    class MyFactory { ... };
    
    int RegisterMyFactory()
    {
        static MyFactory fac;
        ::RegisterFactory(&fac);
    }
    
    int g_registerHelper = RegisterMyFactory();
    
    } // namespace
    

    Problematisch deswegen, weil es hier u.U. gar keine Funktionen in der Translation Unit gibt, die direkt von aussen aufgerufen werden dürfen/können.
    Die Absicht wäre hier RegisterMyFactory() nur wegen der Nebeneffekte (nämlich das Registrieren der Factory) auszuführen. Wenn der Compiler aber meint, kann er die Initialisierung von g_registerHelper einfach weglassen, da ja nie Funktionen aus der Übersetzungseinheit aufgerufen werden.

    Ich kenne zwar keine Implementierung die den Standard soweit ausreizt dass die Initialisierung nach dem ersten Statement von main() passiert. Allerdings gibt es viele die eben das was ganz wegoptimiert werden kann auch ganz wegoptimieren.

    Das ist grundsätzlich auch OK, da der Linker dadurch Teile verwerfen kann, die nicht gebraucht werden. Nur führt es eben auch dazu, dass das Registrieren von MyFactory so wie im Beispiel oben nicht funktioniert.

    Als Workaround packen wir eine dynamisch initialisierte Variable in jede Übersetzungseinheit, in der Code aus unserer Library verwendet wird. Da wir die Header-Files unserer Library kontrollieren geht das einfach, wir machen einfach ein "mylib/common.h" das von allen andere "mylib/*.h" Files inkludiert wird, und da drin steht der Code den ich hier ursprünglich gepostet habe.

    Natürlich gibt es auch wieder Fälle wo selbst das nicht funktioniert, nämlich wenn wir nicht wollen/nicht erzwingen können dass ein Clientprogramm so ein Headerfile inkludiert. z.B. weil die .lib bloss wie im "MyFactory" Beispiel ein Plugin registrieren soll, und wir sie nur beim Linken angeben wollen/können. Das lässt sich über Standard-C++ dann nichtmehr erschlagen.

    Ich hab's nicht ausprobiert, aber bei MSVC könnte es sein dass man das Wegoptimieren von solchen Variablen über

    #pragma comment(linker, "/include:__mySymbol")
    

    verbieten kann.
    Also verbieten kann man es damit sicher, aber was ich nicht weiss, ist, ob es reicht wenn das in der .lib steht, oder ob dieses #pragma im Code der .exe/.dll stehen müsste. Wenn letzteres, dann wäre es für das worum es hier geht ja wieder witzlos.

    Achja, falls jmd. weiss ob/wie man das "Behalten" einer Variable mit GCC erzwingen kann... wäre auch interessant.

    p.S.:
    Der Workaround verlässt sich auch darauf, dass der Compiler/Linker genau das nicht macht was er dürfte, aber kein mir bekannter Compiler/Linker wirklich macht: die Initialisierung zu verzögern. In einigen Fällen wäre das nämlich genau so ein Problem, da die Initialisierung dann zu spät erfolgen würde. Aber wie gesagt: ich kennen keine Implementierung die das macht. Falls jemand eine kennt: bitte melden 🙂



  • @ drakon: Die Null-Initialisierung ist für den Fall hier nicht interessant, weil eben die Nebeneffekte (aufgerufene Funktion mit Registrierung) dadurch nicht zum Tragen kommen. Im Prinzip ist der Wert der initialisierten Variable egal, nur der Prozess der Initialisierung an sich ist relevant.

    @ hustbaer: Danke für die Erläuterungen. Den Punkt habe ich sogar auch noch gesehen, aber mir nichts weiter dabei gedacht. Aber eigentlich ist es logisch... Normalerweise versuche ich auch, globale Variablen und frühzeitige Initialisierung genau wegen solcher Dinge zu vermeiden. Aber ab und zu ist das Ganze einfach zu praktisch 🙂

    Allerdings könnte man manche Fälle auch lösen, indem man bei der ersten Verwendung einer Funktion die Initialisierung vornimmt (über eine funktionslokale static -Variable). Vielleicht sollte ich mir das überlegen... Ein static bool in einer Funktion hat wahrscheinlich nicht einmal ein verstecktes if (__initialized) , oder? (Bin mir gerade nicht sicher, ob das auch unter die normale static storage duration geht oder wieder zu irgendeinem Spezialfall gehört)



  • @Nexus:
    Ein "static bool" in einer Funktion wird nur dann kein "if (!initialized)" haben, wenn es konstant initialisiert wird.
    Allerdings wüsste ich nicht wozu man so ein static bool jetzt bräuchte. Ausser um seinen Code thread-unsafe zu machen.

    Verwende gleich das "Meyers Singleton", das ist wenigstens mit GCC thread-safe.
    Bzw. nimm folgenden Code (der vom Prinzip her das selbe macht wie das "Meyers Singleton", nur ohne Singleton *g*)

    int MyInitFunction()
    {
       // ...
       return 42;
    }
    
    void MyPublicFunction()
    {
        static int const s_myInitDummy = MyInitFunction();
        // ...
    }
    

    s_myInitDummy muss hier ein "if (!initialized)" vom Compiler verpasst bekommen, sonst würde es ja immer wieder überschrieben, und das erlaubt der Standard nicht. Dadurch sparst du dir selbst das entsprechende "if" zu schreiben, und bekommst von GCC wie schon gesagt auch threadsicheren Code geschenkt.

    Ansonsten kann man boost::call_once verwenden um es unabhängig vom Compiler thread-safe zu machen.

    Aber davon abgesehen

    Allerdings könnte man manche Fälle auch lösen, indem man bei der ersten Verwendung einer Funktion die Initialisierung vornimmt (über eine funktionslokale static-Variable). Vielleicht sollte ich mir das überlegen...

    Wenn das geht, dann ist ja alles wunderbar.
    Der hier diskutierte Workaround ist vor allem für Fälle gedacht, wo das nicht funktioniert, weil man Dinge vor Programmstart erledigen möchte. z.B. das Registrieren von Plugins/Modulen.

    Bzw. allgemein Fälle, wo man die Nebeneffekte braucht, bevor eine Funktion aufgerufen wird deren Code man anfassen will/kann/darf.



  • hustbaer schrieb:

    Allerdings wüsste ich nicht wozu man so ein static bool jetzt bräuchte. Ausser um seinen Code thread-unsafe zu machen.
    [...]
    s_myInitDummy muss hier ein "if (!initialized)" vom Compiler verpasst bekommen, sonst würde es ja immer wieder überschrieben, und das erlaubt der Standard nicht. Dadurch sparst du dir selbst das entsprechende "if" zu schreiben, und bekommst von GCC wie schon gesagt auch threadsicheren Code geschenkt.

    Ich meinte genau sowas wie s_myIntDummy , aber halt ein bool (spielt ja keine Rolle). Ok, jetzt sehe ich auch, dass bool im dem Zusammenhang verwirrend ist, sorry. Ist so eine static -Konstante auf gcc nun threadsafe oder nicht? Denn irgendwie sehen deine Aussagen für mich wie ein Widerspruch aus 🙂

    Konkret brauch ich das für einen Zufallsgenerator, dessen Seed beim Starten automatisch gesetzt werden soll. Da wäre Threadsicherheit wahrscheinlich nicht schlecht. Allerdings müsste ich dann auch noch einiges mehr threadsicher machen...



  • Ich dachte du willst das "if (!initialized)" selbst schreiben (wegen bool und so, ja, war verwirrend). Das wäre dann nicht threadsafe, es sei denn du kümmerst dich selbst darum (Mutex oder so).

    Wenn du es nicht selbst schreibst, sondern eine function-local static Variable verwendest, dann macht der Compiler es für dich. Und GCC macht es threadsafe. MSVC nicht.

    Was der aktuelle C++0x Draft dazu sagt weiss ich nicht sicher. Ich glaube der sieht das so wie GCC es sieht, nämlich dass es threadsafe sein muss.

    Konkret brauch ich das für einen Zufallsgenerator, dessen Seed beim Starten automatisch gesetzt werden soll. Da wäre Threadsicherheit wahrscheinlich nicht schlecht.

    Zufallsgenerator mach ich immer als Instanzen, keine globalen Funktionen. Daher gibt's das Problem auch nicht.

    Ansonsten ist thread-local Storage angesagt. Sonst kann ein Thread durch das Ziehen von Zahlen aus dem RNG die Sequenz die ein anderer Thread bekommt beeinflussen. -> nicht gut.

    p.S.: und mit thread-local Storage ist thread-safety bei der Initialisierung dann wieder kein Problem, da je eh jeder Thread seine eigenen Variablen hat.



  • hustbaer schrieb:

    Zufallsgenerator mach ich immer als Instanzen, keine globalen Funktionen. Daher gibt's das Problem auch nicht.

    Ja, wäre sicher flexibler, gerade wenn man an verschiedenen Orten voneinander unabhängige Zufallszahlen haben will. Allerdings brauch ich oftmals nur kurz in einem temporären Ausdruck eine Zufallszahl, dafür immer vorher ein Objekt anzulegen ist unpraktisch und liefert auch schlechte Zufallszahlen, wenn er erst gerade mit dem Seed initialisiert wurde. Oder man hätte halt zusätzlich eine globale Instanz, wobei dann wieder das Problem auftritt.

    Wie machst du das, findest du immer einen genügend grossen Kontext, in dem du so ein RNG-Objekt anlegst, oder hast du dann auch häufig lokale "Wegwerf-Objekte"?



  • Nexus schrieb:

    Allerdings brauch ich oftmals nur kurz in einem temporären Ausdruck eine Zufallszahl, dafür immer vorher ein Objekt anzulegen ist unpraktisch und liefert auch schlechte Zufallszahlen, wenn er erst gerade mit dem Seed initialisiert wurde.

    Ja, unpraktisch. Ich habe gerne ein globales Objekt.

    Aber daß der frisch geseedete Zufallszahlengwenerator schlechten Zufall liefert, sehe ich jetzt noch nicht.

    int getSeed(){
       static int seed=time(0);//beim ersten mal uhrzeit lesen
       ++seed;//bei mehreren aufrufen unique machen
    //nicht schlampig verwürfeln, dazu ist der Zufallszahlengenerator besser geeignet
       return seed;
    }
    
    Random::Random(){
       x=getSeed();
       this->rand();//die angekündigte initiale Verwürfelung
    }
    unsigned int Random::rand(){
      x=(x*17+5)%1000;//oder bessere Formel!
      return x;
    }
    

Log in to reply