C ++ / CMake: создание системы плагинов для многих «исходных плагинов» - PullRequest
1 голос
/ 27 октября 2019

Я работаю над библиотекой 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 и препроцессора. Есть ли лучший способ достичь моих целей? Кроме того, ручное обновление map и switch для каждого возможного плагина довольно громоздко. У меня есть требование, чтобы пользователь не должен был изменять CMakeLists.txt вручную. Заранее спасибо!

Редактировать: Я хочу сделать плагины доступными как по их идентификаторам, так и по их именам, отсюда две функции. Кроме того, статическое связывание является предпочтительным;Я не вижу причин для динамической загрузки.

Ответы [ 2 ]

2 голосов
/ 05 ноября 2019

Вместо того, чтобы вручную создавать отображение из 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 во время запуска программы.

В вашей основной функции теперь вы можете получить экземпляр определенного плагина через

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_subdirectory или include в основной файл 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()

CMakeLists.txt для плагина 1 вТогда папка plugins/plugin1 содержит только

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 вы можете добавить все, что вам нужно, чтобы найти и использовать повышение к CMakeLists.txt в Plugin2, не загрязняя основной CMakeLists.txt файл.

1 голос
/ 30 октября 2019
  1. чтение списка плагинов из внешнего файла. прочитайте здесь . В основном переместите

set (plugins plugin1 plugin2 plugin3 ...)

в файл plugins.cmake и используйте

include(plugins.cmake)в вашем главном файле cmake.

Во избежание необходимости вручную изменять карту и переключаться: если можно предположить, например, что максимальное количество плагинов равно 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;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...