Введение
Уже долгое время не пишу статьи о разработке, хотя сам процесс написания мне очень нравится и позволяет привести мысли в порядок. И все от того, что все это время был занят разработкой довольно интересного проекта.
Но вот, есть возможность сейчас рассказать о наработках, которые появились за последнее время. Надеюсь, кому-то этот текст сильно упростит жизнь и даст толчок к покорению новых вершин.
В этот раз речь пойдет о создании кроссплатформенных приложений с плагинами на C++ с использованием библиотеки wxWidgets. Рассматриваться будут операционные системы Windows, Linux и OS X, как наиболее популярные.
Как обычно, первая часть будет обзорной, для того, чтобы снизить порог входа для читателей. Кому-то информация из первой части покажется очевидной (особенно то, что касается инструментария), но, все же, я считаю ее необходимой, ибо для новичков информация из первой части позволит с минимальными усилиями организовать процесс разработки.
Инструментарий
wxWidgets
Для начала нам понадобятся:
Библиотека wxWidgets в исходных кодах. Я использую наиболее новые версии из SVN. Они, конечно, не без багов, зато в них реализован функционал, которого обычно не хватает в официальных релизах.
Исходный код можно взять здесь: http://svn.wxwidgets.org/svn/wx/wxWidgets/trunk
Более подробно о процессе сборки библиотеки можно почитать здесь: http://habrahabr.ru/post/123588/
Разница в процессе сборки, по сравнению с указанной выше статьей заключается лишь в том, что нужно использовать конфигурацию DLL Debug
и DLL Release
вместо Debug
и Release
. К тому же, обязательно необходимо чтобы в настройках всех проектов, входящих в дистрибутив wxWidgets, в параметре C/C++ -> Code Generation -> Runtime Library
были указаны значения Multi-Threaded Debug DLL и Multi-Threaded DLL
. Именно с «DLL» в конце. В этом случае у нас wxWidgets будет собрана в виде динамических библиотек и с динамическим CRT.
При сборке конфигураций DLL Debug
и DLL Release
может быть такое что не все библиотеки соберутся с первого раза. Все это из-за проблем с указанием зависимостей. Если не собралось, запускаем сборку еще раз. Обычно 2-3 итераций достаточно для того, чтоб получить полный комплект динамических библиотек.
Напомню также, что для работы с wxWidgets необходимо наличие переменной окружения %WXWIN%
(для Windows), которая указывает на папку с исходными кодами wxWidgets. Для Linux и OS X достаточно выполнить configure && make && make install
.
Параметры для configure:
- Debug:
configure --enable-shared --disable-static --enable-unicode \
--disable-compat28 --disable-final --enable-debug - Release:
configure --enable-shared --disable-static --enable-unicode \
--disable-compat28 --enable-final --disable-debug
CMake
Для того, чтобы облегчить работу по созданию файлов проектов для различных платформ на разных рабочих машинах с разными настройками, будем использовать систему генерации проектов CMake, о которой, кстати, есть несколько неплохих обзорных статей на Хабре, например вот:
В общем, CMake – это инструмент, с помощью которого на разных машинах мы сможем генерировать файлы проектов Visual Studio (Windows), Makefile/CodeBlocks (Linux), Makefile/XCode (OS X) с правильно прописанными путями к исходным кодам и сторонним библиотекам, что позволит нам избавиться от довольно большого объема лишней работы по настройке сборки.
Скачать CMake можно здесь: http://www.cmake.org/cmake/resources/software.html
Если вы собрали wxWidgets (Linux, OS X) с отладочной информацией, а потом хотите установить Release-версию, то надо сделать make uninstall для Debug-версии и вручную удалить файлы
- /usr/local/bin/wx-config
- /usr/local/bin/wxrc
Если указанные выше файлы не удалить вручную, то для Release-версии библиотеки будут использоваться настройки от Debug-версии. Приложение соберется, но не запустится.
Также надо иметь в виду тот факт, что если вы установили Debug-версию wxWidgets, то в Linux и OS X у вас, скорее всего, получится собрать только Debug-версию приложения. Это же касается и Release-версии. А все потому что CMake берет параметры компиляции и линковки из скрипта wx-config, который по умолчанию отдает параметры для одной текущей конфигурации. Или для Debug отдельно, или отдельно для Release.
Visual C++ (Windows)
Для сборки wxWidgets и нашего приложения из исходных кодов в Windows будем использовать Visual C++ 2012. Express редакция тоже подойдет. Это значит, что все средства разработки, включая IDE и компилятор, будут бесплатными.
Для тех, кто в танке, ссылка на бесплатный Visual C++ 2012: http://www.microsoft.com/visualstudio/rus/products/visual-studio-express-products
DialogBlocks
Для создания интерфейса пользователя, дабы не писать все руками, рекомендую использовать приложение DialogBlocks. Таки-да, он платный, но есть бесплатная пробная версия, которой достаточно для создания несложных форм. Хотя опять же, никто не мешает писать все руками (кстати, это даже неплохо в воспитательных целях и явно положительно сказывается на понимании кода).
Скачать DialogBlocks можно здесь: http://www.anthemion.co.uk/dialogblocks/download.htm
Начало
Структура папок
Я понимаю, что на вкус и цвет фломастеры разные и навязывать свою структуру каталогов это дело неблагодарное, но за несколько лет работы мы в компании пришли к определенной структуре, которая неплохо зарекомендовала себя на довольно сложных проектах и которая довольно проста для понимания. Поэтому в данной статье будем использовать ее.
- build – папка с общим CMake скриптом и shell-скриптами для генерирования проектов
- build/bin/<Configuration> – папка, куда компилятор складывает бинарные файлы
- /include – папка с общими заголовками (например, для precompiled headers)
- /<ProjectName> – папка с исходными кодами проекта из главного решения (может быть более одного проекта в решении, у каждого своя папка)
- /<ThirdParty> – папка, в которой лежат сторонние библиотеки (в виде исходников или собранные, каждая в своем подкаталоге)
- /ThirdParty/build – папка с общим CMake скриптом и shell-скриптами для генерирования проектов сторонних библиотек (если вы решите вынести их в отдельный solution)
- /ThirdParty/<LibName> – папка с исходными кодами сторонней библиотеки (их может быть более одной)
- /<ProjectName>/<OS-Name> – сюда CMake складывает файлы проектов для каждой ОС.
Главный CMakeList
Главный скрипт CMake содержит общие параметры и настройки для всех проектов, а также описание некоторых общих переменных.
build/CMakeLists.txt
cmake_minimum_required(VERSION 2.6.0) # We will generate both Debug and Release project files at the same time # for Windows and OS X if(WIN32 OR APPLE) set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE) set(LIB_SUFFIX "") endif(WIN32 OR APPLE) # For Linux we will need to execute CMake twice in order to generate # Debug and Release versions of Makefiles if(UNIX AND NOT APPLE) set(LINUX ON) set(LIB_SUFFIX /${CMAKE_BUILD_TYPE}) endif(UNIX AND NOT APPLE) set(PROJECT_NAME wxModularHost) project(${PROJECT_NAME}) # If there are any additional CMake modules (e.g. module which searches # for OpenCV or for DirectShow libs), then CMake should start searching # for them in current folder set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}) if(APPLE) set(OS_BASE_NAME Mac) set(CMAKE_OSX_SYSROOT "macosx10.6") endif(APPLE) if(LINUX) set(OS_BASE_NAME Linux) endif(LINUX) if(WIN32) set(OS_BASE_NAME Win) endif(WIN32) # Here we specify the list of wxWidgets libs which we will use in our project set(wxWidgets_USE_LIBS base core adv aui net gl xml propgrid html) # Here we specify that we need DLL version of wxWidgets libs and dynamic CRT # This is a MUST for applications with plugins. Both app and DLL plugin MUST # use the same instance of wxWidgets and the same event loop. set(BUILD_SHARED_LIBS 1) # Find wxWidgets library on current PC # You should have %WXWIN% environment variable which should point to the # directory where wxWidgets source code is placed. # wxWidgets libs MUST be compiled for both Debug and Release versions find_package(wxWidgets REQUIRED) # For some reason CMake generates wrong list of definitions. # Each item should start with /D but it does not. # We need to fix that manually set(wxWidgets_DEFINITIONS_TEMP) foreach(DEFINITION ${wxWidgets_DEFINITIONS}) if(NOT ${DEFINITION} MATCHES "/D.*") set(DEFINITION "/D${DEFINITION}") endif() set(wxWidgets_DEFINITIONS_TEMP ${wxWidgets_DEFINITIONS_TEMP} ${DEFINITION}) endforeach(${DEFINITION}) set(wxWidgets_DEFINITIONS ${wxWidgets_DEFINITIONS_TEMP}) # Here we add some definitions which prevent Visual Studio from # generating tons of warnings about unsecure function calls. # See http://msdn.microsoft.com/en-us/library/ttcz0bys.aspx if(WIN32) set(wxWidgets_DEFINITIONS ${wxWidgets_DEFINITIONS}; /D_CRT_SECURE_NO_DEPRECATE; /D_CRT_NONSTDC_NO_DEPRECATE; /D_UNICODE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP /wd4996") endif(WIN32) # Since we are going to use wxWidgets in all subrojects, # it's OK to create the variable which will contain # common preprocessor definitions. This variable will be # used in subprojects. set(PREPROCESSOR_DEFINITIONS ${PREPROCESSOR_DEFINITIONS}; ${wxWidgets_DEFINITIONS}) # Variable which points to root folder of our source code set(PROJECT_ROOT_DIR ${PROJECT_SOURCE_DIR}/..) # If any ThirdParty libraries are going to be # used in our project then it would be better to put # them into separate subfolder. We will create # the variable which points to this subfolder. set(THIRD_PARTY_DIR ${PROJECT_ROOT_DIR}/ThirdParty) set(BASE_INCLUDE_DIRECTORIES ${PROJECT_ROOT_DIR}/include) # Add wxWidgets include paths to the list of # include directories for all projects. include_directories(${wxWidgets_INCLUDE_DIRS}) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /D__WXDEBUG__=1" ) # Now we can include all our subprojects. # CMake will generate project files for them add_subdirectory (../wxModularHost ../../wxModularHost/${OS_BASE_NAME}${LIB_SUFFIX})
Скрипты для генерирования проектов
Для простоты использования CMake лучше использовать shell- или batch-скрипты. Это позволит немного сэкономить время на рутинных операциях типа вызова CMake и настройки переменных окружения.
Windows (cm.bat)
Для удобства, лучше использовать раздельные batch-скрипты для создания проектов Visual Studio для x86 и x64, а также один общий скрипт, который будет определять, под какую платформу собираем приложение:
rem @echo off IF "%1" == "" GOTO NO_PARAMS IF "%1" == "x86" GOTO CMAKE_86 IF "%1" == "86" GOTO CMAKE_86 IF "%1" == "x64" GOTO CMAKE_64 IF "%1" == "64" GOTO CMAKE_64 ECHO %1 ECHO "Nothing to do" GOTO End :CMAKE_86 ECHO "Configuring for x86" cm86.bat GOTO End :CMAKE_64 ECHO "Configuring for x64" cm64.bat GOTO End :NO_PARAMS ECHO "No parameters specified" IF EXIST "%ProgramW6432%" GOTO CMAKE_64 GOTO CMAKE_86 :End
Windows (cm86.bat)
rmdir /S /Q Win mkdir Win cd Win cmake ../ -G "Visual Studio 11" cd ..
Windows (cm64.bat)
rmdir /S /Q Win mkdir Win cd Win cmake ../ -G "Visual Studio 11 Win64" cd ..
Linux (cmLinux.sh)
#!/bin/bash echo OS Type: $OSTYPE # ---------------------------------- # build Debug configuration makefile # ---------------------------------- echo building Debug configuration makefile echo directory "LinuxDebug" rm -dr "LinuxDebug" mkdir "LinuxDebug" cd "LinuxDebug" cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE:STRING=Debug ../ cd .. # ---------------------------------- # build Release configuration makefile # ---------------------------------- echo building Release configuration makefile echo directory "LinuxRelease" rm -dr "LinuxRelease" mkdir "LinuxRelease" cd "LinuxRelease" cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE:STRING=Release ../ cd ..
Минимальное wxWidgets-приложение с CMake
Для начала работы нам нужен шаблон приложения, в который мы будем добавлять функционал. Создадим простое приложение, состоящее из класса приложения (например wxModularHostApp) и класса главной формы (например MainFrame).
Если использовать DialogBlocks, то, помимо пары файлов h/cpp для каждого класса, получим еще .rc файл с описанием ресурсов приложения.
Код приводить не буду. Пример можно взять из прошлых статей или из папки %WXWIN%\samples\minimal
Теперь можно переходить к созданию CMake-скрипта.
wxModularHost/CMakeLists.txt
set(SRCS MainFrame.cpp wxModularHostApp.cpp) set(HEADERS MainFrame.h wxModularHostApp.h) set(INCLUDE_DIRECTORIES ${BASE_INCLUDE_DIRECTORIES}) if(WIN32) set(SRCS ${SRCS} wxModularHost.rc) set(PREPROCESSOR_DEFINITIONS ${PREPROCESSOR_DEFINITIONS}; /D_USRDLL; /DwxUSE_NO_MANIFEST=1; /D__STDC_CONSTANT_MACROS) endif(WIN32) set(LIBS ${wxWidgets_LIBRARIES}) set(EXECUTABLE_NAME wxModularHost) add_definitions(${PREPROCESSOR_DEFINITIONS}) include_directories(${INCLUDE_DIRECTORIES}) if(WIN32) set(EXECUTABLE_TYPE WIN32) endif(WIN32) if(APPLE) set(MACOSX_BUNDLE YES) set(EXECUTABLE_TYPE MACOSX_BUNDLE) endif(APPLE) if(LINUX) set(EXECUTABLE_TYPE "") endif(LINUX) set(PROJECT_FILES ${SRCS} ${HFILES}) add_executable(${EXECUTABLE_NAME} ${EXECUTABLE_TYPE} ${PROJECT_FILES}) set(EXE_DIR bin) set(TARGET_LOCATION ${PROJECT_SOURCE_DIR}/${EXE_DIR}${LIB_SUFFIX}) set_target_properties(${EXECUTABLE_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${TARGET_LOCATION}) target_link_libraries(${EXECUTABLE_NAME} ${LIBS})
Предварительно откомпилированные заголовки (Precompiled Headers)
Для ускорения процесса компиляции, есть возможность использовать предварительно откомпилированные заголовки (http://en.wikipedia.org/wiki/Precompiled_header).
Для реализации этой возможности нам понадобятся два файла:
include/stdwx.h
#ifndef _STDWX_H_ #define _STDWX_H_ #if defined(WIN32) || defined(WINDOWS) #include <windows.h> #include <winnt.h> #define PLUGIN_EXPORTED_API WXEXPORT #else #define PLUGIN_EXPORTED_API extern "C" #endif // SYSTEM INCLUDES // For compilers that support precompilation, includes "wx/wx.h". #include "wx/wxprec.h" #ifdef __BORLANDC__ #pragma hdrstop #endif #include "wx/wx.h" #include <wx/cmdline.h> #include <wx/config.h> #include <wx/defs.h> #include <wx/dir.h> #include <wx/display.h> #include <wx/dynlib.h> #include <wx/dynload.h> #include <wx/fileconf.h> #include <wx/filename.h> #include <wx/frame.h> #include <wx/glcanvas.h> #include <wx/hashmap.h> #include <wx/image.h> #include <wx/imaglist.h> #include <wx/intl.h> #include <wx/list.h> #include <wx/notebook.h> #include <wx/stdpaths.h> #include <wx/sstream.h> #include <wx/thread.h> #include <wx/treebook.h> #include <wx/wfstream.h> #include <wx/wupdlock.h> #include <wx/textfile.h> #include <wx/socket.h> #include <wx/mimetype.h> #include <wx/ipc.h> #endif
include/stdwx.cpp
#include "stdwx.h"
Помимо файлов с исходным кодом C++ нам надо еще научить CMake добавлять в проект Visual Studio нужные правила для работы с предварительно откомпилированными заголовками. Для этого нам поможет специальный модуль. Не припомню, откуда он взялся, но вроде отсюда (http://public.kitware.com/Bug/file_download.php?file_id=901&type=bug). Исходный код CMake-модуля для поддержки предварительно компилируемых заголовков можно посмотреть здесь: https://github.com/T-Rex/wxModularApp/blob/master/build/PCHSupport.cmake.
Этот модуль надо включить в build/CmakeLists.txt таким образом:
build/CMakeLists.txt
cmake_minimum_required(VERSION 2.6.0) include(PCHSupport.cmake) ...
После подключения предварительно откомпилированных заголовков в проект, первой строкой во всех .CPP файлах проекта должна быть строка
#include "stdwx.h"
Простейший плагин без GUI
Библиотека с базовыми классами
Для разработки, собственно, плагина, и для того, чтобы приложение умело загружать плагины нужного типа, необходимо сделать подготовительную работу. Нужно создать библиотеку, которая будет содержать базовый абстрактный класс плагина, функционал которого мы должны будем реализовать в каждом конкретном плагине. Также наше основное приложение будет содержать указатели этого типа, которые были созданы внутри библиотек и ссылаются на конкретные реализации плагинов.
Т.е. в результате у нас должна подучиться библиотека, содержащая типы данных, используемые и в плагинах и в основном приложении.
wxNonGuiPluginBase/Declarations.h
#ifndef _DECLARATIONS_H #define _DECLARATIONS_H #if defined(__WXMSW__) #ifdef DEMO_PLUGIN_EXPORTS #define DEMO_API __declspec(dllexport) #else #define DEMO_API __declspec(dllimport) #endif #else #define DEMO_API #endif #endif // _DECLARATIONS_H
wxNonGuiPluginBase/wxNonGuiPluginBase.h
#pragma once #include "Declarations.h" class DEMO_API wxNonGuiPluginBase : public wxObject { DECLARE_ABSTRACT_CLASS(wxNonGuiPluginBase) public: wxNonGuiPluginBase(); virtual ~wxNonGuiPluginBase(); virtual int Work() = 0; }; typedef wxNonGuiPluginBase * (*CreatePlugin_function)(); typedef void (*DeletePlugin_function)(wxNonGuiPluginBase * plugin);
Файл Declarations.h содержит определение макроса DEMO_API
, который указывает, экспортируемый у нас класс wxNonGuiPluginBase
или импортируемый. Делается это с помощью атрибутов dllexport/dllimport
(см. http://msdn.microsoft.com/en-us/library/3y1sfaz2(v=vs.90).aspx) в зависимости от наличия директивы препроцессора DEMO_PLUGIN_EXPORTS
. При сборке библиотеки wxNonGuiPluginBase мы указываем DEMO_PLUGIN_EXPORTS
в списке директив препроцессора, а при сборке плагинов, зависящих от библиотеки wxNonGuiPluginBase и при сборке основного приложения – не указываем. Таким образом для проекта wxNonGuiPluginBase значение DEMO_API
будет содержать атрибут dllexport
, а для всех остальных проектов – значение dllimport
.
wxNonGuiPluginBase/wxNonGuiPluginBase.cpp
#include "stdwx.h" #include "wxNonGuiPluginBase.h" IMPLEMENT_ABSTRACT_CLASS(wxNonGuiPluginBase, wxObject) wxNonGuiPluginBase::wxNonGuiPluginBase() { } wxNonGuiPluginBase::~wxNonGuiPluginBase() { }
wxNonGuiPluginBase/CMakeLists.txt
set (SRCS wxNonGuiPluginBase.cpp) set (HEADERS Declarations.h wxNonGuiPluginBase.h) set(LIBRARY_NAME wxNonGuiPluginBase) if(WIN32) # Only for Windows: # we add additional preprocessor definitons set(PREPROCESSOR_DEFINITIONS ${PREPROCESSOR_DEFINITIONS}; /D_USRDLL;/DDEMO_PLUGIN_EXPORTS;/D__STDC_CONSTANT_MACROS) endif(WIN32) # Add 2 files for precompiled headers set(SRCS ${SRCS} ${HEADERS} ${PROJECT_ROOT_DIR}/include/stdwx.h ${PROJECT_ROOT_DIR}/include/stdwx.cpp) # Set preprocessor definitions add_definitions(${PREPROCESSOR_DEFINITIONS}) # Set include directories include_directories(${INCLUDE_DIRECTORIES} ${BASE_INCLUDE_DIRECTORIES}) # Set library search paths link_directories(${LINK_DIRECTORIES}) # Setup the project name and assign the source files for this project add_library(${LIBRARY_NAME} SHARED ${SRCS}) #Setup the output folder set(DLL_DIR bin) set(TARGET_LOCATION ${PROJECT_SOURCE_DIR}/${DLL_DIR}${LIB_SUFFIX}) set_target_properties(${LIBRARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${TARGET_LOCATION}) # Set additional dependencies target_link_libraries(${LIBRARY_NAME} ${wxWidgets_LIBRARIES}) # Setup precompiled headers set_precompiled_header(${LIBRARY_NAME} ${PROJECT_ROOT_DIR}/include/stdwx.h ${PROJECT_ROOT_DIR}/include/stdwx.cpp)
Как было сказано ранее, макрос PREPROCESSOR_DEFINITIONS
содержит декларацию макроса DEMO_PLUGIN_EXPORTS
, который используется в файле Definitions.h
Первый плагин
В плагине нам надо сделать класс, производный от wxNonGuiPluginBase
, реализовать в нем рабочий функционал, а также сделать экспортируемые функции для создания экземпляра класса и для его удаления. Эти функции будут вызываться основным приложением.
SampleNonGuiPlugin/SampleNonGuiPlugin.h
#pragma once #include <wxNonGuiPluginBase.h> class SampleNonGuiPlugin : public wxNonGuiPluginBase { DECLARE_DYNAMIC_CLASS(SampleNonGuiPlugin) public: SampleNonGuiPlugin(); virtual ~SampleNonGuiPlugin(); virtual int Work(); };
SampleNonGuiPlugin/SampleNonGuiPlugin.cpp
#include "stdwx.h" #include "SampleNonGuiPlugin.h" IMPLEMENT_DYNAMIC_CLASS(SampleNonGuiPlugin, wxObject) SampleNonGuiPlugin::SampleNonGuiPlugin() { } SampleNonGuiPlugin::~SampleNonGuiPlugin() { } int SampleNonGuiPlugin::Work() { return 10; }
SampleNonGuiPlugin/SampleNonGuiPlugin.def
LIBRARY "SampleNonGuiPlugin" EXPORTS CreatePlugin=CreatePlugin DeletePlugin=DeletePlugin
SampleNonGuiPlugin/SampleNonGuiPluginExports.cpp
#include "stdwx.h" #include <wxNonGuiPluginBase.h> #include "SampleNonGuiPlugin.h" PLUGIN_EXPORTED_API wxNonGuiPluginBase * CreatePlugin() { return new SampleNonGuiPlugin; } PLUGIN_EXPORTED_API void DeletePlugin(wxNonGuiPluginBase * plugin) { wxDELETE(plugin); }
SampleNonGuiPlugin/CMakeLists.txt
set (SRCS SampleNonGuiPlugin.cpp SampleNonGuiPluginExports.cpp) set (HEADERS SampleNonGuiPlugin.h) set(LIBRARY_NAME SampleNonGuiPlugin) if(WIN32) set(SRCS ${SRCS} ${LIBRARY_NAME}.def) set(PREPROCESSOR_DEFINITIONS ${PREPROCESSOR_DEFINITIONS};/D_USRDLL;/D__STDC_CONSTANT_MACROS) set(LINK_DIRECTORIES ${PROJECT_ROOT_DIR}/wxNonGuiPluginBase/${OS_BASE_NAME}${LIB_SUFFIX}/$(ConfigurationName)) set(DEMO_LIBS wxNonGuiPluginBase.lib) endif(WIN32) set(SRCS ${SRCS} ${HEADERS} ${PROJECT_ROOT_DIR}/include/stdwx.h ${PROJECT_ROOT_DIR}/include/stdwx.cpp) add_definitions(${PREPROCESSOR_DEFINITIONS}) include_directories(${INCLUDE_DIRECTORIES} ${BASE_INCLUDE_DIRECTORIES} ${PROJECT_ROOT_DIR}/wxNonGuiPluginBase) link_directories(${LINK_DIRECTORIES}) add_library(${LIBRARY_NAME} SHARED ${SRCS}) set(DLL_DIR bin) set(TARGET_LOCATION ${PROJECT_SOURCE_DIR}/${DLL_DIR}/${CMAKE_CFG_INTDIR}/plugins) set_target_properties(${LIBRARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${TARGET_LOCATION}) target_link_libraries(${LIBRARY_NAME} ${DEMO_LIBS} ${wxWidgets_LIBRARIES}) add_dependencies(${LIBRARY_NAME} wxNonGuiPluginBase) set_precompiled_header(${LIBRARY_NAME} ${PROJECT_ROOT_DIR}/include/stdwx.h ${PROJECT_ROOT_DIR}/include/stdwx.cpp)
DEF-файл включен в список файлов исходного кода не случайно. Без его использования, имена экспортируемых функций будут декорированными и приложение не сможет получить указатель на эти функции из DLL. Почитать на тему использования DEF-файлов и экспортирования функций из DLL можно здесь:
- http://msdn.microsoft.com/en-us/library/office/bb687850.aspx
- http://msdn.microsoft.com/en-us/library/d91k01sh.aspx
Модуль управления плагинами
Итак, на данный момент у нас есть хост-приложение и минимальный плагин. Теперь надо реализовать загрузку и использование плагина в приложении. В целях универсальности, лучше выделить код, который будет заниматься поиском, загрузкой и выгрузкой плагинов из памяти, в отдельный класс, а еще лучше – в отдельную библиотеку.
Вспомним еще раз реализацию наших плагинов:
- Плагин – это динамическая библиотека
- В библиотеке есть экспортируемые функции
CreatePlugin()
иDeletePlugin()
- Весь функционал плагина реализуется в соответствующем классе внутри динамической библиотеки, объект этого класса возвращается функцией
CreatePlugin()
- Класс внутри библиотеки реализует публичный интерфейс
wxNonGuiPluginBase
, о котором знает и приложение. - Библиотека должна быть загружена в память на протяжении всего времени жизни объект, который приложение получает из функции
CreatePlugin()
- По завершении работы с плагином нам необходимо удалить объект из памяти (это делает функция
DeletePlugin()
) и выгрузить из памяти библиотеку. - Помимо загрузки и выгрузки данных из памяти, приложение должно еще уметь находить однотипные плагины в специально предназначенной для этого папке.
Исходя из этих требований, можно прийти к таким выводам:
- Раз плагинов может быть более одного, то надо хранить список загруженных библиотек в памяти
- Раз библиотек может быть более одной, то надо хранить список загруженных из библиотек объектов в памяти
- Раз библиотека должна быть выгружена из памяти не ранее, чем удалится соответствующий рабочий объект, надо как-то обеспечить возможность отлеживать соответствия библиотеки этому объекту.
Исходя из таких требований и выводов, реализовываем класс управления плагинами и контейнеры:
wxModularCore/wxModularCore.h
#pragma once #include <wxNonGuiPluginBase.h> // We need to know which DLL produced the specific plugin object. WX_DECLARE_HASH_MAP(wxNonGuiPluginBase*, wxDynamicLibrary*, wxPointerHash, wxPointerEqual, wxNonGuiPluginToDllDictionary); // We also need to keep the list of loaded DLLs WX_DECLARE_LIST(wxDynamicLibrary, wxDynamicLibraryList); // And separate list of loaded plugins for faster access. WX_DECLARE_LIST(wxNonGuiPluginBase, wxNonGuiPluginBaseList); class wxModularCoreSettings; class wxModularCore { public: wxModularCore(); virtual ~wxModularCore(); virtual wxString GetPluginsPath(bool forceProgramPath) const; virtual wxString GetPluginExt(); bool LoadPlugins(bool forceProgramPath); bool UnloadPlugins(); const wxNonGuiPluginBaseList & GetNonGuiPlugins() const; void Clear(); private: bool LoadNonGuiPlugins(const wxString & pluginsDirectory); bool UnloadNonGuiPlugins(); bool RegisterNonGuiPlugin(wxNonGuiPluginBase * plugin); bool UnRegisterNonGuiPlugin(wxNonGuiPluginBase * plugin); wxDynamicLibraryList m_DllList; wxNonGuiPluginToDllDictionary m_MapNonGuiPluginsDll; wxNonGuiPluginBaseList m_NonGuiPlugins; wxModularCoreSettings * m_Settings; };
Рассмотрим код подробно:
- В заголовочном файле есть декларация списка загруженных библиотек (
wxDynamicLibraryList
), списка загруженных из библиотеки объектов-плагинов (wxNonGuiPluginBaseList
), а также хеш-таблицы, которая позволяет отследить соответствие библиотеки плагину (wxNonGuiPluginToDllDictionary
) - Класс управления плагинами содержит метод, который возвращает путь к папке, в которой приложение будет искать плагины, а также метод, который возвращает расширение файлов плагинов (по умолчанию для Windows это .dll, а для Linux и OS X это .so)
- Также класс содержит список библиотек, список объектов-плагинов и таблицу соответствий плагинов библиотекам.
- Есть методы загрузки и выгрузки библиотек из памяти.
- В классе есть поле
m_Settings
. Это указатель на объект, который будет хранить настройки системы (например, флаг, который определяет, где искать плагины и, возможно, данные или конфигурационные файлы для них, в папке с программой или в специальной папке настроек, путь к которой определяется системой). Более подробно класс настроек мы рассмотрим далее.
wxModularCore/wxModularCore.cpp
#include "stdwx.h" #include "wxModularCore.h" #include "wxModularCoreSettings.h" #include <wx/listimpl.cpp> WX_DEFINE_LIST(wxDynamicLibraryList); WX_DEFINE_LIST(wxNonGuiPluginBaseList); wxModularCore::wxModularCore() :m_Settings(new wxModularCoreSettings) { // This will allow to delete all objects from this list automatically m_DllList.DeleteContents(true); } wxModularCore::~wxModularCore() { Clear(); wxDELETE(m_Settings); } void wxModularCore::Clear() { UnloadPlugins(); // TODO: Add the code which resets the object to initial state } bool wxModularCore::LoadPlugins(bool forceProgramPath) { wxString pluginsRootDir = GetPluginsPath(forceProgramPath); wxFileName fn; fn.AssignDir(pluginsRootDir); wxLogDebug(wxT("%s"), fn.GetFullPath().data()); fn.AppendDir(wxT("plugins")); wxLogDebug(wxT("%s"), fn.GetFullPath().data()); if (!fn.DirExists()) return false; return LoadNonGuiPlugins(fn.GetFullPath()); } bool wxModularCore::UnloadPlugins() { return UnloadNonGuiPlugins(); } bool wxModularCore::LoadNonGuiPlugins(const wxString & pluginsDirectory) { wxFileName fn; fn.AssignDir(pluginsDirectory); wxLogDebug(wxT("%s"), fn.GetFullPath().data()); fn.AppendDir(wxT("nongui")); wxLogDebug(wxT("%s"), fn.GetFullPath().data()); if (!fn.DirExists()) return false; if(!wxDirExists(fn.GetFullPath())) return false; wxString wildcard = wxString::Format(wxT("*.%s"), GetPluginExt().GetData()); wxArrayString pluginPaths; wxDir::GetAllFiles(fn.GetFullPath(), &pluginPaths, wildcard); for(size_t i = 0; i < pluginPaths.GetCount(); ++i) { wxString fileName = pluginPaths[i]; wxDynamicLibrary * dll = new wxDynamicLibrary(fileName); if (dll->IsLoaded()) { wxDYNLIB_FUNCTION(CreatePlugin_function, CreatePlugin, *dll); if (pfnCreatePlugin) { wxNonGuiPluginBase* plugin = pfnCreatePlugin(); RegisterNonGuiPlugin(plugin); m_DllList.Append(dll); m_MapNonGuiPluginsDll[plugin] = dll; } else wxDELETE(dll); } } return true; } bool wxModularCore::UnloadNonGuiPlugins() { bool result = true; wxNonGuiPluginBase * plugin = NULL; while (m_NonGuiPlugins.GetFirst() && (plugin = m_NonGuiPlugins.GetFirst()->GetData())) { result &= UnRegisterNonGuiPlugin(plugin); } return result; } wxString wxModularCore::GetPluginsPath(bool forceProgramPath) const { wxString path; if (m_Settings->GetStoreInAppData() && !forceProgramPath) path = wxStandardPaths::Get().GetConfigDir(); else path = wxPathOnly(wxStandardPaths::Get().GetExecutablePath()); return path; } wxString wxModularCore::GetPluginExt() { return #if defined(__WXMSW__) wxT("dll"); #else wxT("so"); #endif } bool wxModularCore::RegisterNonGuiPlugin(wxNonGuiPluginBase * plugin) { m_NonGuiPlugins.Append(plugin); return true; } bool wxModularCore::UnRegisterNonGuiPlugin(wxNonGuiPluginBase * plugin) { wxNonGuiPluginBaseList::compatibility_iterator it = m_NonGuiPlugins.Find(plugin); if (it == NULL) return false; do { wxDynamicLibrary * dll = m_MapNonGuiPluginsDll[plugin]; if (!dll) // Probably plugin was not loaded from dll break; wxDYNLIB_FUNCTION(DeletePlugin_function, DeletePlugin, *dll); if (pfnDeletePlugin) { pfnDeletePlugin(plugin); m_NonGuiPlugins.Erase(it); m_MapNonGuiPluginsDll.erase(plugin); return true; } } while (false); // If plugin is not loaded from DLL (e.g. embedded into executable) wxDELETE(plugin); m_NonGuiPlugins.Erase(it); return true; } const wxNonGuiPluginBaseList & wxModularCore::GetNonGuiPlugins() const { return m_NonGuiPlugins; }
Есть смысл обратить внимание на метод LoadNonGuiPlugins()
, в котором с помощью макроса wxDYNLIB_FUNCTION
мы получаем указатель на функцию CreatePlugin()
. Тип указателя CreatePlugin_function
определен в wxNonGuiPluginBase.h.
Также есть смысл обратить внимание на метод UnRegisterNonGuiPlugin()
, в котором происходит поиск плагина в таблице соответствий, если плагин найден то для него из найденной библиотеки вызывается функция DeletePlugin()
и библиотека выгружается. Если плагин не найден в таблице (например, он реализован в приложении и мы его добавляли в список вручную), то он просто удаляется из памяти.
wxModularCore/wxModularCoreSettings.h
#pragma once class wxModularCoreSettings { public: wxModularCoreSettings(); wxModularCoreSettings(const wxModularCoreSettings & settings); wxModularCoreSettings & operator = (const wxModularCoreSettings & settings); virtual ~wxModularCoreSettings(); void SetStoreInAppData(const bool & val); bool GetStoreInAppData() const; protected: virtual void CopyFrom(const wxModularCoreSettings & settings); private: bool m_bStoreInAppData; // Should we store data in Application Data folder or in .exe folder };
wxModularCore/wxModularCoreSettings.cpp
#include "stdwx.h" #include "wxModularCoreSettings.h" wxModularCoreSettings::wxModularCoreSettings() : m_bStoreInAppData(false) { } wxModularCoreSettings::wxModularCoreSettings(const wxModularCoreSettings & settings) { CopyFrom(settings); } wxModularCoreSettings & wxModularCoreSettings::operator = (const wxModularCoreSettings & settings) { if (this != &settings) { CopyFrom(settings); } return *this; } wxModularCoreSettings::~wxModularCoreSettings() { } void wxModularCoreSettings::CopyFrom(const wxModularCoreSettings & settings) { m_bStoreInAppData = settings.m_bStoreInAppData; } void wxModularCoreSettings::SetStoreInAppData(const bool & value) { m_bStoreInAppData = value; } bool wxModularCoreSettings::GetStoreInAppData() const { return m_bStoreInAppData; }
wxModularCore/CMakeLists.txt
set (SRCS wxModularCore.cpp wxModularCoreSettings.cpp) set (HEADERS wxModularCore.h wxModularCoreSettings.h) set(LIBRARY_NAME wxModularCore) if(WIN32) set(PREPROCESSOR_DEFINITIONS ${PREPROCESSOR_DEFINITIONS};/D__STDC_CONSTANT_MACROS) set(LINK_DIRECTORIES ${PROJECT_ROOT_DIR}/wxNonGuiPluginBase/${OS_BASE_NAME}${LIB_SUFFIX}/$(ConfigurationName)) set(DEMO_LIBS wxNonGuiPluginBase.lib) endif(WIN32) set(SRCS ${SRCS} ${HEADERS} ${PROJECT_ROOT_DIR}/include/stdwx.h ${PROJECT_ROOT_DIR}/include/stdwx.cpp) add_definitions(${PREPROCESSOR_DEFINITIONS}) include_directories(${INCLUDE_DIRECTORIES} ${BASE_INCLUDE_DIRECTORIES} ${PROJECT_ROOT_DIR}/wxNonGuiPluginBase) link_directories(${LINK_DIRECTORIES}) add_library(${LIBRARY_NAME} STATIC ${SRCS}) set(DLL_DIR bin) set(TARGET_LOCATION ${PROJECT_SOURCE_DIR}/${DLL_DIR}/${CMAKE_CFG_INTDIR}) set_target_properties(${LIBRARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${TARGET_LOCATION}) target_link_libraries(${LIBRARY_NAME} ${DEMO_LIBS} ${wxWidgets_LIBRARIES}) add_dependencies(${LIBRARY_NAME} wxNonGuiPluginBase) set_precompiled_header(${LIBRARY_NAME} ${PROJECT_ROOT_DIR}/include/stdwx.h ${PROJECT_ROOT_DIR}/include/stdwx.cpp)
И еще надо не забыть включить путь к проекту wxModularCore в основной CMakeLists.txt:
build/CMakeLists.txt
... add_subdirectory (../wxModularCore ../../wxModularCore/${OS_BASE_NAME}${LIB_SUFFIX}) ...
Использование плагинов без GUI в приложении
Раз класс, управляющий плагинами, у нас уже готов, то можно начать им пользоваться в приложении.
Для начала поле-указатель на wxModularCore
в класс приложения:
wxModularHost/wxModularHostApp.h
... class wxModularHostApp: public wxApp { void TestNonGuiPlugins(); ... wxModularCore * m_PluginManager; ... };
wxModularHost/wxModularHostApp.cpp
void wxModularHostApp::Init() { ////@begin wxModularHostApp member initialisation m_PluginManager = new wxModularCore; ////@end wxModularHostApp member initialisation }
И вот таким образом мы будем вызывать метод загрузки плагинов и пользоваться самими плагинами:
wxModularHost/wxModularHostApp.cpp
bool wxModularHostApp::OnInit() { ... TestNonGuiPlugins(); MainFrame* mainWindow = new MainFrame( NULL ); mainWindow->Show(true); return true; } /* * Cleanup for wxModularHostApp */ int wxModularHostApp::OnExit() { wxDELETE(m_PluginManager); ////@begin wxModularHostApp cleanup return wxApp::OnExit(); ////@end wxModularHostApp cleanup } void wxModularHostApp::TestNonGuiPlugins() { if(m_PluginManager) { if(m_PluginManager->LoadPlugins(true)) { for(wxNonGuiPluginBaseList::Node * node = m_PluginManager->GetNonGuiPlugins().GetFirst(); node; node = node->GetNext()) { wxNonGuiPluginBase * plugin = node->GetData(); if(plugin) { wxLogDebug(wxT("Non-GUI plugin returns %i"), plugin->Work()); } } } } }
В методе TestNonGuiPlugins()
мы сначала вызываем метод LoadPlugins()
из wxModularCore
, если он отработал корректно, то проходимся по списку плагинов и для каждого элемента списка вызываем метод Work()
(напомню, он задекларирован в проекте wxNonGuiPluginBase, а фактически имеет разную реализацию для каждой из загруженных библиотек).
Простейший GUI-плагин
Как создавать модули, содержащие только логику, разобрались. Теперь рассмотрим пример модуля, который кмеет создавать окно:
wxGuiPluginBase/wxGuiPluginBase.h
#pragma once #include "Declarations.h" class DEMO_API wxGuiPluginBase : public wxObject { DECLARE_ABSTRACT_CLASS(wxGuiPluginBase) public: wxGuiPluginBase(); virtual ~wxGuiPluginBase(); virtual wxString GetName() const = 0; virtual wxString GetId() const = 0; virtual wxWindow * CreatePanel(wxWindow * parent) = 0; }; typedef wxGuiPluginBase * (*CreateGuiPlugin_function)(); typedef void (*DeleteGuiPlugin_function)(wxGuiPluginBase * plugin);
Публичные виртуальные методы:
GetName()
– возвращает название модуляGetId()
– возвращает уникальный идентификатор модуля (можно использовать GUID для этого, в Visual Studio для этих целей есть специальная утилита. См. менюTools -> Create GUID
)CreatePanel()
– создает элемент управления (для демонстрации нас устроит любой контрол) и возвращает указатель на него.
Реализация плагина на основе этого интерфейса:
SampleGuiPlugin1/SampleGuiPlugin1.h
#pragma once #include <wxGuiPluginBase.h> class SampleGuiPlugin1 : public wxGuiPluginBase { DECLARE_DYNAMIC_CLASS(SampleGuiPlugin1) public: SampleGuiPlugin1(); virtual ~SampleGuiPlugin1(); virtual wxString GetName() const; virtual wxString GetId() const; virtual wxWindow * CreatePanel(wxWindow * parent); };
SampleGuiPlugin1/SampleGuiPlugin1.cpp
#include "stdwx.h" #include "SampleGuiPlugin1.h" IMPLEMENT_DYNAMIC_CLASS(SampleGuiPlugin1, wxObject) SampleGuiPlugin1::SampleGuiPlugin1() { } SampleGuiPlugin1::~SampleGuiPlugin1() { } wxString SampleGuiPlugin1::GetName() const { return _("GUI Plugin 1"); } wxString SampleGuiPlugin1::GetId() const { return wxT("{4E97DF66-5FBB-4719-AF17-76C1C82D3FE1}"); } wxWindow * SampleGuiPlugin1::CreatePanel(wxWindow * parent) { wxWindow * result= new wxPanel(parent, wxID_ANY); result->SetBackgroundColour(*wxRED); return result; }
CMakeLists.txt для этого плагина почти аналогичен тому, который мы написали для плагина без GUI. Отличия будут только в названии проекта и в списке входящих в проект файлов.
Рефакторинг модуля управления плагинами
На данный момент у нас есть два типа плагинов. Для плагинов без GUI в классе управления плагинами есть специализированный метод для загрузки библиотек, регистрации плагинов, отключения плагинов. С таким подходом нам нужно будет дублировать все эти методы для каждого типа плагинов. И есть таковых у нас будет 5-10, то класс неоправданно разрастется в размерах. Поэтому методы LoadXXXPlugins()
, UnloadXXXPlugins()
, RegisterXXXPlugin()
, UnRegisterXXXPlugin()
было решено сделать шаблонными, списки и хеш-таблицы вынести в отдельный класс-наследник класса wxModularCore
, который будет содержать код, специфичный для нашего приложения.
wxModularCore/wxModularCore.h
#pragma once // We need to keep the list of loaded DLLs WX_DECLARE_LIST(wxDynamicLibrary, wxDynamicLibraryList); class wxModularCoreSettings; class wxModularCore { public: wxModularCore(); virtual ~wxModularCore(); virtual wxString GetPluginsPath(bool forceProgramPath) const; virtual wxString GetPluginExt(); virtual bool LoadAllPlugins(bool forceProgramPath) = 0; virtual bool UnloadAllPlugins() = 0; virtual void Clear(); protected: wxDynamicLibraryList m_DllList; wxModularCoreSettings * m_Settings; template<typename PluginType, typename PluginListType> bool RegisterPlugin(PluginType * plugin, PluginListType & list) { list.Append(plugin); return true; } template<typename PluginType, typename PluginListType, typename PluginToDllDictionaryType, typename DeletePluginFunctionType> bool UnRegisterPlugin( PluginType * plugin, PluginListType & container, PluginToDllDictionaryType & pluginMap) { typename PluginListType::compatibility_iterator it = container.Find(plugin); if (it == NULL) return false; do { wxDynamicLibrary * dll = (wxDynamicLibrary *)pluginMap[plugin]; if (!dll) // Probably plugin was not loaded from dll break; wxDYNLIB_FUNCTION(DeletePluginFunctionType, DeletePlugin, *dll); if (pfnDeletePlugin) { pfnDeletePlugin(plugin); container.Erase(it); pluginMap.erase(plugin); return true; } } while (false); // If plugin is not loaded from DLL (e.g. embedded into executable) wxDELETE(plugin); container.Erase(it); return true; } template<typename PluginType, typename PluginListType, typename PluginToDllDictionaryType, typename DeletePluginFunctionType> bool UnloadPlugins(PluginListType & list, PluginToDllDictionaryType & pluginDictoonary) { bool result = true; PluginType * plugin = NULL; while (list.GetFirst() && (plugin = list.GetFirst()->GetData())) { result &= UnRegisterPlugin<PluginType, PluginListType, PluginToDllDictionaryType, DeletePluginFunctionType>(plugin, list, pluginDictoonary); } return result; } template <typename PluginType, typename PluginListType, typename PluginToDllDictionaryType, typename CreatePluginFunctionType> bool LoadPlugins(const wxString & pluginsDirectory, PluginListType & list, PluginToDllDictionaryType & pluginDictionary, const wxString & subFolder) { wxFileName fn; fn.AssignDir(pluginsDirectory); wxLogDebug(wxT("%s"), fn.GetFullPath().data()); fn.AppendDir(subFolder); wxLogDebug(wxT("%s"), fn.GetFullPath().data()); if (!fn.DirExists()) return false; if(!wxDirExists(fn.GetFullPath())) return false; wxString wildcard = wxString::Format(wxT("*.%s"), GetPluginExt().GetData()); wxArrayString pluginPaths; wxDir::GetAllFiles(fn.GetFullPath(), &pluginPaths, wildcard); for(size_t i = 0; i < pluginPaths.GetCount(); ++i) { wxString fileName = pluginPaths[i]; wxDynamicLibrary * dll = new wxDynamicLibrary(fileName); if (dll->IsLoaded()) { wxDYNLIB_FUNCTION(CreatePluginFunctionType, CreatePlugin, *dll); if (pfnCreatePlugin) { PluginType * plugin = pfnCreatePlugin(); RegisterPlugin(plugin, list); m_DllList.Append(dll); pluginDictionary[plugin] = dll; } else wxDELETE(dll); } } return true; } };
wxModularHost/SampleModularCore.h
#pragma once #include <wxModularCore.h> #include <wxNonGuiPluginBase.h> #include <wxGuiPluginBase.h> // We need to know which DLL produced the specific plugin object. WX_DECLARE_HASH_MAP(wxNonGuiPluginBase*, wxDynamicLibrary*, wxPointerHash, wxPointerEqual, wxNonGuiPluginToDllDictionary); WX_DECLARE_HASH_MAP(wxGuiPluginBase*, wxDynamicLibrary*, wxPointerHash, wxPointerEqual, wxGuiPluginToDllDictionary); // And separate list of loaded plugins for faster access. WX_DECLARE_LIST(wxNonGuiPluginBase, wxNonGuiPluginBaseList); WX_DECLARE_LIST(wxGuiPluginBase, wxGuiPluginBaseList); class SampleModularCore : public wxModularCore { public: virtual ~SampleModularCore(); virtual bool LoadAllPlugins(bool forceProgramPath); virtual bool UnloadAllPlugins(); const wxNonGuiPluginBaseList & GetNonGuiPlugins() const; const wxGuiPluginBaseList & GetGuiPlugins() const; private: wxNonGuiPluginToDllDictionary m_MapNonGuiPluginsDll; wxNonGuiPluginBaseList m_NonGuiPlugins; wxGuiPluginToDllDictionary m_MapGuiPluginsDll; wxGuiPluginBaseList m_GuiPlugins; };
wxModularHost/SampleModularCore.cpp
#include "stdwx.h" #include "SampleModularCore.h" #include <wx/listimpl.cpp> WX_DEFINE_LIST(wxNonGuiPluginBaseList); WX_DEFINE_LIST(wxGuiPluginBaseList); SampleModularCore::~SampleModularCore() { Clear(); } bool SampleModularCore::LoadAllPlugins(bool forceProgramPath) { wxString pluginsRootDir = GetPluginsPath(forceProgramPath); bool result = true; result &= LoadPlugins<wxNonGuiPluginBase, wxNonGuiPluginBaseList, wxNonGuiPluginToDllDictionary, CreatePlugin_function>(pluginsRootDir, m_NonGuiPlugins, m_MapNonGuiPluginsDll, wxT("nongui")); result &= LoadPlugins<wxGuiPluginBase, wxGuiPluginBaseList, wxGuiPluginToDllDictionary, CreateGuiPlugin_function>(pluginsRootDir, m_GuiPlugins, m_MapGuiPluginsDll, wxT("gui")); // You can implement other logic which takes in account // the result of LoadPlugins() calls return true; } bool SampleModularCore::UnloadAllPlugins() { return UnloadPlugins<wxNonGuiPluginBase, wxNonGuiPluginBaseList, wxNonGuiPluginToDllDictionary, DeletePlugin_function>(m_NonGuiPlugins, m_MapNonGuiPluginsDll) && UnloadPlugins<wxGuiPluginBase, wxGuiPluginBaseList, wxGuiPluginToDllDictionary, DeleteGuiPlugin_function>(m_GuiPlugins, m_MapGuiPluginsDll); } const wxNonGuiPluginBaseList & SampleModularCore::GetNonGuiPlugins() const { return m_NonGuiPlugins; } const wxGuiPluginBaseList & SampleModularCore::GetGuiPlugins() const { return m_GuiPlugins; }
После реализации шаблонных методов, добавление поддержки GUI-плагинов заняло совсем немного кода.
Использование GUI-плагинов в приложении
В приложении у нас есть главная форма с менеджером Docking-окон и wxAuiNotebook в качестве центральной панели. Рассмотрим как можно добавить контролы из плагинов в этот wxAuiNotebook:
wxModularHost/MainFrame.cpp
void MainFrame::AddPagesFromGuiPlugins() { SampleModularCore * pluginManager = wxGetApp().GetPluginManager(); for(wxGuiPluginBaseList::Node * node = pluginManager->GetGuiPlugins().GetFirst(); node; node = node->GetNext()) { wxGuiPluginBase * plugin = node->GetData(); if(plugin) { wxWindow * page = plugin->CreatePanel(m_Notebook); if(page) { m_Notebook->AddPage(page, plugin->GetName()); } } } }
В результате получим такое окно с вкладками:
Заголовки вкладок берутся из метода GetName()
каждого плагина, сами же вкладки создаются методом CreatePanel()
плагина.
Доработки CMake-скриптов для Linux
В Windows для указания папки, в которую будут собираться динамические библиотеки, мы указывали с помощью настройки RUNTIME_OUTPUT_DIRECTORY
. В Linux, т.к. плагин – это динамическая библиотека (именно библиотека), используется настройка LIBRARY_OUTPUT_DIRECTORY
. Но здесь мы сталкиваемся с проблемой: если собирать библиотеки прямо внутрь папки bin, то линкер не будет находить эту библиотеку при сборке зависимых проектов. Для этих целей нужно добавить скрипт, который будет отрабатывать после сборки библиотеки и копировать ее в нужное место внутрь папки bin. Сделать это нужно будет для всех динамических библиотек (и для базовых и для плагинов):
SampleGuiPlugin2/CMakeLists.txt
... set(DLL_DIR bin) if(LINUX) set(TARGET_LOCATION ${PROJECT_SOURCE_DIR}/${DLL_DIR}${LIB_SUFFIX}/plugins/nongui) else(LINUX) set(TARGET_LOCATION ${PROJECT_SOURCE_DIR}/${DLL_DIR}/${CMAKE_CFG_INTDIR}/plugins/nongui) get_target_property(RESULT_FULL_PATH ${LIBRARY_NAME} LOCATION) get_filename_component(RESULT_FILE_NAME ${RESULT_FULL_PATH} NAME) endif(LINUX) set_target_properties(${LIBRARY_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${TARGET_LOCATION}) ... if(LINUX) add_custom_command(TARGET ${LIBRARY_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${TARGET_LOCATION} COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:${LIBRARY_NAME}> ${TARGET_LOCATION}/${RESULT_FILE_NAME} ) endif(LINUX)
Для всех плагинов в Linux мы также должны указать список зависимостей:
SampleGuiPlugin2/CMakeLists.txt
... if(WIN32) set(SRCS ${SRCS} ${LIBRARY_NAME}.def) set(PREPROCESSOR_DEFINITIONS ${PREPROCESSOR_DEFINITIONS};/D_USRDLL;/D__STDC_CONSTANT_MACROS) set(LINK_DIRECTORIES ${PROJECT_ROOT_DIR}/wxNonGuiPluginBase/${OS_BASE_NAME}${LIB_SUFFIX}/$(ConfigurationName)) set(DEMO_LIBS wxNonGuiPluginBase.lib) endif(WIN32) if(LINUX) set(DEMO_LIBS wxNonGuiPluginBase) endif(LINUX) ...
Вроде все хорошо, проект собирается. Но при попытке запустить приложение мы обнаружим что динамические библиотеки не загружаются (это может стать неприятной неожиданностью в случае переноса на другую машину уже собранного приложения).
А все потому, что при сборке внутрь библиотеки прописываются ссылки на зависимости с полными путями. Так, например, для каждого плагина будет указан полный путь к библиотеке с базовыми классами, которой на другой рабочей машине не будет. Проверить это можно с помощью утилиты ldd:
ldd libSampleGuiPlugin2.so | grep wxSampleGuiPluginBase
Для того, чтобы избавиться от полных путей, в CMake надо указать опцию RPATH
для библиотек, которые ссылаются на другие динамические библиотеки из нашего решения:
SampleGuiPlugin2/CMakeLists.txt
if(WIN32) set(SRCS ${SRCS} ${LIBRARY_NAME}.def) set(PREPROCESSOR_DEFINITIONS ${PREPROCESSOR_DEFINITIONS};/D_USRDLL;/D__STDC_CONSTANT_MACROS) set(LINK_DIRECTORIES ${PROJECT_ROOT_DIR}/wxNonGuiPluginBase/${OS_BASE_NAME}${LIB_SUFFIX}/$(ConfigurationName)) set(DEMO_LIBS wxNonGuiPluginBase.lib) endif(WIN32) if(LINUX) set(DEMO_LIBS wxNonGuiPluginBase) SET(CMAKE_SKIP_BUILD_RPATH FALSE) SET(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) SET(CMAKE_INSTALL_RPATH ".:./../../") endif(LINUX)
Т.к. плагин находится в подкаталоге plugins/gui, то бибиотеку wxGuiPluginBase надо искать на два уровня выше, что и указано в CMakeLists.txt
Доработка CMake-скриптов для OS X
Так же, как и в Linux, в OS X у нас появляется проблема с загрузкой зависимостей у плагинов. В OS X для исправления путей к динамическим библиотекам, можно использовать утилиту install_name_tool.
Допишем код в CMakeLists.txt, который заменяет пути к библиотекам на относительные:
SampleGuiPlugin2/CMakeLists.txt
if(APPLE) FOREACH(DEP_LIB ${DEMO_LIBS}) get_filename_component(ABS_ROOT_DIR ${PROJECT_ROOT_DIR} ABSOLUTE) set(LIBNAME_FULL "${ABS_ROOT_DIR}/${DEP_LIB}/${OS_BASE_NAME}${LIB_SUFFIX}/$(CONFIGURATION)/lib${DEP_LIB}.dylib") add_custom_command(TARGET ${LIBRARY_NAME} POST_BUILD COMMAND install_name_tool -change "${LIBNAME_FULL}" "@executable_path/../Frameworks/lib${DEP_LIB}.dylib" $<TARGET_FILE:${LIBRARY_NAME}>) ENDFOREACH(DEP_LIB) endif(APPLE)
В CMake-скрипте приложения тоже надо сделать аналогичные правки
wxModularHost/CMakeLists.txt
if(APPLE) FOREACH(DEP_LIB ${DEMO_LIBS_SHARED}) get_filename_component(ABS_ROOT_DIR ${PROJECT_ROOT_DIR} ABSOLUTE) set(LIBNAME_FULL "${ABS_ROOT_DIR}/${DEP_LIB}/${OS_BASE_NAME}${LIB_SUFFIX}/$(CONFIGURATION)/lib${DEP_LIB}.dylib") add_custom_command(TARGET ${EXECUTABLE_NAME} POST_BUILD COMMAND install_name_tool -change "${LIBNAME_FULL}" "@executable_path/../Frameworks/lib${DEP_LIB}.dylib" $<TARGET_FILE:${EXECUTABLE_NAME}>) ENDFOREACH(DEP_LIB) endif(APPLE)
В завершение
Мы рассмотрели способ создания кросс-платформенных модульных приложений и сборку под Windows и Linux.
Полный исходный код проекта, рассмотренного в статье, можно найти на GitHub: https://github.com/T-Rex/wxModularApp
Также в GitHub-проекте есть дополнения к CMake-скриптам, которые позволяют собрать все под OS X.
PS: За время написания статьи вышла новая версия wxWidgets (3.0), с которой CMake еще не умеет работать (по крайней мере скрипты, которые работали для 2.9.x не отрабатывают с 3.0. Для тестирования лучше пока использовать код из ветки 2.9: svn.wxwidgets.org/svn/wx/wxWidgets/tags/WX_2_9_5/