request-free-img

无源码无LIB文件情况下动态加载C++ DLL并使用导出类完整指南

这是一个非常有深度的问题。

粉丝提问

有一个无源码的C++ DLL,有头文件,但是没有LIB文件的情况下,如何动态导出其中的类。

嗯,这个问题涉及动态加载 DLL,类构造函数的底层实现,底层内存布局,C++编译原理等。

强烈建议所有C++程序员能仔细看看这个视频,它将帮助你更深入的理解C++的底层实现。

正式开始演示

好,我们正式开始。

做为演示,我针对粉丝提出的问题写了一个测试DLL,在这个DLL中它导出了一个函数叫ShowMessage。

在这个函数中,它会弹出一个消息框。

1. 导出一个 C 风格的函数

#ifdef __cplusplus
extern "C" {
#endif

// 此函数会弹出一个消息框,表明该函数已被调用
__declspec(dllexport) void ShowMessage()
{
    MessageBox(NULL, L"这是一个从 DLL 导出的函数",
        L"DLL Message", MB_OK);
}

#ifdef __cplusplus
}
#endif

2. 导出一个 C++ 类(Dog类)

// 导出一个 C++ 类(狗狗类)
class __declspec(dllexport) Dog {
public:
    // 构造函数
    Dog() {
    }

    // 析构函数
    ~Dog() {
    }

    // static void* operator new(size_t size) {
    //     MessageBoxA(NULL, "自定义 operator new 被调用", "Memory Allocation", MB_OK);
    //     return malloc(size);
    // }
    // static void operator delete(void* ptr) {
    //     MessageBoxA(NULL, "自定义 operator delete 被调用", "Memory Deallocation", MB_OK);
    //     free(ptr);
    // }

    // 成员函数:发出狗叫声
    void Bark() {
        MessageBox(NULL, L"汪汪!",
            L"Dog Bark", MB_OK);
    }
};

测试程序:动态加载DLL

我另外再写一个测试程序,用来加载这个DLL。

因为粉丝的问题是没有LIB文件,所以在这个程序中,我们得采用动态加载DLL的方式来调用导出函数和导出类。

首先使用LoadLibrary加载这个DLL,同时我们定义好ShowMessage的函数指针类型,然后使用GetProcAddress获得ShowMessage的内存地址,这样就可以调用ShowMessage了。

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

// 为了动态调用函数,先定义函数指针类型
typedef void (*PFN_ShowMessage)();

int main()
{
    std::cout << "正在加载 DLL" << std::endl;

    // 加载 DLL
    HMODULE hDLL = LoadLibrary(L"dllproj.dll");
    if (hDLL == NULL) {
        std::cerr << L"加载 DLL 失败!错误代码:" 
                  << GetLastError() << std::endl;
        return -1;
    }

    // 获取 ShowMessage 函数的地址
    PFN_ShowMessage ShowMessage = (PFN_ShowMessage)
        GetProcAddress(hDLL, "ShowMessage");
    if (ShowMessage == NULL) {
        std::cerr << L"获取 ShowMessage 地址失败!错误代码:" 
                  << GetLastError() << std::endl;
        FreeLibrary(hDLL);
        return -1;
    }

    // 调用 ShowMessage 函数
    ShowMessage();

    return 1;
}

通过这种动态加载DLL的方式调用导出函数非常简单,人人都会是不是?

核心问题:如何动态使用导出的C++类?

现在粉丝的问题是,这种动态加载DLL的方式要如何使用它导出的类呢?

其实是有办法的。

使用DUMPBIN查看DLL导出表

首先咱们用VS自带的DUMPBIN工具看一下DLL的导出信息:

1    0 000112D0 ??0Dog@@QAE@XZ = @ILT+715(??0Dog@@QAE@XZ)
2    1 000112C6 ??1Dog@@QAE@XZ = @ILT+705(??1Dog@@QAE@XZ)
3    2 00011280 ??4Dog@@QAEAAV0@ABV0@@Z = @ILT+635(??4Dog@@QAEAAV0@ABV0@@Z)
4    3 0001100A ?Bark@Dog@@QAEXXZ = @ILT+5(?Bark@Dog@@QAEXXZ)
5    4 000110D7 ShowMessage = @ILT+210(_ShowMessage)

从导出表中可以看到,第一个导出符号是??0,然后是DOG……

我们能猜出它肯定是和DOG类有关,但是这些符号到底是什么呢?

C++编译器名称修饰规则(VS编译器)

其实这是编译器的导出符号修饰符,在不同的编译器下会有些差别,因为我们用的是VS编译器。

在VS中,??0开头表示类构造函数,??1表示类析构函数,??2表示new操作符重载,??3表示delete操作符重载,而??4表示赋值运算符,以一个?问号开头的则表示成员函数。

这是VS编译器的规则,如果是GCC又会不同,如果你采用GCC编译器那它的构造函数会是这样_ZN7DogC1Ev。

基于这些修饰规则,现在我们知道第一个导出符号是Dog类的构造函数,第二个是析构函数,第三个是赋值运算符,第四个是Dog类的Bark函数。

C++类对象创建的底层原理

那我们要如何创建这个导出的类对象呢?

当我们有一个类Dog,我们要创建它的类对象时,在C++中我们会这样写:

Dog* dog = new Dog();

其实它等价于这样:

void* memory = operator new(sizeof(Dog));
Dog* dog = static_cast<Dog*>(memory);
dog->Dog();

首先创建内存,然后将内存转换成Dog结构,然后调用构造函数。

在这里,我们看到构造函数Dog被调用时并没有传入任何参数。

然而,在底层二进制实现中,C++ 编译器会隐式地将 this 指针作为第一个参数传递给所有的非静态成员函数,包括构造函数和析构函数。

划重点,这一句理解本视频知识的关键。

完整实现:动态调用导出类

明白了这点之后,再来解决粉丝的问题就容易了。

首先,因为粉丝说他是有类的头文件的,所以我们把Dog类的声明写在这里。

class Dog {
public:
    Dog();
    ~Dog();
    // 成员函数:发出狗叫声
    void Bark();
};

然后我们先分别定义构造函数,析构函数,以及成员函数Bark的函数指针类型。

然后我们依然调用GetProcAddress分别获取这几个函数在内存中的地址。

现在传给GetProcAddress的函数名称务必是编译器添加修饰符后的完整名称。

// 为了动态调用函数,先定义函数指针类型
typedef void (*PFN_ShowMessage)();

// 构造函数:接受 this 指针
typedef void (*ConstructorFunc)(void*);

// 析构函数:接受 this 指针
typedef void (*DestructorFunc)(void*);

// 成员函数:接受 this 指针
typedef void (*BarkFunc)(void*);

主函数完整代码

int main()
{
    std::cout << "正在加载 DLL" << std::endl;

    // 加载 DLL
    HMODULE hDLL = LoadLibrary(L"dllproj.dll");
    if (hDLL == NULL) {
        std::cerr << L"加载 DLL 失败!错误代码:" 
                  << GetLastError() << std::endl;
        return -1;
    }

    // 获取 ShowMessage 函数的地址
    PFN_ShowMessage ShowMessage = (PFN_ShowMessage)
        GetProcAddress(hDLL, "ShowMessage");
    if (ShowMessage == NULL) {
        std::cerr << L"获取 ShowMessage 地址失败!错误代码:" 
                  << GetLastError() << std::endl;
        FreeLibrary(hDLL);
        return -1;
    }

    // 调用 ShowMessage 函数
    ShowMessage();

    ConstructorFunc Constructor = (ConstructorFunc)GetProcAddress(hDLL, "??0Dog@@QAE@XZ");
    DestructorFunc Destructor = (DestructorFunc)GetProcAddress(hDLL, "??1Dog@@QAE@XZ");
    BarkFunc Bark = (BarkFunc)GetProcAddress(hDLL, "?Bark@Dog@@QAEXXZ");

    // 正确的内存分配,确保对齐
    void* dogMemory = ::operator new(sizeof(Dog));

    // 4. 调用构造函数,初始化对象(此时 this 指针就是 dogMemory)
    Constructor(dogMemory);

    // 5. 使用对象,调用成员函数
    Bark(dogMemory);

    // 6. 调用析构函数,销毁对象
    Destructor(dogMemory);

    // 7. 释放内存
    ::operator delete(dogMemory);

    // 8. 卸载 DLL
    FreeLibrary(hDLL);

    return 1;
}

运行结果与总结

好,现在我们运行这个测试程序。

看到了吗,首先是弹出了ShowMessage函数中的消息框,然后弹出了Bark类成员函数中的消息框。

说明我们成功使用动态加载DLL的方式创建这个导出类对象并调用了它的成员函数。

怎么样,你们学会了吗?

本文适合中高级C++开发者阅读,掌握此技术后,你将能更灵活地处理各种DLL集成场景。


更多问题探讨,请关注公众号:程序员角