Разработка кроссплатформенных модульных приложений на C++ с библиотекой wxWidgets

от автора

Введение

Уже долгое время не пишу статьи о разработке, хотя сам процесс написания мне очень нравится и позволяет привести мысли в порядок.

И вот, есть возможность сейчас рассказать о наработках, которые появились за последнее время. Надеюсь, кому-то этот текст сильно упростит жизнь и даст толчок к покорению новых вершин.

В этот раз речь пойдет о создании кроссплатформенных приложений с плагинами на 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 можно здесь:

Модуль управления плагинами

Итак, на данный момент у нас есть хост-приложение и минимальный плагин. Теперь надо реализовать загрузку и использование плагина в приложении. В целях универсальности, лучше выделить код, который будет заниматься поиском, загрузкой и выгрузкой плагинов из памяти, в отдельный класс, а еще лучше – в отдельную библиотеку.

Вспомним еще раз реализацию наших плагинов:

  • Плагин – это динамическая библиотека
  • В библиотеке есть экспортируемые функции 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()); 			} 		} 	} } 

В результате получим такое окно с вкладками:

image

Заголовки вкладок берутся из метода 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 и OS X Makefile с помощью CMake. Надеюсь, кому-то этот материал будет полезен.
Полный исходный код проекта, рассмотренного в статье, можно найти на GitHub: https://github.com/T-Rex/wxModularApp

PS: За время написания статьи вышла новая версия wxWidgets (3.0), с которой CMake еще не умеет работать (по крайней мере скрипты, которые работали для 2.9.x не отрабатывают с 3.0. Для тестирования лучше пока использовать код из ветки 2.9: svn.wxwidgets.org/svn/wx/wxWidgets/tags/WX_2_9_5/

ссылка на оригинал статьи http://habrahabr.ru/post/203322/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *