Export QObject in multi library setup

Categories: Tool chain

Problem

Suppose you want to split your application into components (shared libraries), in which the libraries can communicate via Qt’s signal slot mechanism.

The components in your application may change over time and you want to have a solution, which automatically generates all necessary header and definitions to let your libraries export symbols.

Recap

In this codeschnipsel we worked out, how to export symbols from a library, by defining an export header for the library and setting up a compiler definition for the export.

Export header for the library:

#pragma once

#include <QtCore/QtGlobal>

#ifdef D_EXPORT
#define EXPORT_API Q_DECL_EXPORT
#else
#define EXPORT_API Q_DECL_IMPORT
#endif

Define D_EXPORT for the library:

TARGET_COMPILE_DEFINITIONS(LibA   PRIVATE D_EXPORT)

Usage of D_EXPORT:

#pragma once

#include "LibA/export.h"

#include <QtCore/QObject>

class EXPORT_API Foo : public QObject
{
        // ...
}

Setup

Let’s suppose you have the following architecture:

We have a Programm (our executable) and some components (our libraries).

If we follow the example from this codeschnipsel, we must write an export header for each component and we also must define a compile definition for each component.

And, as you might see, the EXPORT_API and the D_EXPORT definitions are not sufficient, since it not discriminates between libraries. Hence the definitions must be named differently for every library.

Here CMake setup for our architecture:

# ...
QT_ADD_LIBRARY(
    CompA SHARED
    source/CompA/export.h
    source/CompA/ClassA.h
    source/CompA/ClassA.cpp
)

QT_ADD_LIBRARY(
    CompB SHARED
    source/CompB/export.h
    source/CompB/ClassB.h
    source/CompB/ClassB.cpp
)

QT_ADD_LIBRARY(
    CompC SHARED
    source/CompC/export.h
    source/CompC/ClassC.h
    source/CompC/ClassC.cpp
)

QT_ADD_LIBRARY(
    CompD SHARED
    source/CompD/export.h
    source/CompD/ClassD.h
    source/CompD/ClassD.cpp
)

QT_ADD_EXECUTABLE(
    Programm
    source/Programm/Application.h
    source/Programm/Application.cpp
    source/Programm/main.cpp
)

TARGET_INCLUDE_DIRECTORIES(CompA     PUBLIC ${PROJECT_SRC_PATH})
TARGET_INCLUDE_DIRECTORIES(CompB     PUBLIC ${PROJECT_SRC_PATH})
TARGET_INCLUDE_DIRECTORIES(CompC     PUBLIC ${PROJECT_SRC_PATH})
TARGET_INCLUDE_DIRECTORIES(CompD     PUBLIC ${PROJECT_SRC_PATH})
TARGET_INCLUDE_DIRECTORIES(Programm  PUBLIC ${PROJECT_SRC_PATH})

TARGET_LINK_LIBRARIES(CompA     PRIVATE                         Qt6::Core)
TARGET_LINK_LIBRARIES(CompB     PRIVATE CompA                   Qt6::Core)
TARGET_LINK_LIBRARIES(CompC     PRIVATE       CompB             Qt6::Core)
TARGET_LINK_LIBRARIES(CompD     PRIVATE       CompB             Qt6::Core)
TARGET_LINK_LIBRARIES(Programm  PRIVATE CompA       CompC CompD Qt6::Core)

TARGET_COMPILE_DEFINITIONS(CompA   PRIVATE EXPORT_COMPA)
TARGET_COMPILE_DEFINITIONS(CompB   PRIVATE EXPORT_COMPB)
TARGET_COMPILE_DEFINITIONS(CompC   PRIVATE EXPORT_COMPC)
TARGET_COMPILE_DEFINITIONS(CompD   PRIVATE EXPORT_COMPD)

# ...

In a complex environment this is error-prone, because one has to keep track of all libraries, name, defines, and it violates the Don’t repeat yourself – principle. It might shoot us not in the knee, since we notice errors very early in the process, but it is hard to maintain and the CMake file can grow quickly.

Solution

What can we do?

Generate all export headers from a single template

Let’s create the this export header template and store it in source/Globals/export_tpl.h

#pragma once

#include <QtCore/QtGlobal>

#ifdef EXPORT_${COMPNAME}
#define ${COMPNAME}_API Q_DECL_EXPORT
#else
#define ${COMPNAME}_API Q_DECL_IMPORT
#endif

The header is basically the same version as above, with the difference, that we generate the defines EXPORT_API and the D_EXPORT via the CMke variable COMPNAME.

In the case of CompA for example we get.

#ifdef EXPORT_CompA
#define CompA_API Q_DECL_EXPORT
#else
#define CompA_API Q_DECL_IMPORT
#endif

What we now need to do is, to tell CMake which libraries we want to let export there symbols and from where the libraries can include the generated export headers.

Let’s define a path, where we store generated header.

SET(PROJECT_GENERATED_INCLDUE_PATH ${CMAKE_CURRENT_BINARY_DIR}/generated)

The path expands to the <build/path>/generated. CMAKE_CURRENT_BINARY_DIR is simply the build path, you define when you run CMake the first time.

And let’s define a list of all libraries. Note that the list of components must match the names of the source folder of the libraries.

SET(COMPONENT_LIST CompA CompB CompC CompD)

What we now need is a definition of the variable ${COMPNAME} and the generation of the export header.

In CMake we can use the command CONFIGURE_FILE to replace names variables and store the generated file to some path.

And to define ${COMPNAME} we use the uppercase name from COMPONENT_LIST. Why upper case? Because I like my defines to be upper case and we use the defines in our code.

STRING(TOUPPER "${COMPONENT}" COMPNAME)
SET(COMPOUTPATH ${PROJECT_GENERATED_INCLDUE_PATH}/${COMPONENT})

CONFIGURE_FILE(${PROJECT_SRC_PATH}/Globals/export_tpl.h ${COMPOUTPATH}/export.h)

The export.h is stored in <build/path>/generated/${COMPONENT}. The usage of the components name as folder name makes it easier for us to define a general include path for all components, since it has the same structure as your source folder.

And in terms of additional include path. Let’s also add this one to the component.

TARGET_INCLUDE_DIRECTORIES(${COMPONENT}  PUBLIC  ${PROJECT_GENERATED_INCLDUE_PATH})

The last thing we need here, is a loop over the libraries names.

SET(COMPONENT_LIST CompA CompB CompC CompD)

FOREACH(COMPONENT ${COMPONENT_LIST})
    STRING(TOUPPER "${COMPONENT}" COMPNAME)
    SET(COMPOUTPATH ${PROJECT_GENERATED_INCLDUE_PATH}/${COMPONENT})

    CONFIGURE_FILE(${PROJECT_SRC_PATH}/Globals/export_tpl.h ${COMPOUTPATH}/export.h)
    
    TARGET_INCLUDE_DIRECTORIES(${COMPONENT}  PUBLIC  ${PROJECT_SRC_PATH})
    TARGET_INCLUDE_DIRECTORIES(${COMPONENT}  PUBLIC  ${PROJECT_GENERATED_INCLDUE_PATH})
    
    TARGET_COMPILE_DEFINITIONS(${COMPONENT}  PRIVATE EXPORT_${COMPNAME})
ENDFOREACH()

Our CMake definition of the libraries now look like this. Note we deleted the explicit export headers.

QT_ADD_LIBRARY(
    CompA SHARED
    source/CompA/ClassA.h
    source/CompA/ClassA.cpp
)

QT_ADD_LIBRARY(
    CompB SHARED
    source/CompB/ClassB.h
    source/CompB/ClassB.cpp
)

QT_ADD_LIBRARY(
    CompC SHARED
    source/CompC/ClassC.h
    source/CompC/ClassC.cpp
)

QT_ADD_LIBRARY(
    CompD SHARED
    source/CompD/ClassD.h
    source/CompD/ClassD.cpp
)

That it so far. ๐Ÿ™‚

Automate more and have less CMake code

Okay, you want to have less code. Keep in mind, that we here fixture the project setup it that sense, that all future addition of libraries follows the same principle of the current libraries. The later addition of special cases could make it harder to integrate them.

But letโ€™s suppose, we are sure, that additional libraries like to export symbols and will use globbing (search for specific files in a place) of files.

The main draw back in the current component definitions in QT_ADD_LIBRARY is the explicit naming of each file. Here we can instrument CMakeโ€™s file globbing mechanism to find all files used by component.

We use FILE GLOB_RECURSE to find all files in the libraries folder.

FILE(GLOB_RECURSE COMPFILES ${PROJECT_SRC_PATH}/${COMPONENT}/*)

QT_ADD_LIBRARY(${COMPONENT} SHARED ${COMPFILES})

We can also use this method for Programm.

FILE(GLOB_RECURSE PROGRAMMFILES ${PROJECT_SRC_PATH}/Programm/*)

QT_ADD_EXECUTABLE(Programm ${PROGRAMMFILES})

And voila, we have much less code.

The final version looks like this.

# ...

QT_STANDARD_PROJECT_SETUP()

SET(COMPONENT_LIST CompA CompB CompC CompD)

FOREACH(COMPONENT ${COMPONENT_LIST})
    STRING(TOUPPER "${COMPONENT}" COMPNAME)
    SET(COMPOUTPATH ${PROJECT_GENERATED_INCLDUE_PATH}/${COMPONENT})

    CONFIGURE_FILE(${PROJECT_SRC_PATH}/Globals/export_tpl.h ${COMPOUTPATH}/export.h)

    FILE(GLOB_RECURSE COMPFILES ${PROJECT_SRC_PATH}/${COMPONENT}/*)

    QT_ADD_LIBRARY(${COMPONENT} SHARED ${COMPFILES})

    TARGET_INCLUDE_DIRECTORIES(${COMPONENT}  PUBLIC  ${PROJECT_SRC_PATH})
    TARGET_INCLUDE_DIRECTORIES(${COMPONENT}  PUBLIC  ${PROJECT_GENERATED_INCLDUE_PATH})
    
    TARGET_COMPILE_DEFINITIONS(${COMPONENT}  PRIVATE EXPORT_${COMPNAME})
ENDFOREACH()

FILE(GLOB_RECURSE PROGRAMMFILES ${PROJECT_SRC_PATH}/Programm/*)

QT_ADD_EXECUTABLE(Programm ${PROGRAMMFILES})

TARGET_INCLUDE_DIRECTORIES(Programm  PUBLIC ${PROJECT_SRC_PATH})

TARGET_LINK_LIBRARIES(CompA     PRIVATE                         Qt6::Core)
TARGET_LINK_LIBRARIES(CompB     PRIVATE CompA                   Qt6::Core)
TARGET_LINK_LIBRARIES(CompC     PRIVATE       CompB             Qt6::Core)
TARGET_LINK_LIBRARIES(CompD     PRIVATE       CompB             Qt6::Core)
TARGET_LINK_LIBRARIES(Programm  PRIVATE CompA       CompC CompD Qt6::Core)

#...

Done ๐Ÿ™‚

What does the sample project do?

It uses the following structure.

Where

  • class Application is in Programm
  • class ClassC in CompC
  • class ClassB in CompB
  • class ClassA in CompA

It travels down and up again the dependency tree by only using signal and slots and no direct calls. With this we can check if all objects are exported and imported properly.

The project can be found here.

Have fun with it. ๐Ÿ™‚

Nice to know

Notes on includes paths

In the project we use:

TARGET_INCLUDE_DIRECTORIES(${COMPONENT}  PUBLIC  ${PROJECT_SRC_PATH})
TARGET_INCLUDE_DIRECTORIES(${COMPONENT}  PUBLIC  ${PROJECT_GENERATED_INCLDUE_PATH})

In our projects source tree we have the following folders:

  • src
    • CompA
    • CompB
    • CompC
    • CompD
    • Programm

In our generates files tree we have these folders.

  • generated
    • CompA
    • CompB
    • CompC
    • CompD

From the view of all source files while compile time, we work on the same logical include source tree, from which we can access all components via a fixed path. #include \<component\>/someinclude.h.

Keep in mind, that you can keep up with this principle, when adding other locations of header files.

You can always merge them virtually together, to have same logical include structure.

This makes things much easier, because one must not consider different types of folder structures and can therefore think less while we code.

Notes on the library dependencies

We have our component list SET(COMPONENT_LIST CompA CompB CompC CompD).

With this we name libraries and pick all folder containing the sources of those libraries.

When we define our component dependencies via TARGET_LINK_LIBRARIES we shall put everything in a table order, to have an overview over our system. This makes it much easier to extend our system, since we can see right away the dependencies.

Here the linking definition again:

TARGET_LINK_LIBRARIES(CompA     PRIVATE                         Qt6::Core)
TARGET_LINK_LIBRARIES(CompB     PRIVATE CompA                   Qt6::Core)
TARGET_LINK_LIBRARIES(CompC     PRIVATE       CompB             Qt6::Core)
TARGET_LINK_LIBRARIES(CompD     PRIVATE       CompB             Qt6::Core)
TARGET_LINK_LIBRARIES(Programm  PRIVATE CompA       CompC CompD Qt6::Core)

This can also be seen as a dependency matrix.

CompACompACompACompAProgamm
CompA
CompBX
CompCX
CompDX
ProgammXXX

Keep in mind, that the a dependency tree is a directed acyclic graph.

Why?

  • Directed means, that A depends on B, but B not on A. If violated, the linker fails, because he links in the order B, then A. But if B depends on A, he cannot use A, since it not present when linking B.
  • Acyclic means, that some parent P of A must not be child of A. This reduces to A depends on B example.
  • Has no self-loops. The library does simply not depend on itself.

When written out as matrix, the directed acyclic graph forms a binary strictly triangular matrix.

CompACompBCompCCompDProgamm
CompA00000
CompB10000
CompC01000
CompD01000
Progamm10110

One goal in the system architecture, beside the logical consistent decomposition of the system, is to find a visualization of the dependency tree, which leads structured directed acyclic graph and thus to the binary strictly triangular matrix of dependencies, from which one can easily define, extend or modify the TARGET_LINK_LIBRARIES definitions in the CMake project setup.

How does one find a “good” visualization? If you have a diagram, where the connections go strictly from the top to bottom (as seen above in the setup diagram). That basically it. From here you can write down your dependencies.


    Leave a Reply

    Your email address will not be published.

    Hi Human, please solve this: 2 + 7 =