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

时间:2019-05-21 22:28:04

标签: c++ winapi visual-c++

尝试从已加载的DLL调用方法时出现奇怪的问题。

让我们从简单的Log类开始,方法是使用写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时只有标头。

第一个“所有者” DLL还导出了名为CreateLogInstance的extern 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无法解决的外部符号错误而自行失败在调用Write方法的地方。

对于我的情况(对于某些多态性),我不需要将其设为virtual,而我的问题是-为什么virtual说明符才能进行这项工作?

当我选择使用加载时动态链接并在针对.lib文件的构建过程中进行链接时,这也适用(没有virtual说明符),但是我喜欢坚持使用运行时动态链接

谢谢。

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

1 个答案:

答案 0 :(得分:0)

对于来自外部模块的调用功能,我们需要此功能的地址。

如果我们将函数标记为虚拟-对象内部存在指向表的指针(所谓的 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 函数。请注意,在这种情况下,我们不需要将类标记为 dllexport dllimport

这就是为什么要使用 virtual 函数-您需要调用 virtual 函数的所有功能-指向对象的指针的原因。

如果函数不是 virtual 的情况-并标记为 __ 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

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

#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 文件

中准确声明此名称