Shared libraries, symbol visibility and RPATH

CMake · Expert · 15 min

Shared-library details decide whether your binaries run after installation and whether your ABI stays stable.

Shared libraries add concerns static ones don't have.

Symbol visibility. On Linux every function is exported by default; on Windows nothing is unless marked __declspec(dllexport). Make both behave the same by hiding symbols by default and exporting your API explicitly:

  • set_target_properties(geo PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON)
  • include(GenerateExportHeader) and generate_export_header(geo) create geo_export.h with a GEO_EXPORT macro to put on public classes and functions.

Smaller export tables load faster, avoid symbol clashes and make ABI breakage visible.

Versioning. VERSION 1.4.2 and SOVERSION 1 properties produce libgeo.so.1.4.2 with the SONAME libgeo.so.1 and symlinks. Bump SOVERSION only when you break the ABI.

Finding libraries at run time. An executable records an RPATH listing directories to search. CMake gives build-tree binaries an RPATH pointing at the build tree, and by default strips it at install time, so installed programs may fail with "cannot open shared object file". Set CMAKE_INSTALL_RPATH to a relative path, $ORIGIN/../lib on Linux or @loader_path/../lib on macOS, to make the installed tree relocatable. Windows has no RPATH: DLLs must sit next to the executable or on PATH, which $<TARGET_RUNTIME_DLLS:app> (3.21+) helps copy.

Example

cmake_minimum_required(VERSION 3.25)
project(geo VERSION 1.4.2 LANGUAGES CXX)
include(GNUInstallDirs)
include(GenerateExportHeader)

add_library(geo SHARED src/geo.cpp)
generate_export_header(geo)                 # writes geo_export.h with GEO_EXPORT
target_include_directories(geo PUBLIC
  "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include;${CMAKE_CURRENT_BINARY_DIR}>")
set_target_properties(geo PROPERTIES
  CXX_VISIBILITY_PRESET hidden
  VISIBILITY_INLINES_HIDDEN ON
  VERSION ${PROJECT_VERSION}                # libgeo.so.1.4.2
  SOVERSION ${PROJECT_VERSION_MAJOR})       # SONAME libgeo.so.1

add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE geo)
set_target_properties(app PROPERTIES INSTALL_RPATH "$ORIGIN/../${CMAKE_INSTALL_LIBDIR}")
install(TARGETS geo app)

# include/geo.h:  #include "geo_export.h"
#                 GEO_EXPORT double area(double r);   // exported; helpers stay hidden
# $ readelf -d build/libgeo.so | grep SONAME   -> [libgeo.so.1]

Remember

  • Hide symbols by default and export the API with generate_export_header
  • VERSION and SOVERSION set file names and the SONAME
  • Set INSTALL_RPATH to $ORIGIN/../lib so installed binaries find their libraries

Watch out for

  • Installing a program linked to your shared library without an install RPATH, so it only runs from the build tree.

Check yourself

  1. An installed executable fails with 'error while loading shared libraries: libgeo.so.1'. It ran fine from the build directory. Why?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: cmake.org/cmake/help/latest/module/GenerateExportHeader.html