To help you get started, you might want to have a look at the following sample plugins. They illustrate the basic concepts needed to develop plugins for OpenCOR:
Note: these plugins only get built if the ENABLE_SAMPLE_PLUGINS
option is set to ON
in CMake.
All plugins come under one of the following categories:
Data Store: plugins to store and manipulate data.
Editing: plugins to edit files.
Miscellaneous: plugins that do not fit in any other category.
Organisation: plugins to organise files.
Sample: plugins that illustrate various plugin-related aspects.
Simulation: plugins to simulate files.
Solver: plugins to access various solvers.
Support: plugins to support various third-party libraries.
Test: plugins to test things.
Third-party: plugins to access various third-party libraries.
Tools: plugins to access various tools.
Widget: plugins to access various ad hoc widgets.
A category is used by OpenCOR to group plugins together to improve user experience.
From a developer’s perspective, a category determines where a plugin’s code should be located.
Thus, the different folders under [OpenCOR]/src/plugins/
are for our different categories.
For example, [OpenCOR]/src/plugins/miscellaneous/
contains the code of our different Miscellaneous plugins and [OpenCOR]/src/plugins/miscellaneous/Core/
the code of our Core plugin in particular.
Note: the Sample and Test categories are special categories that are only available when building OpenCOR with the ENABLE_SAMPLE_PLUGINS
and ENABLE_TEST_PLUGINS
options set to ON
, respectively.
They should only be used for plugins that are aimed at helping people who want to learn how to write plugins for OpenCOR and at testing things, respectively.
Plugins can implement different interfaces.
Those interfaces allow a plugin, through the implementation of various methods, to interact with OpenCOR.
Current interfaces can be found under [OpenCOR]/src/plugins/
.
They are:
CLI: to support command line execution.
Core: to control some of OpenCOR’s core aspects.
Data Store: to let OpenCOR know about the name of a data store, as well as retrieve some data from it and provide an instance of its exporter.
File Handling: to save a file, as well as to be told when a file has been opened, modified, closed, etc.
File Type: to let OpenCOR know about supported file types and their description.
GUI: to let OpenCOR know about the menus and menu actions that we want to see added to the GUI, as well as to be told when the GUI needs updating.
Internationalisation: to be told when we should retranslate ourselves.
Plugin: to initialise/finalise a plugin, load/save its settings, etc.
Preferences: to specify a plugin’s default behaviour, settings, etc.
Python: to register Python classes with OpenCOR.
Solver: to let OpenCOR know about the type, name, and properties of a solver, as well as to provide OpenCOR with an instance of that solver.
View: to let OpenCOR know about the name of a view, its mode, the MIME types it supports, whether we have a view for the current file, etc.
Window: to let OpenCOR know about the widget, action, and default location of a window.
Some plugins do not implement any interface (e.g., the LLVM+Clang plugin) while others may implement one or several interfaces (e.g., the Core plugin implements the Core, File Handling, GUI, Internationalisation, and Plugin interfaces).
Note: the Core interface is only, and can only be, implemented by the Core plugin. Any other plugin that tries to implement the Core interface will be reported by OpenCOR as being invalid.
OpenCOR is built and packaged using CMake.
When it comes to plugins, this requires creating a CMakeLists.txt
file in the plugin’s root folder and calling the add_plugin()
macro, which is defined in [OpenCOR]/cmake/common.cmake
.
That macro uses information passed to it to build and package the plugin.
That information comes in the form of a series of parameters, some of which are keywords:
SOURCES
: implementation files.
UIS
: user interface files.
PLUGINS
: plugins needed by the plugin.
QT_MODULES
: Qt modules needed by the plugin.
EXTERNAL_BINARIES_DIR
: location of external binaries needed by the plugin.
EXTERNAL_BINARIES
: external binaries needed by the plugin.
EXTERNAL_DESTINATION_DIR
: location where external dependencies are to be copied.
EXTERNAL_SOURCE_DIR
: location of external dependencies.
SYSTEM_BINARIES
: system binaries needed by the plugin.
NO_STRIP
: whether we should not strip the plugin.
DEPENDS_ON
: CMake targets on which the plugin depends.
BYPRODUCTS
: byproducts for the plugin.
TESTS
: tests for the plugin.
Following those keywords are the parameters themselves, as can be seen in [OpenCOR]/src/plugins/miscellaneous/Core/CMakeLists.txt
for the Core plugin.
For a plugin to be recognisable by OpenCOR, it must provide some basic information about itself, as well as define a plugin class.
For this, we need a .cpp
, a .h
, and a .json
file, such as [OpenCOR]/src/plugins/miscellaneous/Core/src/coreplugin.cpp
, [OpenCOR]/src/plugins/miscellaneous/Core/src/coreplugin.h
, and [OpenCOR]/src/plugins/miscellaneous/Core/src/coreplugin.json
for the Core plugin.
.json
file¶The .json
file is a simple JSON file, which sole purpose is to reference the name of the plugin class.
In the case of the Core plugin, the contents of that file is:
{
"Keys": [ "CorePlugin" ]
}
The code for the basic information and plugin class must be in the plugin’s own namespace within the OpenCOR
namespace.
More generally, any plugin-related code should be within those two namespaces, this to ensure the integrity of the plugin’s code.
Thus, in the case of the Core plugin, we have:
...
namespace OpenCOR {
namespace Core {
...
} // namespace Core
} // namespace OpenCOR
...
Plugins must provide the following basic information:
Category: category under which the plugin is to be listed.
Selectable: whether the plugin can be selected by the user (for loading upon starting OpenCOR).
CLI support: whether the plugin works from the command line.
Dependencies: plugins on which the plugin depends directly.
Descriptions: description of the plugin in various languages.
Load before: plugins before which the plugin should be loaded.
This information is made available to OpenCOR through a function, which in the case of the Core plugin has the following declaration:
PLUGININFO_FUNC CorePluginInfo();
Note: to ensure the uniqueness of a plugin, OpenCOR uses the name of a plugin to determine the name of its function.
In other words, the name of the function is expected to be <PluginName>PluginInfo()
.
If it is not, OpenCOR will not recognise the plugin.
In the case of the Core plugin, the body of that function is:
PLUGININFO_FUNC CorePluginInfo()
{
static const Descriptions descriptions = {
{ "en", QString::fromUtf8("the core plugin.") },
{ "fr", QString::fromUtf8("l'extension de base.") }
};
return new PluginInfo(PluginInfo::Category::Miscellaneous, false, false,
{},
descriptions);
}
Note: support for the internationalisation of a plugin’s description would normally be done using Qt’s tr()
function, but the C nature of the function means that it cannot be done.
So, instead, we use a QMap
-based approach.
We rely on Qt’s support for plugins, which means that plugins must define a specific class.
The class must inherit from QObject
, as well as from any interface the plugin implements.
For example, the Core plugin implements the Core, File Handling, GUI, Internationalisation, and Plugin interfaces, so its class definition is:
...
class CorePlugin : public QObject, public CoreInterface,
public FileHandlingInterface, public GuiInterface,
public I18nInterface, public PluginInterface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "OpenCOR.CorePlugin" FILE "coreplugin.json")
Q_INTERFACES(OpenCOR::CoreInterface)
Q_INTERFACES(OpenCOR::FileHandlingInterface)
Q_INTERFACES(OpenCOR::GuiInterface)
Q_INTERFACES(OpenCOR::I18nInterface)
Q_INTERFACES(OpenCOR::PluginInterface)
public:
#include "coreinterface.inl"
#include "filehandlinginterface.inl"
#include "guiinterface.inl"
#include "i18ninterface.inl"
#include "plugininterface.inl"
...
};
...
On the other hand, our LLVM+Clang plugin does not need to implement any interface since its sole purpose is to provide other plugins with access to LLVM and Clang. Hence, its much simpler class definition:
...
class LLVMClangPlugin : public QObject
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "OpenCOR.LLVMClangPlugin" FILE "llvmclangplugin.json")
};
...
There may be cases where a plugin declares a function or defines a class that we want to be able to use from another plugin. On Linux and macOS, nothing needs to be done about it, but on Windows, the function or class needs to be exported by the original plugin:
void __declspec(dllexport) myFunction();
class __declspec(dllexport) myClass;
and imported by the plugin that wants to use it:
void __declspec(dllimport) myFunction();
class __declspec(dllimport) myClass;
Each plugin that exports functions and/or classes must therefore define a macro that refers either to __declspec(dllexport)
or to __declspec(dllimport)
, depending on how the plugin’s code is to be compiled.
In the case of the Compiler plugin, we have:
...
#ifdef Compiler_PLUGIN
#define COMPILER_EXPORT Q_DECL_EXPORT
#else
#define COMPILER_EXPORT Q_DECL_IMPORT
#endif
...
Compiler_PLUGIN
(or, more generally, <PluginName>_PLUGIN
) is automatically defined, if at all, at build time, and is used to determine the value of COMPILER_EXPORT
(or, more generally, the value of <PLUGINNAME>_EXPORT
), which can then be used as follows without having to worry about whether the function or class should be imported or exported:
void COMPILER_EXPORT myFunction();
class COMPILER_EXPORT myClass;