尝试从已加载的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中,我将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无法解决的外部符号错误而自行失败在调用Write
方法的地方。
对于我的情况(对于某些多态性),我不需要将其设为virtual
,而我的问题是-为什么virtual
说明符才能进行这项工作?
当我选择使用加载时动态链接并在针对.lib文件的构建过程中进行链接时,这也适用(没有virtual
说明符),但是我喜欢坚持使用运行时动态链接。
谢谢。
使用Windows 10(1809)和Visual Studio 2019和最新的Windows SDK。
答案 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 的指针。 em>函数。请注意,在这种情况下,我们不需要将类标记为 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 文件
中准确声明此名称