CMake: Exporting Header Files

CMake: Exporting Header Files
CMake: Exporting Header Files

Introduction:

In fact, the relevant content of this article has been detailed in the CMake: Detailed Explanation of Static and Dynamic Libraries (Linux/Windows) notes, where it shows that CMake provides a platform-independent way to achieve its functionality. However, the issue of symbol visibility has not been addressed. The best approach regarding symbol visibility is to specify that dynamic libraries only expose the minimal set of symbols, thereby limiting the visibility of objects and functions defined in the code.

We hope that, by default, all symbols defined in dynamic libraries are hidden from external visibility. This will allow contributors to the project to clearly delineate the interface between the library and external code, as they must explicitly mark all symbols to be used outside the project.

CMake: Exporting Header Files

Project Structure

├── CMakeLists.txt
├── src
│   ├── CMakeLists.txt
│   ├── hello_world.cpp
│   ├── message.cpp
│   └── message.hpp
└── test
    └── CMakeLists.txt

Project Address:

https://gitee.com/jiangli01/tutorials/tree/master/cmake-tutorial/chapter9/02

Related Source Code

CMakeLists.txt

# CMake 3.6 needed for IMPORTED_TARGET option
# to pkg_search_module
cmake_minimum_required(VERSION 3.6 FATAL_ERROR)

project(
  example
  LANGUAGES CXX
  VERSION 1.0.0
)

# <<< General set up >>>

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Check if the installation prefix has been set
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
    # Set the installation directory to the output folder under the project source directory
    set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/output/" CACHE PATH "..." FORCE)
endif()

message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX}")

if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

message(STATUS "Build type set to ${CMAKE_BUILD_TYPE}")

include(GNUInstallDirs)

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})

# Offer the user the choice of overriding the installation directories
set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Installation directory for libraries")
set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Installation directory for executables")
set(INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Installation directory for header files")
if(WIN32 AND NOT CYGWIN)
  set(DEF_INSTALL_CMAKEDIR CMake)
else()
  set(DEF_INSTALL_CMAKEDIR share/cmake/${PROJECT_NAME})
endif()
set(INSTALL_CMAKEDIR ${DEF_INSTALL_CMAKEDIR} CACHE PATH "Installation directory for CMake files")

# Report to user
foreach(p LIB BIN INCLUDE CMAKE)
  file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${INSTALL_${p}DIR} _path )
  message(STATUS "Installing ${p} components to ${_path}")
  unset(_path)
endforeach()

add_subdirectory(src)

enable_testing()

add_subdirectory(test)

src/CMakeLists.txt

# Search for pkg-config and UUID
find_package(PkgConfig QUIET)
if(PKG_CONFIG_FOUND)
  pkg_search_module(UUID uuid IMPORTED_TARGET)
  if(TARGET PkgConfig::UUID)
    message(STATUS "Found libuuid")
    set(UUID_FOUND TRUE)
  endif()
endif()

# <<< Build targets >>>

# SHARED library
add_library(message-shared SHARED "")

target_sources(message-shared
  PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}/message.cpp
  )

target_compile_definitions(message-shared
  PUBLIC
    $<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
  )

target_link_libraries(message-shared
  PUBLIC
    $<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
  )

set_target_properties(message-shared
  PROPERTIES
    POSITION_INDEPENDENT_CODE 1
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN 1
    SOVERSION ${PROJECT_VERSION_MAJOR}
    OUTPUT_NAME "message"
    DEBUG_POSTFIX "_d"
    PUBLIC_HEADER "message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/message_export.h"
    MACOSX_RPATH ON
  )

include(GenerateExportHeader)
generate_export_header(message-shared
  BASE_NAME "message"
  EXPORT_MACRO_NAME "MESSAGE_LIB_API"
  EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/message_export.h"
  STATIC_DEFINE "MESSAGE_STATIC_DEFINE"
  DEFINE_NO_DEPRECATED
  )

target_include_directories(message-shared
  PUBLIC
    ${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}
  )

# STATIC library
add_library(message-static STATIC "")

target_sources(message-static
  PRIVATE
    ${CMAKE_CURRENT_LIST_DIR}/message.cpp
  )

target_compile_definitions(message-static
  PUBLIC
    MESSAGE_STATIC_DEFINE
    $<$<BOOL:${UUID_FOUND}>:HAVE_UUID>
  )

target_include_directories(message-static
  PUBLIC
    ${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}
  )

target_link_libraries(message-static
  PUBLIC
    $<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID>
  )

set_target_properties(message-static
  PROPERTIES
    POSITION_INDEPENDENT_CODE 1
    ARCHIVE_OUTPUT_NAME "message"
    DEBUG_POSTFIX "_sd"
    RELEASE_POSTFIX "_s"
    PUBLIC_HEADER "message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/message_export.h"
  )

# EXECUTABLES
add_executable(hello-world_wDSO hello_world.cpp)

target_link_libraries(hello-world_wDSO
  PUBLIC
    message-shared
  )

# Prepare RPATH

file(RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX})
if(APPLE)
  set(_rpath "@loader_path/${_rel}")
else()
  set(_rpath "$ORIGIN/${_rel}")
endif()
file(TO_NATIVE_PATH "${_rpath}/${INSTALL_LIBDIR}" message_RPATH)

set_target_properties(hello-world_wDSO
  PROPERTIES
    MACOSX_RPATH ON
    SKIP_BUILD_RPATH OFF
    BUILD_WITH_INSTALL_RPATH OFF
    INSTALL_RPATH "${message_RPATH}"
    INSTALL_RPATH_USE_LINK_PATH ON
  )

add_executable(hello-world_wAR hello_world.cpp)

target_link_libraries(hello-world_wAR
  PUBLIC
    message-static
  )

# <<< Install and export targets >>>

install(
  TARGETS
    message-shared
    message-static
    hello-world_wDSO
    hello-world_wAR
  ARCHIVE
    DESTINATION ${INSTALL_LIBDIR}
    COMPONENT lib
  RUNTIME
    DESTINATION ${INSTALL_BINDIR}
    COMPONENT bin
  LIBRARY
    DESTINATION ${INSTALL_LIBDIR}
    COMPONENT lib
  PUBLIC_HEADER
    DESTINATION ${INSTALL_INCLUDEDIR}/message
    COMPONENT dev
  )
set_target_properties(message-shared
  PROPERTIES
    POSITION_INDEPENDENT_CODE 1
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN 1
    SOVERSION ${PROJECT_VERSION_MAJOR}
    OUTPUT_NAME "message"
    DEBUG_POSTFIX "_d"
    PUBLIC_HEADER "message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/message_export.h"
    MACOSX_RPATH ON
  )
  • CXX_VISIBILITY_PRESET hidden: Sets the default symbol visibility. Here, it is set to hidden, which means that unless explicitly specified, all symbols will not be exported. This helps to reduce the size of the final binary, improve loading speed, and provide a degree of encapsulation.

  • VISIBILITY_INLINES_HIDDEN 1: When using hidden visibility, this option also hides the symbols of inline functions. This further reduces the number of exported symbols.

  • OUTPUT_NAME “message”: This specifies the output name of the target. In this case, regardless of the actual name of the target (here it is message-shared), the generated file will be named message.

  • PUBLIC_HEADER “message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/message_export.h”: Specifies the public header files. These files will be treated specially when installing the target, usually copied to the include directories.

  • MACOSX_RPATH ON: This is a macOS-specific option that enables the use of relative paths (rpath). RPath is a mechanism for runtime searching of shared libraries, which is important for ensuring that applications can find their dependent shared libraries.

generate_export_header(message-shared
  BASE_NAME "message"
  EXPORT_MACRO_NAME "MESSAGE_LIB_API"
  EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/message_export.h"
  STATIC_DEFINE "MESSAGE_STATIC_DEFINE"
  DEFINE_NO_DEPRECATED
  )
  • BASE_NAME “message”: This specifies the base name of the generated macro. The generated macro will be based on the name “message”, e.g., MESSAGE_EXPORT.

  • EXPORT_MACRO_NAME “MESSAGE_LIB_API”: This defines the macro name used for exporting symbols. You can use MESSAGE_LIB_API to mark classes, functions, or variables that need to be exported.

  • EXPORT_FILE_NAME “${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/message_export.h”: Specifies the name and location of the generated header file. This header file will contain the necessary preprocessor directives to change the behavior of the macro based on whether the library itself is being compiled or used.

  • STATIC_DEFINE “MESSAGE_STATIC_DEFINE”: This macro will be defined when the library is statically linked. This is useful for seamlessly switching between static and shared linking.

  • DEFINE_NO_DEPRECATED: This will define a macro to disable warnings for deprecated functions in the library. This is very useful for maintaining older codebases as it allows developers to compile old code without triggering deprecation warnings (of course, this macro can be omitted).

src/message.hpp

#pragma once

#include <iosfwd>
#include <string>
#include "message_export.h"
class MESSAGE_LIB_API Message {
public:
  Message(const std::string &m) : message_(m) {}

  friend std::ostream &operator<<(std::ostream &os, Message &obj) {
    return obj.PrintObject(os);
  }

private:
  std::string message_;
  std::ostream &PrintObject(std::ostream &os);
};

std::string GetUUID();

src/message.cpp

src.hello_world.cpp

#include "message.hpp"

#include <iostream>
#include <string>

#ifdef HAVE_UUID
#include <uuid/uuid.h>
#endif

std::ostream &Message::PrintObject(std::ostream &os) {
  os << "This is my very nice message: " << std::endl;
  os << message_ << std::endl;
  os << "...and here is its UUID: " << GetUUID();

  return os;
}

#ifdef HAVE_UUID
std::string GetUUID() {
  uuid_t uuid;
  uuid_generate(uuid);
  char uuid_str[37];
  uuid_unparse_lower(uuid, uuid_str);
  uuid_clear(uuid);
  std::string uuid_cxx(uuid_str);
  return uuid_cxx;
}
#else
std::string GetUUID() { return "Ooooops, no UUID for you!"; }
#endif

test/CMakeLists.txt

add_test(
  NAME test_shared
  COMMAND $<TARGET_FILE:hello-world_wDSO>
  )

add_test(
  NAME test_static
  COMMAND $<TARGET_FILE:hello-world_wAR>
  )

Results Display

& mkdir build 
& cd build
& cmake ..
& cmake --build . --target install


Scanning dependencies of target message-static
[ 12%] Building CXX object src/CMakeFiles/message-static.dir/message.cpp.o
[ 25%] Linking CXX static library ../lib/libmessage_s.a
[ 25%] Built target message-static
Scanning dependencies of target hello-world_wAR
[ 37%] Building CXX object src/CMakeFiles/hello-world_wAR.dir/hello_world.cpp.o
[ 50%] Linking CXX executable ../bin/hello-world_wAR
[ 50%] Built target hello-world_wAR
Scanning dependencies of target message-shared
[ 62%] Building CXX object src/CMakeFiles/message-shared.dir/message.cpp.o
[ 75%] Linking CXX shared library ../lib/libmessage.so
[ 75%] Built target message-shared
Scanning dependencies of target hello-world_wDSO
[ 87%] Building CXX object src/CMakeFiles/hello-world_wDSO.dir/hello_world.cpp.o
[100%] Linking CXX executable ../bin/hello-world_wDSO
[100%] Built target hello-world_wDSO
Install the project...
-- Install configuration: "Release"
-- Installing: /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/02/output/lib/libmessage.so.1
-- Up-to-date: /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/02/output/lib/libmessage.so
-- Up-to-date: /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/02/output/include/message/message.hpp
-- Installing: /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/02/output/include/message/message_export.h
-- Installing: /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/02/output/lib/libmessage_s.a
-- Up-to-date: /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/02/output/include/message/message.hpp
-- Up-to-date: /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/02/output/include/message/message_export.h
-- Installing: /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/02/output/bin/hello-world_wDSO
-- Set runtime path of "/home/jiangli/repo/tutorials/cmake-tutorial/chapter9/02/output/bin/hello-world_wDSO" to "$ORIGIN/../lib"
-- Installing: /home/jiangli/repo/tutorials/cmake-tutorial/chapter9/02/output/bin/hello-world_wAR

Output:CMake: Exporting Header FilesFinally, I wish everyone to become stronger!!! If this note has helped you, please like and share!!!🌹🌹🌹

CMake: Exporting Header Files
CMake: Exporting Header Files

Leave a Comment