从共享库加载 base/derived 和 class 的安全性

Safety of base/derived with class loaded from shared library

我目前正在尝试在我的应用程序中构建一个基本的插件系统。理想情况下,我对插件的 class 信息一无所知,所以我在获取适当的内存管理功能时使用基础 Plugin class ,如下所示:

  void* handle = nullptr;

  if (!(handle = dlopen(path.c_str(), RTLD_LOCAL | RTLD_NOW))) {
    throw std::runtime_error("Failed to load library: " + path);
  }

  using allocClass = Plugin *(*)();
  using deleteClass = void (*)(Plugin *);


  auto allocFunc = reinterpret_cast<allocClass>(
    dlsym(handle, allocClassSymbol.c_str()));
  auto deleteFunc = reinterpret_cast<deleteClass>(
    dlsym(handle, deleteClassSymbol.c_str()));

  if (!allocFunc || !deleteFunc) {
    throw std::runtime_error("Allocator or deleter not found");
  }

  return std::shared_ptr<Plugin>(
    allocFunc(),
    [deleteFunc](Plugin *p){ deleteFunc(p); }
  );

插件端的alloc/delete函数基本上就是调用new或者delete,例如:

extern "C" {
  TestPlugin *allocator() {
        return new TestPlugin();
    }

    void deleter(TestPlugin *ptr) {
        delete ptr;
    }
}

我的问题基本上是关于这种类型不匹配的安全性——插件使用自己的类型,但加载程序将其声明为基本类型。从有限的测试来看,似乎 没有出错,但我不确定它是否正在删除插件部分而不是派生部分的内存片。

有没有更好的方法来解决这个问题而不需要应用程序导入每个插件的 headers?

撇开reinterpret-casting一个void*函数指针的有效性,通过不同类型的指针调用函数是UB。类型是否相关并不重要,例如 Plugin* (*)()TestPlugin* ()。通过不同类型的指针访问一种类型的对象也是UB,即使这些类型分别是派生和基类class。例如

Derived derived;
Base *base;
base = &derived; // OK, will access base subobject
base = reinterpret_cast<Base*>(&derived); // not OK, will access derived object

如果仅涉及单个 non-virtual 继承,您的代码 可能 可以按预期与许多(但可能不是全部)流行的编译器一起工作。然而,没有必要冒这样的风险。所有这些都可以通过让库仅公开基于 Plugin* 的接口来轻松解决。

extern "C" {
    Plugin *allocator() {
        return new TestPlugin();
    }

    void deleter(Plugin *ptr) {
        delete ptr;
    }
}

此实现要求插件具有虚拟析构函数。无论如何,这可能是个好主意。 OTOH 删除功能并不是真正需要的,客户端可以直接调用 delete Plugin。更好的是,return 来自分配器的 shared_ptr,可能带有自定义删除器。