c++中如何为一个程序写扩展

c++中如何为一个程序写扩展

https://www.zhihu.com/question/52538590

 

作者:mnzn2530
链接:https://www.zhihu.com/question/52538590/answer/2421788573

首先,不同的范围选择是不一样的。

1。 如果是同一个编译器,同一个版本,你可以直接用动态库,导出函数,变量,类都是可以的。

2。 如果不同的编译器,那么COM基本上是唯一的选择,当然COM的强大不止这一点,遵守com规范,你的插件可以给vba用,给微软的wscript用,可以直接嵌入到office里用。

3。 如果跨平台,那么qt的plugin很不错,但是plugin的实现感觉和com还不大一样,qt的plugin只导出了两个函数,但是它也可以直接使用导出类。。非常方便。。。。

当然,这都是在有头文件的情况下。。。如果没有头文件,你完全不知道dll里在干啥。。。那就要参考以前的外挂怎么写了。。。。也不是不行。。。。

 

作者:诸葛不亮
链接:https://www.zhihu.com/question/52538590/answer/137136003

我简单的说明下c/c++里的插件系统是怎么运行的吧。包括.COM、Qt Plugin等各种框架的插件机制,基本都是这样的原理。

Windows/Linux均支持通过文件名运行时加载动态链接库,通过函数符号名称获得函数指针,故:

  1. 定义纯虚基类作为Interface(如果有Java基础比较好理解)。
  2. 把实现类封装为dll文件,用LoadLibrary运行时载入。
  3. 通过C API获取插件对象实例。因为C++ ABI在不同编译器、不同编译器版本之间有差异,而的C ABI是稳定的。

所以就可以这么做了——

  1. 写一个接口类,内部都是纯虚函数,用作定义对外接口。
  2. 写一个实例类,继承实现这个接口。这个类不用导出。
  3. 导出一个C函数getInstance如下。
  4. 使用插件者,通过文件名在运行时加载dll;
  5. 使用插件者,通过字符串"getInstance"获取到函数指针;
  6. 运行函数指针,得到对象实例。然后就可以通过接口调用了。
extern "C" std::shared_ptr<ISomeInterface> getInstance()
{
    return std::dynamic_pointer_cast<ISomeInterface>(std::make_shared<MyImplementClass>());
}

上述make_shared这一步,需要封装在库里,暴露一个函数接口,然后可以用上面那个模板函数进一步封装以方便使用。

注:

对纯虚方法的引用,可以直接编译通过,不需要链接方法实现。

所以使用者(程序本身)只需要include接口描述,就可以在代码里使用该接口类型的指针对象了,可以顺利编译通过,不需要链接。

然后实际运行时,就可以随意替换实现类(替换插件dll库),然后通过配置文件或其他手段,通知程序从某个dll插件加载实例即可。

使用者代码如下:

// 加载dll
HMODULE lib = LoadLibrary("xxx.dll");
// 解析函数指针
std::function<std::shared_ptr<ISomeInterface>(void)> getInstanceFunc = GetProcAddress(lib, "getInstance");
// 获得对象
this->myPlugInstance = getInstanceFunc();
// 通过接口随便操作咯
this->myPlugInstance->doSomething();

程序退出前别忘了通过FreeLibrary卸载dll库。

Linux下同理,只不过变成了.so库,同样有对应的系统API完成这些操作。

======== 更新 ========

前面的写法是手敲的,没考虑是否能编译,当伪码看就行。

评论中就遇到了问题——VC编译器给extern "C"添加了额外的约束,不能用来传递类对象。

所以我更新下实际业界中的实现,比上面的复杂一些,但是更实用。

实际实现中,使用了抽象工厂+单例两个设计模式:

  1. 软件框架统一提供一个插件工厂,用户通过约定的插件id(比如.COM的GUID,比如company.product.module.class这样的字符串标识)创建插件实例。
  2. 插件工厂提供注册接口,插件加载进内存后将各类型的构造器和id注册进去。
  3. 插件动态库制作一个static全局静态对象,构造函数里注册插件类,析构函数里取消注册,这样可以在动态库加载时自动注册,卸载时自动取消注册。
  4. 整个插件库不需要导出任何接口,因为纯虚接口无需链接,对象则是统一从框架的插件工厂获取。

代码如下,在MinGW和MSVC编译器上都可通过。

本方法需要开启RTII和C++11,实际上几乎所有C++插件框架,都依赖于RTII。

手机慎入,因为有大量模板。电脑可流畅阅读,已尽量控制行宽80字符。

// IPluginFactory.h 框架接口,所有用户代码/插件代码均链接这个框架库,类似Qt里的Qt5Core.dll
// IBase: 所有插件接口的基类,可以使用各类框架的Object类型,比如Qt的QObject
// 最好内置引用计数,如此处
struct IBase : public std::enable_shared_from_this<IBase>
{
    virtual ~IBase() = default;
};

// 插件工厂接口
struct IPluginFactory
{
    virtual ~IPluginFactory() = default;

    template<typename T>
    std::shared_ptr<T> createInstance(const std::string id)
    { return std::static_pointer_cast<T>(createInstanceWithBase(id); }

protected:
   virtual std::shared_ptr<IBase> createInstanceWithBase(const std::string& id) = 0;
};

// 整个dll,只需要导出这唯一一个符号,其他所有类都不需要导出
extern "C" IPluginFactory* getPluginFactory();



// PluginFactory.cpp
// 插件工厂实例,此处使用std::string作为类标识,便于使用
class PluginFactory : public IPluginFactory
{
public:
    PluginFactory() {}
    virtual ~MyPlugin() = default;

    bool registerClass(const std::string& id,
                       std::function<std::shared_ptr<IBase>()> constructor)
    {
        if (constructors.find(id) != constructors.end()) 
            return false;
        constructors[id] = constructor;
    }

    void unregisterClass(const std::string& id)
    { 
        auto it = constructors.find(id);
        if (it != constructors.end())
            constructors.erase(it);
    }

protected:
    virtual std::shared_ptr<IBase> createInstanceWithBase(const std::string& id)
    {
        auto it = factories.find(id);
        if(it == factories.end())
            return nullptr;
        return it->second();
    }

private:
    PluginFactory(const PluginFactory&) = delete;
    PluginFactory& operator=(const PluginFactory&) = delete;

    // 构造各个类实例的工厂函数
    std::unordered_map<std::string, std::function<std::shared_ptr<IBase>()>> constructors;
};

extern "C" IPluginFactory* getPluginFactory()
{
   static PluginFactory instance;
   return &instance;
}



// IMyPlugin.h 
// 类接口
struct IMyClassA : public IBase
{
    virtual void func() const = 0;
};



// MyPlugin.cpp
// 类实例
class MyClassA : public IMyClassA
{
public:
    MyClassA() = default;
    virtual ~MyClassA() = default;

    virtual void func() const { std::cout << "hello world" << std::endl; }
};

// 插件自动注册
struct MyPlugin
{
    MyPlugin()
    {
        PluginFactory* factory = dynamic_cast<PluginFactory*>(getPluginFactory());
        factory->registerClass("MyPlugin.MyClassA",
                               []{ return std::static_pointer_cast<IBase>(std::shared_ptr<MyClassA>::create()); });
    }
    ~MyPlugin()
    {
        PluginFactory* factory = dynamic_cast<PluginFactory*>(getPluginFactory());
        factory->unregisterClass("MyPlugin.MyClassA");
    }
}
static MyPlugin myPlugin;



// main.cpp
int main(int argc, char* argv[])
{
    (void)argc;
    (void) argv;

    // 加载插件
    HANDLE hModule = ::LoadLibrary("MyPlugin.dll");

    // 获取实例
    IPluginFactory* factory = getPluginFactory();
    auto myClassA = factory->createInstance<IMyClassA>("MyPlugin.MyClassA");
    if (myClassA)
        myClassA->func();

    // 释放插件
    myClassA.reset();
    ::FreeLibrary (hModule);

   return 0;
}

实际实现中,各插件框架,都会有个宏用于声明插件,这个宏有一般是放置在全局域,展开后自动生成类似MyPlugin类的代码和对象声明。

这个宏的输入参数里至少会包含插件类的ID和插件类名。

虽然全局静态对象的初始化顺序是未知的,但这里的依赖是安全的——工厂对象是函数内的静态对象,只有在函数调用时才会完成初始化,所以可以确保,宏定义出的这个全局对象构造时,通过getPlugin()肯定能获取到已经初始化完成的插件工厂。

 

对了,如果你约束好所有插件接口函数的输入输出类型,通过可以跨编译器平台的数据结构实现,如C++11的标准布局(Standard Layout),那么你这个插件哪怕是用MinGW编译的,也能拿给MSVC使用——.COM就是这么实现的。

 

最后的就是一些扩展功能了,比如运行时热更新:

  1. 为IBase提供start()/stop()两个方法,并且在IBase的构造/析构中,更新框架内部持有的对象列表;
  2. 热更新插件时,根据对象列表,stop()该插件所有类实例;
  3. 就地析构(显示调用构造函数),销毁该插件所有类实例;
  4. 卸载插件,更新动态库,重新加载插件;
  5. 就地构造(placement new),在原有插件对象的指针地址上重新创建类实例,以避免用户代码里持有的插件对象指针失效。

如果再通过命令模式/状态机等约束插件行为的话,甚至可以还原卸载前的现场。

 

================ End

 

posted @ 2022-04-05 23:23  lsgxeva  阅读(248)  评论(0编辑  收藏  举报