我在尝试从加载的 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 中,我调用 LoadLibrary
和 GetProcAddress
并正确转换为函数指针。我只是用一些文本调用 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的指针em> 功能。请注意,在这种情况下,我们不需要将类标记为 dllexport 或 dllimport
这就是使用虚拟函数的原因 - 所有你需要调用虚拟函数的东西 - 指向对象的指针。
如果函数不是虚拟 - 并标记为__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/