插件式设计

C++ dlopen mini HOWTO: How to dynamically load C++ functions and classes using the dlopen API.

写插件或模块的时候可能需要在运行时加载库并使用其函数。c语言加载一个库很简单,调dlopen、dlsym和dlclose就行了,但c++有Name Mangling,且c++标准没有定义mangling的标准所以不同编译器都用了自己的算法,所以需要通过extern "C"来声明被加载的函数,然后像c语言一样通过dlsym。

例子:

main.cpp

#include <dlfcn.h>
#include <iostream>

int main() {
  using std::cerr;
  using std::cout;

  cout << "C++ dlopen demo\n\n";

  // open the library
  cout << "Opening hello.so...\n";
  void *handle = dlopen("./hello.so", RTLD_LAZY);

  if (!handle) {
    cerr << "Cannot open library: " << dlerror() << '\n';
    return 1;
  }

  // load the symbol
  cout << "Loading symbol hello...\n";
  typedef void (*hello_t)();

  // reset errors
  dlerror();
  hello_t hello = (hello_t)dlsym(handle, "hello");
  const char *dlsym_error = dlerror();
  if (dlsym_error) {
    cerr << "Cannot load symbol 'hello': " << dlsym_error << '\n';
    dlclose(handle);
    return 1;
  }

  // use it to do the calculation
  cout << "Calling hello...\n";
  hello();

  // close the library
  cout << "Closing library...\n";
  dlclose(handle);
}

hello.cpp

#include <iostream>

extern "C" void hello() { std::cout << "hello" << '\n'; }

extern "C"和带大括号的extern "C" { … }区别:第一种形式的声明具有extern链接和C语言链接;第二个仅影响语言链接。所以下面两种声明是等效的:

extern "C" int foo;
extern "C" void bar();

extern "C" {
  extern int foo;
  extern void bar();
}

dlopen API另一个问题是只能加载函数,不能加载,因为我们需要类的实例,而不仅仅是指向函数的指针。解决方法是把类的创建过程也api化了,即工厂模式。

例子:use a generic polygon class as interface and the derived class triangle as implementation

polygon.hpp

#ifndef POLYGON_HPP
#define POLYGON_HPP

class polygon {
protected:
  double side_length_;

public:
  polygon() : side_length_(0) {}

  virtual ~polygon() {}

  void set_side_length(double side_length) {
    side_length_ = side_length;
  }

  virtual double area() const = 0;
};

// the types of the class factories
typedef polygon *create_t();
typedef void destroy_t(polygon *);

#endif

main.cpp

#include "polygon.hpp"
#include <dlfcn.h>
#include <iostream>

int main() {
  using std::cerr;
  using std::cout;

  // load the triangle library
  void *triangle = dlopen("./triangle.so", RTLD_LAZY);
  if (!triangle) {
    cerr << "Cannot load library: " << dlerror() << '\n';
    return 1;
  }

  // reset errors
  dlerror();

  // load the symbols
  create_t *create_triangle = (create_t *)dlsym(triangle, "create");
  const char *dlsym_error = dlerror();
  if (dlsym_error) {
    cerr << "Cannot load symbol create: " << dlsym_error << '\n';
    return 1;
  }

  destroy_t *destroy_triangle =
      (destroy_t *)dlsym(triangle, "destroy");
  dlsym_error = dlerror();
  if (dlsym_error) {
    cerr << "Cannot load symbol destroy: " << dlsym_error << '\n';
    return 1;
  }

  // create an instance of the class
  polygon *poly = create_triangle();

  // use the class
  poly->set_side_length(7);
  cout << "The area is: " << poly->area() << '\n';

  // destroy the class
  destroy_triangle(poly);

  // unload the triangle library
  dlclose(triangle);
}

triangle.cpp

#include "polygon.hpp"
#include <cmath>

class triangle : public polygon {
public:
  virtual double area() const {
    return side_length_ * side_length_ * sqrt(3) / 2;
  }
};

// the class factories

extern "C" polygon *create() { return new triangle; }
extern "C" void destroy(polygon *p) { delete p; }

由此可见c++的插件系统基本都是基于这样的原理:

  1. 定义纯虚基类作为interface。
  2. 把实现类封装为dll文件,用LoadLibrary运行时载入。
  3. 扩展接口不要用c++,用纯c。即通过C API获取插件对象实例。因为一个基本的常识,C++ ABI在不同编译器、不同编译器版本之间有差异,而的C ABI是稳定的。

插件式可扩展架构设计心得 ES2049 Studio