Workspace 7.0.2
|
In this section, we're going to understand the code that was automatically generated by Workspace so that we know how to customise plugins to our needs. Click here to continue on with the tutorial Writing a Simple Workspace Plugin Building your plugin with CMake (continue on with tutorial Writing a Simple Workspace Plugin )
The start of every header file contains a header guard followed by any other header files this one depends on:
The name following #ifndef
and #define
must be unique among all header files in Workspace or your own project. A good guideline is to choose a name based on the namespaces and class name, all converted to uppercase and with _H
appended to the end.
The minimal set of header files for a Workspace plugin is just the file Workspace/Application/workspaceplugin.h which contains the definition of the WorkspacePlugin base class. The simpleplugin_api.h
file defines the SIMPLEPLUGIN_API
symbol which we will use shortly, but don't worry about it for the moment.
Next we will see namespace and class definitions. First item in our namespace is the forward declaration of our implementation class:
Next we can see our class definition. The class itself is derived from WorkspacePlugin and provides:
These methods will be discussed in more detail when we look in the implementation file next.
Don't forget to end with the #endif
preprocessing symbol. This is the end of our header guard block.
Building your plugin with CMake (continue on with tutorial Writing a Simple Workspace Plugin )
simpleplugin.cpp contains the definitions of our class's members. Firstly it includes some required headers needed, which normally amounts to the headers for your custom data types and Workspace operations, plus the header declaring your plugin class.
Don't worry about the STRINGIFY
macros for now, we will discuss them shortly.
SimplePluginImpl is a private implementation class that will actually implement the functionality of our plug-in.
Next we see the constructor for our SimplePlugin class:
Here we can see our "Project Internal Name" that we defined in the wizard. We are setting our plugin up so that Workspace can uniquely identify our plugin from the others that it has already loaded.
The constructor also assigns a display name to the plugin. This is what the user sees in things like the user interface of GUI applications. It should be relatively short but also human readable and give some indication of what the plugin is about. The display name may contain spaces.
The constructor specifies the plugin version as well. The example code uses the two #define's related to STRINGIFY
to embed the version number from the project file. We will skip over this for now and return to it towards the end of the tutorial where the project file is discussed in detail. You could just embed the version number here as an ordinary string, but then you would have to manually keep the version number in sync with the project file. Finally we see how our unique pointer to the implementation class is initialised. We will skip over the simple class destructor.
The getInstance()
function should be the only way to obtain a reference to a SimplePlugin
object, since all Workspace plugins should be singletons (ie no more than one SimplePlugin
object should ever exist at any time). The above example code uses the simplest form for ensuring this singleton behavior.
The setup()
function is where you list all your custom data types, operations, widgets and tell Workspace about their existence. If you follow the recommended way to define data factories, operation factories and widget factories in these tutorial, then all you need to do is use another addFactory(...)
line for each data type or operation or widget and replace MyClass
and MyOperation
with the class/operation/widget you want to add.
Next we see two Workspace-specific methods that assist with code maintenance by allowing developers to rename operations or datatypes. Naturally it is preferable not to rename such items but given the realities of working with large evolving codebases it is better practice to provide a mechanism than leave it up to individual developers to fend for themselves.
If an operation is renamed, getAliasedOperationFactory
provides a backwards compatibility mechanism so that the plugin can provide the new factory that corresponds to an old name. See the getAliasedOperationFactory documentation for more details.
If a data type is renamed, getAliasedDataFactory
provides a backwards compatibility mechanism so that the plugin can provide the new factory that corresponds to an old name. See the getAliasedDataFactory documentation for more details.
This method provides the path to any custom widget defined in our plug-in. Essentially this value is add to the list of paths used when searching for custom .ui files when creating widgets for our operations, inputs and outputs. The path supplied here is a resource path.
The last thing to be done is to provide the function Workspace looks for in your plugin's final binary to identify it as a Workspace plugin (we have to close off the namespace scopes first too):
The extern "C"
block ensures that the functions can be called as an ordinary C function. This is a portability requirement. Inside the extern block there are two functions.
getWorkspacePlugin
function simply returns the plugin's singleton instance.builtAgainstWorkspace
identifies which version of Workspace the plugin was built against, if you try to load a plug-in built against an incompatible version of Workspace then this will be detected and the plugin will not be loaded. #ifndef
surrounding the extern "C"
block is to handle the situation where you might want to build the plugin as a static library and link it into a standalone executable rather than as a shared library. In this case, the exported functions are not required and would cause name clashes with other Workspace plugins if not handled in this way.Building your plugin with CMake ((continue on with tutorial Writing a Simple Workspace Plugin ))
Workspace implements plugins as shared libraries on unix-like platforms, or their equivalent (DLL's) under Windows. When creating shared libraries, it is necessary to tell the compiler what things you want to be visible from outside your plugin or else your code won't be usable by Workspace or any other calling applications. Unfortunately, different platforms and compilers use different syntax for how they specify what should be externally visible.
To assist with this, Workspace provides a header to help with defining the required preprocessor symbols so that you can use one specification across your whole plugin for all compilers and platforms. The simpleplugin_api.h
example file shows how to make use of it:
As you can see, the file simply defines SIMPLEPLUGIN_API to be an import or export specification depending on whether or not CSIRO_EXPORT is defined. The Workspace/api_workspace.h header provides the platform-specific definitions for CSIRO_EXPORTSPEC and CSIRO_IMPORTSPEC, so you don't need to worry about them. In fact, you don't really need to understand how any of this works except for two key points:
Export symbols may seem unnecessarily complex, but rest assured that when writing your own plugins, you will quickly realize that it just boils down to a bit of cut and paste that mostly only needs to be done when first setting up your new plugin.
Building your plugin with CMake ((continue on with tutorial Writing a Simple Workspace Plugin ))
You now have all the source files you need to create your plugin. All you need now is a way to build it. By far, the easiest way to do this is to use the same build system that Workspace uses.
With CMake, the project file is always called CMakeLists.txt
and it is case-sensitive on all platforms except Windows. It is strongly recommended that you use this case-specific form as a habit on all platforms (if you're using the code wizard, this will be done for you). You would normally have each Workspace plugin/project in its own directory, as this allows you to keep your CMakeLists.txt
project file relatively simple. The example used here will assume that your CMakeLists.txt
project file and all your sources and headers (*.cpp
and *.h
) are in the same directory.
As the file extension suggests, the CMakeLists.txt
is just an ordinary human-readable text file. The example used in this tutorial can be thought of as having three sections: preamble, source file definitions and build target details.
The start of the CMakeLists.txt
project file contains the preamble section:
The first part of the header section gives a name to the project. The project
line defines a name for your project which must be unique among all other cmake projects that you might be building. Since we are only building a single plugin in this tutorial, the name can essentially be whatever you like, but a good convention to adopt is to make your project name the same as the name of the plugin except in capitals (the reason for the capitals will become clearer further below). The first if/endif
block should appear exactly as shown and can simply be cut and pasted directly. It's purpose is to allow the build system to find the files provided by Workspace, whether your project is using an installed version of Workspace or whether it is incorporated within another project (this latter case is how plugins that come with Workspace are built). We will postpone discussion of this section until the end where running cmake on the project is explained.
The line set
is responsible for setting the version number of the plugin. Notice how the first argument to the set()
command is the same variable name that we saw inside the TOSTRING()
macro used in the simpleplugin.cpp
file. This is how we set the project version in the project file and have it picked up automatically within the C++ source files. You are strongly encouraged to define your project version in this way rather than having it set in more than one location or else you risk the multiple version numbers getting out of sync. The next line of the header section relating to string(REGEX_MATCH ...)
extracts the first part of the version number and stores it in the variable called SIMPLE_PLUGIN_SOVERSION
. It will be used shortly, but you don't really need to know much about it. You can simply cut and paste this particular line and put it just after you set the project version number.
major.minor.patch
form. There is a fairly well established convention on the meaning of these three numbers and if possible, you are encouraged to follow that convention. Roughly speaking, the convention goes like this: major.minor
version, but at the very least, if someone builds code against a particular patch number then that code should not need to be recompiled for that or any later patch number within a given major.minor
combination. major.minor
version should be able to be used against any later version up until the major
number changes. For a discussion with examples of this convention, the interested reader may wish to look at the Apache APR library versioning information which provides a good treatment of this subject. The last part of the header sets up Qt-related information. The find_package
and include
lines will make sure that Qt's headers, libraries and build tools can be found. For most Workspace plugins, you will likely only need the QtCore and QtGui modules, but if you use others, all you need to do is add them to the find_package
line.
The second section of the CMakeLists.txt
project file specifies the sources and headers for your plugin. In this example, the files are straightforward to specify:
The set(...)
blocks list the C++ headers, install headers, MOC headers and source files respectively.
_HEADERS
or _SOURCES
appended. This will avoid potential conflicts with other projects should you later want to build more than one Workspace plugin. The third and final section of the CMakeLists.txt
project file controls how the plugin is built:
The add_definitions
line makes the plugin version available to C++ files as a defined symbol called SIMPLE_PLUGIN_VERSION
. This is used in the simpleplugin.cpp
file to set the plugin version in the SimplePlugin
constructor. For your projects, change the name of the variable (it appears twice on this line) to match the one you used just after the project
line. If you needed to add other compiler defines, you can simply repeat this line as often as necessary. If you just want to define a symbol without giving it a value, omit the equals sign and what comes after it.
The add_library
line is where we tell CMake to build the plugin library. The first argument to add_library is the name of the target for our plugin. A useful convention is to use your project name in lowercase followed by the word "plugin". The rest of the arguments specify the source files to include in the library. In our example, we use the SOURCES
variable to hold the list of source files.
The target_link_libraries
line specifies the other libraries that the plugin needs to link in. The first argument is the target name given to add_library and the rest of the arguments are library names. These library names can be either the name of another CMake target or they can be the library name that compilers would normally understand. For example, let's say that there is a library called foobar
. Under Windows, its name would be foobar.dll
and under linux it would be libfoobar.so
. The library name to use in the target_link_libraries
command would simply be foobar
- under linux, lib
is prepended automatically. At the end of the target_link_libraries
line you can see that we have also included the contents of a variable called QT_LIBRARIES
. This variable was set for us by the find_package
and include(${QT_USE_FILE})
lines earlier in the project file.
The set_target_properties
command is doing a number of things, so we will describe each line within it individually. The first line contains the target for the plugin as the first argument. This will be the same as what was used for add_library
and target_link_libraries
. The PROPERTIES
entry is a CMake keyword indicating that the following arguments are properties on the target specified.
The DEFINE_SYMBOL
line tells CMake to define the preprocessor symbol CSIRO_EXPORT
only when compiling files for this plugin. If you were building multiple plugins within the one CMakeLists.txt
project file, this would ensure that the symbol was only defined for this target and not the others. The symbol name matches the one used in simpleplugin_api.h
, and in fact that is the only file that uses the symbol directly. You may wonder why we go to all this trouble instead of just defining it with an add_definitions
or even just hard-coding it. The answer lies in when another plugin wants to include a header from this target. It will be built without the CSIRO_EXPORT
symbol defined, which will result in symbols being imported instead of exported. This is precisely what we want, because the simple plugin will define everything and the other plugin will simply create external references and import them.
The VERSION
and SOVERSION
lines have different effects on different platforms. A Workspace plugin is just a shared library, but libraries usually have version information stored in them and/or encoded as part of their file name. Under Windows, the VERSION
number may be embedded in the plugin itself and can be inspected by right-clicking on the library and viewing its properties. Under linux, the VERSION
is appended to the file name and symbolic links are set up for related names as is common on linux platforms. In this example, after building the Workspace plugin, the following three entries will exist in the file system:
libsimpleplugin.so libsimpleplugin.so.0 libsimpleplugin.so.0.1.0
The first two of these will simply be symbolic links to the last entry. Under linux, the SOVERSION
is also encoded into the library itself so that compilers know what form of the library name to link to. Following standard linux conventions, the SOVERSION
is just the major
part of the version number in this example (recall that this was set near the top of the project file). Any executable that links against this plugin will link to the name that matches the SOVERSION
, which in this case would be libsimpleplugin.so.0
and this would be the only file that you would need to include in a software package to be installed on other systems.
The last line in this block is the setTargetOutputDirectory
command. This is a custom command provided by the framework to help set the desired output directory for the built target while also working around an issue with CMake and Visual Studio. The parameters to the command are the target name and the target output directory.
The setTargetOutputDirectory
command sets three target properties XXX_OUTPUT_DIRECTORY
which control where the library and any associated files will be put. Unfortunately, their precise meanings vary depending on platform. Under non-Windows platforms, it is fairly straightforward. The RUNTIME_OUTPUT_DIRECTORY
controls where executables will be put, LIBRARY_OUTPUT_DIRECTORY
controls where shared libraries are put and ARCHIVE_OUTPUT_DIRECTORY
controls where static libraries are put. Under Windows, things are a bit more murky. The RUNTIME_OUTPUT_DIRECTORY
is where executables and DLL files will be built. The LIBRARY_OUTPUT_DIRECTORY
specifies where export libraries (*.exp
), import libraries (*.lib
) and other things like PDB files go for shared libraries. The ARCHIVE_OUTPUT_DIRECTORY
controls where static libraries are put, just like for other platforms, but note that these also have a *.lib
file extension. This confusion of the RUNTIME
and LIBRARY
locations is an artefact of how Windows treats DLL files almost like executables (eg they share the PATH environment variable when the operating system is searching for them).
Happily, for Workspace plugins, you can simply set all three XXX_OUTPUT_DIRECTORY
entries to the same location which is what setTargetOutputDirectory
does. This will ensure that all your plugin library files end up in a predictable place. The location shown in this tutorial is the canonical place where Workspace plugins are put for the plugins that come with Workspace itself. You are encouraged to keep the same structure for simplicity, but you are free to choose something else if you prefer.
When building Visual Studio project files with CMake, the targets that are built (ie the libraries, executables, etc.) are placed in a configuration-specific subdirectory below the locations specified with the XXX_OUTPUT_DIRECTORY
properties. These subdirectories would have names such as Debug
, Release
and so on. However, this can interfere with how Workspace tries to load plugins at runtime and can be an annoying nuisance during development.
The setTargetOutputDirectory
command addresses this issue by also setting configuration specific versions of XXX_OUTPUT_DIRECTORY
to the target output directory. These properties are used by the CMake Visual Studio generator and force the output to the intended directory.
Building your plugin with CMake (continue on with tutorial Writing a Simple Workspace Plugin )