Bei der Entwicklung eines C/C++ Projektes kommt man ab einer gewissen Größe des Projektes an den Punkt, in dem man sein Projekt in verschiedene einzelne Targets, also ausführbare Dateien und Bibliotheken, aufteilen möchte. In diesem Artikel gehe ich auf die verschiedene Arten von Bibliotheken ein, die mit CMake erstellt werden können und gebe dazu einige Beispiele. Als Basis für die Erstellung von CMake-Bibliotheken dient der Befehl add_library(), der, je nachdem welche Keywords verwendet werden, insgesamt sieben verschiedene Arten von Bibliotheken erzeugen kann. Den Code, den ich in den Beispielen dieses Artikels vorstelle, könnt ihr gerne weiter verwenden, ihr findet ihn dazu auf GitHub. Alle beschriebenen CMake-Befehle, -Variablen und -Properties sind mit einem Link zur entsprechenden Stelle in der CMake-Dokumentation hinterlegt.
Diesen und andere Beiträge von mir kannst du als sauber formatierte PDF-Datei zum Ausdrucken oder offline Lesen erwerben. Mehr Informationen dazu findest du hier.
Nötige Vorkenntnisse
- CMake Grundkenntnisse: Dazu gehört etwa die Erstellung und Kompilierung eines ausführbaren Programms mit CMake. Ihr solltet dahin gehend auch mit den CMake-Befehlen cmake_minimum_required(), project() und add_executable() vertraut sein. Solltet ihr nicht wissen, wie ihr ein Programm mit CMake erstellt und kompiliert, schaut euch am besten zuvor diesen Artikel oder dieses YouTube-Video von mir an.
- CMake-Variablen: CMake-Variablen stelle ich sowohl in einem Artikel als auch in einem YouTube-Video vor. Zudem findet sich eine Erklärung zu CMake-Variablen auch in der CMake-Dokumentation.
- CMake-Properties: In diesem Video auf YouTube gehe ich auf Properties in CMake ein. Zudem findet man eine Auflistung von Properties in der CMake-Dokumentation, eine genauere Erläuterung, was Properties eigentlich sind, fehlt hier jedoch.
- If-Verzweigung: If-Verzweigungen in CMake unterscheiden sich generell nicht von if-Verzweigungen in anderen Programmiersprachen und sollten daher wohl den meisten Lesern geläufig sein. If-Verzweigungen in CMake erwähne ich kurz in diesem YouTube-Video, ansonsten hilft hier natürlich auch wieder die CMake-Dokumentation weiter.
- Ich verwende zudem häufig die CMake-Befehle target_include_directories() und target_link_libraries(). Wenn euch diese Befehle nicht geläufig sind, liest am besten den ersten Abschnitt über normale Bibliotheken, bis ihr zum ersten Beispiel gelangt. Bevor ihr euch das Beispiel anschaut, informiert euch zuerst über diese beiden Befehle. Weitere Informationen findet ihr dann an den folgenden Stellen:
- target_include_directories(): in der CMake-Dokumentation oder in einem YouTube-Video.
- target_link_libraries(): in der CMake-Dokumentation oder in einem YouTube-Video.
- Der find_package()-Befehl: Im Grunde ist es für diesen Artikel ausreichend zu wissen, dass durch diesen Befehl externer Code in CMake eingebunden werden kann. Wer dennoch weitere Informationen benötigt, findet diese wie immer in der CMake-Dokumentation oder in diesem YouTube-Video.
- An einer Stelle verwende ich Generator Expressions. Genauere Kenntnisse sind dabei nicht unbedingt erforderlich. Mehr Informationen zu Generator Expressions findet ihr in der CMake-Dokumentation.
- Alle hier benötigten Vorkenntnisse findet ihr auch in meinem Buch CMake für Einsteiger.
Normale Bibliotheken
Unter dem Begriff „normale“ Bibliotheken fasse ich an dieser Stelle statische und dynamische Bibliotheken zusammen. Zusätzlich gibt es CMake-Bibliotheken, die dazu gedacht sind, zur Laufzeit eingebunden zu werden. Auf diese drei Arten von Bibliotheken gehe ich auch in diesem YouTube-Video ein. Sie können mit dem folgenden add_library()-Befehl erstellt werden:
add_library (
<BibliotheksName>
[STATIC|SHARED|MODULE]
[EXCLUDE_FROM_ALL]
[<SourceDatei1> <SourceDatei2> ...]
)
Das erste Argument des add_library()-Befehls ist der Name der Bibliothek <BibliotheksName>. Dieser kann frei gewählt werden, muss aber einzigartig innerhalb des CMake-Projektes sein, denn unter diesem Namen wird die Bibliothek im weiteren Verlauf des CMake-Scriptes angesprochen. Der Name der Bibliothek ist auch Grundlage für die Benennung der erzeugten Bibliotheksdatei auf dem System. Da sich die Namensgebung je nach Art der Bibliothek unterscheidet, komme ich später noch einmal darauf zurück.
Wenn man bei der Kompilierung des CMake-Projektes kein spezifisches Target angibt, so wird das hypothetische Target „all“ gebaut, zu dem standardmäßig alle ausführbaren Dateien und Bibliotheken hinzugefügt werden. Wird das optionale Keyword EXCLUDE_FROM_ALL angegeben, so wird die Bibliothek nicht ins Target „all“ geschrieben und infolgedessen nicht automatisch erzeugt.
Am Ende des Befehls folgen die Source-Dateien <SourceDatei1>, <SourceDatei2> usw. aus denen die Bibliothek erstellt werden soll. Ab CMake 3.11 können die Source-Dateien optional weggelassen werden, wenn diese später durch den Befehl target_sources() hinzugefügt werden. Wenn man versucht, eine Bibliothek ohne Source-Dateien zu erstellen, gibt CMake am Ende einen entsprechenden Fehler aus.
Die Verwendung eines der drei Keywords STATIC (statisch), SHARED (dynamisch) oder MODULE (modular) bestimmt den Typ der Bibliothek, die CMake erstellt. Es kann nur eines dieser Keywords an dieser Stelle verwendet werden, symbolisiert durch das „oder“-Symbol |. Wird keines dieser Keywords verwendet, so entscheidet der Wert der CMake-Variablen BUILD_SHARED_LIBS, ob eine statische oder dynamische Bibliothek erstellt wird.
Statische Bibliotheken
Bei Verwendung des Keywords STATIC wird eine statische Bibliothek erzeugt. Wird eine statische Bibliothek mit einem Target verlinkt, so werden die benötigten Programmteile dieser Bibliothek in das Target kopiert. Die Datei, die aus diesem Target erzeugt wird, benötigt entsprechend mehr Speicher auf der Festplatte, als bei einer dynamischen Bibliothek. Das liegt daran, dass die benötigten Programmteile in der Regel mehr Speicher benötigen, als die Verlinkung zu einer dynamischen Bibliothek. Unter Windows ist der Standardname dieser Bibliothek <BibliotheksName>.lib während er auf Unix basierten Betriebssystemen lib<BibliotheksName>.a lautet.
Dynamische Bibliotheken
Bei Verwendung des Keywords SHARED wird eine dynamische Bibliothek erzeugt. Im Gegensatz zu einer statischen Bibliothek werden beim Verlinken eines Targets mit einer dynamischen Bibliothek die benötigten Programmteile lediglich verlinkt und erst zur Laufzeit eingebunden bzw. geladen. Damit ist das erstellte Target in der Regel kleiner als ein äquivalentes Target, das mit einer statischen Bibliothek verlinkt wurde. Unter Windows ist der Standardname einer dynamischen Bibliothek <BibliotheksName>.dll, während er auf macOS lib<BibliotheksName>.dylib und auf anderen Unix basierten Betriebssystemen lib<BibliotheksName>.so lautet.
Modulare Bibliotheken
Das MODULE-Keyword erzeugt eine Bibliothek, die einer dynamischen Bibliothek sehr ähnlich ist. Jedoch ist die Intention bei einer solchen Bibliothek, dass diese als eine Art Plug-in zur Laufzeit geladen wird und nicht direkt mit dem ausführbaren Programm verlinkt wird.
Beispiel 1: Erstellen von normalen CMake-Bibliotheken
In diesem Beispiel erstelle ich einmal jede der drei oben genannten „normalen“ CMake-Bibliotheken.
cmake_minimum_required(VERSION 3.7...3.22)
project(normal_libs LANGUAGES CXX)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE)
set(LIB_SOURCES
src/mathe/rechteck.cpp
src/mathe/quadrat.cpp
)
add_library(mathe_shared SHARED ${LIB_SOURCES})
add_library(mathe_static STATIC ${LIB_SOURCES})
add_library(mathe_module MODULE ${LIB_SOURCES})
target_include_directories(mathe_shared PUBLIC src/mathe/)
target_include_directories(mathe_static PUBLIC src/mathe/)
target_include_directories(mathe_module PUBLIC src/mathe/)
add_executable(main_shared src/main.cpp)
add_executable(main_static src/main.cpp)
target_link_libraries(main_shared PRIVATE mathe_shared)
target_link_libraries(main_static PRIVATE mathe_static)
Die CMake-Variable CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS in Zeile 5 setze ich auf TRUE, damit unter Windows die im Folgenden erstellte dynamische Bibliothek korrekt verlinkt werden kann. Weitere Informationen dazu finden sich zum Beispiel in der CMake-Dokumentation zur CMake-Property WINDOWS_EXPORT_ALL_SYMBOLS.
In den Zeilen 12-14 wird jeweils eine Bibliothek des Typs STATIC, SHARED und MODULE erstellt. Dazu werden die in der Variable LIB_SOURCES gespeicherten Source-Dateien (Zeile 7-10) verwendet und die entsprechenden zu inkludierenden Ordner (Zeile 16-18) gesetzt.
Am Ende dieser CMakeLists.txt-Datei erstelle ich zwei ausführbare Dateien (Zeile 20 und 21) unter Verwendung der gleichen Source-Datei main.cpp und verlinke jeweils einmal die erstellte dynamische und statische Bibliothek (Zeile 23 und 24) mit dem target_link_libraries()-Befehl. Für die Bibliothek mathe_module des Typs MODULE ist dies nicht möglich, da wie oben bereits geschrieben diese nur zur Laufzeit eingebunden werden kann. Um eine Bibliothek zur Laufzeit einzubinden, kann unter anderem die Qt-Bibliothek oder die libltdl-Bibliothek verwendet werden.
Blicken wir nun einmal in die main.cpp-Datei:
#include "quadrat.h"
#include "rechteck.h"
#include <iostream>
int main()
{
Rechteck rechteck(2, 6);
Quadrat quadrat(5);
std::cout << "Rechteck Fläche: " << rechteck.flaeche() << std::endl;
std::cout << "Rechteck Umfang: " << rechteck.umfang() << std::endl;
std::cout << "Quadrat Fläche: " << quadrat.flaeche() << std::endl;
std::cout << "Quadrat Umfang: " << quadrat.umfang() << std::endl;
return 0;
}
In der Datei main.cpp können die Klassen Rechteck und Quadrat, die in den Header-Dateien quadrat.h und rechteck.h (inkludiert in Zeile 1 und 2) deklariert und in den Source-Dateien der erstellten Bibliotheken quadrat.cpp und rechteck.cpp definiert sind, verwendet werden. Die Berechnungen sind natürlich äußerst simpel und dienen nur zur Veranschaulichung.
Bei der Kompilierung der Bibliotheken und ausführbaren Dateien passiert wenig Erwähnenswertes. CMake achtet immer darauf, die Targets in der richtigen Reihenfolge zu erstellen. Hier in diesem Beispiel etwa die Bibliothek mathe_shared vor der ausführbaren Datei main_shared, da letztere mit der Bibliothek verlinkt wird. Blicken wir einmal auf die erstellten ausführbaren Dateien unter Ubuntu 20.04, wobei ich an dieser Stelle nicht relevante Ausgaben mit … abkürze.
$ ll
...
... 12581 ... CMakeCache.txt
... 4096 ... CMakeFiles/
... 1698 ... cmake_install.cmake
... 16096 ... libmathe_module.so*
... 16096 ... libmathe_shared.so*
... 4036 ... libmathe_static.a
... 17680 ... main_shared*
... 17840 ... main_static*
... 11255 ... Makefile
Die Zahlen links geben die Größe der einzelnen Dateien in Bytes an. Die modulare und dynamische Bibliothek (libmathe_module.so und libmathe_shared.so) sind exakt gleich groß und dabei viermal größer als die statische Bibliothek libmathe_static.a. Mutmaßlich liegt der Größenunterschied an zusätzlichen Symbolen innerhalb der dynamischen Bibliothek. Auf stackoverflow.com findet ihr eine zugehörige Frage inkl. Diskussion.
Interessanter ist vielleicht der Größenvergleich zwischen den beiden erstellten ausführbaren Dateien main_shared und main_static in Zeile 9 und 10. Die mit der statischen Bibliothek verlinkte, ausführbare Datei main_static ist tatsächlich größer als die äquivalente, ausführbare Datei main_shared, die mit der dynamischen Variante der Bibliothek verlinkt wurde. Der Unterschied beträgt aber lediglich 160 Bytes, was auf die geringe Größe der Bibliothek zurückzuführen ist. Daher blicken wir im nächsten Beispiel einmal auf diesen Aspekt bei der deutlich größeren Boost-Bibliothek.
Beispiel 2: Vergleich statische und dynamische Boost-Bibliothek
In diesem Beispiel betrachten wir ein kleines Programm, indem wir die Boost-Bibliothek einsetzen, um die Größe einer Datei auszulesen. Einmal werden wir die Boost-Bibliothek statisch und einmal dynamisch verlinken und dabei die unterschiedliche Größe der erzeugten ausführbaren Datei betrachten. Entsprechend muss die Boost-Bibliothek auf eurem System installiert sein, wenn ihr dieses Beispiel dort ausführen möchtet. Blicken wir dazu zunächst auf die verwendete main.cpp-Datei:
#include <boost/filesystem.hpp>
#include <iostream>
int main()
{
std::cout << "Dateigröße main.cpp: "
<< boost::filesystem::file_size("../../main.cpp")
<< " bytes" << std::endl;
return 0;
}
In der ersten Zeile wird die Header-Datei der Boost-Filesystem-Bibliothek inkludiert. In Zeile 7 wird dann die Funktion file_size() aus dieser Bibliothek verwendet, um die Größe dieser main.cpp-Datei auszulesen. Lasst euch durch die relative Pfadangabe an dieser Stelle nicht verwirren, diese ist notwendig, da das spätere Programm immer vom eigenen Speicherort ausgeht. Eventuell müsst ihr diesen Pfad auf eurem System anpassen, wenn ihr euren Build-Ordner an einer anderen Stelle als ich speichert. Ihr könnt natürlich auch einfach die Größe einer anderen Datei auslesen, die ihr an die entsprechende Stelle kopiert.
Ich habe jeweils eine eigene CMakeLists.txt-Datei für die statische und dynamische Variante dieses Beispiels angelegt. Blicken wir zunächst auf die statische Variante:
cmake_minimum_required(VERSION 3.7...3.22)
project(boost_static LANGUAGES CXX)
set(Boost_USE_STATIC_LIBS ON)
find_package(
Boost 1.50
REQUIRED
COMPONENTS filesystem
)
add_executable(boost_static ../main.cpp)
target_link_libraries(boost_static PRIVATE Boost::filesystem)
In Zeile 5 wird die CMake-Variable Boost_USE_STATIC_LIBS auf ON gesetzt, wodurch die statischen Bibliotheken der Boost-Bibliothek beim Linken verwendet werden. Die Boost-Bibliothek binden wir mithilfe des find_package()-Befehls (Zeile 7-11) eingebunden. Genauer gesagt binden wir lediglich die filesystem Komponente der Boost-Bibliothek ein. Diese verlinken wir in Zeile 14 mit der erstellten ausführbaren Datei boost_static.
Die dynamische Variante dieses Beispiel sieht fast genauso aus, lediglich das Setzen der CMake-Variablen Boost_USE_STATIC_LIBS wird weggelassen:
cmake_minimum_required(VERSION 3.7...3.22)
project(boost_shared LANGUAGES CXX)
find_package(
Boost 1.50
REQUIRED
COMPONENTS filesystem
)
add_executable(boost_shared ../main.cpp)
target_link_libraries(boost_shared PRIVATE Boost::filesystem)
Die Ausgabe der Kompilierung ist für diesen Artikel wieder weniger interessant, schauen wir uns daher direkt die erstellten Dateien an.
Static/build$ ls -lh
total 200K
... 167K ... boost_static
... 14K ... CMakeCache.txt
... 4,0K ... CMakeFiles
... 1,7K ... cmake_install.cmake
... 6,8K ... Makefile
Shared/build$ ls -lh
total 112K
... 79K ... boost_shared
... 14K ... CMakeCache.txt
... 4,0K ... CMakeFiles
... 1,7K ... cmake_install.cmake
... 6,8K ... Makefile
Um der Größer der erstellten Dateien gerecht zu werden, erfolgt hier die Angabe der Größe in Kilobytes, also 1K = 1 Kilobyte. Während sich die Größe aller anderen Dateien nicht verändert, ist die ausführbare Datei boost_static mehr als doppelt so groß wie die ausführbare Datei boost_share, dabei wurde lediglich die eine kleine Funktion file_size() aus dieser Bibliothek eingebunden. Dafür kann die ausführbare Datei boost_static nun auch auf System ausgeführt werden, auf der die Boost-Bibliothek nicht installiert ist.
Objekt-Bibliotheken
Eine Objekt-Bibliothek kompiliert in CMake die übergebenen Source-Dateien, fügt diese jedoch nicht zu einer Bibliotheksdatei zusammen. Durch Verwendung des Keywords OBJECT können Objekt-Bibliotheken im add_library()-Befehl definiert werden.
add_library(
<BibliotheksName>
OBJECT
[<SourceDatei1> <SourceDatei2> ...]
)
Im Grunde unterscheidet sich dieser Befehl nicht von dem zuvor gezeigten add_library()-Befehl für „normale“ Bibliotheken. Statt eines der drei Keywords STATIC, SHARED oder MODULE wird an dieser Stelle das Keyword OBJECT verwendet. Zudem steht das Keyword EXCLUDE_FROM_ALL nicht zur Verfügung, da auch kein Target dieser Objekt-Bibliothek erstellt wird.
In den Befehlen add_library() und add_executable() können Objekt-Bibliotheken durch Verwendung von Generator Expressions eingebunden werden:
add_library(... $<TARGET_OBJECTS:<BibliotheksName>> ...)
add_executable(... $<TARGET_OBJECTS:<BibliotheksName>> ...)
Generator Expressions werden in spitze Klammern <…> eingeschlossen und fügen in diesem Beispiel die Bibliothek zum jeweiligen Target hinzu. Ab CMake 3.12 können Objekt-Bibliotheken aber auch durch Verwendung des Befehls target_link_libraries() mit einem Target verlinkt werden, wodurch man nicht gezwungen ist, Generator Expressions zu verwenden.
Die Verwendung des target_link_libraries()-Befehl bietet einen weiteren großen Vorteil: bei Verwendung der Generator Expression TARGET_OBJECTS, wie im obigen Code gezeigt, wird kein Target verlinkt, sondern es werden lediglich die Dateien (Objekte) der Objekt-Bibliothek <BibliotheksName> eingebunden. Damit gehen weitere Informationen aus der Objekt-Bibliothek, wie die Angabe von „Include Directories“ mittels target_include_directories(), verloren. Schauen wir uns dazu das folgende Beispiel an.
Beispiel 3: Objekt-Bibliothek erstellen und verlinken
In diesem Beispiel erstellen wir aus der aus Beispiel 1 bekannten Bibliothek mathe eine Objekt-Bibliothek. Anschließend werden zwei ausführbare Dateien erstellt, die diese Bibliothek benötigen. Eine ausführbare Datei bindet die Objekt-Bibliothek über die oben gezeigte Generator Expression ein und die andere ausführbare Datei mittels des Befehls target_link_libraries().
cmake_minimum_required(VERSION 3.12...3.22)
project(object_libs LANGUAGES CXX)
add_library(
mathe_object
OBJECT
src/mathe/rechteck.cpp
src/mathe/quadrat.cpp
)
target_include_directories(mathe_object PUBLIC src/mathe/)
add_executable(
main_object_ge
src/main.cpp
$<TARGET_OBJECTS:mathe_object>
)
target_include_directories(main_object_ge PRIVATE src/mathe/)
add_executable(main_object_tll src/main.cpp)
target_link_libraries(main_object_tll PRIVATE mathe_object)
In Zeile 1 wird die mindestens benötigte CMake-Version auf 3.12 gesetzt, da wir später den target_link_libraries()-Befehl in Verbindung mit einer Objekt-Bibliothek nutzen wollen. Diese Objekt-Bibliothek mit dem Namen mathe_object wird in den Zeilen 5-10 unter Verwendung des Keywords OBJECT und den beiden bereits benötigten Source-Dateien erstellt. In Zeile 12 wird wie in Beispiel 1 der benötigte Ordner mit den Header-Dateien der Bibliothek inkludiert.
In den Zeilen 14-18 wird die ausführbare Datei main_object_ge erstellt und direkt die Objekt-Bibliothek mathe_object eingebunden. Da, wie oben beschrieben, nicht das Target an dieser Stelle verlinkt wird, müssen die include-Ordner zusätzlich mit dem target_include_directories()-Befehl in Zeile 19 angegeben werden. Eine andere Möglichkeit wäre übrigens gewesen, den Pfad zur Header-Datei in der main.cpp-Datei anzupassen. Diese Variante ist jedoch durchaus fehleranfälliger und kann zu erhöhtem Wartungsaufwand führen, falls sich die Pfade später ändern sollten.
Für die in Zeile 21 erstellte ausführbare Datei main_object_tll ist dies nicht notwendig, da wir die Objekt-Bibliothek mathe_object in Zeile 22 mit dem target_link_libraries()-Befehl verlinken. Die Information über den zu inkludierenden Ordner aus Zeile 12 wird an dieser Stelle, wie in CMake üblich, mit übergeben.
Schauen wir uns im Folgenden einmal die Ausgabe des make-Befehls an, die CMake-Ausgabe bietet keine interessanten Informationen:
$ make
Scanning dependencies of target mathe_object
[ 16%] Building CXX object CMakeFiles/mathe_object.dir/src/mathe/rechteck.cpp.o
[ 33%] Building CXX object CMakeFiles/mathe_object.dir/src/mathe/quadrat.cpp.o
[ 33%] Built target mathe_object
Scanning dependencies of target main_object_tll
[ 50%] Building CXX object CMakeFiles/main_object_tll.dir/src/main.cpp.o
[ 66%] Linking CXX executable main_object_tll
[ 66%] Built target main_object_tll
Scanning dependencies of target main_object_ge
[ 83%] Building CXX object CMakeFiles/main_object_ge.dir/src/main.cpp.o
[100%] Linking CXX executable main_object_ge
[100%] Built target main_object_ge
Im ersten Schritt wird die Objekt-Bibliothek mathe_object erstellt und anschließend die beiden ausführbaren Dateien. Dies unterscheidet sich grundsätzlich einmal nicht von dem Prozess, wie er auch bei den normalen Bibliotheken abläuft. Interessant ist aber nun der Blick auf die erstellten Dateien:
$ ll
...
... 12581 ... CMakeCache.txt
... 4096 ... CMakeFiles/
... 1698 ... cmake_install.cmake
... 17840 ... main_object_ge*
... 17840 ... main_object_tll*
... 9000 ... Makefile
Neben den in CMake üblichen Dateien wurden die beiden ausführbaren Dateien main_object_ge und main_object_tll erstellt. Wie ihr sehen könnt, wurde für die Objekt-Bibliothek mathe_object keine eigene Datei auf dem System angelegt, sondern die benötigten Dateien direkt in die ausführbare Datei kompiliert. Dies sieht man auch daran, dass mit 17.840 Bytes die Datei genauso groß ist, wie die ausführbare Datei main_static aus Beispiel 1. Letztlich verhält sich eine Objekt-Bibliothek also wie eine statische Bibliothek, nur dass keine Bibliotheks-Datei auf dem System erzeugt wird, die später noch einmal verwendet werden kann.
Interface-Bibliotheken
Eine Interface-Bibliothek ist eine CMake-Bibliothek, die (in der Regel) keine Source-Dateien enthält bzw. kompiliert und infolgedessen keine Bibliotheks-Datei auf dem System erzeugt. Den Zusatz in Klammern „in der Regel“ füge ich deshalb hinzu, weil ab CMake 3.19 es auch möglich ist Source-Dateien hinzuzufügen, wobei diese aber auch weiterhin nicht kompiliert werden und keine Bibliotheks-Datei auf dem System erzeugt wird. Jedoch taucht die Interface-Bibliothek nun als Build-Target im Build-System auf.
Zumeist werden Interface-Bibliotheken genutzt, um ein Interface zu bereits vorhandenen Targets zu bilden. Dazu werden INTERFACE_*-Properties befüllt, wozu in CMake verschiedene Befehle wie set_property(), target_include_directories(), target_link_libraries() usw. zur Verfügung stehen.
Eine Interface-Bibliothek kann in CMake wie folgt erstellt werden
add_library(
<BibliotheksName>
INTERFACE
[<SourceDatei1> <SourceDatei2> ...]
[EXCLUDE_FROM_ALL]
)
wobei die Angaben von Source-Dateien in Zeile 4 und die Verwendung des Keywords EXCLUDE_FROM_ALL, welches den gleichen Effekt hat wie weiter oben bereits beschrieben, in Zeile 5 erst ab CMake 3.19 möglich ist. Nach der Angabe des Namens der Bibliothek <BibliotheksName> folgt das Keyword INTERFACE, welches die Bibliothek als Interface-Bibliothek definiert.
Beispiel 4: Interface-Bibliothek als Interface für mehrere Targets
In diesem Beispiel werden zwei statische Bibliotheken zu einer CMake-Bibliothek des Typs Interface zusammengefasst und anschließend mit einer ausführbaren Datei verlinkt. Die beiden statischen Bibliotheken sind die in zwei Teile aufgeteilte Bibliothek mathe aus Beispiel 1.
cmake_minimum_required(VERSION 3.7...3.22)
project(include_lib LANGUAGES CXX)
add_library(rechteck_bib STATIC src/rechteck/rechteck.cpp)
add_library(quadrat_bib STATIC src/quadrat/quadrat.cpp)
target_include_directories(rechteck_bib PUBLIC src/rechteck/)
target_include_directories(quadrat_bib PUBLIC src/quadrat/)
add_library(mathe INTERFACE)
target_link_libraries(
mathe
INTERFACE
rechteck_bib
quadrat_bib
)
add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE mathe)
In den Zeilen 5 und 6 werden die beiden statischen Bibliotheken rechteck_bib und quadrat_bib erstellt und in den Zeilen 8 und 9 die entsprechenden „Include Directories“ gesetzt.
In Zeile 11 wird die Interface-Bibliothek mathe erstellt und in den folgenden Zeilen 12-17 mit den beiden statischen Bibliotheken unter Verwendung des Keywords INTERFACE verlinkt. Beachtet die unterschiedliche Verwendung des Keywords INTERFACE in diesem Beispiel. Im add_library()-Befehl dient dieses Keyword, um eine Interface-Bibliothek zu spezifizieren.
Im target_link_libraries()-Befehl hingegen gibt dieses Keyword an, dass die verlinkten Bibliotheken nur im Interface der Bibliothek mathe benötigt werden. Das heißt, jede Bibliothek, die mit der Bibliothek mathe verlinkt wird, wird auch automatisch mit den beiden Bibliotheken rechteck_bib und quadrat_bib verlinkt. Zudem bedeutet die Verwendung des Keywords INTERFACE im target_link_libraries()-Befehl, dass diese Bibliotheken intern in der Bibliothek mathe nicht verwendet werden. Da die Interface-Bibliothek mathe keinerlei Source-Dateien kompiliert, sondern gewissermaßen nur als Zusammenfassung der anderen beiden Bibliothek zu sehen ist, ist diese Angabe hier vollkommen korrekt und kompiliert fehlerfrei.
Die Ausführung von CMake und die Kompilierung des Programms bringen an dieser Stelle keine relevante Ausgabe hervor, betrachten wir daher direkt die erstellten Dateien unter Ubuntu 20.04.
$ ll
...
... 12581 ... CMakeCache.txt
... 4096 ... CMakeFiles/
... 1698 ... cmake_install.cmake
... 2136 ... libquadrat_bib.a
... 1974 ... librechteck_bib.a
... 17840 ... main*
... 8571 ... Makefile
Wie zu sehen, werden die beiden statischen Bibliotheken libquadrat_bib.a und librechteck_bib.a ganz normal auf dem System erstellt, wie jedoch weiter oben bereits beschrieben und hier zu sehen, erzeugt eine Interface-Bibliothek keine Dateien auf dem System. Das heißt, es gibt keine physische Datei auf dem System, dem die Interface-Bibliothek mathe entspricht.
Imported-Bibliothek
Eine CMake-Bibliothek des Typs IMPORTED wird verwendet, um bereits vorhandene Bibliotheksdateien auf dem System in CMake einzubinden. Das heißt, die Bibliothek wird nicht erst während des Kompiliervorgangs erstellt, sondern es wird eine bereits existierende Bibliothek als CMake-Bibliothek des Typs IMPORTED erstellt. So können diese Bibliotheken als logische Targets innerhalb des CMake-Projekts verwendet werden.
Diese Art von Bibliothek wird auch in aller Regel in Find<PackageName>.cmake-Dateien erstellt, die durch den find_package(<PackageName> …)-Befehl aufgerufen werden. In solchen Find<PackageName>.cmake-Dateien wird nach den Pfaden zu den Bibliotheks-Dateien und Include-Ordnern des Packages <PackageName> auf dem System gesucht und die entsprechenden Imported-Bibliotheken erstellt. Diese Imported-Bibliotheken können dann mit den selbst erstellten Targets verlinkt werden.
Eine CMake-Bibliothek des Typs IMPORTED wird durch den folgenden Befehl erstellt:
add_library(
<BibliotheksName>
<BibliotheksTyp>
IMPORTED
[GLOBAL]
)
Auf den Namen der Bibliothek <BibliotheksName> folgt der Typ der Bibliothek <BibliotheksTyp>, die importiert werden soll. Für den Typ der Bibliothek stehen die folgenden Optionen zur Verfügung: STATIC, SHARED, MODULE, UNKNOWN, OBJECT und INTERFACE. Bis auf das Keyword UNKNOWN habe ich die anderen Keywords bzw. Typen von Bibliotheken ja bereits erläutert.
Mit dem Keyword UNKNOWN können Bibliotheken importiert werden, bei denen man selbst nicht weiß, um welchen Typ es sich handelt. Die Verwendung dieses Keywords findet man zumeist in Find<PackageName>.cmake-Dateien. Besonders nützlich ist dieses Keyword unter Windows, wo eine statische Bibliothek und die Import-Bibliothek einer DLL beide die gleiche Dateiendung *.lib haben.
Anschließend folgt das Keyword IMPORTED, welches dem add_library()-Befehl mitteilt, dass eine Bibliothek des Typs IMPORTED erstellt werden soll.
Die erstellte Bibliothek ist in dem Scope sichtbar in dem sie erstellt wurde und in darunterliegenden Scopes. Soll die Bibliothek auch in darüberliegenden Scopes, also global, sichtbar sein, kann optional das Keyword GLOBAL mit übergeben werden.
Da mit einer CMake-Bibliothek des Typs IMPORTED lediglich vorhandene Bibliotheken importiert werden sollen, wird eine solche Bibliothek nicht kompiliert. Entsprechend ist es an dieser Stelle auch nicht möglich, Source-Dateien zu übergeben.
Beispiel 5: Importieren einer dynamischen Bibliothek
In diesem Beispiel wird die Bibliothek mathe aus dem ersten Beispiel eingebunden. Dieses Mal wird die Bibliothek mathe jedoch nicht erstellt, sondern die entsprechenden Bibliotheksdateien liegen bereits auf dem System vor und werden als Imported-Bibliothek in CMake eingebunden und abschließend mit einer ausführbaren Datei verlinkt. Die benötigten Bibliotheksdateien, sowie die benötigten Header-Dateien befinden sich ausgehend vom Hauptprojektordner, in der auch die CMakeLists.txt-Datei liegt, im Ordner libs/mathe. Die CMakeLists.txt-Datei ist etwas länger geworden, um eine plattformunabhängige Verwendung zu ermöglichen.
cmake_minimum_required(VERSION 3.7...3.22)
project(imported_lib LANGUAGES CXX)
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/libs/mathe)
if(${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Darwin")
set(LIB_FILE libmathe_shared.dylib)
elseif(${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Windows")
set(LIB_FILE mathe_shared.dll)
else() # Assume Linux-System
set(LIB_FILE libmathe_shared.so)
endif()
add_library(CodingWithMagga::mathe SHARED IMPORTED)
target_include_directories(
CodingWithMagga::mathe
INTERFACE ${LIB_PATH}
)
set_target_properties(
CodingWithMagga::mathe
PROPERTIES
IMPORTED_LOCATION ${LIB_PATH}/${LIB_FILE}
)
if(${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Windows")
set_target_properties(
CodingWithMagga::mathe
PROPERTIES
IMPORTED_IMPLIB ${LIB_PATH}/mathe_shared.lib
)
endif()
add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE CodingWithMagga::mathe)
In Zeile 5 wird der Pfad zu den Bibliotheksdateien in der Variablen LIB_PATH gespeichert. Zudem wird in den Zeilen 7-13 in der Variablen LIB_FILE der Name der Bibliotheksdatei abhängig vom verwendeten Betriebssystem gespeichert. Beachtet, dass der Systemname „Darwin“ auf ein Mac-System verweist.
In Zeile 15 wird dann die benötigte Imported-Bibliothek CodingWithMagga::mathe erstellt. Die hier gezeigte Doppelpunktnotation ist nur für ALIAS– und IMPORTED-Targets erlaubt. Diese Notation kann, ähnlich wie in C++, zur Erstellung von Namespaces verwendet werden. Es handelt sich hier also um die Bibliothek mathe aus dem Namespace CodingWithMagga. Diese Notation habt ihr bereits in Beispiel 2 gesehen, in der die Bibliothek boost::filesystem eingebunden wurde.
In den Zeilen 16-19 wird das Interface der Imported-Bibliothek spezifiziert. Da sich die benötigten Header-Dateien im Ordner LIB_PATH befinden, wird dieser Ordner im target_include_directories()-Befehl unter Verwendung des Keywords INTERFACE übergeben. Da die Header-Dateien innerhalb der Bibliothek CodingWithMagga::mathe nicht benötigt werden, ist es nicht notwendig, hier das Keyword PUBLIC zu verwenden.
In den Zeilen 20-24 wird der Property IMPORTED_LOCATION der Bibliothek CodingWithMagga::mathe der Dateipfad zur importierten Bibliotheksdatei gesetzt. So weiß CMake, dass diese Bibliotheksdatei durch dieses Target importiert werden soll. Unter Windows muss an dieser Stelle zusätzlich die Property IMPORTED_IMPLIB gesetzt werden, siehe Zeile 26-32. In dieser Property muss bei einer dynamischen Bibliothek, wie es hier der Fall ist, der Pfad der zur Windows-DLL gehörenden *.lib-Datei gespeichert werden. Diese *.lib-Datei wird für die Verlinkung der Windows-DLL Bibliothek während der Kompilierung benötigt.
Am Ende der CMakeLists.txt-Datei wird die ausführbare Datei main erstellt und mit der Imported-Bibliothek CodingWithMagga::mathe verlinkt. Da für eine Imported-Bibliothek logischerweise keine Datei auf dem System erzeugt wird, da diese bereits existiert, und auch die Ausgabe der Kompilierung wenig interessant ist, verzichte ich hier darauf, diese wiederzugeben.
Alias-Bibliothek
Eine CMake-Bibliothek des Typs ALIAS erzeugt im Grunde keine neue Bibliothek, sondern bietet innerhalb von CMake die Möglichkeit eine CMake-Bibliothek unter einem anderen Namen anzusprechen. Relativ häufig wird diese Bibliothek daher verwendet, um die in Beispiel 5 genannten Namespaces nutzen zu können. Der Befehl, um eine Bibliothek des Typs ALIAS in CMake erzeugen zu können, lautet wie folgt:
add_library(<NeuerBibliotheksName> ALIAS <BibliotheksName>)
Der Befehl beginnt mit dem neuen Namen der Bibliothek <NeuerBibliotheksName>, worauf das Keyword ALIAS und anschließend der Name der Bibliothek <BibliotheksName>, auf die der neue Name verweisen soll, folgt.
CMake-Bibliotheken des Typs ALIAS, wie <NeuerBibliotheksName>, tauchen nicht im generierten Build-System als erstellte Target auf und es werden keine zusätzlichen Dateien auf dem System erzeugt. Sie können jedoch verlinkt und deren Properties ausgelesen werden. Zudem kann die Existenz des Targets mittels if(<NeuerBibliotheksName>) überprüft werden. Allerdings ist es nicht möglich Eigenschaften, also Properties, der Alias-Bibliothek zu ändern. Dazu muss das ursprüngliche Target <BibliotheksName> genutzt werden. Deshalb werden in Find<PackageName>.cmake-Dateien häufig CMake-Bibliotheken des Typs ALIAS unter Verwendung von Namespaces erzeugt, denn es ist in der Regel nicht notwendig diese Bibliotheken zu verändern.
Beispiel 6: Erstellen und Verwenden einer Alias-Bibliothek
Im folgenden Beispiel greife ich erneut auf Beispiel 1 zurück. Für die dort erstellte Bibliothek mathe erstelle ich eine Alias-Bibliothek und verlinkte diese mit einer ausführbaren Datei.
cmake_minimum_required(VERSION 3.7...3.22)
project(alias_lib LANGUAGES CXX)
add_library(
mathe
STATIC
src/mathe/rechteck.cpp
src/mathe/quadrat.cpp
)
target_include_directories(mathe PUBLIC src/mathe/)
add_library(CodingWithMagga::mathe ALIAS mathe)
add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE CodingWithMagga::mathe)
An dieser Stelle werdet ihr nur relativ wenig Neues sehen, weshalb ich mich an dieser Stelle kurz fasse. In Zeile 13 wird die Alias-Bibliothek CodingWithMagga::mathe erstellt, die auf die Bibliothek mathe verweist. Anschließend wird diese Bibliothek mit erstellten ausführbaren Datei in Zeile 16 verlinkt. Ich verzichte hier erneut auf weitere Ausgaben der Kompilierung oder der erstellten Dateien, da dort nichts Spannendes passiert.
Zusammenfassung CMake-Bibliotheken
Wie ihr in diesem Beitrag lesen konntet, gibt es eine große Anzahl verschiedener CMake-Bibliotheken. In den meisten Fällen werdet ihr selbst Bibliotheken vom Typ STATIC oder SHARED erzeugen. Soll eine dynamische Bibliothek erst als Plug-in zur Laufzeit geladen werden, dann verwendet den Typ MODULE.
Möchtet Ihr hingegen Speicherplatz sparen und benötigt die Bibliotheksdatei später nicht mehr, dann erstellt eine Bibliothek des OBJECT. Für eine Header-only Bibliothek bietet sich eine CMake-Bibliothek des Typs INTERFACE an. Falls die benötigten Bibliotheken bereits als Dateien auf eurem System existieren, so könnt ihr diese mit einer Bibliothek des Typs IMPORTED innerhalb eures CMake-Projektes verfügbar machen.
Letztlich könnt ihr eure Bibliotheken auch unter einem anderen Namen ansprechen, zum Beispiel um Namespaces zu verwenden. Dazu nutzt ihr einfach eine CMake-Bibliothek des Typs ALIAS.
Weitere Informationen
Den Code zu diesem Beitrag könnt ihr gerne weiter verwenden, ihr findet ihn dazu auf GitHub.
In meinem Buch „CMake für Einsteiger“ und in meinen Videos auf YouTube stelle ich dieses und weitere CMake Themen noch einmal detaillierter vor. Bei Fragen oder Anmerkungen schreibt mir gerne einen Kommentar 🙂
Meine Webseite ist komplett werbefrei. Falls dir dieser Beitrag gefallen hat und du meine Arbeit gerne unterstützen möchtest, schau daher doch einmal auf meiner Support-Seite vorbei. Das würde mich sehr freuen :).