113 votes

Charger dynamiquement une fonction à partir d'une DLL

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

J'ai créé un fichier .dll qui contient 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 pas de plaintes) :

#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é la dll, j'obtiens 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 fichier .dll est différent d'un fichier d'en-tête et je sais donc que je ne peux pas importer une fonction comme celle-ci, mais c'est le mieux que j'ai pu faire pour montrer que j'ai essayé.

Ma question est la suivante : comment puis-je utiliser le hGetProcIDDLL pour accéder à la fonction dans la dll.

J'espère que cette question a un sens et que je ne suis pas en train d'écorcer un mauvais arbre une fois de plus.

181voto

Niklas B. Points 40619

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

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

Ejemplo

#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 l'exportation votre fonction de la DLL correctement. Cela peut se faire de la manière suivante :

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

Comme l'indique M. Lundin, il est de bonne pratique de libérer la poignée de la bibliothèque si vous n'en avez pas besoin plus longtemps. Elle sera déchargée si aucun autre processus ne détient encore un handle sur la même DLL.

41voto

Lundin Points 21616

En plus de la réponse déjà publiée, j'ai pensé partager une astuce pratique que j'utilise pour charger toutes les fonctions DLL dans le programme par le biais de pointeurs de fonction, sans écrire un appel GetProcAddress séparé pour chaque fonction. J'aime aussi appeler les fonctions directement, comme cela a été 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 à la quantité 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 fonctions qui pointent vers l'espace mémoire de la DLL.

Le problème suivant est que GetProcAddress s'attend à ce que les noms des fonctions soient des chaînes de caractères. Créez donc un tableau similaire composé des noms de fonctions dans la DLL :

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

Nous pouvons maintenant 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 s'est déroulée correctement, le seul problème qui se pose est celui de l'appel des fonctions. Le typedef de pointeur de fonction de tout à l'heure n'est pas utile, car chaque fonction aura sa propre signature. Ce problème 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;

Enfin, pour les relier 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 à l'aide de la boucle pratique, mais les appeler par l'intermédiaire de la fonction by_type membre du syndicat.

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.

Il s'avère que c'est la raison pour laquelle j'ai ajouté le postfixe "ptr" aux noms : Je voulais les différencier des noms de fonctions réels. Nous pouvons maintenant éliminer la syntaxe maladroite de la structure et obtenir les noms souhaités, en utilisant quelques 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 si elles étaient liées 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 est un comportement non défini. Cependant, dans le monde Windows, les pointeurs de fonction ont toujours la même taille quel que soit leur type et les conversions entre eux sont prévisibles sur toutes les versions de Windows que j'ai utilisées.

De plus, il pourrait en théorie y avoir du padding inséré dans l'union/structure, ce qui ferait tout échouer. Cependant, il se trouve que les pointeurs ont la même taille que l'alignement requis dans Windows. A static_assert pour s'assurer que la structure/union n'est pas remplie.

1voto

Peter Driscoll Points 131

Ce n'est pas vraiment un sujet d'actualité, 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 cherchais mais que je n'arrivais pas à trouver exactement.

Il s'appelle ainsi,

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 DLL.

DEFINE_INTERFACE est utilisé pour donner une interface à l'identifiant d'une classe. Elle est placée à l'intérieur de l'interface ;

Une classe d'interface se présente comme suit,

class IMyInterface
{
    DEFINE_INTERFACE(IMyInterface);

public:
    virtual ~IMyInterface() {};

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

Le fichier d'en-tête se présente comme suit

#if !defined(SN_FACTORY_H_INCLUDED)
#define SN_FACTORY_H_INCLUDED

#pragma once

Les bibliothèques sont énumérées dans la définition de cette macro. Une ligne par bibliothèque/exécutable. Il serait intéressant de pouvoir 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 dressez la liste de ses implémentations. Def signifie qu'il s'agit de l'implémentation par défaut de l'interface. S'il ne s'agit pas de l'implémentation par défaut, vous donnez un nom à l'interface utilisée pour l'identifier. Par exemple, si elle est spéciale, 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)

Une fois les bibliothèques installées, le fichier d'en-tête utilise les définitions des macros 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 permet de créer une liste d'implémentations d'interfaces.

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

    long CallEntryPoint(long id, long interfaceId);

Elle définit la classe de l'usine. Il n'y a pas grand-chose à faire 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

Le CPP l'est donc,

#include "sn_factory.h"

#include <windows.h>

Créer le point d'entrée externe. Vous pouvez vérifier qu'il existe en utilisant depends.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 définissent 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. Tous les en-têtes compilés spécifiques.

#include "sn_pch.h"

Mettre en place 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 y a différentes façons de procéder. Cette approche a fonctionné pour moi.

#include "../inc/sn_factory.cpp"

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X