Linken inkompatibler Bibliotheken erkennen



  • Hallo zusammen,

    Bei gewissen C++-Bibliotheken sehe ich folgendes Problem: Leute linken eine Version einer statischen oder dynamischen Bibliothek, die mit dem Projekt inkompatibel ist. "Inkompatibel" kann z.B. unterschiedliche Compilerversionen, Mischen von Debug/Release oder verschiedene Compilerflags bedeuten. In manchen Fällen wird das Problem bereits automatisch zur Linkzeit erkannt, allerdings nicht immer. Oft kracht es dann mit einer Zugriffsverletzung beim Aufruf einer entsprechenden Funktion, teilweise passiert auch gar nichts. Erschwerend ist natürlich, dass das Ganze je nach Compiler und Betriebssystem wieder etwas anders aussieht. Unter Linux ist z.B. das Linken von Release-Bibliotheken in Debug-Builds unproblematisch. Ausserdem sind Begriffe wie "Release"/"Debug" schwammig definiert, am portabelsten hält man sich dafür vielleicht an das NDEBUG -Makro.

    Was für Möglichkeiten seht ihr, zumindest einen Teil solcher Fehler zu erkennen und mittels vernünftiger Fehlermeldungen mitzuteilen? Etwas konkreter:

    1. Linkzeit
    Wie weit kann man statische Bibliotheken und Importbibliotheken bereits zur Linkzeit überprüfen? Hinweise auf nicht-portable Mechanismen sind durchaus willkommen, habt ihr für einen spezifischen Compiler/Linker dazu Erfahrung?

    Meine spontane Idee mit C++-Mitteln: Konfigurationen mittels Präprozessor in einen Funktionsnamen einbauen. Falls sich die Konfiguration der Bibliothek und des User-Projekts unterscheidet, wird die Funktionsdefinition nicht gefunden. Damit lässt sich ein aussagekräftiger Linkerfehler erzeugen.

    // Ganzer Codeauszug in Library.hpp
    
    // Einzelne Konfigurationen mit Präprozessor zu Bezeichnern verarbeiten
    // Bezeichner für Debug oder Release
    #ifdef NDEBUG
      #define DEBUG_OR_RELEASE Release
    #else
      #define DEBUG_OR_RELEASE Debug
    #endif
    
    // Bezeichner für Compiler + Version
    #if defined(_MSC_VER)
      #define COMPILER PP_CAT(MicrosoftCompiler, _MSC_VER)
    #elif ...
     ...
    #endif
    
    // Alle Bezeichner zusammenhängen => eindeutiger Funktionsname
    #define LINKER_CONFIG PP_CAT(COMPILER, DEBUG_OR_RELEASE)
    #define FUNCTION_NAME PP_CAT(linkerTest_, LINKER_CONFIG)
    
    // Funktionsdeklaration (Definition in Library.cpp)
    int DLL_API FUNCTION_NAME();
    
    // Linken der Funktion erzwingen. Kann das wegoptimiert werden?
    const int dummy = FUNCTION_NAME();
    

    2. Laufzeit
    Falls man den Test ganz oder teilweise auf die Laufzeit verlegt, wie würdet ihr vorgehen?

    Meine Idee hierzu: Konfiguration separat in .cpp- und .hpp-Dateien der Bibliothek ermitteln und vergleichen.

    // Ganzer Codeauszug in Library.hpp
    
    // Klasse mit Konfiguration
    struct LinkerConfig
    {
        const char* compilerName;
        int         compilerVersion;
        bool        debug;
        ...
    }; 
    
    // inline => Code liegt im User-Projekt
    inline LinkerConfig getUserConfig()
    {
        ...
    }
    
    // definiert in Library.cpp => Code liegt in Bibliothek
    LinkerConfig DLL_API getLibConfig();
    
    // Vergleicht User-Projekt- und Bibliotheks-Einstellungen
    inline int compareConfig()
    {
       if (getUserConfig() != getLibConfig());
           warning(); // Ausgabe, Assert, Breakpoint, was auch immer
       return 13;
    }
    
    // Code-Ausführung erzwingen. Kann das wegoptimiert werden?
    const int dummy = compareConfig();
    


  • Bei letzterem Vorschlag kommt mir das so vor, als würde dann der Check evtl. auch abstürzen 😉

    Ich habe keinen Ratschlag parat, aber möchte anmerken, dass es seit CMake und Konsorten immer einfacher wird, sich die Bibs einfach in's Projekt zu integrieren, sodass man gar keine Probleme mehr in diese Richtung hat. Wenn die Quellen nicht vorhanden sind, ist man wahrscheinlich sowieso am besten mit C-Bibs bedient...



  • EinerVonUns schrieb:

    Ich habe keinen Ratschlag parat, aber möchte anmerken, dass es seit CMake und Konsorten immer einfacher wird, sich die Bibs einfach in's Projekt zu integrieren, sodass man gar keine Probleme mehr in diese Richtung hat.

    CMake hilft nur bedingt. Es gibt genügend Fälle, wo man im Projekt ohne CMake effizienter ist, gerade wenn man gute Projektverwaltung wie Visual Studio hat und Portabilität nicht von Anfang an ein Thema ist.

    Davon abgesehen kann man auch mit CMake sehr leicht Fehler machen. Die Fehlerrate hängt effektiv von der Qualität der Skripte ab, genauer gesagt welche Fehlerquellen der Entwickler explizit einkalkuliert. Zum einen sind originale CMake-Fehlermeldungen nicht besonders aussagekräftig, ausserdem wird vieles nicht automatisch erkannt (z.B. konnte ich in einem MinGW-Projekt Visual-Studio-Bibliotheken als Abhängigkeit angeben, der Fehler wurde erst beim Linken erkennbar). Wenn der Benutzer einen Pfad angibt, indem zu linkende Bibliotheken zwar vorhanden, aber inkompatibel mit dem User-Projekt sind, hat er wieder verloren. Vieles muss immer noch nicht-portabel gelöst werden. Jedes grössere Projekt hat explizite Fallunterscheidungen nach Compiler drin, was dem Abstraktionsgedanken zuwider läuft. Aber das nur nebenbei...

    EinerVonUns schrieb:

    Wenn die Quellen nicht vorhanden sind, ist man wahrscheinlich sowieso am besten mit C-Bibs bedient...

    Welche Quellen meinst du? Ich ziele auf C++-Bibliotheken ab, die Open Source sind. Ich will mich nicht auf C beschränken.



  • Nexus schrieb:

    EinerVonUns schrieb:

    Ich habe keinen Ratschlag parat, aber möchte anmerken, dass es seit CMake und Konsorten immer einfacher wird, sich die Bibs einfach in's Projekt zu integrieren, sodass man gar keine Probleme mehr in diese Richtung hat.

    CMake hilft nur bedingt. Es gibt genügend Fälle, wo man im Projekt ohne CMake effizienter ist, gerade wenn man gute Projektverwaltung wie Visual Studio hat und Portabilität nicht von Anfang an ein Thema ist.

    Hast du mal ein Beispiel wo Visual Studio besser ist als CMake, wenn es um das gleiche Ziel geht? Ich generiere VS-Projekte immer mit CMake. Keine Ahnung warum man das anders machen sollte.



  • Ich arbeite normalerweise direkt in Visual Studio. CMake wird allenfalls verwendet, um das Projekt zu portieren. CMake-generierte VS-Projekte finde ich nicht ideal zum längeren Arbeiten, da die Projektstruktur weniger konfigurierbar ist (man hat zusätzliche Projekte wie ZERO_CHECK, BUILD, INSTALL; Include-Dateien werden standardmässig nicht zum Projekt hinzugefügt; man hat keine Filter). Ich arbeite lieber mit der integrierten Projektverwaltung als einem externen Makefile. Einstellungen sind über die GUI einfacher zu ändern als im Skript (man muss nicht all die compiler-spezifischen Schalter auswendig kennen).

    Zurück zum Thema: Ich habe mal ein bisschen herumexperiementiert, der erste Code mit der Linkzeit-Erkennung funktioniert recht gut. Das Problem ist, dass NDEBUG nicht zuverlässig definiert wird (da es primär das assert -Makro steuert), für MSVC bin ich daher auf _DEBUG umgestiegen.

    Momentan prüfe ich folgende Eigenschaften:

    • Build-Type (Release/Debug)
    • Linkage (Static/Dynamic)
    • Compiler (Name und Version)

    Ein Teil davon wird zwar ohnehin erkannt, allerdings kann ich so genauere Fehlermeldungen erzeugen. Andere Ideen, was recht eindeutig auf inkompatible Binaries hindeuten könnte? Verwendete Standard-/Laufzeitbibliotheken (wie könnte ich die am besten erkennen)?



  • Vielleicht ist auch Boost.Predef hilfreich? Bin ich gerade drüber gestolpert.
    http://www.boost.org/doc/libs/1_55_0/libs/predef/doc/html/index.html



  • Sieht nützlich aus, danke 🙂



  • Tyroxx, wie löst Du denn das Problem, dass das mit der Synchronisation von Dateien in Solutions und dem CMake-Script etwas hakelig ist? In VS hinzufügen und dann im CMake-Skript per Hand eintragen finde ich schon etwas nervig. Ich hab mir schön öfter Gedanken darüber gemacht, die Scripte um ein paar Meta-Informationen anzureichern und mir einen Service zu schreiben, der sln und Skripte synchronisiert, aber naja, komme irgendwie nicht dazu.



  • decimad schrieb:

    Tyroxx, wie löst Du denn das Problem, dass das mit der Synchronisation von Dateien in Solutions und dem CMake-Script etwas hakelig ist? In VS hinzufügen und dann im CMake-Skript per Hand eintragen finde ich schon etwas nervig.

    CMake berücksichtigt natürlich keine Änderungen, die man am VS-Projekt vornimmt. Hinzufügen/Umbenennen/Löschen von Quelldateien erfordert bei meinen Setups nur ein erneutes "Generate" im CMake GUI, weil das CMake-Skript einfach alle Dateien in seinem Verzeichnis hinzufügt. VS fragt dann, ob die geänderten Projekte neu geladen werden sollen.

    @Nexus: Ich kann das Problem noch nicht ganz nachvollziehen. VS lässt es bei C++ gar nicht zu gegen .lib -Dateien unterschiedlicher Compiler-Versionen oder Debug-Modi zu linken. Konflikte zwischen statischen und dynamischen Bibliotheken erzeugen ebenfalls sofort Linker-Fehler.
    Bei GCC muss man nur mit -std=c++0x aufpassen und uralte Compiler-Versionen (3.x) bzw ABIs vermeiden.
    Beschreibe doch mal genau was "inkompatibel" für dich bedeutet.

    CMake hilft auch nur das eigentliche Problem unter dem Teppich zu kehren: C++ fehlt ein Modulsystem.

    $ echo -e "namespace boost = import(\"boost\", ver >= \"1.55\");\nint main() {boost::thread t;}" > test.cpp
    $ cpppm deps test.cpp
    boost ver >= 1.55
    $ cpppm install boost
    $ c++ test.cpp
    $ ./a.out
    


  • TyRoXx schrieb:

    @Nexus: Ich kann das Problem noch nicht ganz nachvollziehen. VS lässt es bei C++ gar nicht zu gegen .lib -Dateien unterschiedlicher Compiler-Versionen oder Debug-Modi zu linken.

    Doch, Debug und Release kann man teilweise mischen, verschiedene Compilerversionen ebenfalls. Ersteres habe ich erst gerade selbst ausprobiert, um die Checks zu testen.

    Hier ein paar Beispiele aus dem SFML-Forum, wo genau diese Fehler begangen wurden, und erst zur Laufzeit zu einem Fehler "Stack around the variable .. was corrupted" geführt haben:
    http://en.sfml-dev.org/forums/index.php?topic=3119
    http://en.sfml-dev.org/forums/index.php?topic=4858
    http://en.sfml-dev.org/forums/index.php?topic=7885


Anmelden zum Antworten