【问题标题】:C++ / CMake : Making a plugin system for many 'source plugins'C++ / CMake:为许多“源插件”制作插件系统
【发布时间】:2019-10-27 15:50:05
【问题描述】:

我正在开发一个由许多插件组成的 C++ 库,这些插件可以相互独立地包含在内。插件集仅取决于编译时的用户要求。 插件只是源代码,它们不是独立的二进制文件。 为此,库的主要(也是唯一的)CMakeLists.txt 具有预定义的插件列表,并且在 plugins 目录中找到的每个插件都将添加到二进制目标中。 另外,设置了一个带有插件名称的预处理器#define

set (plugins
    plugin1
    plugin2
    plugin3
     ...)

#optional plugins 
foreach(library ${plugins})
    file(TO_CMAKE_PATH "plugins/${library}" librarypath)
    get_filename_component(librarypath ${librarypath} ABSOLUTE) 
    if(EXISTS ${librarypath})
        message("--> found ${library}")
        include_directories(${librarypath}/include)
        file(GLOB libsources "${librarypath}/src/*.cpp" "${librarypath}/src/*.f90")
        set(sources ${sources} ${libsources})
        string(TOUPPER ${library} LIBRARY)
        add_definitions(-D${LIBRARY})
    endif()
endforeach(library)

现在在我的主库中,我基本上做的事情如下:

#ifdef PLUGIN1
#    include "plugin1.h"
#endif
#ifdef PLUGIN2
#    include "plugin2.h"
#endif
#ifdef PLUGIN3
#    include "plugin3.h"
#endif

...

// each plugin has a unique id:
enum PluginID : int {
    Plugin1                          = 1,
    Plugin2                          = 2,
    Plugin3                          = 3,
};

// the name of each plugin is associated with its ID, 

PluginID getPluginIDFromName( const std::string& PluginName )
{
    static std::map<std::string, PluginID> PluginIDMap = {
        {"PLUGIN1", Plugin1},
        {"PLUGIN2", Plugin2},
        {"PLUGIN3", Plugin3},
    };

    return PluginIDMap[PluginName];
}

// Load a plugin by its ID
PluginBaseClass* pluginFactory( PluginID  pluginID)
{
    switch ( pluginID ) {
        #ifdef PLUGIN1 
        case Plugin1: { return new class Plugin1();}
        #endif
       #ifdef PLUGIN2 
        case Plugin2: { return new class Plugin2();}
        #endif
       #ifdef PLUGIN3 
        case Plugin3: { return new class Plugin3();}
       #endif
}}

所以结果是在主源中我可以通过以下方式加载插件:

PluginBaseClass* thePlugin1 = pluginFactory ( getPluginIDFromName ("PLUGIN1") );

一切都按预期工作,但我觉得我所做的是某种滥用 cmake 和预处理器宏的行为。有没有更好的方法来实现我的目标? 此外,为每个可能的插件手动更新mapswitch 相当麻烦。 我的要求是用户不需要手动修改CMakeLists.txt。提前谢谢!

编辑:我想通过它们的 ID 或名称使插件可用,因此这两个功能。此外,静态链接是首选;我认为没有理由动态加载。

【问题讨论】:

  • 将插件系统实现为动态模块。这样你就可以单独构建它们(例如,甚至作为一个完全独立的项目)。
  • 谢谢,但我更喜欢静态链接;没有真正需要动态加载。
  • 为什么没有动态加载?例如,在 Linux 上,dlopen 是加载插件的规范方式(否则,不要称其为插件,而是软件组件)
  • 事实是,并非所有“插件”都完全相互独立。一些插件导入或重用其他插件的函数和类。这意味着至少在Windows下必须导出/导入符号等等。

标签: c++ plugins cmake


【解决方案1】:

您可以使用所谓的“自注册”,让编译器为您完成大部分工作,而不是手动创建从 id 到插件名称和工厂函数的映射。

静态插件工厂

首先,我们需要一个工厂类,各个插件可以在其中注册自己。声明可能如下所示:

class PluginFactory
{
public:
  using PluginCreationFunctionT = PluginBaseClass(*)();

  PluginFactory() = delete;

  static bool Register(std::string_view plugin name,
                       PluginID id,
                       PluginCreationFunctionT creation_function);

  static PluginBaseClass* Create(std::string_view name);
  static PluginBaseClass* Create(PluginID id);

private:
  static std::map<std::string_view, PluginCreationFunctionT> s_CreationFunctionByName;
  static std::map<PluginID, PluginCreationFunctionT> s_CreationFunctionById;
};

相应的源文件则包含

std::map<std::string_view, PluginFactory::PluginCreationFunctionT>
PluginFactory::s_CreationFunctionByName;

std::map<PluginID, PluginFactory::PluginCreationFunctionT>
PluginFactory::s_CreationFunctionById;

bool PluginFactory::Register(std::string_view const plugin name,
                             PluginId const id,
                             PluginCreationFunctionT const creation_function)
{
  // assert that no two plugins accidentally try to register
  // with the same name or id
  assert(s_CreationFunctionByName.find(name) == s_CreationFunctionByName.end());
  assert(s_CreationFunctionById.find(id) == s_CreationFunctionById.end());

  s_CreateFunctionByName.insert(name, creation_function);
  s_CreateFunctionById.insert(id, creation_function);

  return true;
}

PluginBaseClass* PluginFactory::Create(std::string_view const name)
{
  auto const it = s_CreationFunctionByName.find(name);
  return it != s_CreationFunctionByName.end() ? it->second() : nullptr;
}

PluginBaseClass* PluginFactory::Create(std::string_view const id)
{
  auto const it = s_CreationFunctionById.find(name);
  return it != s_CreationFunctionById.end() ? it->second() : nullptr;
}

请注意,Register 总是返回 true - 我们需要它返回一个值,以便将 Register 函数用作全局变量的初始化程序。作为全局变量的初始值设定项会导致编译器在程序启动期间发出代码以调用Register 函数。

在您的 main 函数中,您现在可以通过

获取特定插件的实例
PluginBaseClass* thePlugin1 = PluginFactory::Create("PLUGIN1");

或通过

PluginBaseClass* thePlugin1 = PluginFactory::Create(PluginID::Plugin1);

修改插件

插件本身现在需要修改,以便它们自己注册。理论上任何全局变量都可以,但为了避免不同插件之间的名称冲突,最简单的方法是向每个插件类添加一个静态数据成员,例如

class Plugin1 : public PluginBaseClass {
public:
  ...

private:
  static bool s_IsRegistered;
};

然后将以下内容添加到插件的源文件中:

namespace {
  PluginBaseClass* create_plugin1()
  {
    return new Plugin1{};
  }
}

bool Plugin1::s_IsRegistered 
  = PluginFactory::Register("PLUGIN1", PluginID::Plugin1, create_plugin1);

简化 CMake 代码

现在编译器生成了映射,您不再需要预处理器定义。您的 CMake 代码现在需要做的就是添加正确的包含目录和源。但这不需要是主 CMake 文件的一部分。相反,您可以将 CMakeLists.txt 放入每个插件文件夹,然后通过 add_subdirectoryinclude 将它们包含到主 CMakefile 中:

foreach(library ${plugins})
    if(EXISTS ${CMAKE_CURRENT_LIST_DIR}/plugins/${library}/CMakeLists.txt)
      message(STATUS "--> found ${library}"
      include(${CMAKE_CURRENT_LIST_DIR}/plugins/${library}/CMakeLists.txt)
    else()
      message(FATAL "Unknown plugin ${library} requested!")
    endif()
endforeach()

plugins/plugin1 文件夹中 plugin1 的 CMakeLists.txt 然后只包含

include_directories(${CMAKE_CURRENT_LIST_DIR}/include)
file(GLOB sources_plugin1 "${CMAKE_CURRENT_LIST_DIR}/src/*.cpp" "${CMAKE_CURRENT_LIST_DIR}/src/*.f90")
list(APPEND sources ${sources_plugin1})

在这种特殊情况下看起来可能没有太大改进,但现在拥有这些单独的 CMakeLists.txt 文件也允许有条件地添加依赖项。

例如,假设 Plugin2 是唯一使用 boost 的插件。使用单独的 CMakeLists.txt,您可以将查找和使用 boost 所需的所有内容添加到 Plugin2 的 CMakeLists.txt,而不会污染主 CMakeLists.txt 文件。

【讨论】:

    【解决方案2】:
    1. 从外部文件中读取插件列表。阅读here。基本搬家

    设置(插件 插件1 插件2 插件3 ...)

    放入plugins.cmake文件,并使用

    include(plugins.cmake) 在你的主 cmake 文件中。

    1. 为了避免需要手动修改地图和切换:如果可以假设例如插件的最大数量为 64,您可以通过以下方式使用位掩码:

              auto plugin_mask = PLUGINS;
      auto id = 1;
      
      while (plugin_mask)
      {
          if (plugin_mask & 1)
          {
              // found plugin
      
              // add to map
              std::pair<std::string, PluginID> p;
              p.first = "PLUGIN" + std::to_string(id);
              p.second = static_cast<PluginID>(id++);
              PluginIDMap.insert(p);
          } plugin_mask >> 1;
      }
      

    【讨论】:

    • 是的,你也不需要假设插件的最大数量,你可以使用std::bitset
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多