【问题标题】:What C++ compiler/linker does when using runtime DLL loading?使用运行时 DLL 加载时 C++ 编译器/链接器会做什么?
【发布时间】:2014-12-15 22:52:49
【问题描述】:

我想了解 DLL 机制以及当我在运行时加载 DLL 时编译器会做什么(即我不会使用生成的 .lib)。

考虑以下 C++ 代码:

DLL接口头文件

#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif

class MYDLL_API Base
{
public:
  Base();

  virtual ~Base();

  virtual int get_number() const;
  virtual const char* what() const = 0;

private:
  int i_;
};

class MYDLL_API Child : public Base
{
public:
  Child();

  virtual ~Child();

  virtual int get_number() const override;
  virtual const char* what() const override;
private:
  int j_;
};

extern "C" {
  MYDLL_API Base* __cdecl initializeObject();
}

DLL 实现源文件

#include "MyDLL.hh"

Base::Base()
  : i_(42)
{}

Base::~Base()
{}

int Base::get_number() const
{
  return i_;
}

Child::Child()
  : Base()
  , j_(24)
{}

Child::~Child()
{}

int Child::get_number() const
{
  return j_;
}

const char* Child::what() const
{
  return "Hello!";
}

Base* initializeObject()
{
  return new Child();
}

这个 DLL 的目标是拥有一个由 Base 类定义的公共接口,但它允许在运行时加载的不同 DLL 中编译的特定实现(这里暴露了 Child 类的目的是例子)。

在这个阶段,如果我天真地包含 DLL 的标头:

#include "MyDLL.hh"

int main()
{
  Base* b = new Child();

  std::cout << b->get_number() << std::endl;
  std::cout << b->what() << std::endl;

  delete b;

  getchar();
  return 0;
}

链接器抱怨LNK2019LNK2001 错误:它无法解析符号。所以,它的行为符合预期(我没有使用.lib)。

现在考虑一下我用来在运行时加载 DLL 的以下代码:

#include "MyDLL.hh"

typedef Base* (*initFuncType)();

int main()
{
  HINSTANCE handle = LoadLibrary(L"MyDLL.dll");
  initFuncType init = nullptr;
  init = (initFuncType)(GetProcAddress(handle, "initializeObject"));
  if (init)
  {
    Base* b = init(); //< Use init() !

    std::cout << b->get_number() << std::endl;
    std::cout << b->what() << std::endl;

    delete b;
  }
  getchar();
  FreeLibrary(handle);
  return 0;
}

这次成功了,链接就完成了。

  • 第一个问题:发生了什么?编译器和链接器发生了什么变化?在 initializeObject() 上使用函数指针解决了这个问题。

另一个我不太了解的问题是当我删除get_number()virtualoverride 时:

int get_number() const;

我有一个LNK2019 错误,因为_main 函数中的Base::get_number(void) const 符号未解析。我知道virtual 关键字将动态解析成员函数(在运行时)。在我们的例子中,DLL 尚未加载,get_number 符号不可用。

  • 第二个问题:这是否意味着方法必须始终为virtual 使用 DLL run-time 链接?

  • 第三个问题:如何使用 Windows API 导出 C++ 函数?这样我就可以删除 extern "C" { ... } 的东西了。

感谢您的阅读!我希望我能读到有趣的答案! :)

【问题讨论】:

  • 编译器没有做任何特别/不同的事情。您的操作系统的动态链接器会尝试使用 DLL 中的符号表找到适当的符号。
  • 您正在重新发明 COM,可能并不完美。只要方法是虚拟的,就会通过对象的v-table间接调用。当您将其设为非虚拟时,您必须再次使用 GetProcAddress()。
  • @TheParamagneticCroissant 我假设这是由LoadLibraryGetProcAddress 完成的,但是当我使用init() 时,类成员函数上没有链接器错误。你能解释一下原因吗?
  • @PierrePagnoux 为什么会出现链接器错误?
  • @HansPassant 我尽量不要过分依赖 Windows 技术。 v-table(在分配期间创建,我忘了)有效地完成了工作。这是一种使用它的好方法吗?这样,我就不用自己绑定每个方法了?

标签: c++ visual-c++ dll dllimport dllexport


【解决方案1】:

链接dll文件有两种方式。

第二种方式(它的工作方式)是 C 绑定方法,您可以在 dll 中查询特定函数名,然后它会返回一个函子给您。

使用第二种方式,您将无法扩展基类,因为它们没有定义(在链接时您没有任何要复制粘贴的代码)。

为了有一个可以扩展类的dll,你需要使用动态绑定。您需要编译 .dll 并提供符号库(或导出库)。您在 VS studio 中的项目属性中有此选项。

机制如下:

  • 编译 Dll 项目 -> 输出:myLib.dll , myLib.lib
  • 在主项目中使用从 myLib.lib 导出的符号(主项目将 myLib.lib 作为依赖项)
  • 在运行时,由于绑定,您的程序会知道它需要 myLib.dll 才能工作,因此它会加载它(如果找到,否则会出现运行时错误)

使用导出库的另一个优点是您可以导出 C++ 函数(导出时会损坏)。

很难对损坏的函数进行 C 绑定。

另一方面,C Binding 与动态绑定相比,如果 myLib.dll is not found 不会让你的程序尖叫,你只会得到一个指向函数的空指针。

【讨论】:

  • 使用.lib 可以解决我的大部分问题,但它是 load-time 加载 DLL 的方式,它不符合我的需要。最后,按照HowTo: Export C++ classes from a DLL的说法,看来我已经接近C++成熟方法了。最后,我选择创建一个只包含纯虚方法而没有数据的接口。我必须导出一个 C 函数来创建对象,而其他一切都依赖于对象 v-table。
  • 嗯,您提到的 C++ 成熟方法是对您没有提出的问题的回答 :) 。它实际上是一种设计模式,它与 dll 或库导出无关,当你想隐藏你的实现时,它被广泛用于多种语言:)。检查桥接模式和工厂模式。您的 Exposed 纯虚拟类遵循桥接模式,而公开的 C 函数遵循工厂模式。
猜你喜欢
  • 2011-04-29
  • 1970-01-01
  • 2016-01-21
  • 1970-01-01
  • 1970-01-01
  • 2011-12-17
  • 1970-01-01
  • 1970-01-01
  • 2023-03-06
相关资源
最近更新 更多