C++ Library Packages
- Creating and Adding Packages to the Workspace
- Integrating 3rd-party library
- Picking the C++ Standard
- Package Dependencies
- Working around Unconventional 3rd Party Packages
- Conventions for library design
- Optional Features
- Tests
- Debugging in VSCode
- The Rock CMake macros
- Using Rock's text logger
- Rock.cmake Reference Documentation
While Rock is designed to allow you for the separation between functionality and framework, if you feel so inclined, Rock provides a C++ library template. This template solves some of the common problems with setting up a C++ library (build system, …) and obviously integrate as-is with the rest of a Rock system.
However there is nothing that forces you to use the Rock library template. Autoproj generically integrates with autotools and cmake packages. One can also set up a custom package for more exotic systems (such as some old packages that use "plain make")
The only constraint when the aim is to create a library that will be integrated in a Rock component is to provide a pkg-config file for it. This is how orogen resolves its dependencies.
Creating and Adding Packages to the Workspace
This is covered in details in the Workspace and Packages section
Executive Summary: You need to first pick a category and a name the workspace conventions for information about how to name your library package.
Rock then provides a library template. One can create a new library with
rock-create-lib library_dir
e.g.
cd drivers
rock-create-lib imu_advanced_navigation_anpp
This library creates a dummy class and a dummy executable that uses this class. It is great at providing you with the example CMake code for both library and tests.
When using VSCode, the package will not be picked up by the vscode-rock
extension
until it is part of the main manifest.
Adding the package path to the layout section of autoproj/manifest
is
enough at this stage. You will however have to define the
package in the autobuild and source.yml
files before you push the updated manifest to your team members. For instance,
when creating drivers/imu-myahrs
one would do
rock-create-lib drivers/imu-myahrs
and then add
layout:
- ...
- drivers/imu-myahrs
Integrating 3rd-party library
Rock packages - even those using the Rock CMake macros - are not dependent on autoproj. Autoproj is an external helper tool, but in no way does it interact with the package's build process. It is therefore perfectly feasible to build and use 3rd party libraries in a Rock system.
When doing so, try to follow these guidelines:
- do not change the package unless strictly necessary. The one exception for which there is currently no good solution is to provide a pkg-config file for orogen integration. If that is needed, you will probably want to integrate this change as a patch into your build configuration. Also, try to get this patch in the package's mainline, it will make things easier down the line.
- provide a package manifest in the package set.
Picking the C++ Standard
The Rock CMake macros use CMake's own mechanism to pick the C++ standard that should be used to build the code. The template still picks the system's local default. If you want to choose one explicitely, use (here for C++11)
set(CMAKE_CXX_STANDARD 11)
This lets CMake use at least C++11 - some other dependencies might want a later standard. If you want to have exactly C++11, add the following line
set(CMAKE_CXX_STANDARD_REQUIRED 11)
The choice of the C++ standard is automatically propagated to oroGen projects when developing components based on a C++ library.
Package Dependencies
It is common for packages to depend on other packages. All Rock-generated C++
packages for instance depend on base/cmake
, which defines the
Rock CMake helper macros. Most of them also depend on
base/types
, which define the common Rock datatypes.
Every time you want to add a new dependency to a C++ package, you need to do two things:
- update (the package's
manifest.xml
)[../workspace/add_packages.html#manifest_xml] to declare the new dependency. - update
src/CMakeLists.txt
to add the dependency to the package's targets.
In the latter case, there are more than one modality for dependency resolution. This is the beautiful world of C++ build systems after all.
If the other package defines a pkg-config
file, add the name of its .pc
file to a DEPS_PKGCONFIG
section, for instance
rock_library(mylib
DEPS_PKGCONFIG myahrs)
All Rock-generated packages define a pkg-config file using the package's basename
(i.e. drivers/imu-myahrs
will have a imu-myahrs
pkg-config file)
If the other package installs CMake dependency file, use the DEPS_CMAKE section
rock_library(mylib
DEPS_CMAKE cmakepkgname)
The name in DEPS_CMAKE
is also dependent on the naming scheme of the other package.
Look for *-config.cmake
or Find*.cmake
in the package's prefix. The *
is the
name you need to put in the rock_library
, e.g. FindGDAL.cmake
will be used by
DEPS_CMAKE GDAL
.
Finally, if you create more than one target (library/executable) within your package, you can define inter-dependencies
When using DEPS_CMAKE
and DEPS_PKGCONFIG
, you ensure that the dependencies
are saved in the generated pkg-config file (and therefore made available to
packages that would also depend on your package).
Working around Unconventional 3rd Party Packages
CMake has become de-facto standard in the C++ community as a build system, and pkg-config is common (even if not universal) in the open source community. However, there are still regular cases where there is support for neither means to resolve a dependency.
Our recommendation in that case is to use autoproj to generate a pkg-config file for said package on install. Writing a pkg-config file for a given use case is relatively straightforward.
- The hard part is to identity whether the library needs specific compiler and linker flags. If it does, you also need to identify whether these flags will always be present in your workspace's configuration.
If they do (a common occurence), go to step 2.
Otherwise, the best way is to actually patch the package to make it generate a pkg-config file for itself. How exactly you should do that is really beyond the point of this documentation. It will heavily depend on the build system used by the package.
- Create a ERB template for the package's pkg-config file, and save it in the
package set. The file should be named
PACKAGE_BASENAME.pc.erb
, where PACKAGE_BASENAME will be the name used by other package to refer to the dependency. For instance, libdatachannel's would be calledlibdatachannel.pc.erb
. A good starting point for this file is:
``` includedir=<%= pkg.prefix %>/include libdir=<%= pkg.prefix %>/lib
Name:
- Install the pkg-config file on package install. Modify the autobuild code that defines the package to add something along the lines of:
cmake_package "tools/libdatachannel" do |pkg|
pkg.post_install do
template = ERB.new(File.read(File.join(__dir__, "libdatachannel.pc.erb")))
pkgconfig = template.result(binding)
pkgconfig_dir = File.join(pkg.prefix, "lib", "pkgconfig")
FileUtils.mkdir_p pkgconfig_dir
File.write(File.join(pkgconfig_dir, "libdatachannel.pc"), pkgconfig)
end
end
- Run
amake --force PACKAGE
to install the new file - Verify it is OK by re-sourcing the
env.sh
environment and runningpkg-config --cflags PACKAGE
andpkg-config --libs PACKAGE
Conventions for library design
There's a small number of conventions that Rock libraries follow:
- Extensions header files are
.hpp
, source.cpp
- Naming classes should be
CamelCase
. The library must be defined under a namespace that matches the package basename (e.g.imu_advanced_navigation_anpp
fordrivers/imu_advanced_navigation_anpp
). Each class has its own file, with named like the class (i.e. theDriver
class is insrc/Driver.hpp
andsrc/Driver.cpp
) - File Structure source and header files tests excluded are saved in
src/
. Tests are intest/
. When applicable, split unit tests following the same structure than insrc/
, e.g. createtest/test_Spline.cpp
to test theSpline
class implemented insrc/Spline.(hpp|cpp)
If the ultimate goal of a data type is to be used as an interface type on a Rock component, you must take this into account when designing the type. Read first the documentation on type definitions for components.
Optional Features
First rule of optional features is to keep optional features to a minimum. Unless you are creating something of the size of OpenCV, they are not worth the complexity.
The second rule of optional features is to avoid enabling or disabling them based on auto-detection mechanism. These mechanisms are fragile, and will end up being disabled for unknown reasons on a machine where they were expected to be enabled, and vice-versa. Simply control their availability through a CMake option and fail if the feature is enabled but its dependencies are not present.
See also this page for a more general discusssion on the subject.
Tests
Testing is now an integral part of modern development process, and Rock provides support to integrate unit testing in the development workflow.
The default library template creates a test suite that uses boost test. There is also the possibility to use GTest and GMock, the Google-maintained C++ testing libraries.
To use GTest,
-
modify the package's
manifest.xml
to add<test_depend package="google-test" />
- replace
rock_testsuite
intest/CMakeLists.txt
byrock_gtest
, -
and replace the content of
test/suite.cpp
with the following:// Do NOT add anything to this file #include <gtest/gtest.h> int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
All C++ libraries that have a test/
folder will be assumed to have a test suite.
However, testing is disabled by default - since building all the tests from all
the workspace's packages would be fairly expensive. One needs to enable a package
tests to build them:
acd package/name
autoproj test enable .
aup
amake
The aup
step is needed if the package has test-specific dependencies, as
defined by the test_depend
tag of its manifest
file.
Once the tests are built, run them manually if you want to see their results.
Autoproj can also run them with autoproj test [package]
, but will redirect
the test's output to a log file (that can be visualized later with
alog).
Debugging in VSCode
When within a C++ package, the rock - Add launch config
command will help
you with selecting the binary you want to run. It discovers binaries within
the package's build directory and proposes them to add to the new
configuration. The equivalent functionality is available through the "Add Entry"
button when editing an existing launch.json
file.
NOTE due to a bug in VSCode, newly created launch.json
files are
sometimes not taken into account. If this is the case, you have to reload
VSCode with the Reload Window
command.
Click on the image below for a demo video:
The generated C++ launch configurations are standard cppdbg launch configurations. To ease integration within an autoproj workspace, the extension provides the possibility to use expansions to query information about the autoproj environment. These expansions can be used in any field within the debug configuration.
${rock:srcDir}
expands to the package's source directory${rock:buildDir}
expands to the package's build directory. You usually will need this to resolve programs or files that are not installed by the package, such as test programs.${rock:prefixDir}
expands to the package's prefix (install) directory. You will need this to resolve path to files installed by the package.${rock:which:cmd}
expands to the full path to the commandcmd
within the autoproj workspace's PATH. Use this to resolve programs that are installed by the packages.
The Rock CMake macros
To ease the use of CMake within a Rock system - i.e. in packages that follow
Rock conventions, Rock provides CMake macros that are somewhat easier to use.
The following describes them. The macros can be found in
base/cmake/modules/Rock.cmake
in a rock installation.
Note the Rock CMake macros are only dependent on files present in the
CMake distribution, and with pkg-config. These are all standard tools, and as such
using the Rock CMake macros in a package adds only the (very minimal)
dependency on the base/cmake
package itself. It will need nothing else
within Rock, and definitely nothing from Autoproj itself.
Using Rock's text logger
Rock provides a logger that facilitates and unifies debugging of system libraries. The following section describes the functionality of the base logger and how to use it.
When using the Rock CMake Macros, integration of the base logger is fairly easy. All you need to do is adding base-logging to your list of package config dependencies and include the header _base/logging/Logging.hpp where logging is needed.
rock_library(test_library
DEPS_PKGCONFIG base-logging
...)
and
#include <base-logging/Logging.hpp>
The logger allows you to log either using a printf-style function, or stream-style.
#include <base-logging/Logging.hpp>
...
// printf-style logging
std::string information("test-logging happening here");
LOG_DEBUG("I provide you the following debug information: %s",
information.c_str());
LOG_INFO(...);
LOG_WARN(...);
LOG_ERROR(..);
LOG_FATAL(..);
// stream-style logging
LOG_DEBUG_S << information;
LOG_INFO_S << information;
...
LOG_FATAL << information;
The default output format of a log message looks like the following (without linebreak):
[20120120-11:58:00:577] [FATAL] - test_logger::A fatal error occurred (/tmp/test_logger/src/Main.cpp:10 - int main(int, char**))
The logger can be configured at compile time to remove unwanted logging statement in a release version, and at runtime to control the verbosity level and logging format.
At compile time, the following defines can be set to configure the logger:
- BASE_LOG_NAMESPACE a string that will be displayed in all log messages. Set it to a unique string that allows you to filter the logging output. The Rock CMake macros set it to the package name.
- BASE_LOG_DISABLE set to ON to deactivates all logging statements, e.g. for a realease binary.
- BASE_LOG_xxx compile only logs equal or higher than a certain level xxx into your binary, i.e. BASE_LOG_WARN disable all logs except for WARN, ERROR and FATAL. Since the remaining levels are compiled out, the runtime settings below will be limited to the remaining levels.
- BASE_LONG_NAMES set this define in order to enforce a BASE_ prefix for the logging Macros, to avoid clashes with other libraries, i.e. BASE_LOG_DEBUG(…)
At runtime, the following environment variables can be used to control the behaviour of the logger:
- BASE_LOG_LEVEL Set to one of DEBUG, INFO, WARN, ERROR or FATAL to define the maximum logging level, e.g. export BASE_LOG_LEVEL="DEBUG"
- BASE_LOG_COLOR Activate colored logging (current configuration best for dark background coloured terminals), e.g. export BASE_LOG_COLOR
- BASE_LOG_FORMAT Select from a set of predefined logging formats: DEFAULT, MULTILINE, SHORT, example outputs are given below:
The DEFAULT format contains no linebreaks (here only due to fit the narrow presentation):
[20120120-11:58:00:577] [FATAL] - test_logger::A fatal error
occurred (/tmp/test_logger/src/Main.cpp:10 - int main(int, char**))
The SHORT format reduces information to the log priority and the message:
[FATAL] - test_logger::A fatal error occurred
The MULTILINE format split each individual log message across a minimum of three lines:
[20120120-11:58:59:976] in int main(int, char**)
/tmp/test_logger/src/Main.cpp:10
[FATAL] - test_logger::A fatal error occurred
Rock.cmake Reference Documentation
Executable Targets (rock_executable
)
rock_executable(name
SOURCES source.cpp source1.cpp ...
[LANG_C]
[DEPS target1 target2 target3]
[DEPS_PKGCONFIG pkg1 pkg2 pkg3]
[DEPS_CMAKE pkg1 pkg2 pkg3]
[MOC qtsource1.hpp qtsource2.hpp])
Creates a C++ executable and (optionally) installs it.
The following arguments are mandatory:
SOURCES: list of the C++ sources that should be built into that library
The following optional arguments are available:
LANG_C: build as a C rather than a C++ library
DEPS: lists the other targets from this CMake project against which the library should be linked
DEPS_PKGCONFIG: list of pkg-config packages that the library depends upon. The necessary link and compilation flags are added
DEPS_CMAKE: list of packages which can be found with CMake's find_package, that the library depends upon. It is assumed that the Find*.cmake scripts follow the CMake accepted standard for variable naming
MOC: if the library is Qt-based, this is a list of either source or header files of classes that need to be passed through Qt's moc compiler. If headers are listed, these headers should be processed by moc, with the resulting implementation files are built into the library. If they are source files, they get added to the library and the corresponding header file is passed to moc.
Library Targets (rock_library
)
rock_library(name
[SOURCES source.cpp source1.cpp ...]
[HEADERS header1.hpp header2.hpp header3.hpp ...]
[LANG_C]
[DEPS target1 target2 target3]
[DEPS_PKGCONFIG pkg1 pkg2 pkg3]
[DEPS_CMAKE pkg1 pkg2 pkg3]
[MOC qtsource1.hpp qtsource2.hpp]
[NOINSTALL])
Creates and (optionally) installs a shared library.
As with all rock libraries, the target must have a pkg-config file along, that
gets generated and (optionally) installed by the macro. The pkg-config file
needs to be in the same directory and called package_name.pc.in. See the template
created by rock-create-lib
for an example.
The following arguments are mandatory:
SOURCES: list of the C++ sources that should be built into that library. If absent, the library is assumed to be header-only (i.e. only the headers and pkg-config file will be installed). Note that even in this case the DEPS_* arguments can be provided as they are passed to the pkg-config file generation.
HEADERS: list of the C++ headers that should be installed. Headers are installed
in include/<target_name>/
.
The following optional arguments are available:
LANG_C: build as a C rather than a C++ library
DEPS: lists the other targets from this CMake project against which the library should be linked
DEPS_PKGCONFIG: list of pkg-config packages that the library depends upon. The necessary link and compilation flags are added
DEPS_CMAKE: list of packages which can be found with CMake's find_package, that the library depends upon. It is assumed that the Find*.cmake scripts follow the CMake accepted standard for variable naming
MOC: if the library is Qt-based, this is a list of either source or header files of classes that need to be passed through Qt's moc compiler. If headers are listed, these headers should be processed by moc, with the resulting implementation files are built into the library. If they are source files, they get added to the library and the corresponding header file is passed to moc.
NOINSTALL: by default, the library gets installed on 'make install'. If this argument is given, this is turned off
Boost Test Suite Targets (rock_testsuite
)
rock_testsuite(name
SOURCES source.cpp source1.cpp ...
[LANG_C]
[DEPS target1 target2 target3]
[DEPS_PKGCONFIG pkg1 pkg2 pkg3]
[DEPS_CMAKE pkg1 pkg2 pkg3]
[MOC qtsource1.hpp qtsource2.hpp])
Creates a C++ test suite that is using the boost unit test framework
The following arguments are mandatory:
SOURCES: list of the C++ sources that should be built into that library
The following optional arguments are available:
LANG_C: build as a C rather than a C++ library
DEPS: lists the other targets from this CMake project against which the library should be linked
DEPS_PKGCONFIG: list of pkg-config packages that the library depends upon. The necessary link and compilation flags are added
DEPS_CMAKE: list of packages which can be found with CMake's find_package, that the library depends upon. It is assumed that the Find*.cmake scripts follow the CMake accepted standard for variable naming
MOC: if the library is Qt-based, this is a list of either source or header files of classes that need to be passed through Qt's moc compiler. If headers are listed, these headers should be processed by moc, with the resulting implementation files are built into the library. If they are source files, they get added to the library and the corresponding header file is passed to moc.
GTest Test Suite Targets (rock_gtest
)
rock_gtest(name
SOURCES source.cpp source1.cpp ...
[LANG_C]
[DEPS target1 target2 target3]
[DEPS_PKGCONFIG pkg1 pkg2 pkg3]
[DEPS_CMAKE pkg1 pkg2 pkg3]
[MOC qtsource1.hpp qtsource2.hpp])
Creates a C++ test suite that is using the Google unit test framework
The following arguments are mandatory:
SOURCES: list of the C++ sources that should be built into that library
The following optional arguments are available:
LANG_C: build as a C rather than a C++ library
DEPS: lists the other targets from this CMake project against which the library should be linked
DEPS_PKGCONFIG: list of pkg-config packages that the library depends upon. The necessary link and compilation flags are added
DEPS_CMAKE: list of packages which can be found with CMake's find_package, that the library depends upon. It is assumed that the Find*.cmake scripts follow the CMake accepted standard for variable naming
MOC: if the library is Qt-based, this is a list of either source or header files of classes that need to be passed through Qt's moc compiler. If headers are listed, these headers should be processed by moc, with the resulting implementation files are built into the library. If they are source files, they get added to the library and the corresponding header file is passed to moc.
Next let's look at the creation of ruby packages