【问题标题】:Why invoking class method from DLL requires virtual specifier?为什么从 DLL 调用类方法需要虚拟说明符?
【发布时间】:2019-10-08 08:49:32
【问题描述】:

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

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

class ENGINE_API Log
{
private:
    const char* Category;

public:
    Log(const char* Category);

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

该类由__declspec(使用ENGINE_API宏)标记为dllexport,同时在“所有者”DLL中构建它并标记为dllimport,同时在构建另一个时仅使用标题动态链接库。

第一个“所有者”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 未解决的外部符号错误上失败,其中 @ 987654336@方法被调用。

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

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

谢谢。

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

【问题讨论】:

  • 您是否将 lib 文件包含在 exe 构建中?认为没有。以及通过动态链接导入某些函数和通过GetProcAddress 导入另一个函数有什么意义?包含 lib 文件而不使用 GetProcAddressGetProcAddress 用于每个导出的函数
  • 你的windows版本,你的vs版本,你用的是vs——这里绝对不相关
  • 当你使用 virtual 时(比如 Writevirtual 方法) - Output->Write 通过对象上的 vtable 调用。当你不使用 virtual - 你从哪里得到Write 地址? (现在它不存在于 vtable 中)。或通过GetProcAddress 或通过导入表。你什么都不做
  • RbMm 是对的。函数的地址在编译时确定,但Log::Write 的代码实际上并未在第二个 DLL 中编译。你不应该导出 C++ 的东西(比如 Log 类)。相反,CreateLogInstance 应该返回 void*,而一些新函数 Log_Write 应该采用两个参数,void*const char*
  • @Dialectus:导出 C++ 的东西非常好。不应该做的是导出 c++ STL-stuff(编译器和版本依赖)!

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


【解决方案1】:

对于来自外部模块的调用函数,我们需要该函数的地址。

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

这就是为什么要使用 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。 loader,当加载你的 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 文件中准确声明这个名称

【讨论】:

  • 有道理,谢谢。不确定这是否超出了我原来的问题的范围,但是 - 如果我使用 dllimport/dllexport 并链接到 .lib 文件,它将根据 dumpbin 输出在 DLL/exe 中创建“硬”依赖项到另一个文件,即美好的。但是,如果我不想在运行时进行“硬”依赖和加载/链接,我必须使用 LoadLibrary 和虚拟方法来保持代码干净,而不使用链接器备用名称等“神奇”的东西。对吗?
  • @Wefod - 我尝试描述这是如何实现的。你需要有指向函数的指针来调用它。或者使用virtual函数,实际上你需要实现纯com接口并通过这个接口调用全部,或者你需要实现变量,它将持有指向成员函数的指针。并通过 GetProcAddress 初始化它
  • 知道了。 virtual变种会慢吗?我的意思是,如果该函数被多次调用,比如说每秒 60 次,与 dllimport/dllexport 和与 .lib 的链接相比,它的性能会受到显着影响吗?但我想这是特定于项目的,所以也许我应该自己测试一下。也感谢您的宝贵时间。
  • @Wefod - 不,virtual 不会很慢。每秒 60 次 - 这没什么。是的,每次调用 2 条额外的指令,但这没什么
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-04-07
  • 1970-01-01
  • 2021-04-28
  • 1970-01-01
  • 1970-01-01
  • 2014-11-21
  • 1970-01-01
相关资源
最近更新 更多