是否safe/possible 跨DLL边界访问虚方法?

Is it safe/possible to access virtual methods across a DLL boundary?

虚函数 table 是否安全,甚至可以跨 dll 边界访问?

应用程序和 dll(从其他地方编译)知道 BaseClass,但只有 dll 知道覆盖虚拟方法的附加超类。

如果 dll 向应用程序提供指向 SuperClass 的指针,应用程序是否可以安全地访问并执行正确的方法(IE 将调用 SuperClass 覆盖的方法)?

是的,只要在实例化任何 SuperClass 对象时不卸载 DLL。就应用程序而言,它正在使用的对象是一个 BaseClass 对象。

典型的 vtable 实现有点像这样。

struct bob;
struct bob_vtable {
  void(*speak)(bob*) = 0;
};
struct bob {
  static void populate_vtable(bob_vtable* v){
    v->speak = bob::speak_impl;
  }
  static bob_vtable make_vtable(){
    bob_vtable vtable;
    populate_vtable(&vtable);
    return vtable;
  }
  bob_vtable const* get_vtable() {
    static const bob_vtable vtable = make_vtable();
    return &vtable;
  }
  static void speak_impl(bob* self){
    std::cout << self->name;
  }
  bob_vtable const* vtable=0;
  void speak(){
    vtable->speak(this);
  }
  bob():bob(get_vtable()){}
  bob(bob_vtable const* ptable){
    vtable=ptable;
  }
  std::string name="bob";
};

struct alice;
struct alice_vtable : bob_vtable {
  // new virtual methods in alice go here
  bool (*is_alice)(alice const*) = 0;
};
struct alice:bob{
  static void speak_impl(bob* bself){
    alice* self = static_cast<alice*>(bself);
    std::cout << "alice is not " << self->name;
  }
  static bool is_alice_impl(alice const*) {
    return true;
  }
  bool is_alice() const {
    return static_cast<alice_vtable const*>(vtable)->is_alice(this);
  }
  
  static void populate_vtable(alice_vtable* table){
    bob::populate_vtable(table);
    table->speak=alice::speak_impl; // this is an override of a bob_vtable method
    table->is_alice=alice::is_alice_impl; // new virtual method
  }
  static alice_vtable make_vtable(){
    alice_vtable vtable;
    populate_vtable(&vtable);
    return vtable;
  }
  alice_vtable const* get_vtable() {
    static const alice_vtable vtable = make_vtable();
    return &vtable;
  }
  alice():alice(get_vtable()){}
  alice(alice_vtable const* ptable):bob(ptable){
  }
};

struct charlie;
struct charlie_vtable : alice_vtable {};
struct charlie:alice {
  static bool is_alice_impl(alice const*) {
    return false;
  }
  static void populate_vtable(charlie_vtable* table){
    alice::populate_vtable(table);
    table->is_alice=charlie::is_alice_impl; // this is an override of a alice_vtable method
    // speak is left unoverloaded
  }
  static charlie_vtable make_vtable(){
    charlie_vtable vtable;
    populate_vtable(&vtable);
    return vtable;
  }
  charlie_vtable const* get_vtable() {
    static const charlie_vtable vtable = make_vtable();
    return &vtable;
  }
  charlie():charlie(get_vtable()){}
  charlie(charlie_vtable const* ptable):alice(ptable){
  }
};

拥有 bob* 的人不必知道该对象是 alicebob::speak 代码查看 vtable 并找到在 alice 创建时存储在那里的指向 alice::speak_impl 的指针。

编译器弄清楚如何调用虚拟方法,因为它们同意对象的 布局 及其虚拟函数 table。关于这些布局的协议远远好于大多数其他编译器间协议。

void speak_as_bob( bob& b ) {
    b.speak();
}
int main() {
    bob b;
    alice a;
    speak_as_bob(b);
    std::cout << "\n";
    speak_as_bob(a);
    std::cout << "\n";
}

然而,一个重要的事实是 alice 的 vtable 通常存在于定义 alice 的 DLL 中。如果您卸载 DLL,这些指针可能会悬空在调用函数之前。

Live example.

输出是

bob
alice is not bob

函数 speak_as_bob 不知道 我们所做的 vtable 事情。它只是在对 bob 对象的引用上调用 bob::speak()。这个完全正常的方法然后查找 bob_vtable::speak 并调用它。

默认情况下,创建一个 bob 填充 bob::vtable->speak 并带有指向 speak_impl 的指针。当我们继承一个alice时,我们首先构造一个bob(在它的vtable中有一个指向bob::speak_impl的指针),然后我们覆盖alice::populate_vtable中的字段而是指向 alice::speak_impl 的指针。

在 C++ 之前,人们用 C 编写面向对象的代码的方式与我上面写的类似(但没有方法,所以你会使用自由函数)。有很多方法可以实现面向对象的语言; C++的虚函数就是基于类似上面的设计。

现在有一些实际的版本控制问题。我会和 windows 谈谈,因为我更了解那里的问题。

如果您的 接口 从一个 DLL 版本更改为另一个 DLL 版本,虚函数 table 条目将移动,并且不重新编译的客户会死得很惨。

但是,如果您同时进行虚拟继承和虚拟函数,则您继承的每个虚拟接口都是一个单独的 table,您可以添加到每个虚拟接口的末尾。 (我不知道涉及虚继承的虚函数的布局table,但它比上面的例子更复杂)。

windows 编译器 population vtable 的顺序是方法声明的顺序,[b]除非[/b] 你覆盖了一个方法;覆盖聚集在一起。如果您希望 ABI 稳定性优于 DLL 版本,以便客户端不必重新编译,请不要重载虚拟方法。

现在,如果你不改变界面布局,你就坐得很好。

最后,我发现在 MacOS 上 loaded/unloaded 动态库的细节使得动态库的生命周期更难导航。在 windows 上跟随死 vtable 指针和关机时崩溃的发生频率几乎没有在 macOS 上那么频繁。我不确定为什么,可能是因为 windows 在卸载代码之前到处进行静态破坏,而 macos 在 DLL 中进行静态破坏,然后立即卸载它,然后再继续另一个 DLL。


我还添加了一个 charlie,因此您可以看到如何在层次结构的下方添加新方法。现在,在真正的 C++ 中,vtable 指针在 bob 的构造期间被分配给 bob 的 vtable,然后在 alice 的构造期间重新分配给 alice alice的构造等,这里为了简单起见,只设置为alice的vtable。

临时的“你是 bobbob 构造期间”意味着不会意外调用依赖于 alice 构造函数的方法。