Post

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 using find_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 and linux_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 the my_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, an internal/ directory for private headers not exposed to users, and a source/ 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’s find_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 by find_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 the cmake 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 to 20, 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 named my_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 the my_library library target. The PRIVATE 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 the my_library target. The PUBLIC 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. The PRIVATE 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 the libmicrohttpd library, storing the path in the MICROHTTPD_LIB variable. The NAMES 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 the libmicrohttpd library to the my_library target. The PRIVATE keyword ensures that this linkage is only relevant for the my_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 the my_library library should be installed. The ARCHIVE, LIBRARY, and RUNTIME options specify the directories for different types of build outputs (static libraries, shared libraries, and executables, respectively). The INCLUDES 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 using find_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 public include/ directory to the include 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 by find_package when other projects search for my_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 the my_libraryTargets.cmake file, which contains the actual definitions of the targets (such as the my_library library) that will be exported during the installation process. This inclusion is critical for making the targets available to projects that use find_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 the CMakeLists.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.

This post is licensed under CC BY 4.0 by the author.