Charger dynamiquement une fonction à partir d'une DLL

88

Je regarde un peu les fichiers .dll, je comprends leur utilisation et j'essaie de comprendre comment les utiliser.

J'ai créé un fichier .dll contenant une fonction qui renvoie un entier nommé funci ()

en utilisant ce code, je (pense) avoir importé le fichier .dll dans le projet (il n'y a rien à redire):

#include <windows.h>
#include <iostream>

int main() {
  HINSTANCE hGetProcIDDLL = LoadLibrary("C:\\Documents and Settings\\User\\Desktop  \\fgfdg\\dgdg\\test.dll");

  if (hGetProcIDDLL == NULL) {
    std::cout << "cannot locate the .dll file" << std::endl;
  } else {
    std::cout << "it has been called" << std::endl;
    return -1;
  }

  int a = funci();

  return a;
}

# funci function 

int funci() {
  return 40;
}

Cependant, lorsque j'essaie de compiler ce fichier .cpp qui, je pense, a importé le .dll, j'ai l'erreur suivante:

C:\Documents and Settings\User\Desktop\fgfdg\onemore.cpp||In function 'int main()':|
C:\Documents and Settings\User\Desktop\fgfdg\onemore.cpp|16|error: 'funci' was not     declared in this scope|
||=== Build finished: 1 errors, 0 warnings ===|

Je sais qu'un .dll est différent d'un fichier d'en-tête, donc je sais que je ne peux pas importer une fonction comme celle-ci, mais c'est le meilleur que je puisse proposer pour montrer que j'ai essayé.

Ma question est, comment puis-je utiliser le hGetProcIDDLLpointeur pour accéder à la fonction dans le .dll.

J'espère que cette question a du sens et que je n'aboie pas encore une fois un mauvais arbre.

Clémence
la source
recherche de liens statiques / dynamiques.
Mitch Wheat
Merci, je vais examiner cela
J'ai mis mon code en retrait, mais quand je le mets ici, le format gâche donc je

Réponses:

152

LoadLibraryne fait pas ce que vous pensez qu'il fait. Il charge la DLL dans la mémoire du processus en cours, mais il n'importe pas comme par magie les fonctions qui y sont définies! Cela ne serait pas possible, car les appels de fonction sont résolus par l'éditeur de liens au moment de la compilation alors qu'il LoadLibraryest appelé au moment de l'exécution (rappelez-vous que C ++ est un langage de type statique ).

Vous avez besoin d' une fonction WinAPI distincte pour obtenir l'adresse des fonctions chargées dynamiquement: GetProcAddress.

Exemple

#include <windows.h>
#include <iostream>

/* Define a function pointer for our imported
 * function.
 * This reads as "introduce the new type f_funci as the type: 
 *                pointer to a function returning an int and 
 *                taking no arguments.
 *
 * Make sure to use matching calling convention (__cdecl, __stdcall, ...)
 * with the exported function. __stdcall is the convention used by the WinAPI
 */
typedef int (__stdcall *f_funci)();

int main()
{
  HINSTANCE hGetProcIDDLL = LoadLibrary("C:\\Documents and Settings\\User\\Desktop\\test.dll");

  if (!hGetProcIDDLL) {
    std::cout << "could not load the dynamic library" << std::endl;
    return EXIT_FAILURE;
  }

  // resolve function address here
  f_funci funci = (f_funci)GetProcAddress(hGetProcIDDLL, "funci");
  if (!funci) {
    std::cout << "could not locate the function" << std::endl;
    return EXIT_FAILURE;
  }

  std::cout << "funci() returned " << funci() << std::endl;

  return EXIT_SUCCESS;
}

En outre, vous devez exporter correctement votre fonction à partir de la DLL. Cela peut être fait comme ceci:

int __declspec(dllexport) __stdcall funci() {
   // ...
}

Comme le note Lundin, il est recommandé de libérer la poignée de la bibliothèque si vous n'en avez pas besoin plus longtemps. Cela entraînera son déchargement si aucun autre processus ne détient toujours un handle vers la même DLL.

Niklas B.
la source
Cela peut sembler une question stupide, mais quel est / devrait être le type de f_funci?
8
Autre que cela, la réponse est excellente et facilement compréhensible
6
Notez que , f_funcien fait , est un type (plutôt que possède un type). Le type se f_funcilit comme "pointeur vers une fonction retournant un intet ne prenant aucun argument". Pour plus d'informations sur les pointeurs de fonction en C, consultez newty.de/fpt/index.html .
Niklas B.
Merci encore pour la réponse, funci ne prend aucun argument et renvoie un entier; J'ai modifié la question pour afficher la fonction qui a été compilée? dans le .dll. Quand j'ai essayé de courir après avoir inclus "typedef int ( f_funci) ();" J'ai eu cette erreur: C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp || Dans la fonction 'int main ()': | C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp | 18 | erreur: impossible de convertir 'int ( ) ()' en 'const CHAR *' pour l'argument '2' en 'int (* GetProcAddress (HINSTANCE__ , const CHAR )) () '| || === Construction terminée: 1 erreur, 0 avertissement === |
Eh bien, j'ai oublié un casting là-bas (je l'ai édité). L'erreur semble cependant être une autre, êtes-vous sûr d'utiliser le bon code? Si oui, pouvez-vous coller votre code défaillant et la sortie complète du compilateur sur pastie.org ? De plus, le typedef que vous avez écrit dans votre commentaire est incorrect (il *manque un, ce qui aurait pu causer l'erreur)
Niklas B.
34

En plus de la réponse déjà publiée, j'ai pensé que je devrais partager une astuce pratique que j'utilise pour charger toutes les fonctions DLL dans le programme via des pointeurs de fonction, sans écrire un appel GetProcAddress séparé pour chaque fonction. J'aime aussi appeler les fonctions directement comme tenté dans l'OP.

Commencez par définir un type de pointeur de fonction générique:

typedef int (__stdcall* func_ptr_t)();

Les types utilisés ne sont pas vraiment importants. Créez maintenant un tableau de ce type, qui correspond au nombre de fonctions que vous avez dans la DLL:

func_ptr_t func_ptr [DLL_FUNCTIONS_N];

Dans ce tableau, nous pouvons stocker les pointeurs de fonction réels qui pointent dans l'espace mémoire DLL.

Le problème suivant est que GetProcAddressles noms de fonctions sont attendus sous forme de chaînes. Créez donc un tableau similaire composé des noms de fonction dans la DLL:

const char* DLL_FUNCTION_NAMES [DLL_FUNCTIONS_N] = 
{
  "dll_add",
  "dll_subtract",
  "dll_do_stuff",
  ...
};

Maintenant, nous pouvons facilement appeler GetProcAddress () dans une boucle et stocker chaque fonction dans ce tableau:

for(int i=0; i<DLL_FUNCTIONS_N; i++)
{
  func_ptr[i] = GetProcAddress(hinst_mydll, DLL_FUNCTION_NAMES[i]);

  if(func_ptr[i] == NULL)
  {
    // error handling, most likely you have to terminate the program here
  }
}

Si la boucle a réussi, le seul problème que nous avons maintenant est d'appeler les fonctions. Le pointeur de fonction typedef précédemment n'est pas utile, car chaque fonction aura sa propre signature. Cela peut être résolu en créant une structure avec tous les types de fonctions:

typedef struct
{
  int  (__stdcall* dll_add_ptr)(int, int);
  int  (__stdcall* dll_subtract_ptr)(int, int);
  void (__stdcall* dll_do_stuff_ptr)(something);
  ...
} functions_struct;

Et enfin, pour les connecter au tableau précédent, créez une union:

typedef union
{
  functions_struct  by_type;
  func_ptr_t        func_ptr [DLL_FUNCTIONS_N];
} functions_union;

Vous pouvez maintenant charger toutes les fonctions de la DLL avec la boucle pratique, mais les appeler via le by_typemembre union.

Mais bien sûr, il est un peu fastidieux de taper quelque chose comme

functions.by_type.dll_add_ptr(1, 1); chaque fois que vous souhaitez appeler une fonction.

En fait, c'est la raison pour laquelle j'ai ajouté le suffixe "ptr" aux noms: je voulais les garder différents des noms de fonction réels. Nous pouvons maintenant lisser la syntaxe de la structure icky et obtenir les noms souhaités, en utilisant certaines macros:

#define dll_add (functions.by_type.dll_add_ptr)
#define dll_subtract (functions.by_type.dll_subtract_ptr)
#define dll_do_stuff (functions.by_type.dll_do_stuff_ptr)

Et voilà, vous pouvez maintenant utiliser les noms des fonctions, avec le type et les paramètres corrects, comme s'ils étaient liés statiquement à votre projet:

int result = dll_add(1, 1);

Avertissement: à proprement parler, les conversions entre différents pointeurs de fonction ne sont pas définies par la norme C et ne sont pas sûres. Donc formellement, ce que je fais ici, c'est un comportement indéfini. Cependant, dans le monde Windows, les pointeurs de fonction sont toujours de la même taille quel que soit leur type et les conversions entre eux sont prévisibles sur n'importe quelle version de Windows que j'ai utilisée.

De plus, il pourrait en théorie y avoir un remplissage inséré dans l'union / struct, ce qui ferait échouer tout. Cependant, les pointeurs sont de la même taille que l'exigence d'alignement dans Windows. Un static_assertpour s'assurer que la structure / union n'a pas de remplissage peut être toujours en ordre.

Lundin
la source
1
Cette approche de style C fonctionnerait. Mais ne serait-il pas approprié d'utiliser une construction C ++ pour éviter les #defines?
harper
@harper Eh bien, en C ++ 11, vous pouvez utiliser auto dll_add = ..., mais en C ++ 03, il n'y a pas de construction à laquelle je pourrais penser qui simplifierait la tâche (je ne vois pas non plus de problème particulier avec les #defines ici)
Niklas B.
Comme tout cela est spécifique à WinAPI, vous n'avez pas besoin de saisir le vôtre func_ptr_t. Au lieu de cela, vous pouvez utiliser FARPROC, qui est le type de retour de GetProcAddress. Cela pourrait vous permettre de compiler avec un niveau d'avertissement plus élevé sans ajouter de conversion à l' GetProcAddressappel.
Adrian McCarthy le
@NiklasB. vous ne pouvez utiliser qu'une autoseule fonction à la fois, ce qui va à l'encontre de l'idée de le faire une fois pour toutes en boucle. mais quel est le problème avec un tableau std :: function
Francesco Dondi
1
@Francesco les types std :: function différeront tout comme les types funcptr. Je suppose que des modèles variadiques aideraient
Niklas B.18
1

Ce n'est pas exactement un sujet brûlant, mais j'ai une classe d'usine qui permet à une dll de créer une instance et de la renvoyer sous forme de DLL. C'est ce que je suis venu chercher mais je n'ai pas pu trouver exactement.

Il s'appelle comme,

IHTTP_Server *server = SN::SN_Factory<IHTTP_Server>::CreateObject();
IHTTP_Server *server2 =
      SN::SN_Factory<IHTTP_Server>::CreateObject(IHTTP_Server_special_entry);

où IHTTP_Server est l'interface virtuelle pure pour une classe créée soit dans une autre DLL, soit dans la même.

DEFINE_INTERFACE est utilisé pour donner une interface à un identifiant de classe. Placez l'interface à l'intérieur;

Une classe d'interface ressemble à,

class IMyInterface
{
    DEFINE_INTERFACE(IMyInterface);

public:
    virtual ~IMyInterface() {};

    virtual void MyMethod1() = 0;
    ...
};

Le fichier d'en-tête est comme ça

#if !defined(SN_FACTORY_H_INCLUDED)
#define SN_FACTORY_H_INCLUDED

#pragma once

Les bibliothèques sont répertoriées dans cette définition de macro. Une ligne par bibliothèque / exécutable. Ce serait cool si nous pouvions appeler un autre exécutable.

#define SN_APPLY_LIBRARIES(L, A)                          \
    L(A, sn, "sn.dll")                                    \
    L(A, http_server_lib, "http_server_lib.dll")          \
    L(A, http_server, "")

Ensuite, pour chaque dll / exe, vous définissez une macro et listez ses implémentations. Def signifie qu'il s'agit de l'implémentation par défaut de l'interface. Si ce n'est pas la valeur par défaut, vous donnez un nom à l'interface utilisée pour l'identifier. Ie, special, et le nom sera IHTTP_Server_special_entry.

#define SN_APPLY_ENTRYPOINTS_sn(M)                                     \
    M(IHTTP_Handler, SNI::SNI_HTTP_Handler, sn, def)                   \
    M(IHTTP_Handler, SNI::SNI_HTTP_Handler, sn, special)

#define SN_APPLY_ENTRYPOINTS_http_server_lib(M)                        \
    M(IHTTP_Server, HTTP::server::server, http_server_lib, def)

#define SN_APPLY_ENTRYPOINTS_http_server(M)

Avec les bibliothèques toutes configurées, le fichier d'en-tête utilise les définitions de macro pour définir ce qui est nécessaire.

#define APPLY_ENTRY(A, N, L) \
    SN_APPLY_ENTRYPOINTS_##N(A)

#define DEFINE_INTERFACE(I) \
    public: \
        static const long Id = SN::I##_def_entry; \
    private:

namespace SN
{
    #define DEFINE_LIBRARY_ENUM(A, N, L) \
        N##_library,

Cela crée une énumération pour les bibliothèques.

    enum LibraryValues
    {
        SN_APPLY_LIBRARIES(DEFINE_LIBRARY_ENUM, "")
        LastLibrary
    };

    #define DEFINE_ENTRY_ENUM(I, C, L, D) \
        I##_##D##_entry,

Cela crée une énumération pour les implémentations d'interface.

    enum EntryValues
    {
        SN_APPLY_LIBRARIES(APPLY_ENTRY, DEFINE_ENTRY_ENUM)
        LastEntry
    };

    long CallEntryPoint(long id, long interfaceId);

Ceci définit la classe d'usine. Pas grand chose ici.

    template <class I>
    class SN_Factory
    {
    public:
        SN_Factory()
        {
        }

        static I *CreateObject(long id = I::Id )
        {
            return (I *)CallEntryPoint(id, I::Id);
        }
    };
}

#endif //SN_FACTORY_H_INCLUDED

Alors le RPC est,

#include "sn_factory.h"

#include <windows.h>

Créez le point d'entrée externe. Vous pouvez vérifier qu'il existe à l'aide de depend.exe.

extern "C"
{
    __declspec(dllexport) long entrypoint(long id)
    {
        #define CREATE_OBJECT(I, C, L, D) \
            case SN::I##_##D##_entry: return (int) new C();

        switch (id)
        {
            SN_APPLY_CURRENT_LIBRARY(APPLY_ENTRY, CREATE_OBJECT)
        case -1:
        default:
            return 0;
        }
    }
}

Les macros configurent toutes les données nécessaires.

namespace SN
{
    bool loaded = false;

    char * libraryPathArray[SN::LastLibrary];
    #define DEFINE_LIBRARY_PATH(A, N, L) \
        libraryPathArray[N##_library] = L;

    static void LoadLibraryPaths()
    {
        SN_APPLY_LIBRARIES(DEFINE_LIBRARY_PATH, "")
    }

    typedef long(*f_entrypoint)(long id);

    f_entrypoint libraryFunctionArray[LastLibrary - 1];
    void InitlibraryFunctionArray()
    {
        for (long j = 0; j < LastLibrary; j++)
        {
            libraryFunctionArray[j] = 0;
        }

        #define DEFAULT_LIBRARY_ENTRY(A, N, L) \
            libraryFunctionArray[N##_library] = &entrypoint;

        SN_APPLY_CURRENT_LIBRARY(DEFAULT_LIBRARY_ENTRY, "")
    }

    enum SN::LibraryValues libraryForEntryPointArray[SN::LastEntry];
    #define DEFINE_ENTRY_POINT_LIBRARY(I, C, L, D) \
            libraryForEntryPointArray[I##_##D##_entry] = L##_library;
    void LoadLibraryForEntryPointArray()
    {
        SN_APPLY_LIBRARIES(APPLY_ENTRY, DEFINE_ENTRY_POINT_LIBRARY)
    }

    enum SN::EntryValues defaultEntryArray[SN::LastEntry];
        #define DEFINE_ENTRY_DEFAULT(I, C, L, D) \
            defaultEntryArray[I##_##D##_entry] = I##_def_entry;

    void LoadDefaultEntries()
    {
        SN_APPLY_LIBRARIES(APPLY_ENTRY, DEFINE_ENTRY_DEFAULT)
    }

    void Initialize()
    {
        if (!loaded)
        {
            loaded = true;
            LoadLibraryPaths();
            InitlibraryFunctionArray();
            LoadLibraryForEntryPointArray();
            LoadDefaultEntries();
        }
    }

    long CallEntryPoint(long id, long interfaceId)
    {
        Initialize();

        // assert(defaultEntryArray[id] == interfaceId, "Request to create an object for the wrong interface.")
        enum SN::LibraryValues l = libraryForEntryPointArray[id];

        f_entrypoint f = libraryFunctionArray[l];
        if (!f)
        {
            HINSTANCE hGetProcIDDLL = LoadLibraryA(libraryPathArray[l]);

            if (!hGetProcIDDLL) {
                return NULL;
            }

            // resolve function address here
            f = (f_entrypoint)GetProcAddress(hGetProcIDDLL, "entrypoint");
            if (!f) {
                return NULL;
            }
            libraryFunctionArray[l] = f;
        }
        return f(id);
    }
}

Chaque bibliothèque inclut ce "cpp" avec un stub cpp pour chaque bibliothèque / exécutable. Tout élément d'en-tête compilé spécifique.

#include "sn_pch.h"

Configurez cette bibliothèque.

#define SN_APPLY_CURRENT_LIBRARY(L, A) \
    L(A, sn, "sn.dll")

Un include pour le cpp principal. Je suppose que ce cpp pourrait être un .h. Mais il existe différentes manières de procéder. Cette approche a fonctionné pour moi.

#include "../inc/sn_factory.cpp"
Peter Driscoll
la source