Введение
Уже долгое время не пишу статьи о разработке, хотя сам процесс написания мне очень нравится и позволяет привести мысли в порядок.
И вот, есть возможность сейчас рассказать о наработках, которые появились за последнее время. Надеюсь, кому-то этот текст сильно упростит жизнь и даст толчок к покорению новых вершин.
В этот раз речь пойдет о создании кроссплатформенных приложений с плагинами на 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%samplesminimal
Теперь можно переходить к созданию 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 и 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/
Автор: t_rex