Tải động một chức năng từ DLL


88

Tôi đang xem xét một chút về tệp .dll, tôi hiểu cách sử dụng của chúng và tôi đang cố gắng hiểu cách sử dụng chúng.

Tôi đã tạo một tệp .dll có chứa một hàm trả về một số nguyên có tên funci ()

bằng cách sử dụng mã này, tôi (nghĩ) tôi đã nhập tệp .dll vào dự án (không có khiếu nại):

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

Tuy nhiên, khi tôi cố gắng biên dịch tệp .cpp này mà tôi nghĩ rằng đã nhập .dll, tôi gặp lỗi sau:

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

Tôi biết .dll khác với tệp tiêu đề vì vậy tôi biết tôi không thể nhập một hàm như thế này nhưng đó là cách tốt nhất tôi có thể đưa ra để cho thấy rằng tôi đã thử.

Câu hỏi của tôi là, làm cách nào tôi có thể sử dụng hGetProcIDDLLcon trỏ để truy cập hàm trong .dll.

Tôi hy vọng câu hỏi này có ý nghĩa và tôi sẽ không sủa một cây sai nào nữa.


tra cứu liên kết tĩnh / động.
Mitch Wheat

Cảm ơn bạn, tôi sẽ nhìn vào điều này

Tôi thụt mã của tôi nhưng khi tôi nhét nó vào đây các messes định dạng lên vì vậy tôi kết thúc thụt nó tất cả 4 dòng

Câu trả lời:


152

LoadLibrarykhông làm những gì bạn nghĩ nó làm. Nó tải DLL vào bộ nhớ của tiến trình hiện tại, nhưng nó không nhập các hàm được định nghĩa trong đó một cách kỳ diệu! Điều này sẽ không thể xảy ra, vì các lệnh gọi hàm được trình liên kết giải quyết tại thời điểm biên dịch trong khi LoadLibraryđược gọi trong thời gian chạy (hãy nhớ rằng C ++ là một ngôn ngữ được nhập tĩnh ).

Bạn cần một hàm WinAPI riêng biệt để có được địa chỉ của các chức năng tự động nạp: GetProcAddress.

Thí dụ

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

Ngoài ra, bạn nên xuất hàm của mình từ DLL một cách chính xác. Điều này có thể được thực hiện như sau:

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

Như Lundin lưu ý, bạn nên giải phóng tay cầm vào thư viện nếu bạn không cần chúng lâu hơn. Điều này sẽ làm cho nó được dỡ bỏ nếu không có tiến trình nào khác vẫn giữ một chốt điều khiển cho cùng một DLL.


Nghe có vẻ giống như một câu hỏi ngu ngốc nhưng loại f_funci là gì?

8
Ngoài ra, câu trả lời là tuyệt vời và dễ hiểu

6
Lưu ý rằng f_funcitrên thực tế là một loại (chứ không phải một loại). Kiểu f_funciđọc là "con trỏ đến một hàm trả về một intvà không nhận đối số". Thông tin thêm về con trỏ hàm trong C có thể được tìm thấy tại newty.de/fpt/index.html .
Niklas B.

Cảm ơn bạn một lần nữa vì đã trả lời, hàm funci không có đối số và trả về một số nguyên; Tôi đã chỉnh sửa câu hỏi để hiển thị chức năng đã được biên dịch? vào .dll. Khi tôi cố gắng chạy sau bao gồm "typedef int ( f_funci) ();" Tôi gặp lỗi này: C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp || Trong hàm 'int main ()': | C: \ Documents and Settings \ User \ Desktop \ fgfdg \ onemore.cpp | 18 | error: không thể chuyển đổi 'int ( ) ()' thành 'const CHAR *' cho đối số '2' thành 'int (* GetProcAddress (HINSTANCE__ , const CHAR )) () '| || === Xây dựng xong: 1 lỗi, 0 cảnh báo === |

Tôi đã quên một dàn diễn viên ở đó (đã chỉnh sửa nó trong). Tuy nhiên, lỗi này có vẻ là một lỗi khác, bạn có chắc mình sử dụng đúng mã không? Nếu có, bạn có thể vui lòng dán mã lỗi của mình và đầu ra trình biên dịch hoàn chỉnh trên pastie.org không? Ngoài ra, các typedef bạn đã viết trong bình luận của bạn là sai (một *là mất tích, mà có thể gây ra lỗi)
Niklas B.

34

Ngoài câu trả lời đã được đăng, tôi nghĩ tôi nên chia sẻ một thủ thuật hữu ích mà tôi sử dụng để tải tất cả các hàm DLL vào chương trình thông qua con trỏ hàm mà không cần viết lệnh gọi GetProcAddress riêng biệt cho từng và mọi hàm. Tôi cũng muốn gọi các chức năng trực tiếp như đã thử trong OP.

Bắt đầu bằng cách xác định một loại con trỏ hàm chung:

typedef int (__stdcall* func_ptr_t)();

Loại nào được sử dụng không thực sự quan trọng. Bây giờ hãy tạo một mảng kiểu đó, tương ứng với số lượng hàm bạn có trong DLL:

func_ptr_t func_ptr [DLL_FUNCTIONS_N];

Trong mảng này, chúng ta có thể lưu trữ các con trỏ hàm thực sự trỏ vào không gian bộ nhớ DLL.

Vấn đề tiếp theo là GetProcAddressmong đợi các tên hàm dưới dạng chuỗi. Vì vậy, hãy tạo một mảng tương tự bao gồm các tên hàm trong DLL:

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

Bây giờ chúng ta có thể dễ dàng gọi GetProcAddress () trong một vòng lặp và lưu trữ từng hàm bên trong mảng đó:

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

Nếu vòng lặp thành công, vấn đề duy nhất mà chúng ta gặp phải bây giờ là gọi các hàm. Con trỏ hàm typedef trước đó không hữu ích, vì mỗi hàm sẽ có chữ ký riêng. Điều này có thể được giải quyết bằng cách tạo một cấu trúc với tất cả các loại hàm:

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;

Và cuối cùng, để kết nối những thứ này với mảng từ trước, hãy tạo một liên hợp:

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

Bây giờ bạn có thể tải tất cả các chức năng từ DLL bằng vòng lặp thuận tiện, nhưng gọi chúng thông qua by_typethành viên liên hiệp.

Nhưng tất nhiên, việc gõ ra những thứ như

functions.by_type.dll_add_ptr(1, 1); bất cứ khi nào bạn muốn gọi một hàm.

Hóa ra, đây là lý do tại sao tôi thêm hậu tố "ptr" vào tên: Tôi muốn giữ chúng khác với tên hàm thực tế. Giờ đây, chúng ta có thể làm mượt cú pháp struct khó hiểu và có được những cái tên mong muốn, bằng cách sử dụng một số macro:

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

Và bạn có thể sử dụng các tên hàm, với loại và tham số chính xác, như thể chúng được liên kết tĩnh với dự án của bạn:

int result = dll_add(1, 1);

Tuyên bố từ chối trách nhiệm: Nói một cách chính xác, các chuyển đổi giữa các con trỏ hàm khác nhau không được xác định bởi tiêu chuẩn C và không an toàn. Vì vậy, về mặt hình thức, những gì tôi đang làm ở đây là hành vi không xác định. Tuy nhiên, trong thế giới Windows, các con trỏ hàm luôn có cùng kích thước bất kể loại nào và chuyển đổi giữa chúng có thể dự đoán được trên bất kỳ phiên bản Windows nào tôi đã sử dụng.

Ngoài ra, về lý thuyết có thể có phần đệm được chèn trong union / struct, điều này sẽ khiến mọi thứ bị lỗi. Tuy nhiên, các con trỏ có cùng kích thước với yêu cầu căn chỉnh trong Windows. A static_assertđể đảm bảo rằng struct / union không có đệm có thể vẫn theo thứ tự.


1
Cách tiếp cận kiểu C này sẽ hoạt động. Nhưng sẽ không thích hợp nếu sử dụng một cấu trúc C ++ để tránh các #defines?
harper

@harper Vâng trong C ++ 11 bạn có thể sử dụng auto dll_add = ..., nhưng trong C ++ 03 không có cấu trúc nào mà tôi có thể nghĩ ra sẽ đơn giản hóa công việc (tôi cũng không thấy bất kỳ vấn đề cụ thể nào với các #defines ở đây)
Niklas B.

Vì đây là tất cả dành riêng cho WinAPI, bạn không cần phải gõ tên riêng của mình func_ptr_t. Thay vào đó, bạn có thể sử dụng FARPROC, đó là kiểu trả về GetProcAddress. Điều này có thể cho phép bạn biên dịch với mức cảnh báo cao hơn mà không cần thêm diễn viên vào GetProcAddresscuộc gọi.
Adrian McCarthy

@NiklasB. bạn chỉ có thể sử dụng autocho một chức năng tại một thời điểm, điều này đánh bại ý tưởng thực hiện nó một lần cho tất cả trong một vòng lặp. nhưng những gì xảy ra với một mảng std :: Chức năng
Francesco Dondi

1
@Facesco các loại hàm std :: sẽ khác giống như các loại funcptr. Tôi đoán các mẫu khác nhau sẽ hữu ích
Niklas B.

1

Đây không hẳn là một chủ đề nóng, nhưng tôi có một lớp nhà máy cho phép một dll tạo một thể hiện và trả về nó dưới dạng một DLL. Đó là những gì tôi đã tìm kiếm nhưng không thể tìm thấy chính xác.

Nó được gọi là,

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

trong đó IHTTP_Server là giao diện ảo thuần túy cho một lớp được tạo trong một DLL khác hoặc cùng một lớp.

DEFINE_INTERFACE được sử dụng để cung cấp giao diện id lớp. Đặt giao diện bên trong;

Một lớp giao diện trông giống như,

class IMyInterface
{
    DEFINE_INTERFACE(IMyInterface);

public:
    virtual ~IMyInterface() {};

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

Tệp tiêu đề giống như thế này

#if !defined(SN_FACTORY_H_INCLUDED)
#define SN_FACTORY_H_INCLUDED

#pragma once

Các thư viện được liệt kê trong định nghĩa macro này. Một dòng cho mỗi thư viện / tệp thực thi. Sẽ rất tuyệt nếu chúng ta có thể gọi vào một tệp thực thi khác.

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

Sau đó, đối với mỗi dll / exe, bạn xác định một macro và liệt kê các triển khai của nó. Def có nghĩa là nó là cài đặt mặc định cho giao diện. Nếu nó không phải là mặc định, bạn đặt tên cho giao diện được sử dụng để xác định nó. Tức là, đặc biệt và tên sẽ là 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)

Với tất cả các thư viện được thiết lập, tệp tiêu đề sử dụng các định nghĩa macro để xác định những thứ cần thiết.

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

Điều này tạo ra một enum cho các thư viện.

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

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

Điều này tạo ra một enum cho việc triển khai giao diện.

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

    long CallEntryPoint(long id, long interfaceId);

Điều này xác định lớp nhà máy. Không nhiều về nó ở đây.

    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

Sau đó, CPP là,

#include "sn_factory.h"

#include <windows.h>

Tạo điểm vào bên ngoài. Bạn có thể kiểm tra xem nó có tồn tại hay không bằng cách sử dụng 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;
        }
    }
}

Các macro thiết lập tất cả dữ liệu cần thiết.

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

Mỗi thư viện bao gồm "cpp" này với một cpp sơ khai cho mỗi thư viện / tệp thực thi. Bất kỳ nội dung tiêu đề được biên dịch cụ thể nào.

#include "sn_pch.h"

Thiết lập thư viện này.

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

Một bao gồm cho cpp chính. Tôi đoán cpp này có thể là một .h. Nhưng có nhiều cách khác nhau để bạn có thể làm điều này. Cách tiếp cận này đã làm việc cho tôi.

#include "../inc/sn_factory.cpp"
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.