Customize Consent Preferences

We use cookies to help you navigate efficiently and perform certain functions. You will find detailed information about all cookies under each consent category below.

The cookies that are categorized as "Necessary" are stored on your browser as they are essential for enabling the basic functionalities of the site. ... 

Always Active

Necessary cookies are required to enable the basic features of this site, such as providing secure log-in or adjusting your consent preferences. These cookies do not store any personally identifiable data.

No cookies to display.

Functional cookies help perform certain functionalities like sharing the content of the website on social media platforms, collecting feedback, and other third-party features.

No cookies to display.

Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics such as the number of visitors, bounce rate, traffic source, etc.

No cookies to display.

Performance cookies are used to understand and analyze the key performance indexes of the website which helps in delivering a better user experience for the visitors.

No cookies to display.

Advertisement cookies are used to provide visitors with customized advertisements based on the pages you visited previously and to analyze the effectiveness of the ad campaigns.

No cookies to display.

Skip to content Skip to footer

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

Введение

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

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

В этот раз речь пойдет о создании кроссплатформенных приложений с плагинами на C++ с использованием библиотеки wxWidgets. Рассматриваться будут операционные системы Windows, Linux и OS X, как наиболее популярные.

Как обычно, первая часть будет обзорной, для того, чтобы снизить порог входа для читателей. Кому-то информация из первой части покажется очевидной (особенно то, что касается инструментария), но, все же, я считаю ее необходимой, ибо для новичков информация из первой части позволит с минимальными усилиями организовать процесс разработки.

Инструментарий

wxWidgets

Для начала нам понадобятся:

Библиотека wxWidgets в исходных кодах. Я использую наиболее новые версии из SVN. Они, конечно, не без багов, зато в них реализован функционал, которого обычно не хватает в официальных релизах.

Исходный код можно взять здесь: http://svn.wxwidgets.org/svn/wx/wxWidgets/trunk

Более подробно о процессе сборки библиотеки можно почитать здесь: http://habrahabr.ru/post/123588/

Разница в процессе сборки, по сравнению с указанной выше статьей заключается лишь в том, что нужно использовать конфигурацию DLL Debug и DLL Release вместо Debug и Release. К тому же, обязательно необходимо чтобы в настройках всех проектов, входящих в дистрибутив wxWidgets, в параметре C/C++ -> Code Generation -> Runtime Library были указаны значения Multi-Threaded Debug DLL и Multi-Threaded DLL. Именно с «DLL» в конце. В этом случае у нас wxWidgets будет собрана в виде динамических библиотек и с динамическим CRT.

При сборке конфигураций DLL Debug и DLL Release может быть такое что не все библиотеки соберутся с первого раза. Все это из-за проблем с указанием зависимостей. Если не собралось, запускаем сборку еще раз. Обычно 2-3 итераций достаточно для того, чтоб получить полный комплект динамических библиотек.

Напомню также, что для работы с wxWidgets необходимо наличие переменной окружения %WXWIN% (для Windows), которая указывает на папку с исходными кодами wxWidgets. Для Linux и OS X достаточно выполнить configure && make && make install.

Параметры для configure:

  • Debug: configure --enable-shared --disable-static --enable-unicode \
    --disable-compat28 --disable-final --enable-debug
  • Release: configure --enable-shared --disable-static --enable-unicode \
    --disable-compat28 --enable-final --disable-debug

CMake

Для того, чтобы облегчить работу по созданию файлов проектов для различных платформ на разных рабочих машинах с разными настройками, будем использовать систему генерации проектов CMake, о которой, кстати, есть несколько неплохих обзорных статей на Хабре, например вот:

В общем, CMake – это инструмент, с помощью которого на разных машинах мы сможем генерировать файлы проектов Visual Studio (Windows), Makefile/CodeBlocks (Linux), Makefile/XCode (OS X) с правильно прописанными путями к исходным кодам и сторонним библиотекам, что позволит нам избавиться от довольно большого объема лишней работы по настройке сборки.

Скачать CMake можно здесь: http://www.cmake.org/cmake/resources/software.html

Если вы собрали wxWidgets (Linux, OS X) с отладочной информацией, а потом хотите установить Release-версию, то надо сделать make uninstall для Debug-версии и вручную удалить файлы

  • /usr/local/bin/wx-config
  • /usr/local/bin/wxrc

Если указанные выше файлы не удалить вручную, то для Release-версии библиотеки будут использоваться настройки от Debug-версии. Приложение соберется, но не запустится.

Также надо иметь в виду тот факт, что если вы установили Debug-версию wxWidgets, то в Linux и OS X у вас, скорее всего, получится собрать только Debug-версию приложения. Это же касается и Release-версии. А все потому что CMake берет параметры компиляции и линковки из скрипта wx-config, который по умолчанию отдает параметры для одной текущей конфигурации. Или для Debug отдельно, или отдельно для Release.

Visual C++ (Windows)

Для сборки wxWidgets и нашего приложения из исходных кодов в Windows будем использовать Visual C++ 2012. Express редакция тоже подойдет. Это значит, что все средства разработки, включая IDE и компилятор, будут бесплатными.

Для тех, кто в танке, ссылка на бесплатный Visual C++ 2012: http://www.microsoft.com/visualstudio/rus/products/visual-studio-express-products

DialogBlocks

Для создания интерфейса пользователя, дабы не писать все руками, рекомендую использовать приложение DialogBlocks. Таки-да, он платный, но есть бесплатная пробная версия, которой достаточно для создания несложных форм. Хотя опять же, никто не мешает писать все руками (кстати, это даже неплохо в воспитательных целях и явно положительно сказывается на понимании кода).

Скачать DialogBlocks можно здесь: http://www.anthemion.co.uk/dialogblocks/download.htm

Начало

Структура папок

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

  • build – папка с общим CMake скриптом и shell-скриптами для генерирования проектов
  • build/bin/<Configuration> – папка, куда компилятор складывает бинарные файлы
  • /include – папка с общими заголовками (например, для precompiled headers)
  • /<ProjectName> – папка с исходными кодами проекта из главного решения (может быть более одного проекта в решении, у каждого своя папка)
  • /<ThirdParty> – папка, в которой лежат сторонние библиотеки (в виде исходников или собранные, каждая в своем подкаталоге)
  • /ThirdParty/build – папка с общим CMake скриптом и shell-скриптами для генерирования проектов сторонних библиотек (если вы решите вынести их в отдельный solution)
  • /ThirdParty/<LibName> – папка с исходными кодами сторонней библиотеки (их может быть более одной)
  • /<ProjectName>/<OS-Name> – сюда CMake складывает файлы проектов для каждой ОС.

Главный CMakeList

Главный скрипт CMake содержит общие параметры и настройки для всех проектов, а также описание некоторых общих переменных.

build/CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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.
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, а также один общий скрипт, который будет определять, под какую платформу собираем приложение:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)
1
2
3
4
5
rmdir /S /Q Win
mkdir Win
cd Win
cmake ../ -G "Visual Studio 11"
cd ..
Windows (cm64.bat)
1
2
3
4
5
rmdir /S /Q Win
mkdir Win
cd Win
cmake ../ -G "Visual Studio 11 Win64"
cd ..

Linux (cmLinux.sh)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#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

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

1
2
3
cmake_minimum_required(VERSION 2.6.0)
include(PCHSupport.cmake)
...

После подключения предварительно откомпилированных заголовков в проект, первой строкой во всех .CPP файлах проекта должна быть строка

1
#include "stdwx.h"

Простейший плагин без GUI

Библиотека с базовыми классами

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

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

wxNonGuiPluginBase/Declarations.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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

1
2
3
4
5
6
7
8
9
10
11
12
#include "stdwx.h"
#include "wxNonGuiPluginBase.h"
 
IMPLEMENT_ABSTRACT_CLASS(wxNonGuiPluginBase, wxObject)
 
wxNonGuiPluginBase::wxNonGuiPluginBase()
{
}
 
wxNonGuiPluginBase::~wxNonGuiPluginBase()
{
}

wxNonGuiPluginBase/CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once
 
#include <wxNonGuiPluginBase.h>
 
class SampleNonGuiPlugin : public wxNonGuiPluginBase
{
    DECLARE_DYNAMIC_CLASS(SampleNonGuiPlugin)
public:
    SampleNonGuiPlugin();
    virtual ~SampleNonGuiPlugin();
 
    virtual int Work();
};

SampleNonGuiPlugin/SampleNonGuiPlugin.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "stdwx.h"
#include "SampleNonGuiPlugin.h"
 
IMPLEMENT_DYNAMIC_CLASS(SampleNonGuiPlugin, wxObject)
 
SampleNonGuiPlugin::SampleNonGuiPlugin()
{
}
 
SampleNonGuiPlugin::~SampleNonGuiPlugin()
{
}
 
int SampleNonGuiPlugin::Work()
{
    return 10;
}

SampleNonGuiPlugin/SampleNonGuiPlugin.def

1
2
3
4
5
LIBRARY "SampleNonGuiPlugin"
 
EXPORTS
    CreatePlugin=CreatePlugin
    DeletePlugin=DeletePlugin

SampleNonGuiPlugin/SampleNonGuiPluginExports.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

1
2
3
4
...
add_subdirectory (../wxModularCore
    ../../wxModularCore/${OS_BASE_NAME}${LIB_SUFFIX})
...

Использование плагинов без GUI в приложении

Раз класс, управляющий плагинами, у нас уже готов, то можно начать им пользоваться в приложении.

Для начала поле-указатель на wxModularCore в класс приложения:

wxModularHost/wxModularHostApp.h

1
2
3
4
5
6
7
8
...
class wxModularHostApp: public wxApp
{
    void TestNonGuiPlugins();
...
    wxModularCore * m_PluginManager;
...
};

wxModularHost/wxModularHostApp.cpp

1
2
3
4
5
6
void wxModularHostApp::Init()
{
////@begin wxModularHostApp member initialisation
    m_PluginManager = new wxModularCore;
////@end wxModularHostApp member initialisation
}

И вот таким образом мы будем вызывать метод загрузки плагинов и пользоваться самими плагинами:

wxModularHost/wxModularHostApp.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
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

1
2
3
4
5
6
7
8
9
10
11
12
...
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:

1
ldd libSampleGuiPlugin2.so | grep wxSampleGuiPluginBase

Для того, чтобы избавиться от полных путей, в CMake надо указать опцию RPATH для библиотек, которые ссылаются на другие динамические библиотеки из нашего решения:

SampleGuiPlugin2/CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
if(APPLE)
    FOREACH(DEP_LIB ${DEMO_LIBS_SHARED})
        get_filename_component(ABS_ROOT_DIR ${PROJECT_ROOT_DIR} ABSOLUTE)
        set(LIBNAME_FULL "${ABS_ROOT_DIR}/${DEP_LIB}/${OS_BASE_NAME}${LIB_SUFFIX}/$(CONFIGURATION)/lib${DEP_LIB}.dylib")
                add_custom_command(TARGET ${EXECUTABLE_NAME} POST_BUILD
                    COMMAND install_name_tool -change "${LIBNAME_FULL}" "@executable_path/../Frameworks/lib${DEP_LIB}.dylib" $<TARGET_FILE:${EXECUTABLE_NAME}>)
        ENDFOREACH(DEP_LIB)
endif(APPLE)

В завершение

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

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

Leave a comment

0.0/5