Memuat fungsi dari DLL secara dinamis

88

Saya melihat sedikit file .dll, saya memahami penggunaannya dan saya mencoba memahami cara menggunakannya.

Saya telah membuat file .dll yang berisi fungsi yang mengembalikan integer bernama funci ()

menggunakan kode ini, saya (pikir) saya telah mengimpor file .dll ke dalam proyek (tidak ada keluhan):

#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;
}

Namun ketika saya mencoba untuk mengkompilasi file .cpp ini yang menurut saya telah mengimpor .dll saya mengalami kesalahan berikut:

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 ===|

Saya tahu .dll berbeda dari file header jadi saya tahu saya tidak bisa mengimpor fungsi seperti ini tapi itu yang terbaik yang bisa saya lakukan untuk menunjukkan bahwa saya sudah mencoba.

Pertanyaan saya adalah, bagaimana saya bisa menggunakan hGetProcIDDLLpenunjuk untuk mengakses fungsi dalam .dll.

Saya harap pertanyaan ini masuk akal dan saya tidak menyalak lagi.

Kelonggaran
sumber
cari tautan statis / dinamis.
Mitch Wheat
Terima kasih, saya akan memeriksa ini
Saya membuat indentasi kode saya tetapi ketika saya memasukkannya ke sini, formatnya kacau jadi saya akhirnya membuat indentasi semuanya dengan 4 baris

Jawaban:

153

LoadLibrarytidak melakukan apa yang Anda pikirkan. Ini memuat DLL ke dalam memori proses saat ini, tetapi tidak secara ajaib mengimpor fungsi yang ditentukan di dalamnya! Ini tidak mungkin dilakukan, karena panggilan fungsi diselesaikan oleh linker pada waktu kompilasi ketika LoadLibrarydipanggil saat runtime (ingat bahwa C ++ adalah bahasa yang diketik secara statis ).

Anda membutuhkan fungsi WinAPI terpisah untuk mendapatkan alamat fungsi dinamis dimuat: GetProcAddress.

Contoh

#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;
}

Selain itu, Anda harus mengekspor fungsi Anda dari DLL dengan benar. Ini bisa dilakukan seperti ini:

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

Sebagai catatan Lundin, praktik yang baik untuk membebaskan pegangan ke perpustakaan jika Anda tidak membutuhkannya lebih lama. Ini akan menyebabkannya dibongkar jika tidak ada proses lain yang masih menangani DLL yang sama.

Niklas B.
sumber
Mungkin terdengar seperti pertanyaan bodoh tapi apa itu / seharusnya menjadi tipe f_funci?
8
Selain itu, jawabannya sangat bagus dan mudah dimengerti
6
Perhatikan bahwa f_funcisebenarnya adalah tipe (daripada memiliki tipe). Tipe ini f_funcidibaca sebagai "penunjuk ke fungsi yang mengembalikan intdan tidak mengambil argumen". Informasi lebih lanjut tentang pointer fungsi di C dapat ditemukan di newty.de/fpt/index.html .
Niklas B.
Sekali lagi terima kasih atas balasannya, funci tidak membutuhkan argumen dan mengembalikan integer; Saya mengedit pertanyaan untuk menunjukkan fungsi yang telah dikompilasi? ke dalam .dll. Ketika saya mencoba untuk menjalankan setelah memasukkan "typedef int ( f_funci) ();" Saya mendapat kesalahan ini: C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp || Dalam fungsi 'int main ()': | C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp | 18 | kesalahan: tidak dapat mengubah 'int ( ) ()' menjadi 'const CHAR *' untuk argumen '2' menjadi 'int (* GetProcAddress (HINSTANCE__ , const CHAR )) () '| || === Pembuatan selesai: 1 kesalahan, 0 peringatan === |
Yah saya lupa pemeran di sana (diedit di). Namun kesalahan tersebut tampaknya satu sama lain, apakah Anda yakin menggunakan kode yang benar? Jika ya, dapatkah Anda menempelkan kode yang gagal dan keluaran kompiler lengkap di pastie.org ? Juga, typedef yang Anda tulis di komentar Anda salah (ada *yang hilang, yang bisa menyebabkan kesalahan)
Niklas B.
34

Selain jawaban yang sudah diposting, saya pikir saya harus membagikan trik praktis yang saya gunakan untuk memuat semua fungsi DLL ke dalam program melalui penunjuk fungsi, tanpa menulis panggilan GetProcAddress terpisah untuk setiap fungsi. Saya juga suka memanggil fungsi secara langsung seperti yang dicoba di OP.

Mulailah dengan mendefinisikan tipe penunjuk fungsi generik:

typedef int (__stdcall* func_ptr_t)();

Jenis apa yang digunakan tidak terlalu penting. Sekarang buat larik jenis itu, yang sesuai dengan jumlah fungsi yang Anda miliki di DLL:

func_ptr_t func_ptr [DLL_FUNCTIONS_N];

Dalam larik ini kita dapat menyimpan penunjuk fungsi aktual yang mengarah ke ruang memori DLL.

Masalah berikutnya adalah GetProcAddressmengharapkan nama fungsi sebagai string. Jadi buat array serupa yang terdiri dari nama fungsi di DLL:

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

Sekarang kita bisa dengan mudah memanggil GetProcAddress () dalam satu lingkaran dan menyimpan setiap fungsi di dalam larik itu:

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
  }
}

Jika perulangan berhasil, satu-satunya masalah yang kita miliki sekarang adalah memanggil fungsi. Penunjuk fungsi typedef dari sebelumnya tidak membantu, karena setiap fungsi akan memiliki tanda tangannya sendiri. Ini dapat diselesaikan dengan membuat struct dengan semua jenis fungsi:

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;

Dan terakhir, untuk menghubungkan ini ke array dari sebelumnya, buat gabungan:

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

Sekarang Anda dapat memuat semua fungsi dari DLL dengan loop yang nyaman, tetapi memanggilnya melalui by_type anggota serikat.

Tapi tentu saja, agak memberatkan untuk mengetik sesuatu seperti

functions.by_type.dll_add_ptr(1, 1); kapan pun Anda ingin memanggil suatu fungsi.

Ternyata, inilah alasan mengapa saya menambahkan postfix "ptr" ke nama: Saya ingin membuatnya berbeda dari nama fungsi sebenarnya. Kami sekarang dapat memuluskan sintaks struktur icky dan mendapatkan nama yang diinginkan, dengan menggunakan beberapa makro:

#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)

Dan voila, Anda sekarang dapat menggunakan nama fungsi, dengan jenis dan parameter yang benar, seolah-olah keduanya ditautkan secara statis ke proyek Anda:

int result = dll_add(1, 1);

Penafian: Sebenarnya, konversi antara pointer fungsi yang berbeda tidak ditentukan oleh standar C dan tidak aman. Jadi secara formal, yang saya lakukan di sini adalah perilaku yang tidak terdefinisi. Namun, di dunia Windows, pointer fungsi selalu memiliki ukuran yang sama apa pun tipenya dan konversi di antara keduanya dapat diprediksi pada versi Windows apa pun yang saya gunakan.

Juga, mungkin secara teori ada padding yang dimasukkan ke dalam union / struct, yang akan menyebabkan semuanya gagal. Namun, pointer kebetulan memiliki ukuran yang sama dengan persyaratan penyelarasan di Windows. A static_assertuntuk memastikan bahwa struct / union tidak memiliki padding mungkin masih teratur.

Lundin
sumber
1
Pendekatan gaya C ini akan berhasil. Tapi bukankah lebih tepat menggunakan konstruksi C ++ untuk menghindari #defines?
harper
@harper Nah di C ++ 11 Anda dapat menggunakan auto dll_add = ..., tetapi di C ++ 03 tidak ada konstruksi yang dapat saya pikirkan yang akan menyederhanakan tugas (saya juga tidak melihat masalah khusus dengan #defines di sini)
Niklas B.
Karena ini semua khusus WinAPI, Anda tidak perlu mengetikkan milik Anda sendiri func_ptr_t. Sebagai gantinya Anda dapat menggunakan FARPROC, yang merupakan tipe kembalian dari GetProcAddress. Ini memungkinkan Anda untuk mengompilasi dengan tingkat peringatan yang lebih tinggi tanpa menambahkan cast ke GetProcAddresspanggilan.
Adrian McCarthy
@Niklas. Anda hanya dapat menggunakan autountuk satu fungsi dalam satu waktu, yang mengalahkan gagasan untuk melakukannya sekali untuk semua dalam satu putaran. tapi apa yang salah dengan array std :: function
Francesco Dondi
1
@Francesco jenis std :: function akan berbeda seperti jenis funcptr. Saya kira templat variadic akan membantu
Niklas B.
1

Ini bukan topik hangat, tapi saya memiliki kelas pabrik yang memungkinkan dll untuk membuat instance dan mengembalikannya sebagai DLL. Ini adalah apa yang saya cari tetapi tidak dapat menemukan dengan tepat.

Ini disebut seperti,

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

di mana IHTTP_Server adalah antarmuka virtual murni untuk kelas yang dibuat baik di DLL lain, atau yang sama.

DEFINE_INTERFACE digunakan untuk memberikan sebuah antarmuka id kelas. Tempatkan di dalam antarmuka;

Kelas antarmuka terlihat seperti,

class IMyInterface
{
    DEFINE_INTERFACE(IMyInterface);

public:
    virtual ~IMyInterface() {};

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

File header seperti ini

#if !defined(SN_FACTORY_H_INCLUDED)
#define SN_FACTORY_H_INCLUDED

#pragma once

Pustaka dicantumkan dalam definisi makro ini. Satu baris per perpustakaan / dapat dieksekusi. Akan keren jika kita bisa memanggil executable lain.

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

Kemudian untuk setiap dll / exe Anda menentukan makro dan mencantumkan implementasinya. Def berarti itu adalah implementasi default untuk antarmuka. Jika bukan default, Anda memberi nama untuk antarmuka yang digunakan untuk mengidentifikasinya. Yaitu khusus, dan namanya adalah 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)

Dengan perpustakaan semua pengaturan, file header menggunakan definisi makro untuk menentukan yang diperlukan.

#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,

Ini menciptakan enum untuk perpustakaan.

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

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

Ini membuat enum untuk implementasi antarmuka.

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

    long CallEntryPoint(long id, long interfaceId);

Ini mendefinisikan kelas pabrik. Tidak banyak di sini.

    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

Maka CPPnya adalah,

#include "sn_factory.h"

#include <windows.h>

Buat titik masuk eksternal. Anda dapat memeriksa apakah itu ada menggunakan 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;
        }
    }
}

Makro menyiapkan semua data yang dibutuhkan.

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);
    }
}

Setiap perpustakaan menyertakan "cpp" ini dengan cpp rintisan untuk setiap perpustakaan / yang dapat dieksekusi. Semua hal header terkompilasi tertentu.

#include "sn_pch.h"

Siapkan perpustakaan ini.

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

Termasuk untuk cpp utama. Saya kira cpp ini bisa jadi .h. Tetapi ada berbagai cara untuk melakukan ini. Pendekatan ini berhasil untuk saya.

#include "../inc/sn_factory.cpp"
Peter Driscoll
sumber