CMake - Publish Your Own Library
1. Introduction to Publishing CMake Projects
When developing a C++ library, especially one meant to be reused across multiple projects or distributed to others, it’s crucial to structure your project effectively and use a build system that facilitates this process. CMake is a powerful and widely-used build system that helps manage the build process in a platform-independent manner. This tutorial will guide you through the process of structuring and publishing your own CMake project, ensuring that your library is not only well-organized but also easy to build, test, and distribute.
CMake projects are organized to streamline the development process, allowing for easy integration of dependencies, consistent build processes across different platforms, and a clear separation between the public API, private code, and other components like tests. By following a well-defined structure, you make your project more maintainable and accessible to other developers, who can easily understand and contribute to your code.
2. Objectives
The primary objective of this article is to provide a comprehensive guide on setting up a CMake-based C++ project for public release. We will cover essential topics such as project structure, CMake configuration, handling platform-specific dependencies, and defining installation rules. By the end of this guide, you will be equipped to publish a well-organized, portable, and easily integrable library.
However, this guide will focus on the CMake configuration and project structuring aspects, rather than delving deeply into the testing or the internal implementation of the library itself. Testing will be mentioned briefly, but the core emphasis will be on preparing your library for public consumption through CMake.
3. Project Structure Overview
The structure of a CMake project plays a significant role in how easily it can be built, tested, and integrated into other projects. A well-organized project directory helps in separating different concerns such as public headers, source files, dependencies, and configuration files. Below is a typical structure for a CMake-based C++ project, which we will dissect:
my_library_project/ # Project Folder
│
├── CMakeLists.txt # Root CMake configuration file
├── cmake/
│ └── my_libraryConfig.cmake.in # CMake config template for installation
│
├── dependencies/ # External dependencies for different platforms
│ ├── win_x86_64/ # Dependencies for Windows x64
│ └── linux_x86_64/ # Dependencies for Linux x64
│
├── include/ # Public header files exposed to users
│ └── my_library/ # Library-specific headers
│ ├── my_library.h # Main public API header
│ └── my_project_export.h # Export macros for DLLs/shared libraries
│
├── my_library/ # Source code for the library
│ ├── CMakeLists.txt # Module-specific CMake configuration
│ │
│ ├── internal/ # Internal headers (not exposed to users)
│ │ └── internal_helpers.h # Helpers and private interfaces
│ │
│ └── source/ # Implementation files
│ ├── my_library.cpp # Implementation of the public API
│ └── internal_helpers.cpp # Implementation of internal functionality
│
│
└── tests/ # Test code for the library
├── CMakeLists.txt # CMake Configuration for tests
└── test_my_library.cpp # Unit or integration tests
Breakdown of the Project Structure
-
CMakeLists.txt (Root Level):
This file serves as the entry point for CMake, defining the project’s configuration and build settings. It sets the required CMake version, specifies the C++ standard, configures platform-specific dependencies, and adjusts compiler flags based on the build type. The file also defines output directories and includes subdirectories for building the main library and associated tests. Additionally, it manages required packages and ensures compatibility across different platforms. (jump to this section)
-
cmake/ Directory:
Contains custom CMake modules and configuration templates, such as
my_libraryConfig.cmake.in
, which is used for packaging and installing your library. This template allows users to find and link against your library usingfind_package
. (jump to this section) -
dependencies/ Directory:
This is where you would store any external dependencies your project relies on, especially if they are platform-specific. Subdirectories can be organized by platform, such as
win_x86_64
for Windows andlinux_x86_64
for Linux. Including dependencies here makes it easier to manage third-party libraries that are not readily available through package managers. Once your library is exported, you can re-utilize your library in another project, by putting it here. -
include/ Directory:
This folder contains the public API headers. These are the files that other projects will include when they use your library. The headers are usually organized in a subdirectory named after your library (e.g.,
my_library/
). The main API header (my_library.h
) should include the core functions and classes, while themy_project_export.h
file handles macro definitions that control symbol visibility, which is crucial when building shared libraries. -
my_library/ Directory:
This is the main codebase of your library. It typically contains a
CMakeLists.txt
file if the module needs its own build rules, aninternal/
directory for private headers not exposed to users, and asource/
directory for the implementation files (.cpp
). This separation between internal and public components helps in encapsulating the internal details of your library. -
tests/ Directory:
Contains unit or integration tests that validate your library’s functionality. These tests are usually written using a testing framework (like Google Test, Catch2, or Boost.Test) and should cover both the public API and critical internal components. The
test_my_library.cpp
file is an example test that might include various test cases to ensure your library works as expected. Here we will, however, focus on the project structure rather than on testing.
3.1. Export Overview
Once you install your library, the installation/
directory will contain everything required for others to use and integrate your library into their own projects. Let’s break down each component within this directory:
installation/ # Project installation root directory
│
├── bin/ # Contains runtime binaries (e.g., DLLs) if provided
│ └── libmy_library.dll # Compiled dynamic library (Windows)
|
├── include/ # Public header files that exposed to users
│ └── my_library/ # Namespace for your library headers
│ ├── my_library.h # Primary public API header
│ ├── ... # Your public API headers
│ └── my_project_export.h # Export macros for cross-platform compatibility
│
└── lib/ # Library and CMake configuration files
├── cmake/ # CMake package configuration directory
| └── my_library/ # CMake files for library integration
| └── my_libraryConfig.cmake # Configuration file for `find_package`
| └── my_libraryConfigVersion.cmake # Version-specific config for compatibility checks
| └── my_libraryTargets.cmake # Target definitions for linking the library
└── libmy_library.dll.a # Import library for GCC/MinGW (or .lib for MSVC)
Breakdown of the Installation Structure
-
bin/
Directory:The
bin/
directory holds the runtime binaries of the library. It contains the dynamically linked libraries (DLLs) for Windows, which are necessary at runtime when your library is used by external applications. These binaries are dynamically loaded during program execution and contain the compiled functionality of the library.libmy_library.dll
: This is the main dynamic library file for Windows, containing the compiled code that is loaded at runtime. It provides the core functionality of your library to the consumers. -
include/
Directory:This directory contains all the public API headers that your users will include in their projects to interface with your library. These headers define the classes, functions, and macros that are exposed to external projects. Organizing them in a subdirectory named after the library ensures clear separation and namespace management.
my_library.h
: The main public header file, this aggregates all the core functions and classes available in your library. It serves as the primary interface for users to access your library’s functionality.my_project_export.h
: This header defines export macros used for symbol visibility across platforms. -
lib/
Directory:The
lib/
directory contains the static or import libraries and the CMake configuration files necessary for integrating your library with other projects. This directory allows your library to be discovered and linked using CMake’sfind_package
command.-
cmake/
Directory: This subdirectory contains the CMake configuration files needed to allow other projects to find and link to your library. The files ensure that CMake knows how to configure, locate, and use your library’s targets. -
my_libraryConfig.cmake
: This is the main CMake configuration file used byfind_package
. It provides instructions on how to locate the library, including setting the paths for the library binaries and headers, and ensuring that the library is correctly linked. You define this file by using a template in thecmake
folder in your project. (jump to this section) -
my_libraryConfigVersion.cmake
: This file ensures version compatibility between the library and the consuming project. It allows CMake to check whether the version of the library being used meets the version constraints specified in the consuming project. -
my_libraryTargets.cmake
: This file defines the targets associated with your library. It provides CMake with the necessary details for linking the library, including any dependencies or settings required by the consumer project. -
libmy_library.dll.a
: This is the import library for GCC/MinGW, which allows projects to link against the.dll
file. For MSVC, this would be a.lib
file. It plays a critical role in the linking phase of the build process, providing the required interface to the shared library.
-
4. Root CMakeLists.txt
The root CMakeLists.txt
file is the cornerstone of your project’s build configuration. It defines the essential settings and directives that guide CMake in compiling, linking, and packaging your project. Writing a well-structured CMakeLists.txt
ensures that your project is not only maintainable but also portable across different environments and platforms. Below is a detailed explanation of the key components of this file.
4.1. Minimum CMake Version and Project Definition
To start, you must define the minimum version of CMake that is required to build your project, as well as the project name and version. This is not only a formality but also a fundamental part of your project’s identity.
cmake_minimum_required(VERSION 3.20)
project(my_library_project VERSION 0.1.0)
The cmake_minimum_required
command specifies the minimum version of CMake that is required to build your project. This ensures that all developers and continuous integration systems use a compatible version of CMake, avoiding potential issues caused by deprecated or unavailable features in older versions. Here, version 3.20 is chosen to leverage modern CMake capabilities.
The project
command defines the name of the project and its version. This is critical as it not only names your project but also associates a version number with it, which can be useful for packaging and version control purposes.
4.2. C++ Standard Configuration
In a C++ project, it’s critical to enforce a specific C++ standard across all compiled files to ensure consistency and avoid compatibility issues between different compilers or environments.
# This specifies that the C++20 standard should be enforced for all targets.
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
-
CMAKE_CXX_STANDARD
is set to20
, which enforces the C++20 standard. -
The
CMAKE_CXX_STANDARD_REQUIRED
directive ensures that the build process will halt if the compiler does not support C++20, preventing potential issues later. -
Disabling
CMAKE_CXX_EXTENSIONS
avoids using compiler-specific language extensions, which enhances the portability of your code across different compilers.
4.3. Compiler Flags Based on Build Type
Compiler flags are instrumental in controlling the behavior of the compiler, particularly when distinguishing between Debug and Release builds. This section configures these flags to optimize for either debugging or performance, depending on the build type.
if(CMAKE_BUILD_TYPE STREQUAL Debug)
message(STATUS "Configuring for a Debug build")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -g3 -Og")
add_definitions(-DDEBUG_MODE)
else()
message(STATUS "Configuring for a Release build")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -g0 -O3")
endif()
In Debug mode, the flags enable detailed debugging information (-g3
) and turn off aggressive optimizations (-Og
) to make the debugging process more straightforward.
Additionally, a DEBUG_MODE
macro is defined, which can be used within your code to conditionally include debug-specific logic.
In Release mode, the configuration focuses on optimization (-O3
) and minimal debug information (-g0
) to ensure maximum performance.
4.4. Platform-Specific Dependency Configuration
Handling dependencies can be complex, especially when targeting multiple platforms. This section of the CMakeLists.txt is designed to manage platform-specific dependencies and packaging configurations.
# Define the platform-specific paths and packaging configurations.
# This ensures that the correct dependencies are used depending on the platform.
if(WIN32)
# Set the dependency folder for Windows x64.
set(DEPENDENCY_FOLDER "${CMAKE_SOURCE_DIR}/dependencies/win_x86_64/")
# Use ZIP as the packaging format for Windows.
set(CPACK_GENERATOR "ZIP")
elseif (UNIX AND NOT APPLE)
# Set the dependency folder for Linux x64.
set(DEPENDENCY_FOLDER "${CMAKE_SOURCE_DIR}/dependencies/linux_x86_64/")
# Use TGZ (tarball gzip) as the packaging format for Linux.
set(CPACK_GENERATOR "TGZ")
else()
# Fatal error if the platform is unsupported.
message(FATAL_ERROR "Unsupported Platform: ${CMAKE_SYSTEM_NAME}")
endif()
This code block first checks the platform being used (WIN32
for Windows or UNIX
for Linux) and then sets the appropriate directory for dependencies and the correct packaging format (ZIP
for Windows and TGZ
for Linux). If the platform is not supported, CMake will throw a fatal error, stopping the build process.
4.5. CMake Prefix Path
The CMAKE_PREFIX_PATH
variable is used to specify additional directories where CMake should look for packages, particularly when they are not located in standard system paths. This is especially useful when working with third-party libraries that are bundled with your project.
# Set the CMake prefix path to include platform-specific dependency directories.
# This helps CMake locate the required libraries and packages.
set(CMAKE_PREFIX_PATH
"${DEPENDENCY_FOLDER}/pkgconfig/"
"${DEPENDENCY_FOLDER}/some_library/"
)
By setting the CMAKE_PREFIX_PATH, you direct CMake to search in the specified directories for the necessary libraries and packages, ensuring that the build process can locate and link all required dependencies.
Of course this assumes the dependencies are set for all platforms.
my_library_project/
└── dependencies/
├── win_x86_64/
| ├── pkgconfig/
| └── some_library/
└── linux_x86_64/
├── pkgconfig/
└── some_library/
4.6. Finding and Configuring Packages
Finding and linking external libraries is a common requirement in C++ projects. The find_package command helps automate this process by locating the necessary libraries and integrating them into your project.
find_package(PkgConfig REQUIRED)
Here, we use find_package to locate the PkgConfig tool, which is required to manage the inclusion of external libraries. By marking it as REQUIRED, we ensure that the build process will stop with an error if PkgConfig is not found, preventing any further issues.
4.7. Output Directory Configuration
To keep the build process organized, it’s important to specify where the compiled binaries and libraries should be output. This not only makes it easier to locate the build artifacts but also keeps your project directory clean.
# Set the output directories for the project.
# These settings ensure that all binaries, libraries, and archives are organized in one place.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/output")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/output")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/output/lib")
This configuration sets all the runtime executables, shared libraries, and static libraries to be placed in a central output
directory. The distinction between runtime, library, and archive outputs ensures that the files are well-organized.
4.8. Adding Subdirectories
In complex projects, organizing your code into subdirectories makes it easier to manage and maintain. The add_subdirectory
command allows you to include additional CMake configurations from different parts of your project.
# Add subdirectories for the library and tests.
# This modular approach keeps the project organized and easy to manage.
add_subdirectory(my_library)
add_subdirectory(tests)
By adding subdirectories, you modularize your project, allowing each component, such as the core library and its tests, to have its own CMakeLists.txt file. This makes the overall project more maintainable and scalable.
5. Library CMakeLists.txt
Now we will look at each section of the my_library/CMakeLists.txt
file, explaining the purpose and nuances of each part.
5.1. Defining the Library Target
In modern CMake, source files for a library are explicitly listed and associated with the target. This approach makes the build process clear and maintainable.
# Define the my_library library target
add_library(my_library SHARED)
# Add sources to the my_library library target
target_sources(my_library PRIVATE
source/my_library.cpp
source/internal_helpers.cpp
# Add more files as needed
)
-
add_library(my_library SHARED)
: This command defines a new library target namedmy_library
and specifies that it should be built as a shared library (SHARED
). -
target_sources(my_library PRIVATE ...)
: This command explicitly adds the specified source file (my_library.cpp
) to themy_library
library target. ThePRIVATE
keyword indicates that the source file is only relevant to this specific target and will not be exposed to other targets.
5.2. Passing Version Information to the Source Code
Passing the version number to the source code can be useful for embedding version information into the compiled library, which can be accessed at runtime.
target_compile_definitions(my_library PRIVATE MY_LIBRARY_VERSION="${PROJECT_VERSION}")
target_compile_definitions(my_library PRIVATE MY_LIBRARY_VERSION="${PROJECT_VERSION}")
: This command defines a preprocessor macro (MY_LIBRARY_VERSION
) with the value of the project’s version (${PROJECT_VERSION}
), making this version accessible in the source code. The PRIVATE
keyword indicates that this definition is only available within the my_library
target and not propagated to targets that link against this library.
5.3. Setting Build Flags for the Library
Defining specific macros during the build process is essential for controlling how the library is compiled and differentiating between internal and external usage of the library. (see the section about declaring visiblity)
target_compile_definitions(my_library PRIVATE BUILDING_MY_LIBRARY="1")
target_compile_definitions(my_library PRIVATE BUILDING_MY_LIBRARY="1")
: This command establishes the BUILDING_MY_LIBRARY macro with a value of “1”, signaling that the current build is for the library itself. This distinction is crucial for correctly applying the visibility attributes defined earlier, ensuring that internal components are exported properly while maintaining clean separation from the end-user’s environment.
5.4. Specifying Include Directories
Include directories are necessary for resolving header files during compilation. It’s important to specify both build-time and install-time include directories.
# Specify the include directories for the target
target_include_directories(my_library
PUBLIC
# Use absolute path for building
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
# Use relative path for installation
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/internal
)
target_include_directories(my_library PUBLIC ...)
: This command sets up include directories for themy_library
target. ThePUBLIC
keyword indicates that these directories should be used both when building the library and when other targets include this library.$<BUILD_INTERFACE:...>
and$<INSTALL_INTERFACE:...>
: These generator expressions ensure that different paths are used depending on whether the library is being built (BUILD_INTERFACE
) or installed (INSTALL_INTERFACE
). This separation ensures that the correct paths are used in each context, enhancing portability.PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/internal
: This directory is used for internal headers that should not be exposed to users of the library. ThePRIVATE
keyword indicates that these include directories are only used when compiling this target.
5.5. Linking External Libraries
To use external libraries like libmicrohttpd
, you need to find and link them to your project. CMake offers several ways to find libraries, including find_library
.
# Find and link the required libmicrohttpd library using pkg-config
find_library(MICROHTTPD_LIB NAMES microhttpd libmicrohttpd)
# Link the static microhttpd library
target_link_libraries(my_library PRIVATE ${MICROHTTPD_LIB})
find_library(MICROHTTPD_LIB NAMES microhttpd libmicrohttpd)
: This command searches for thelibmicrohttpd
library, storing the path in theMICROHTTPD_LIB
variable. TheNAMES
option specifies possible names for the library, accommodating different naming conventions on various systems.target_link_libraries(my_library PRIVATE ${MICROHTTPD_LIB})
: This command links thelibmicrohttpd
library to themy_library
target. ThePRIVATE
keyword ensures that this linkage is only relevant for themy_library
target and is not inherited by other targets that depend on it.
5.6. Configuring Visibility for Internal Functions
To reduce the binary size and improve load times, it’s important to hide internal symbols that do not need to be exposed outside the library.
target_compile_options(my_library PRIVATE -fvisibility=hidden)
set_target_properties(my_library PROPERTIES CXX_VISIBILITY_PRESET hidden)
set_target_properties(my_library PROPERTIES VISIBILITY_INLINES_HIDDEN YES)
target_compile_options(my_library PRIVATE -fvisibility=hidden)
: This option hides symbols by default, making only explicitly marked symbols (using__attribute__((visibility("default")))
) visible outside the library.set_target_properties(... PROPERTIES CXX_VISIBILITY_PRESET hidden)
: This sets the default visibility of C++ symbols to hidden.set_target_properties(... PROPERTIES VISIBILITY_INLINES_HIDDEN YES)
: This command hides inline function symbols, further reducing the number of exported symbols and improving binary efficiency.
5.7. Defining Installation Rules for the Library
Installing your library correctly ensures that it can be easily found and used by other projects. The installation rules specify where the library files will be placed on the target system.
# Installation Rules
# Define the installation directories
install(TARGETS my_library
EXPORT my_libraryTargets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
install(TARGETS my_library ...)
: This command defines how themy_library
library should be installed. TheARCHIVE
,LIBRARY
, andRUNTIME
options specify the directories for different types of build outputs (static libraries, shared libraries, and executables, respectively). TheINCLUDES DESTINATION
specifies where the public headers will be installed.EXPORT my_libraryTargets
: This option exports the target, which allows it to be included in the package configuration files, making the library discoverable by other projects usingfind_package
.
5.8. Installing Public Headers
install(DIRECTORY ../include/ DESTINATION include)
Alongside the compiled library, you must install the public headers so that they can be included by other projects.
install(DIRECTORY ../include/ DESTINATION include)
: This command installs the contents of the publicinclude/
directory to theinclude
directory on the target system. This is essential for making the public API available to other projects that depend on your library.
5.9. Installing Package Configuration Files
# Install the package configuration files
install(EXPORT my_libraryTargets
FILE my_libraryTargets.cmake
NAMESPACE my_library::
DESTINATION lib/cmake/my_library
)
# Create and install the package configuration files
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/my_libraryConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY AnyNewerVersion
)
configure_package_config_file(
"../cmake/my_libraryConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/my_libraryConfig.cmake"
INSTALL_DESTINATION lib/cmake/my_library
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/my_libraryConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/my_libraryConfigVersion.cmake"
DESTINATION lib/cmake/my_library
)
To make your library discoverable by CMake’s find_package
, you need to generate and install package configuration files.
install(EXPORT my_libraryTargets ...)
: This command exports the target configuration, allowing it to be used byfind_package
when other projects search formy_library
.include(CMakePackageConfigHelpers)
: This includes helpers for generating package configuration files.write_basic_package_version_file(...)
: Generates a version file that defines the compatibility of the library with different versions.configure_package_config_file(...)
: Configures the main package configuration file, which helps other projects find and link against your library.install(FILES ...)
: Installs the generated configuration files to the appropriate directory.
5.10. Configuring Package Information for Distribution
Finally, to make the library distributable, package information such as the package name, version, and contact details is configured using CPack
.
# Set the package name and version
set(CPACK_PACKAGE_NAME "my_library")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_CONTACT "Mustafa Alotbah <mustafa.alotbah@gmail.com>")
# CPACK_GENERATOR set by root CMakeLists.txt
include(CPack)
set(CPACK_PACKAGE_NAME "my_library")
: Sets the name of the package for distribution.set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
: Sets the package version, aligning it with the project version.set(CPACK_PACKAGE_CONTACT "Mustafa Alotbah <mustafa.alotbah@gmail.com>")
: Provides contact information for the package, useful for users or maintainers.include(CPack)
: Includes CPack, which handles the creation of distribution packages based on the specified configuration.
6. CMake Template File
The my_libraryConfig.cmake.in
file is a template used by CMake to generate a my_libraryConfig.cmake
file during the installation process. This configuration file plays a crucial role in making your library discoverable and easily integrable into other projects through CMake’s find_package
command.
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/my_libraryTargets.cmake")
The my_libraryConfig.cmake.in
file typically contains initialization code and references to other generated files, such as the my_libraryTargets.cmake
:
-
@PACKAGE_INIT@
Macro is a placeholder that is replaced by CMake with the necessary initialization code when the file is processed. The@PACKAGE_INIT@
macro is essential as it sets up the environment for the package configuration. It ensures that any necessary CMake variables are initialized and that the package configuration is compatible with the CMake version used by the consuming project. -
Including the
my_libraryTargets.cmake
File: This line includes themy_libraryTargets.cmake
file, which contains the actual definitions of the targets (such as themy_library
library) that will be exported during the installation process. This inclusion is critical for making the targets available to projects that usefind_package
to locate your library.Including the
my_libraryTargets.cmake
file is what allows other projects to link against your library. It ensures that all necessary targets, build settings, and dependencies are correctly set up in the consuming project. Without this inclusion, the library would not be properly registered with CMake, making it unavailable for use.
6.1. Defining IMPORTED_IMPLIB
for Windows Platforms
When distributing libraries on Windows, there are often differences in how dynamic libraries (.dll
) are linked, depending on the compiler. For this, we use the IMPORTED_IMPLIB
property in CMake to define the appropriate import library (.lib
or .dll.a
) for Windows toolchains.
We can check if we are on Windows platform if (WIN32)
and in this case we should distinguish between MinGW/GCC where .dll.a
is used, whereas .lib
is used by MSVC compilers.
if (WIN32)
# Check for GNU / Mingw Compilers
if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|MinGW")
# Configure `IMPORTED_IMPLIB` to point to the .dll.a library that will be created
set_target_properties(Logify::Logify PROPERTIES
IMPORTED_IMPLIB "${CMAKE_CURRENT_LIST_DIR}/../../libmy_library.dll.a")
# Otherwise assume it is MSVC
else()
# Configure `IMPORTED_IMPLIB` to point to the .lib library that will be created
set_target_properties(Logify::Logify PROPERTIES
IMPORTED_IMPLIB "${CMAKE_CURRENT_LIST_DIR}/../../libmy_library.lib")
endif()
endif()
7. Source Code
When designing a C++ library that will be used by other projects, it is crucial to manage the visibility of functions and symbols properly. This is particularly important when creating shared libraries, where you want to expose only the necessary API functions while keeping internal details hidden.
7.1. Source Code Structure
For demonstration purposes, we will implement a simple API function, std::string getVersion()
, which returns the version of the library. This API function will internally rely on another function, std::string internalGetVersion()
, which should remain hidden from the library users. The internal function will only be accessible within the library’s codebase.
Given that we have configured the default visibility of all symbols to be hidden (as detailed in the section on configuring visibility for internal functions), we must explicitly declare which symbols should be visible to the outside world. This is achieved using visibility attributes that are platform-specific.
7.2. Declaring Visibility in Header Files
To control the visibility of our API and internal functions, we use different attributes depending on the platform.
7.2.1. For GCC and MSVC on Windows
When compiling a shared library on Windows, the __declspec(dllexport)
attribute is used to export functions from a DLL, making them available to other projects that link against the DLL. Conversely, __declspec(dllimport)
is used in the client code to import these functions. We encapsulate this logic in a macro MY_LIBRARY_API
:
#ifdef BUILDING_MY_LIBRARY
#define MY_LIBRARY_API __declspec(dllexport)
#else
#define MY_LIBRARY_API __declspec(dllimport)
#endif
7.2.2. For GCC on Linux
On Linux, GCC provides a visibility attribute, __attribute__((visibility("default")))
, which is used to mark symbols that should be visible outside the library. We also define an internal visibility attribute to explicitly hide symbols:
#if defined(__GNUC__) && __GNUC__ >= 4
#define MY_LIBRARY_API __attribute__((visibility("default")))
#else
#define MY_LIBRARY_API
#endif
7.2.3. Combined Platform-Independent Definition
To maintain cross-platform compatibility, we combine these definitions into a single header file, my_project_export.h
. This header ensures that the correct visibility attributes are applied based on the target platform:
#pragma once
#ifdef _WIN32
#ifdef BUILDING_MY_LIBRARY
#define MY_LIBRARY_API __declspec(dllexport)
#else
#define MY_LIBRARY_API __declspec(dllimport)
#endif
#else
#if defined(__GNUC__) && __GNUC__ >= 4
#define MY_LIBRARY_API __attribute__((visibility("default")))
#else
#define MY_LIBRARY_API
#endif
#endif
- The flag
BUILDING_MY_LIBRARY
is defined in the building process in theCMakeLists.txt
of the library, see here. It is only defined in the build process of the library but must not be defined in the build process of the user.
7.3. Public API Declaration
In the public API header, my_library.h
, we use the MY_LIBRARY_API
macro to declare the visibility of the getVersion()
function:
#pragma once
#include "my_project_export.h"
#include <string>
MY_LIBRARY_API std::string getVersion();
This ensures that getVersion()
is visible to any project that links against the library, while other internal functions remain hidden.
7.4. Internal Function Declaration
In contrast, the internal function internalGetVersion()
is declared in an internal header, internal_helpers.h
, without any visibility attributes, meaning it will remain hidden:
#pragma once
#include <string>
std::string internalGetVersion();
Since this header is marked as PRIVATE
in the CMake configuration (as discussed in the section on specifying include directories), it is not exposed to the users of the library when the library is installed.
7.5. Implementing the Functions
The internal function internalGetVersion()
is implemented in the source/internal_helpers.cpp
file. This function retrieves the version information, which we previously passed to the source code using a preprocessor definition in our CMake configuration:
#include "internal_server.h"
std::string internalGetVersion() {
return MY_LIBRARY_VERSION;
}
The MY_LIBRARY_VERSION
macro was defined earlier in the CMake configuration (as detailed in the section on passing version information to the source code).
The getVersion()
API function, which calls the internal function, is implemented in source/my_library.cpp
:
#include "my_library/my_library.h"
#include "my_library_internal.h"
std::string getVersion() {
return internalGetVersion();
}
This structure ensures that while getVersion()
is accessible to external projects, internalGetVersion()
remains encapsulated within the library, hidden from external access.
7.6. Verifying Visibility and Symbol Export
To verify that only the intended symbols are exposed, you can inspect the exported symbols of the compiled shared library. On Linux, this can be done using the nm
command:
nm -C -D lib/libmy_library.so | grep " T "
>>>
0000000000001120 T getVersion[abi:cxx11]()
This command lists all symbols marked as globally visible (T
). As expected, only getVersion()
is exposed.
On Windows, you can use the dumpbin
tool to inspect the DLL exports:
dumpbin /EXPORTS libmy_library.dll
>>>
...
ordinal hint RVA name
1 0 00001370 _Z10getVersionB5cxx11v
Summary
...
Again, only the getVersion()
function is visible, confirming that the internal details of the library remain hidden as intended.
8. Installing the Project
After configuring and building the library, it is crucial to install it correctly. This ensures that all necessary files—binaries, headers, and configuration files—are placed in the appropriate directories, ready for use by other projects.
mkdir build
cd build
cmake .. -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release
cmake --build . --config Release
cmake --install . --prefix .
These commands create a build directory, configure the project for a release build, and then install the library. The installation process places the compiled binaries, headers, and CMake configuration files in the specified prefix directory.
To package the project for distribution, you can use CPack:
cpack
CPack generates a package (e.g., a ZIP or TGZ file) containing the installed files, making it easy to distribute the library.