MFC DLL erstellen - Unresolved External Linker Error - nur eine Methode kann aufgerufen werden



  • Hallo 😃

    Ich bin grade dabei eine MFC DLL zu erstellen. Paar Details: Ich arbeite mit Visual Studio 2013. Die DLL erstelle ich in C++ und die Anwendung mit der die DLL aufgerufen werden soll ist ebenfalls in C++ geschrieben.

    Mein Vorgehen war so, dass ich ein neues Projekt erstellt habe (MFC DLL mit C++) und eine kleine Klasse geschrieben habe.

    Header File:

    class CIO_Manager
    {
    public:
    
        __declspec(dllexport) CIO_Manager();
        __declspec(dllexport) virtual ~CIO_Manager();
    
        __declspec(dllexport) void Io_DummyA(char*& c);
        __declspec(dllexport) void Io_DummyB(CString& c);
    };
    

    CPP File:

    #include "stdafx.h"
    #include <afx.h>
    #include "Io_Manager.h"
    
    CIO_Manager::CIO_Manager()
    {
    
    }
    
    CIO_Manager::~CIO_Manager()
    {
    
    }
    
    void CIO_Manager::Io_DummyA(char*& c)
    {
        CString s = _T("Kraut und rüben");
    
        int L = s.GetLength() + 1;
        CT2A ascii(s);
        c = new char[L];
    
        strncpy_s(c, L, ascii, L);
        c[L - 1] = 0;
    }
    
    void CIO_Manager::Io_DummyB(CString& c)
    {
        c = c + _T(":hallo");
    }
    

    Dann habe ich den Header zu meiner "Clienten"-Anwendung hinzugefügt. Die erzeugte DLL zu der Exe gepackt und zusätzlich die .Lib als Additional Dependency bekannt gemacht in den Einstellungen.
    (Kann mir jemand vielleicht erklären wie Lib und DLL zusammenhängen, braucht man beides oder eines nur im Debug Modus? 😕 )

    Der Code aus der Anwendung:

    CIO_Manager manager;
    CString test = "test";
    char* cSave = NULL;
    manager.Io_DummyA(cSave);
    manager.Io_DummyB(test);
    

    Als ich nun getestet habe, ob ich die Methoden aus der DLL aufrufen kann, konnte ich die Methode "IO_DummyA" ohne weiteres aufrufen, bei der Methode "IO_DummyB" bekomme ich allerdings ein Unresolved External Linker Fehler.

    Sobald ich mehr als eine Methode in der DLL habe bekomme ich unresolved External 😞

    Hat jemand vielleicht eine Idee woran das liegen könnte oder einen Hinweis was ich vergessen / falsch gemacht haben könnte?

    Schon mal vielen lieben Dank für eure Hilfe!



  • Das declspec sollte für die Klasse gelten. Außerdem sollte die Benutzerseite auch ein import machen. MS empfiehlt dafür eine Makrokonstruktion:

    #ifdef _EXPORTING
       #define CLASS_DECLSPEC    __declspec(dllexport)
    #else
       #define CLASS_DECLSPEC    __declspec(dllimport)
    #endif
    
    class CLASS_DECLSPEC CIO_Manager
    {
    public:
    
        CIO_Manager();
        virtual ~CIO_Manager();
    
        void Io_DummyA(char*& c);
        void Io_DummyB(CString& c);
    };
    


  • Wenn deine DLL gebaut wird, musst du __declspec(dllexport) verwenden, wenn die Funktionen aus der DLL importiert werden sollen, musst du __declspec(dllimport) verwenden.
    Normalerweise lässt man dafür in den Projekteinstellungen deiner DLL irgendein Flag (sagen wir einfach, IO_MANAGER ) in den Präprozessoreinstellungen definieren, dann prüft man im Code, ob das Flag definiert ist. Wenn dem so ist, werden die Funktionen exportiert, wenn nicht, werden sie importiert:

    #if defined(IO_MANAGER)
    #    define IO_MANAGER_DLL_FUNCTION __declspec(dllexport)
    #else
    #    define IO_MANAGER_DLL_FUNCTION __declspec(dllimport)
    #endif
    

    Dann packst du IO_MANAGER_DLL_FUNCTION vor jede Funktionsdeklaration oder vor jede Klasse, die in der DLL landen soll:

    class IO_MANAGER_DLL_FUNCTION CIO_Manager
    {
    public:
    
        CIO_Manager();
        ~CIO_Manager();
    
        void Io_DummyA(char*& c);
        void Io_DummyB(CString& c);
    };
    

    So kannst du den gleichen Header in unterschiedlichen Projekten verwenden, ohne manuell nach einer Änderung wieder was an den Export-/Import-Einstellungen drehen zu müssen.

    EDIT: Zu der LIB/DLL-Geschichte ... ugh. OK. Wenn du eine statische Lib baust, dann hat die Datei die Endung "LIB". Das ist immer eine statische LIB, sprich, damit hantiert der Linker rum. In einer Lib kann jetzt der komplette Code deiner Lib drinstehen. Muss aber nicht. Denn wenn du eine dynamische Bibliothek baust, dann bekommst du eine LIB UND eine DLL. In einem solchen Fall hantiert der Linker immer noch mit der LIB-Datei rum, aber diesmal steht da im wesentlichen nur drin, dass er die Fresse halten soll, weil dein Code in der DLL X zum Startzeitpunkt deiner Anwendung eingeladen wird. In der DLL ist dann dein eigentlicher Code drin.

    Die LIB-Datei brauchst du IMMER, wenn du deine Anwendung neu bauen willst - weil, sagen wir mal, sich das ABI/API geändert hat und jetzt nicht mehr mit den gleichen Parametern oder Strukturausrichtungen gearbeitet werden kann. Die DLL brauchst du dann, wenn du die Anwendung auch starten willst.

    Klar soweit?

    EDIT 2: Übrigens, noch eine Sache - die Linkage der Funktionen. MS hat sich da einen wunderschönen Schwachsinn ausgedacht und gibt Funktionen, die statisch exportiert werden (die also über eine LIB und nicht über eine DLL kommen), andere Namen - DLL-Funktionen bekommen das Präfix " __imp_ ", statische Bibliotheksfunktionen bekommen unter x86 ein " _ ", und unter x64 gar kein Präfix.

    Warum sage ich das? Weil du dem Linker irgendwie mitteilen musst, dass wenn deine Bibliothek auf einmal statisch gelinkt wurde, dass er dann nicht mehr nach den Funktionen mit __imp_ -Präfix suchen soll. Sonst kann der Linker, wenn er deine Anwendung (nicht die LIB) baut, deine Funktionen nicht finden und stellt die Latschen auf. Wenn statisch gebaut wird, darf im Header später nix mit dllimport stehen.



  • dachschaden schrieb:

    ... Denn wenn du eine dynamische Bibliothek baust, dann bekommst du eine LIB
    UND eine DLL. In einem solchen Fall hantiert der Linker immer noch mit der LIB-Datei rum, aber
    diesmal steht da im wesentlichen nur drin, dass er die Fresse halten soll, weil dein Code in der
    DLL X zum Startzeitpunkt deiner Anwendung eingeladen wird. In der DLL ist dann dein eigentlicher
    Code drin.

    Die LIB-Datei brauchst du IMMER, wenn du deine Anwendung neu bauen willst - weil, sagen
    wir mal, sich das ABI/API geändert hat und jetzt nicht mehr mit den gleichen Parametern oder
    Strukturausrichtungen gearbeitet werden kann. Die DLL brauchst du dann, wenn du die Anwendung
    auch starten willst.

    1. Das trifft es wohl nicht ganz. Natürlich macht eine Lib sinvolle Dinge wie z.B. dafür zu sorgen
    das die DLL geladen wird und die enthaltenen Einsprungstellen auflösen. Und weil das so ist, ist auch
    der Linker zufrieden ...

    Woher soll die Exe wissen welche DLL sie laden soll und wie z.B. enthaltene Funktionen aufzurufen sind?
    Also etwas mehr als "Fresse halten" müsste da schon kommen.

    2. Die Lib Datei braucht man nicht unbedingt, da man die DLL natürlich auch mit Systemaufrufen
    selbst laden könnte. Die DLL wird dann erst benötigt, wenn man sie selbst läd und nicht schon beim
    Start der Exe.

    3. Das von dir ins lächerliche gezogene "namewrangling" lässt sich natürlich auch abschalten.
    (Ob das eine gute Idee ist, muss jeder selbst wissen ..)



  • merano schrieb:

    1. Das trifft es wohl nicht ganz. Natürlich macht eine Lib sinvolle Dinge wie z.B. dafür zu sorgen
    das die DLL geladen wird und die enthaltenen Einsprungstellen auflösen. Und weil das so ist, ist auch
    der Linker zufrieden ...

    dachschaden schrieb:

    dass er die Fresse halten soll, weil dein Code in der DLL X zum Startzeitpunkt deiner Anwendung eingeladen wird.

    Nichts zu danken.

    merano schrieb:

    2. Die Lib Datei braucht man nicht unbedingt, da man die DLL natürlich auch mit Systemaufrufen
    selbst laden könnte. Die DLL wird dann erst benötigt, wenn man sie selbst läd und nicht schon beim
    Start der Exe.

    OK, stimmt natürlich. Ist aber aufwendig und blöd zu machen. Und anstatt Funktionsprototypen hast du dann Funktionszeigertypen UND Funktionszeiger. Manchmal geht es nicht anders. Aber wenn es anders geht, sollte man es auch anders machen.

    merano schrieb:

    3. Das von dir ins lächerliche gezogene "namewrangling" lässt sich natürlich auch abschalten.

    Dann gib mal Beispiele, warum das nicht lächerlich sein soll. Denn .a- und .so-Dateien unter Linux kennen sowas nicht. Für den GCC muss ich nicht noch ein Extra-Flag angeben, welches sagt, dass die Namen andere sind als die, nach denen er normalerweise sucht. Und nein, mir geht es nicht um Funktionsüberladungen. Für die ist das "namewrangling" nachvollziehbar. Aber doch bitte nicht für Code, der unterschiedlich exportiert wurde.

    Oder gibt es einen rationalen Grund für diese Präfix? Denn hier finde ich nix zu.


  • Mod

    dachschaden schrieb:

    OK, stimmt natürlich. Ist aber aufwendig und blöd zu machen. Und anstatt Funktionsprototypen hast du dann Funktionszeigertypen UND Funktionszeiger. Manchmal geht es nicht anders. Aber wenn es anders geht, sollte man es auch anders machen.

    Nö.

    Ich brauche nur eine Funktion, die mir einen Interface Zeiger zurückgibt und schon habe ich das komplexeste Interface, dass ich mir vorstellen kann...
    Das mache ich oft genug. Dann benötige ich keine LIB, lade die DLL on Demand und habe immer den gleichen Prozedur Einsprungpunkt... 😉



  • Martin Richter schrieb:

    Ich brauche nur eine Funktion, die mir einen Interface Zeiger zurückgibt und schon habe ich das komplexeste Interface, dass ich mir vorstellen kann...
    Das mache ich oft genug. Dann benötige ich keine LIB, lade die DLL on Demand und habe immer den gleichen Prozedur Einsprungpunkt... 😉

    typedef return_type(CALLING_CONVENTION*function_ptr_1_t)(all_my_parameters);
    typedef return_type(CALLING_CONVENTION*function_ptr_2_t)(all_my_parameters);
    typedef return_type(CALLING_CONVENTION*function_ptr_3_t)(all_my_parameters);
    typedef return_type(CALLING_CONVENTION*function_ptr_4_t)(all_my_parameters);
    typedef return_type(CALLING_CONVENTION*function_ptr_5_t)(all_my_parameters);
    
    function_ptr_1_t function_ptr_1;
    function_ptr_2_t function_ptr_2;
    function_ptr_3_t function_ptr_3;
    function_ptr_4_t function_ptr_4;
    function_ptr_5_t function_ptr_5;
    
    int main(void)
    {
        int exit_code = EXIT_FAILURE;
        HMODULE my_dll;
    
        if(NULL == (my_dll = LoadLibraryA("my_dll.dll")))
            goto PROG_END;
    
        function_ptr_1 = (function_ptr_1_t)GetProcAddress(my_dll,"function_1");
        function_ptr_2 = (function_ptr_2_t)GetProcAddress(my_dll,"function_2");
        function_ptr_3 = (function_ptr_3_t)GetProcAddress(my_dll,"function_3");
        function_ptr_4 = (function_ptr_4_t)GetProcAddress(my_dll,"function_4");
        function_ptr_5 = (function_ptr_5_t)GetProcAddress(my_dll,"function_5");
    
        if(!function_ptr_1
        || !function_ptr_2
        || !function_ptr_3
        || !function_ptr_4
        || !function_ptr_5)
            goto PROG_END;
    
        /*Mach was damit.*/
    
        exit_code = EXIT_SUCCESS;
    PROG_END:
        if(my_dll)
            FreeLibrary(my_dll);
        exit(exit_success);
    }
    

    Und das ist noch ein relativ einfaches Interface. Verglichen mit dem Linken zur Linkzeit ist solcher Code hässlich.
    Ja, klar kannst du das wegabstrahieren. Aber hässlicher Code wird nicht weniger hässlich, nur weil du ihn nicht mehr siehst. Er stört dich nur nicht mehr.

    EDIT: Kleiner Tippfehler - den Code habe ich so schnell abgetippt.

    EDIT 2: Uhm, wobei es natürlich sein kann, dass MS es genauso macht in ihren Startup-Code. So genau kenn ich mich mit Windows jetzt auch nicht aus. :p


  • Mod

    Nö! Du hast es nicht verstanden.
    Ich habe eine Funktion, die ein Interface auf eine Klasse zurückgibt...

    Ich brauche also nur ein GetProcAddress und einen Aufruf der Funktion und bekomme einen Zeiger auf eine Klasse mit x Methoden zurück. Die Klasse ist pure virtuell definiert, also ähnlich wie bei COM.


Anmelden zum Antworten