Разработка кроссплатформенных модульных приложений на 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/ – папка, куда компилятор складывает бинарные файлы
  • /include – папка с общими заголовками (например, для precompiled headers)
  • / – папка с исходными кодами проекта из главного решения (может быть более одного проекта в решении, у каждого своя папка)
  • / – папка, в которой лежат сторонние библиотеки (в виде исходников или собранные, каждая в своем подкаталоге)
  • /ThirdParty/build – папка с общим CMake скриптом и shell-скриптами для генерирования проектов сторонних библиотек (если вы решите вынести их в отдельный solution)
  • /ThirdParty/ – папка с исходными кодами сторонней библиотеки (их может быть более одной)
  • // – сюда 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 
#include 
#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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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 

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 
#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 

// 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_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 

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
		bool RegisterPlugin(PluginType * plugin,
		PluginListType & list)
	{
		list.Append(plugin);
		return true;
	}

	template
		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
	bool UnloadPlugins(PluginListType & list,
		PluginToDllDictionaryType & pluginDictoonary)
	{
		bool result = true;
		PluginType * plugin = NULL;
		while (list.GetFirst() && (plugin =
			list.GetFirst()->GetData()))
		{
			result &= UnRegisterPlugin(plugin,
					list, pluginDictoonary);
		}
		return result;
	}

	template 
	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 
#include 
#include 

// 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_DEFINE_LIST(wxNonGuiPluginBaseList);
WX_DEFINE_LIST(wxGuiPluginBaseList);

SampleModularCore::~SampleModularCore()
{
	Clear();
}

bool SampleModularCore::LoadAllPlugins(bool forceProgramPath)
{
	wxString pluginsRootDir = GetPluginsPath(forceProgramPath);
	bool result = true;
	result &= LoadPlugins(pluginsRootDir,
		m_NonGuiPlugins,
		m_MapNonGuiPluginsDll,
		wxT("nongui"));
	result &= LoadPlugins(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(m_NonGuiPlugins,
			m_MapNonGuiPluginsDll) &&
		UnloadPlugins(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());
			}
		}
	}
}

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

modular-app-screenshot

Заголовки вкладок берутся из метода 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_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" 
						$)
        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" $)
        ENDFOREACH(DEP_LIB)
endif(APPLE)

В завершение

Мы рассмотрели способ создания кросс-платформенных модульных приложений и сборку под Windows и Linux.
Полный исходный код проекта, рассмотренного в статье, можно найти на GitHub: https://github.com/T-Rex/wxModularApp
Также в GitHub-проекте есть дополнения к CMake-скриптам, которые позволяют собрать все под OS X.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

Please leave these two fields as-is: