c++ - 为什么从 DLL 中调用类方法需要虚拟说明符?

标签 c++ winapi visual-c++

我在尝试从加载的 DLL 中调用方法时遇到奇怪的问题。

让我们从简单的 Log 类开始,方法 Write 采用 const char* 参数。

class ENGINE_API Log
{
private:
    const char* Category;

public:
    Log(const char* Category);

    void Write(const char* format, ...);
};

该类在“所有者”DLL 中构建时被 __declspec(使用 ENGINE_API 宏)标记为 dllexport 并标记为 dllimport 而在构建另一个 DLL 期间仅使用 header 。

第一个“所有者”DLL 还导出了名为 CreateLogInstance 的外部 C 函数,它只是创建 Log 类的实例并返回它。

PUBLIC_FUNCTION Log* CreateLogInstance(const char* name)
{
    return new Log(name);
}

在第二个 DLL 中,我调用 LoadLibraryGetProcAddress 并正确转换为函数指针。我只是用一些文本调用 Write 方法。

typedef Log*(*CreateLogInstanceFunction)(const char*);

HINSTANCE moduleHandle = LoadLibrary("Engine.dll");
CreateLogInstanceFunction createLogInstanceFunction = (CreateLogInstanceFunction)GetProcAddress(moduleHandle, "CreateLogInstanceWithName");

// omitting the null checks etc

Output = createLogInstanceFunction("Game");
Output->Write("Hello Game");

一切正常,满足一个要求,即 Write 方法必须标记为 virtual,如果不是编译,它会在 LNK2019 unresolved external 上自行失败调用 Write 方法的行出现符号错误

在我的情况下(对于某些多态性)我不需要它是 virtual 我的问题是 - 为什么需要 virtual 说明符才能得到这个工作?

当我选择使用 Load-Time Dynamic Linking 并在构建期间针对 .lib 文件进行链接时,这也有效(没有 virtual 说明符),但我喜欢坚持使用运行时动态链接

谢谢。

使用带有最新 Windows SDK 的 Windows 10 (1809) 和 Visual Studio 2019。

最佳答案

为了从外部模块调用函数,我们需要这个函数的地址。

如果我们将函数标记为虚拟 - 在对象内部存在指向表(所谓的vftable)的指针,其中存储了指向该对象所有虚拟函数的指针。编译器通过这个指针为调用函数生成合适的代码。

所以当你写的时候

class log
{
public:
    virtual void Write(const char* format, ...);
};

编译器在对象内部生成隐藏结构vftable

class log
{
public:
    virtual void Write(const char* format, ...);

    struct vftable {
        void (* Write)(const char* format, ...);
    };
};

然后打电话

Output->Write("Hello Game");

真正实现为

(*(log::vftable**)Output)->Write("Hello Game");

所以这里我们有对象指针 (Output),其中存在指向 log::vftable 的指针,并且在此表中存在指向 Write 功能。请注意,在这种情况下,我们不需要将类标记为 dllexportdllimport

这就是使用虚拟函数的原因 - 所有你需要调用虚拟函数的东西 - 指向对象的指针。

如果函数不是虚拟 - 并标记为__declspec(dllimport) 编译器声明隐藏变量,它是指向函数的指针。并且将通过此变量调用函数。所以当我们写:

class __declspec(dllimport) log
{
public:
    virtual void Write(const char* format, ...);
};

void demo(log* Output)
{
    Output->Write("Hello Game");
}

编译器接下来实际做的是:

extern void (* __imp_?Write@log@@QEAAXPEBDZZ)(log* This, const char* format, ...);

void demo(log* Output)
{
    __imp_?Write@log@@QEAAXPEBDZZ(Output, "Hello Game");
}

请注意,指向函数的指针 __imp_?Write@log@@QEAAXPEBDZZ 仅声明(使用 extern)但未实现。如果您在没有适当的 lib 文件的情况下构建(其中实现了 __imp_?Write@log@@QEAAXPEBDZZ 符号),您会遇到链接器错误:未解析的外部符号 __imp_?Write@log @@QEAAXPEBDZZ

因此,如果成员函数声明时没有virtual,并且类声明为__declspec(dllimport),您需要使用适当的lib。加载程序,当加载你的 PE 时,从 Engine.dll 中导出 ?Write@log@@QEAAXPEBDZZ 函数地址,并将此地址写入 __imp_?Write@log@@QEAAXPEBDZZ

当然还有一个选择 - 自己实现所有这些。承担装载机的 self 工作。像这样

#pragma comment(linker, "/alternatename:__imp_?Write@log@@QEAAXPEBDZZ=?__imp__Write_log__QEAAXPEBDZZ@@3PEAXEA")

void* __imp__Write_log__QEAAXPEBDZZ = 0;

BOOL LoadEngine()
{
    if (HMODULE hmod = LoadLibraryW(L"Engine.dll"))
    {
        if (__imp__Write_log__QEAAXPEBDZZ = GetProcAddress(hmod, "?Write@log@@QEAAXPEBDZZ"))
        {
            return TRUE;
        }
    }

    return FALSE;
}

在此之后我们已经可以调用Output->Write("Hello Game");

当然在 c++ 中我们不能直接声明名称 __imp_?Write@log@@QEAAXPEBDZZ 所以需要使用链接器选项 /alternatename。或者我们可以在单独的 asm 文件中声明这个名称

关于c++ - 为什么从 DLL 中调用类方法需要虚拟说明符?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56247091/

相关文章:

c++ - 从文件中读取时遇到问题。似乎过早地达到了 EOF

c++ - 为 "while(true)"发出编译器警告而不为 "for(;;)"发出警告有什么意义?

c++ - 最终的虚函数有什么意义?

c++ - 大写和小写数据类型有什么区别?

c# - 无法计算所有者绘制文本中的位置

c++ - 错误 C2664 : 'errno_t wcstombs_s(size_t *,char *,size_t,const wchar_t *,size_t)' : cannot convert parameter 4

c++ - 如何在 C++ 中将整个 xml 文件源存储为字符串

visual-c++ - OpenCV 手动将 RGB 转换为 YCbCr,反之亦然 (Visual C++)

c++ - "' x' 的问题未在此范围内声明”

c# - 应用程序仅在带有 visual studio 的 pc 上运行