The CTK framework is quite reliable in practical applications, but there is very little information available online. This tutorial focuses on the CTK Plugin Framework, exploring modular technology in C++, and enables rapid construction of a C++ component-based framework using CTK, helping others avoid detours. The source code for this tutorial can be downloaded here: Project Source Code.
CTK Introduction
CTK is a common toolkit for supporting biomedical image computing, with its full name being Common Toolkit. The CTK plugin framework is heavily inspired by the design of OSGi and enables applications to be composed of many different components into an extensible model. This model allows communication through services that share objects among those components.
Currently, the main scope of CTK’s work includes:
DICOM: Provides advanced classes for querying and retrieving from PACS and local databases. Includes Qt widgets that make it easy to set up server connections, send queries, and view results.
DICOM Application Hosting: Aims to create a C++ reference implementation of the DICOM Part 19 Application Hosting specifications. It provides infrastructure for creating hosts and hosted applications.
Widgets: A collection of Qt Widgets for biomedical imaging applications.
Plugin Framework: A dynamic component system for C++, modeled on the OSGi specification. It supports a development model in which applications are (dynamically) composed of many different (reusable) components, following a service-oriented approach.
Command Line Interfaces: A technique that allows algorithms to be written as self-contained executable programs that can be used in multiple end-user application environments without modification.
CTK Plugin Framework
CTK Plugin Framework Architecture Strategy
- Qt Creator's extensibility.
Qt Creator achieves extensibility in a simple and elegant way by using a common QObject pool to implement certain available interfaces. At the same time, it uses embedded text files (.pluginspec files) to add metadata to plugins (e.g., Name, Version, etc.).
Strictly speaking, CTK Plugin Framework draws on ideas from both OSGi and Qt Creator.
- Qt provides the Qt Plugin System and Qt Service Framework.
The Qt Plugin System provides two sets of APIs for creating plugins: a high-level API for extending Qt itself (e.g., custom database drivers, image formats, text codecs, custom styles, etc.) and a low-level API for extending Qt applications. For the Qt Service Framework, it makes service development and access easier. Qt service providers can interact with platform-specific services without exposing platform details to clients. Each service is exposed through a QObject pointer, meaning clients can interact with the service object through the Qt MetaObject system.
- What is the architecture strategy of CTK Plugin Framework?
CTK Plugin Framework is implemented based on the Qt Plugin System and Qt Service Framework, and it adds the following features to enhance these two systems:
- Plugin metadata (provided by MANIFEST.MF file);
- A well-defined plugin lifecycle and context;
- Comprehensive service discovery and registration;
- ...
Note: In the Qt Plugin System, plugin metadata is provided by JSON files.
The core architecture of CTK Plugin Framework consists of two main components: the Plugin System itself and the Service Registry. However, these two components are interrelated, and their combination at the API level makes the system more comprehensive and flexible.
- Plugin System
CTK Core depends on the QtCore module, so the CTK Plugin Framework is based on the Qt Plugin System. The Qt API allows loading and unloading plugins at runtime, a feature enhanced in CTK Plugin Framework to support transparent lazy loading and dependency resolution.
Plugin metadata is compiled into the plugin and can be extracted via the API. Additionally, the plugin system caches metadata in SQLite to avoid application loading time issues. Furthermore, the Plugin System supports using services through a central registry.
- Service Registry
The Qt Service Framework is a Qt solution released as part of the Qt Mobility project. This service framework allows "Declarative Services" (Getting Started with OSGi: Introducing Declarative Services) and on-demand loading of service implementations. To enable dynamic (non-persistent) services, the Qt Mobility service framework can be used together with the Service Registry, similar to what is described in the OSGi Core Specifications.
Advantages of CTK Plugin Framework
Since CTK Plugin Framework is based on OSGi, it inherits a very mature and fully designed component system used in Java to build highly complex applications, bringing these benefits to native (Qt-based) C++ applications. The following is adapted from Benefits of Using OSGi for CTK Plugin Framework:
- Reduce Complexity
Developing with CTK Plugin Framework means developing plugins that hide internal implementations and communicate with other plugins through well-defined services. Hiding internals means you can freely change the implementation later. This not only helps reduce the number of bugs but also makes plugin development simpler because you only need to implement a defined set of functionality interfaces.
- Reusability
A standardized component model makes it very easy to use third-party components in applications.
- Reality
CTK Plugin Framework is a dynamic framework that can dynamically update plugins and services. In the real world, many scenarios match the dynamic service model. Therefore, applications can reuse the powerful primitives of the Service Registry (register, get, list with expressive filter language, wait for services to appear and disappear) in their domain. This not only saves coding but also provides global visibility, debugging tools, and more functionality than what would be implemented for a dedicated solution. Writing code in such a dynamic environment might sound like a nightmare, but fortunately, support classes and frameworks eliminate most (if not all) of the pain.
- Easy Development
CTK Plugin Framework is not only a standard for components; it also specifies how to install and manage them. This API can be used by plugins to provide a management agent, which can be as simple as a command shell, a graphical desktop application, a cloud computing interface for Amazon EC2, or an IBM Tivoli management system. The standardized management API makes it very easy to integrate CTK Plugin Framework into existing and future systems.
- Dynamic Updates
The OSGi component model is a dynamic model: plugins can be installed, started, stopped, updated, and uninstalled without shutting down the entire system.
- Adaptability
The OSGi component model was designed from the ground up to allow mixing and matching of components. This requires that component dependencies must be specified and that components must survive in environments where optional dependencies are not always available. The Service Registry is a dynamic registry where plugins can register, get, and listen for services. This dynamic service model allows plugins to discover available functionality in the system and adjust what they can offer. This makes code more flexible and better able to adapt to change.
- Transparency
Plugins and services are first-class citizens in the CTK plugin environment. The management API provides access to the internal state of plugins and how they are connected. You can stop parts of an application to debug a problem or introduce diagnostic plugins.
- Versioning
In CTK Plugin Framework, all plugins are strictly versioned, and only plugins that can work together are connected.
- Simplicity
The CTK plugin-related API is very simple, with fewer than 25 classes in the core API. This core API is sufficient to write, install, start, stop, update, and uninstall plugins, and includes all listener classes.
- Lazy Loading
Lazy loading is a great feature in software. OSGi technology has many mechanisms to ensure that classes are only loaded when actually needed. For example, plugins can be started eagerly, but can also be configured to start only when other plugins use them. Services can be registered but only created when used. These lazy loading scenarios can save significant runtime costs.
- Non-exclusivity
CTK Plugin Framework does not take over the entire application. You can selectively expose the provided functionality to certain parts of the application, or even run multiple instances of the framework in the same process.
- Non-invasiveness
In a CTK plugin environment, each plugin has its own environment. They can use any facilities without restrictions from the framework. CTK services have no special interface requirements; every QObject can serve as a service, and every class (including non-QObject) can serve as an interface.
CTK Compilation
Use CMake to compile the dynamic libraries corresponding to the system version. Refer to CTK Compilation Tutorial (64-bit Environment Windows + Qt + MinGW or MSVC + CMake).
Using CTKWidgets
New project - Application(Qt) - Qt Console Application, project name UseCTKWidgets, pro file code:
QT += core gui widgets
TARGET = UseCTKWidgets
TEMPLATE = app
# CTK installation path
CTK_INSTALL_PATH = $$PWD/../../CTKInstall
# Path for CTK related libraries (e.g., CTKCore.lib, CTKWidgets.lib)
CTK_LIB_PATH = $$CTK_INSTALL_PATH/lib/ctk-0.1
# Path for CTK related headers (e.g., ctkPluginFramework.h)
CTK_INCLUDE_PATH = $$CTK_INSTALL_PATH/include/ctk-0.1
# Related library files (CTKCore.lib, CTKWidgets.lib)
LIBS += -L$$CTK_LIB_PATH -lCTKCore -lCTKWidgets
INCLUDEPATH += $$CTK_INCLUDE_PATH
Main function loads widgets, code: main.cpp
#include <QApplication>
#include <QFormLayout>
#include <QVBoxLayout>
#include <ctkCheckablePushButton.h>
#include <ctkCollapsibleButton.h>
#include <ctkColorPickerButton.h>
#include <ctkRangeWidget.h>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
// Collapsible button
ctkCollapsibleButton* buttons = new ctkCollapsibleButton("Buttons");
// Checkable push button
ctkCheckablePushButton* checkablePushButton = new ctkCheckablePushButton();
checkablePushButton->setText("Checkable");
// Color picker button
ctkColorPickerButton* colorPickerButton = new ctkColorPickerButton();
colorPickerButton->setColor(QColor("#9e1414"));
ctkCollapsibleButton* sliders = new ctkCollapsibleButton("Sliders");
QFormLayout* buttonsLayout = new QFormLayout;
buttonsLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
buttonsLayout->addRow("ctkCheckablePushButton", checkablePushButton);
buttonsLayout->addRow("ctkColorPickerButton", colorPickerButton);
buttons->setLayout(buttonsLayout);
QVBoxLayout* topLevelLayout = new QVBoxLayout();
topLevelLayout->addWidget(buttons);
topLevelLayout->addWidget(sliders);
QFormLayout* slidersLayout = new QFormLayout;
ctkRangeWidget* rangeWidget = new ctkRangeWidget();
slidersLayout->addRow("ctkRangeWidget", rangeWidget);
sliders->setLayout(slidersLayout);
QWidget topLevel;
topLevel.setLayout(topLevelLayout);
topLevel.show();
return app.exec();
}
Project code: UseCTKWidgets
Preliminary Use of CTK Plugin Framework
Project Structure
Since each plugin requires a subproject, when creating this project in QtCreator, select New - Other Project - Subdirs Project, name the project SampleCTK. Then create a main program entry project, here a console project named App.
Change project output path: app.pro
DESTDIR = $$OUT_PWD/../bin
Load plugins in main function, start the framework: main.cpp
#include <QCoreApplication>
#include "ctkPluginFrameworkFactory.h"
#include "ctkPluginFramework.h"
#include "ctkPluginException.h"
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
app.setApplicationName("SampleCTK"); // Set framework name, required on Linux or it will error
ctkPluginFrameworkFactory frameWorkFactory;
QSharedPointer<ctkPluginFramework> framework = frameWorkFactory.getFramework();
try {
// Initialize and start the plugin framework
framework->init();
framework->start();
qDebug() << "CTK Plugin Framework start ...";
} catch (const ctkPluginException &e) {
qDebug() << "Failed to initialize the plugin framework: " << e.what();
qDebug() << e.message() << e.getType();
}
return app.exec();
}
If you want to encapsulate operations like CTK initialization, plugin installation/startup, and service retrieval into a class, note that CTK-related variables must be class properties, not local variables; otherwise, various issues may occur, such as inability to retrieve services or null service references.
If there is no error, the plugin loaded successfully!
The QSharedPointer<ctkPluginFramework> framework object is interesting; it can be used both as an object and as an object pointer, but to use it as a plugin framework, pointer method calls are required, hence the use of "->".
Loading CTK Framework Plugins in the Project
Create a new text file named CTK in the SampleCTK project, then change the extension to pri. The file loads the CTK installation directory and source code directory. The compiled dynamic library can then be used as a normal dynamic library. The loading code in CTK.pri is:
# CTK installation path
CTK_INSTALL_PATH = $$PWD/../../CTKInstall
# Path for CTK plugin related libraries (e.g., CTKCore.lib, CTKPluginFramework.lib)
CTK_LIB_PATH = $$CTK_INSTALL_PATH/lib/ctk-0.1
# Path for CTK plugin related headers (e.g., ctkPluginFramework.h)
CTK_INCLUDE_PATH = $$CTK_INSTALL_PATH/include/ctk-0.1
# Path for CTK plugin related headers (mainly because of service-related things)
CTK_INCLUDE_FRAMEWORK_PATH = $$PWD/../../CTK-master/Libs/PluginFramework
LIBS += -L$$CTK_LIB_PATH -lCTKCore -lCTKPluginFramework
INCLUDEPATH += $$CTK_INCLUDE_PATH \
$$CTK_INCLUDE_FRAMEWORK_PATH
Include CTK.pri in the pro file: app.pro
include($$PWD/../CTK.pri)
CTK Plugin Interface Handling
The CTK framework consists of separable plugins. The framework has certain requirements for identifying plugins. Many online tutorials dump everything at once, which is not beginner-friendly. This tutorial tries to break things down as much as possible. The basic format for a single plugin consists of an Activator, a qrc file, and a MANIFEST.MF file. Take the say Hello module HelloCTK as an example.
Activator
Each plugin has its own Activator.
Right-click the project and select New Subproject - Other Project - Empty qmake Project, name it HelloCTK. Add code to the pro file:
QT += core
QT -= gui
TEMPLATE = lib
CONFIG += plugin
TARGET = HelloCTK
DESTDIR = $$OUT_PWD/../bin/plugins
include($$PWD/../CTK.pri)
The generated plugin name (TARGET) should not contain underscores, because CTK will replace underscores with dots by default, which will eventually cause the plugin not to be found.
Add a C++ class HelloActivator to the project, code:
hello_activator.h
#ifndef HELLO_ACTIVATOR_H
#define HELLO_ACTIVATOR_H
#include <QObject>
#include "ctkPluginActivator.h"
class HelloActivator : public QObject, public ctkPluginActivator
{
Q_OBJECT
Q_INTERFACES(ctkPluginActivator)
Q_PLUGIN_METADATA(IID "HELLO_CTK")
// Declare to Qt's plugin framework that you want to put this plugin into the framework.
public:
void start(ctkPluginContext* context);
void stop(ctkPluginContext* context);
};
#endif // HELLO_ACTIVATOR_H
hello_activator.cpp
#include "hello_activator.h"
#include <QDebug>
void HelloActivator::start(ctkPluginContext* context)
{
qDebug() << "HelloCTK start";
}
void HelloActivator::stop(ctkPluginContext* context)
{
qDebug() << "HelloCTK stop";
Q_UNUSED(context)
// Q_UNUSED is used to suppress compiler/editor warnings for unused function parameters or variables, no other practical effect.
}
The activator is a standard Qt plugin class that implements the start and stop functions of ctkPluginActivator and provides interfaces externally. I'm using Qt5, so Q_PLUGIN_METADATA is used to declare the plugin. Qt4 requires its own method.
qrc File
Create a resource file for the plugin with the following format:
<RCC>
<qresource prefix="/HelloCTK/META-INF">
<file>MANIFEST.MF</file>
</qresource>
</RCC>
After the plugin is loaded, it will look for the same name prefix /META-INF, so the prefix format is fixed. Add the MANIFEST.MF file.
Content of MENIFEST.MF:
You can directly add your own custom metadata in the MF file.
Plugin-SymbolicName:HelloCTK
Plugin-Version:1.0.0
Plugin-Number:100 # metadata
Note: Plugin-SymbolicName must satisfy the format: TARGET/META-INF. The TARGET name should ideally match the project name, otherwise errors like "device not open" may occur.
The file contains basic information about the CTK plugin. If the CTK framework can correctly identify information like Plugin-SymbolicName from the file, it will recognize it as a CTK plugin and can call the start/stop functions of the activator. This file needs to be copied to the plugin output path. Add code to the pro file:
file.path = $$DESTDIR
file.files = MANIFEST.MF
INSTALLS += file
Enabling CTK Plugins
With the above steps, the CTK plugin interface definition is basically complete. In the App project, call to see if the plugin can be loaded correctly. After the framework starts successfully in the main function, add the following code:
QString dir = QCoreApplication::applicationDirPath();
dir += "/plugins/HelloCTK.dll";
qDebug() << dir;
QUrl url = QUrl::fromLocalFile(dir);
QSharedPointer<ctkPlugin> plugin;
try
{
plugin = framework->getPluginContext()->installPlugin(url);
}catch(ctkPluginException e){
qDebug() << e.message() << e.getType();
}
try{
plugin->start(ctkPlugin::START_TRANSIENT);
}catch(ctkPluginException e){
qDebug() << e.message() << e.getType();
}
Console output:
"C:/d/mmm/qt/ctk/CTK-examples/build-SampleCTK-Desktop_Qt_5_15_1_MSVC2019_64bit-Release/bin/plugins"
HelloCTK start
Successfully calling the print output inside HelloCTK's start indicates that the CTK plugin interface is correctly defined and can be loaded. Using start(ctkPlugin::START_TRANSIENT) means immediately starting the plugin; if no parameter is set, it won't print immediately after loading.
Basic Use of CTK Plugin Framework
CTK Inter-Plugin Communication
CTK framework plugin development achieves functional isolation. Plugin communication needs to follow a fixed standard. Here are two methods for inter-plugin communication.
Communication Method 1: Registering Interfaces and Calling
Registering Interface Calls
Function Interface
An interface is a pure virtual function class, the precursor to the final service.
Above we already compiled the required dynamic library. First, determine what functionality the plugin should expose to the outside. For example, if we need an operation to say "Hello, CTK!", define the header file as follows:
hello_service.h
#ifndef HELLO_SERVICE_H
#define HELLO_SERVICE_H
#include <QtPlugin>
class HelloService
{
public:
virtual ~HelloService() {}
virtual void sayHello() = 0;
};
#define HelloService_iid "org.commontk.service.demos.HelloService"
Q_DECLARE_INTERFACE(HelloService, HelloService_iid)
// This macro declares the current interface class as an interface; the long string is the unique identifier for the interface.
#endif // HELLO_SERVICE_H
Q_DECLARE_INTERFACE declares the interface class to the Qt system. Then add its implementation object:
Interface Implementation
A plugin is the implementation class that implements this interface class, so theoretically, there are as many plugins as there are implementation classes.
hello_impl.h
#ifndef HELLO_IMPL_H
#define HELLO_IMPL_H
#include "hello_service.h"
#include <QObject>
class ctkPluginContext;
class HelloImpl : public QObject, public HelloService
{
Q_OBJECT
Q_INTERFACES(HelloService)
/*
This macro works in conjunction with the Q_DECLARE_INTERFACE macro.
Q_DECLARE_INTERFACE: declares an interface class
Q_INTERFACES: when a class inherits this interface class, it indicates that it needs to implement this interface class.
*/
public:
HelloImpl(ctkPluginContext* context);
void sayHello() Q_DECL_OVERRIDE;
};
#endif // HELLO_IMPL_H
hello_impl.cpp
#include "hello_impl.h"
#include <ctkPluginContext.h>
#include <QtDebug>
HelloImpl::HelloImpl(ctkPluginContext* context)
{
context->registerService<HelloService>(this);
}
void HelloImpl::sayHello()
{
qDebug() << "Hello,CTK!";
}
This is still Qt's plugin definition format, but it will not be exported as a plugin. The external functional interface can be customized.
Service Registration (Activator registers the service)
The activator class has a unique smart pointer pointing to the interface class [using polymorphism, pointers all point to the base class]. In start, a new implementation class is created, registered as a service. The function is to implement the interface class's interface, and then point the smart pointer to this implementation class. Think of it as: when you request this service from the framework later, you actually get this newly created implementation class. If you don't use a smart pointer, you need to manually delete the implementation class in stop.
Each plugin has its own Activator. After the function interface is completed, it is registered into the CTK framework's services when the plugin starts. Code:
hello_activator.cpp
#include "hello_activator.h"
#include "hello_impl.h"
void HelloActivator::start(ctkPluginContext* context)
{
s.reset(new HelloImpl(context));
// Call registering service context->registerService<HelloService>(this);
}
Interface Call
After the CTK plugin is enabled, the interface can be called.
After the main function framework and plugin are loaded, call the plugin interface. Code:
main.cpp
#include "../HelloCTK/hello_service.h"
// Get service reference
ctkServiceReference reference = context->getServiceReference<HelloService>();
if (reference) {
// Get the service object referenced by the specified ctkServiceReference
HelloService* service = qobject_cast<HelloService *>(context->getService(reference));
if (service != Q_NULLPTR) {
// Call the service
service->sayHello();
}
}
There are two overloaded ways to get the service [directly usable]:
1. HelloService* service = context->getService<HelloService>(reference);
2. HelloService* service = qobject_cast<HelloService*>(context->getService(reference));
A service is an instance based on an interface. Each time a service is created, the activator's start is called once. Think of the interface as a class, the service as an object created from the class, and the plugin as a dynamic library dll.
Project code: SampleCTK
Optimized Decoupling (Separating Implementation Class and Activator Class)
Writing a plugin mainly involves three steps: interface class, implementation class, and activator class. Do not register the service in the constructor of the implementation class to reduce coupling. The interface class only declares the interface, the implementation class only implements it, and the activator class is responsible for integrating the service into the CTK framework.
The interface class remains unchanged. The implementation class loses the registration code and has no constructor parameters. The registration process is moved to the activator class.
- Implementation class
.h
HelloImpl( );
.cpp
HelloImpl::HelloImpl( )
{
qDebug()<<"this is imp";
}
- Activator class
.cpp
void HelloActivator::start(ctkPluginContext* context)
{
HelloImpl* helloImpl = new HelloImpl(context);
context->registerService<HelloService>(helloImpl);
s.reset(helloImpl);
}
Relationship Between Interface, Plugin, and Service
- One-to-One
One interface class is implemented by one class, producing one service and one plugin.
The project above is a typical one-to-one relationship.
- Many-to-One
One class implements multiple interface classes, producing multiple services and one plugin. No matter which service you want to use, it is ultimately achieved through the same plugin.
Implementation class implements multiple interfaces.
#include "greet_impl.h"
#include <QtDebug>
GreetImpl::GreetImpl()
{
}
void GreetImpl::sayHello()
{
qDebug() << "Hello,CTK!";
}
void GreetImpl::sayBye()
{
qDebug() << "Bye,CTK!";
}
Get different services.
// Get service reference
ctkServiceReference ref = context->getServiceReference<HelloService>();
if (ref) {
HelloService* service = qobject_cast<HelloService *>(context->getService(ref));
if (service != Q_NULLPTR)
service->sayHello();
}
ref = context->getServiceReference<ByeService>();
if (ref) {
ByeService* service = qobject_cast<ByeService *>(context->getService(ref));
if (service != Q_NULLPTR)
service->sayBye();
}
See the project: PluginAndService/MultipleInterfaces
- One-to-Many
One interface is implemented by multiple classes, meaning there are multiple solutions for a problem. This produces one service and multiple plugins. Use ctkPluginConstants::SERVICE_RANKING and ctkPluginConstants::SERVICE_ID to call different plugins. Although there are two plugins, they are compiled into the same dll. The service retrieval strategy is as follows: the container returns the service with the lowest ranking, i.e., the one with the smallest SERVICE_RANKING property value. If multiple services have the same ranking, the container returns the one with the smallest PID value.
When a plugin calls another plugin, only one instance is created and stored in memory; multiple calls do not create multiple service instances.
When using one interface with two plugins, even though there are two plugins, there will be two activator classes [in principle, one activator can register twice in start]. The IID can only be one. At the Qt plugin level, one dll can only have one IID.
Multiple implementation classes implement one interface.
welcome_ctk_impl.cpp
#include "welcome_ctk_impl.h"
#include <QtDebug>
WelcomeCTKImpl::WelcomeCTKImpl()
{
}
void WelcomeCTKImpl::welcome()
{
qDebug() << "Welcome CTK!";
}
welcome_qt_impl.cpp
#include "welcome_qt_impl.h"
#include <QtDebug>
WelcomeQtImpl::WelcomeQtImpl()
{
}
void WelcomeQtImpl::welcome()
{
qDebug() << "Welcome Qt!";
}
Corresponding multiple activator classes
welcome_ctk_activator.cpp
#include "welcome_ctk_impl.h"
#include "welcome_ctk_activator.h"
#include <QtDebug>
void WelcomeCTKActivator::start(ctkPluginContext* context)
{
ctkDictionary properties;
properties.insert(ctkPluginConstants::SERVICE_RANKING, 2);
properties.insert("name", "CTK");
m_pImpl = new WelcomeCTKImpl();
context->registerService<WelcomeService>(m_pImpl, properties);
}
void WelcomeCTKActivator::stop(ctkPluginContext* context)
{
Q_UNUSED(context)
delete m_pImpl;
}
welcome_qt_activator.cpp
#include "welcome_qt_impl.h"
#include "welcome_qt_activator.h"
#include <QtDebug>
void WelcomeQtActivator::start(ctkPluginContext* context)
{
ctkDictionary properties;
properties.insert(ctkPluginConstants::SERVICE_RANKING, 1);
properties.insert("name", "Qt");
m_pImpl = new WelcomeQtImpl();
context->registerService<WelcomeService>(m_pImpl, properties);
}
void WelcomeQtActivator::stop(ctkPluginContext* context)
{
Q_UNUSED(context)
delete m_pImpl;
}
Get services.
// 1. Get all services
QList<ctkServiceReference> refs = context->getServiceReferences<WelcomeService>();
foreach (ctkServiceReference ref, refs) {
if (ref) {
qDebug() << "Name:" << ref.getProperty("name").toString()
<< "Service ranking:" << ref.getProperty(ctkPluginConstants::SERVICE_RANKING).toLongLong()
<< "Service id:" << ref.getProperty(ctkPluginConstants::SERVICE_ID).toLongLong();
WelcomeService* service = qobject_cast<WelcomeService *>(context->getService(ref));
if (service != Q_NULLPTR)
service->welcome();
}
}
// 2. Use filter expression to get interested services
refs = context->getServiceReferences<WelcomeService>("(&(name=CTK))");
foreach (ctkServiceReference ref, refs) {
if (ref) {
WelcomeService* service = qobject_cast<WelcomeService *>(context->getService(ref));
if (service != Q_NULLPTR)
service->welcome();
}
}
// 3. Get a specific service (determined by Service Ranking and Service ID)
ctkServiceReference ref = context->getServiceReference<WelcomeService>();
if (ref) {
WelcomeService* service = qobject_cast<WelcomeService *>(context->getService(ref));
if (service != Q_NULLPTR)
service->welcome();
}
See the project: PluginAndService/OneInterface
Communication Method 2: Event Listening
Event listening in the CTK framework follows the observer pattern. The flow is: the receiver registers to listen for an event -> the sender sends the event -> the receiver receives the event and responds. Compared to calling plugin interfaces, event listening has weaker dependencies between plugins and does not require specifying who the receiver or sender is.
To use CTK's event service, preparation should start from cmake, compiling a dynamic library that supports event listening, named liborg_commontk_eventadmin.dll. Now, the goal is to use event listening to call a child window from the main window generated above.
- Communication mainly uses the
ctkEventAdminstructure, which defines the following interfaces:
postEvent: Asynchronously send events in a class communication stylesendEvent: Synchronously send events in a class communication stylepublishSignal: Send events via signal-slot communicationunpublishSignal: Cancel sending eventssubscribeSlot: Subscribe to events via signal-slot communication, returns subscription IDunsubscribeSlot: Unsubscribe from eventsupdateProperties: Update the topic for a given subscription ID
- The data for communication is:
ctkDictionary
It is essentially a hash table: typedef QHash<QString,QVariant> ctkDictionary
Event Listening
Specific project: EventAdmin/SendEvent
Load EventAdmin Dynamic Library
Use ctkPluginFrameworkLauncher to add the dynamic library. Code: main.cpp
// Get the plugin location
// Add a path to the plugin search path list
ctkPluginFrameworkLauncher::addSearchPath("../../../../CTKInstall/lib/ctk-0.1/plugins");
// Set and start the CTK plugin framework
ctkPluginFrameworkLauncher::start("org.commontk.eventadmin");
……
// Stop the plugin
ctkPluginFrameworkLauncher::stop();
Register Event Listening (Receiver Plugin)
First, create the receiver module we need and register the event listener. Create a new module BlogEventHandler. For the interface handling of the module, refer to "CTK Plugin Interface Handling" above. Part of the plugin code:
blog_event_handler.h
#ifndef BLOG_EVENT_HANDLER_H
#define BLOG_EVENT_HANDLER_H
#include <QObject>
#include <service/event/ctkEventHandler.h>
// Event handler (or subscriber)
class BlogEventHandler : public QObject, public ctkEventHandler
{
Q_OBJECT
Q_INTERFACES(ctkEventHandler)
public:
// Handle event
void handleEvent(const ctkEvent& event) Q_DECL_OVERRIDE
{
QString title = event.getProperty("title").toString();
QString content = event.getProperty("content").toString();
QString author = event.getProperty("author").toString();
qDebug() << "EventHandler received the message, topic:" << event.getTopic()
<< "properties:" << "title:" << title << "content:" << content << "author:" << author;
}
};
#endif // BLOG_EVENT_HANDLER_H
Different from the custom interface above, here we instantiate a ctkEventHandler object and implement the handleEvent interface. In the constructor, the registered service object is ctkEventHandler, specifying the trigger event. When the event triggers, the object's handleEvent is called to perform the specified operation.
Send Event (Sender Plugin)
After the listening object is complete, calling it is relatively simple. Code: blog_manager.cpp
#include "blog_manager.h"
#include <service/event/ctkEventAdmin.h>
#include <QtDebug>
BlogManager::BlogManager(ctkPluginContext* context)
: m_pContext(context)
{
}
// Publish event
void BlogManager::publishBlog(const Blog& blog)
{
ctkServiceReference ref = m_pContext->getServiceReference<ctkEventAdmin>();
if (ref) {
ctkEventAdmin* eventAdmin = m_pContext->getService<ctkEventAdmin>(ref);
ctkDictionary props;
props["title"] = blog.title;
props["content"] = blog.content;
props["author"] = blog.author;
ctkEvent event("org/commontk/bloggenerator/published", props);
qDebug() << "Publisher sends a message, properties:" << props;
eventAdmin->sendEvent(event);
}
}
Project code: EventAdmin/SendEvent
Event Sending Methods (Class Communication, Signal-Slot Communication)
1. Class Communication
The principle is to directly send information using CTK's eventAdmin interface (send/post). The project above is a typical class communication.
2. Signal-Slot Communication
The principle is to bind Qt's own signals to CTK's sending events, and slots to event subscriptions.
Receiving slot
void BlogEventHandlerUsingSlotsActivator::start(ctkPluginContext* context)
{
m_pEventHandler = new BlogEventHandlerUsingSlots();
ctkDictionary props;
props[ctkEventConstants::EVENT_TOPIC] = "org/commontk/bloggenerator/published";
ctkServiceReference ref = context->getServiceReference<ctkEventAdmin>();
if (ref) {
ctkEventAdmin* eventAdmin = context->getService<ctkEventAdmin>(ref);
eventAdmin->subscribeSlot(m_pEventHandler, SLOT(onBlogPublished(ctkEvent)), props, Qt::DirectConnection);
}
}
Sending signal
BlogManagerUsingSignals::BlogManagerUsingSignals(ctkPluginContext *context)
{
ctkServiceReference ref = context->getServiceReference<ctkEventAdmin>();
if (ref) {
ctkEventAdmin* eventAdmin = context->getService<ctkEventAdmin>(ref);
// Using Qt::DirectConnection is equivalent to ctkEventAdmin::sendEvent()
eventAdmin->publishSignal(this, SIGNAL(blogPublished(ctkDictionary)), "org/commontk/bloggenerator/published", Qt::DirectConnection);
}
}
Specific project: EventAdmin/SignalSlot
Differences Between the Two
Communication via event events directly calls CTK interfaces to send data to the CTK framework; via signal-slot, data is first passed through Qt's signal-slot mechanism and then sent to the CTK framework. Therefore, in terms of efficiency, the event method performs better than the signal-slot method.
Both methods send data (topic + properties) to the CTK framework. The topic is the event topic, and properties are ctkDictionary. Note that the signal definition for the signal method cannot have custom types; it must be ctkDictionary, otherwise a signal-slot parameter mismatch error will occur.
The two methods can be mixed, e.g., send an event event and receive via a slot; send a signal event and receive via an event.
Synchronous: sendEvent, QtDirectConnection; Asynchronous: postEvent, QtQueuedConnection
Synchronous means: after sending the event, all subscribers of this topic will process the data [handleEvent, slot] in the sender's thread. Think of it as: after sending an event, all callback functions subscribed to that event are executed immediately.
Asynchronous means: after sending the event, the sender returns immediately without waiting. All plugins subscribed to this event will process it based on their own message loop when it is their turn. However, if not processed for a long time, CTK has its own timeout mechanism. If an event handler takes longer than the configured timeout, it will be blacklisted. Once a handler is blacklisted, no further events will be sent to it.
Plugin Dependencies
When plugins are loaded, they are generally loaded in alphabetical order. Therefore, when a plugin is started, another plugin may not have been loaded yet, so sending events may have no receiver. This requires considering plugin dependencies. Add dependencies in MANIFEST.MF:
Plugin-SymbolicName:Plugin-xxx-1
Plugin-Version:1.0.0
Require-Plugin:Plugin-xxx-2; plugin-version="[1.0,2.0)"; resolution:="mandatory"
Plugin-xxx-2: The name of the plugin to depend on [i.e., the Plugin-SymbolicName of another plugin in its MANIFEST.MF].[1.0,2.0): The version range of Plugin-xxx-2. This is a left-closed, right-open interval, default is 1.0.resolution: Has two options, optional and mandatory. optional is a weak dependency; even if the dependent plugin is absent, the current plugin can still work. mandatory is a strong dependency; if the dependent plugin is not present, the current plugin cannot be started.
This declares to the framework that when this plugin is loaded, Plugin-xxx-2 must be loaded first. All user plugins should have such a declaration.
See the project: RequirePlugin
Plugin Metadata
Get data from MANIFEST.MF
QHash<QString, QString> headers = plugin->getHeaders();
ctkVersion version = ctkVersion::parseVersion(headers.value(ctkPluginConstants::PLUGIN_VERSION));
QString name = headers.value(ctkPluginConstants::PLUGIN_NAME);
See the project: GetMetaData
Advanced Use of CTK Plugin Framework
CTK Service Factory
When registering a service, you can use a service factory. When accessing a service via getService, the plugin parameter is the plugin that called ctkPluginContext::getService(const ctkServiceReference&). Thus, the factory returns different service implementations based on the name of the calling plugin.
Purpose of service factory:
- Know which other plugin is using the service;
- Lazy loading: only create a new instance when needed;
- There is no difference for other plugins to use a service with or without a factory; the code is the same;
- Can create multiple implementations of the service as needed, i.e., multiple services corresponding to one plugin.
Interface Class
#ifndef HELLO_SERVICE_H
#define HELLO_SERVICE_H
#include <QtPlugin>
class HelloService
{
public:
virtual ~HelloService() {}
virtual void sayHello() = 0;
};
#define HelloService_iid "org.commontk.service.demos.HelloService"
Q_DECLARE_INTERFACE(HelloService, HelloService_iid)
#endif // HELLO_SERVICE_H
Multiple Implementation Classes
#ifndef HELLO_IMPL_H
#define HELLO_IMPL_H
#include "hello_service.h"
#include <QObject>
#include <QtDebug>
// HelloWorld
class HelloWorldImpl : public QObject, public HelloService
{
Q_OBJECT
Q_INTERFACES(HelloService)
public:
void sayHello() Q_DECL_OVERRIDE {
qDebug() << "Hello,World!";
}
};
// HelloCTK
class HelloCTKImpl : public QObject, public HelloService
{
Q_OBJECT
Q_INTERFACES(HelloService)
public:
void sayHello() Q_DECL_OVERRIDE {
qDebug() << "Hello,CTK!";
}
};
#endif // HELLO_IMPL_H
Service Factory Class
#ifndef SERVICE_FACTORY_H
#define SERVICE_FACTORY_H
#include <ctkServiceFactory.h>
#include <ctkPluginConstants.h>
#include <ctkVersion.h>
#include "hello_impl.h"
class ServiceFactory : public QObject, public ctkServiceFactory
{
Q_OBJECT
Q_INTERFACES(ctkServiceFactory)
public:
ServiceFactory() : m_counter(0) {}
// Create service object
QObject* getService(QSharedPointer<ctkPlugin> plugin, ctkServiceRegistration registration) Q_DECL_OVERRIDE {
Q_UNUSED(registration)
qDebug() << "Create object of HelloService for: " << plugin->getSymbolicName();
m_counter++;
qDebug() << "Number of plugins using service: " << m_counter;
QHash<QString, QString> headers = plugin->getHeaders();
ctkVersion version = ctkVersion::parseVersion(headers.value(ctkPluginConstants::PLUGIN_VERSION));
QString name = headers.value(ctkPluginConstants::PLUGIN_NAME);
QObject* hello = getHello(version);
return hello;
}
// Release service object
void ungetService(QSharedPointer<ctkPlugin> plugin, ctkServiceRegistration registration, QObject* service) Q_DECL_OVERRIDE {
Q_UNUSED(plugin)
Q_UNUSED(registration)
Q_UNUSED(service)
qDebug() << "Release object of HelloService for: " << plugin->getSymbolicName();
m_counter--;
qDebug() << "Number of plugins using service: " << m_counter;
}
private:
// Get different services based on different versions
QObject* getHello(ctkVersion version) {
if (version.toString().contains("alpha")) {
return new HelloWorldImpl();
} else {
return new HelloCTKImpl();
}
}
private:
int m_counter; // Counter
};
#endif // SERVICE_FACTORY_H
Different services can be obtained based on the plugin. If the symbolicName of the main framework [main.cpp] is "system.plugin".
Activator Class
#ifndef HELLO_ACTIVATOR_H
#define HELLO_ACTIVATOR_H
#include <ctkPluginActivator.h>
#include <ctkPluginContext.h>
#include "hello_service.h"
#include "service_factory.h"
class HelloActivator : public QObject, public ctkPluginActivator
{
Q_OBJECT
Q_INTERFACES(ctkPluginActivator)
Q_PLUGIN_METADATA(IID "HELLO")
public:
// Register service factory
void start(ctkPluginContext* context) {
ServiceFactory *factory = new ServiceFactory();
context->registerService<HelloService>(factory);
}
void stop(ctkPluginContext* context) {
Q_UNUSED(context)
}
};
#endif // HELLO_ACTIVATOR_H
Accessing the Service in a Plugin
// Access service
ctkServiceReference reference = context->getServiceReference<HelloService>();
if (reference) {
HelloService* service = qobject_cast<HelloService *>(context->getService(reference));
if (service != Q_NULLPTR) {
service->sayHello();
}
}
See the project: ServiceFactory
CTK Event Listening
CTK has three types of events that can be listened to: framework events, plugin events, and service events. However, these events can only be listened to when they change. If you listen after they have already changed and entered a stable state, you cannot listen to them.
Framework Events
These are for the entire framework; there is essentially only one framework, so the framework event is about the framework itself. However, the issue is that listening for this event occurs after the framework is initialized, so you cannot listen to the initialization event, only the stop event. Types:
FRAMEWORK_STARTED
PLUGIN_ERROR
PLUGIN_WARNING
PLUGIN_INFO
FRAMEWORK_STOPPED
FRAMEWORK_STOPPED_UPDATE
FRAMEWORK_WAIT_TIMEDOUT
Service Events
Events that occur during service creation and recycling, mainly reflected in service registration and unregistration. Types:
REGISTERED
MODIFIED
MODIFIED_ENDMATCH
UNREGISTERING
Plugin Events
Events that occur during plugin installation, startup, etc., mainly reflecting changes in the plugin's state. Types:
INSTALLED
RESOLVED
LAZY_ACTIVATION
STARTING
STARTED
STOPPING
STOPPED
UPDATED
UNRESOLVED
UNINSTALLED
Listening Example
Listener class, event_listener.h
#ifndef EVENT_LISTENER_H
#define EVENT_LISTENER_H
#include <QObject>
#include <ctkPluginFrameworkEvent.h>
#include <ctkPluginEvent.h>
#include <ctkServiceEvent.h>
class EventListener : public QObject
{
Q_OBJECT
public:
explicit EventListener(QObject *parent = Q_NULLPTR);
~EventListener();
public slots:
// Listen for framework events
void onFrameworkEvent(const ctkPluginFrameworkEvent& event);
// Listen for plugin events
void onPluginEvent(const ctkPluginEvent& event);
// Listen for service events
void onServiceEvent(const ctkServiceEvent& event);
};
#endif // EVENT_LISTENER_H
event_listener.cpp
#include "event_listener.h"
EventListener::EventListener(QObject *parent)
: QObject(parent)
{
}
EventListener::~EventListener()
{
}
// Listen for framework events
void EventListener::onFrameworkEvent(const ctkPluginFrameworkEvent& event)
{
if (!event.isNull()) {
QSharedPointer<ctkPlugin> plugin = event.getPlugin();
qDebug() << "FrameworkEvent: [" << plugin->getSymbolicName() << "]" << event.getType() << event.getErrorString();
} else {
qDebug() << "The framework event is null";
}
}
// Listen for plugin events
void EventListener::onPluginEvent(const ctkPluginEvent& event)
{
if (!event.isNull()) {
QSharedPointer<ctkPlugin> plugin = event.getPlugin();
qDebug() << "PluginEvent: [" << plugin->getSymbolicName() << "]" << event.getType();
} else {
qDebug() << "The plugin event is null";
}
}
// Listen for service events
void EventListener::onServiceEvent(const ctkServiceEvent &event)
{
if (!event.isNull()) {
ctkServiceReference ref = event.getServiceReference();
QSharedPointer<ctkPlugin> plugin = ref.getPlugin();
qDebug() << "ServiceEvent: [" << event.getType() << "]" << plugin->getSymbolicName() << ref.getUsingPlugins();
} else {
qDebug() << "The service event is null";
}
}
Enable listening, main.cpp:
// Event listener
EventListener listener;
context->connectFrameworkListener(&listener, SLOT(onFrameworkEvent(ctkPluginFrameworkEvent)));
context->connectPluginListener(&listener, SLOT(onPluginEvent(ctkPluginEvent)));
// Filter ctkEventAdmin service
// QString filter = QString("(%1=%2)").arg(ctkPluginConstants::OBJECTCLASS).arg("org.commontk.eventadmin");
context->connectServiceListener(&listener, "onServiceEvent"); //, filter);
See the project: EventListener
CTK Service Tracker
Service tracking: If you want to use service A in plugin B, you can write a class that inherits ctkServiceTracker. In this class, perform the underlying operations for service A. Then, in plugin B, use the interface provided by this class to acquire and release service A.
Theoretically, ctkServiceTracker and service A should be together, somewhat similar to a service factory. The advantage is that the code for acquiring services is simple, without various null pointer checks.
Service A
Service A implementation class, log_impl.cpp
#include "log_impl.h"
#include <QtDebug>
LogImpl::LogImpl()
{
}
void LogImpl::debug(QString msg)
{
qDebug() << "This is a debug message: " << msg;
}
Service A activator class, log_activator.cpp
#include "log_impl.h"
#include "log_activator.h"
#include <ctkPluginContext.h>
#include <QtDebug>
void LogActivator::start(ctkPluginContext* context)
{
m_pPlugin = new LogImpl();
context->registerService<LogService>(m_pPlugin);
}
void LogActivator::stop(ctkPluginContext* context)
{
Q_UNUSED(context)
delete m_pPlugin;
m_pPlugin = Q_NULLPTR;
}
Service Tracker Class for Service A
Tracker class, creation timing:
- Can be created when encapsulating service A, as a utility tool provided externally, but should not be compiled into the plugin; it is not a feature of the plugin but a tool to access the plugin.
- Can also be created in plugin B, completely independent of service A, as a means to access service A.
- Create a separate empty project, create tracker classes for all services in the project, and place them in the same folder. Others can use them as needed.
Note: If plugin B wants to use service A, it needs service_tracker.h, service_tracker.cpp, and the interface class of service A.
This example uses the second approach.
service_tracker.h
#ifndef SERVICE_TRACKER_H
#define SERVICE_TRACKER_H
#include <ctkPluginContext.h>
#include <ctkServiceTracker.h>
#include "../Log/log_service.h"
class ServiceTracker : public ctkServiceTracker<LogService *>
{
public:
ServiceTracker(ctkPluginContext* context) : ctkServiceTracker<LogService *>(context) {}
~ServiceTracker() {}
protected:
// Called when service is registered
LogService* addingService(const ctkServiceReference& reference) Q_DECL_OVERRIDE {
qDebug() << "Adding service:" << reference.getPlugin()->getSymbolicName();
// return ctkServiceTracker::addingService(reference);
LogService* service = (LogService*)(ctkServiceTracker::addingService(reference));
if (service != Q_NULLPTR) {
service->debug("Ok");
}
return service;
}
void modifiedService(const ctkServiceReference& reference, LogService* service) Q_DECL_OVERRIDE {
qDebug() << "Modified service:" << reference.getPlugin()->getSymbolicName();
ctkServiceTracker::modifiedService(reference, service);
}
void removedService(const ctkServiceReference& reference, LogService* service) Q_DECL_OVERRIDE {
qDebug() << "Removed service:" << reference.getPlugin()->getSymbolicName();
ctkServiceTracker::removedService(reference, service);
}
};
#endif // SERVICE_TRACKER_H
Plugin B
Plugin B implementation class, login_impl.cpp
#include "login_impl.h"
#include "service_tracker.h"
LoginImpl::LoginImpl(ServiceTracker *tracker)
: m_pTracker(tracker)
{
}
bool LoginImpl::login(const QString& username, const QString& password)
{
LogService* service = (LogService*)(m_pTracker->getService());
if (QString::compare(username, "root") == 0 && QString::compare(password, "123456") == 0) {
if (service != Q_NULLPTR)
service->debug("Login successfully");
return true;
} else {
if (service != Q_NULLPTR)
service->debug("Login failed");
return false;
}
}
Plugin B activator class, login_activator.cpp
#include "login_impl.h"
#include "login_activator.h"
#include "service_tracker.h"
#include <ctkPluginContext.h>
void LoginActivator::start(ctkPluginContext* context)
{
// Start service tracker
m_pTracker = new ServiceTracker(context);
m_pTracker->open();
m_pPlugin = new LoginImpl(m_pTracker);
m_registration = context->registerService<LoginService>(m_pPlugin);
}
void LoginActivator::stop(ctkPluginContext* context)
{
Q_UNUSED(context)
// Unregister service
m_registration.unregister();
// Close service tracker
m_pTracker->close();
delete m_pPlugin;
m_pPlugin = Q_NULLPTR;
}
Using Plugin B
// Get plugin location
QString path = QCoreApplication::applicationDirPath() + "/plugins";
// Traverse all plugins in the path
QDirIterator itPlugin(path, QStringList() << "*.dll" << "*.so", QDir::Files);
while (itPlugin.hasNext()) {
QString strPlugin = itPlugin.next();
try {
// Install plugin
QSharedPointer<ctkPlugin> plugin = context->installPlugin(QUrl::fromLocalFile(strPlugin));
// Start plugin
plugin->start(ctkPlugin::START_TRANSIENT);
qDebug() << "Plugin start ...";
} catch (const ctkPluginException &e) {
qDebug() << "Failed to install plugin" << e.what();
return -1;
}
}
// Get service reference
ctkServiceReference reference = context->getServiceReference<LoginService>();
if (reference) {
// Get the service object referenced by the specified ctkServiceReference
LoginService* service = qobject_cast<LoginService *>(context->getService(reference));
if (service != Q_NULLPTR) {
// Call the service
service->login("root", "123456");
}
}
See the project: ServiceTracker