Static Libraries
A static library is an archive of compiled object files that gets linked directly into the final executable at build time. The linker copies the needed code from the archive into the binary, producing a self-contained executable with no runtime library dependencies.
.a extension; on Windows they use .lib.
Create a static library with add_library() using the STATIC keyword:
# CMakeLists.txt — Creating a static library
cmake_minimum_required(VERSION 3.20)
project(MathLib VERSION 1.0 LANGUAGES CXX)
# Create a static library from source files
add_library(mathlib STATIC
src/algebra.cpp
src/calculus.cpp
src/statistics.cpp
)
# Specify include directories for consumers
target_include_directories(mathlib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# Link the library to an executable
add_executable(calculator src/main.cpp)
target_link_libraries(calculator PRIVATE mathlib)
When CMake builds this project, it compiles each .cpp into an object file, then archives them into libmathlib.a (Unix) or mathlib.lib (Windows). The executable calculator links against this archive at build time.
When to Use Static Libraries
- Self-contained deployment — No shared library dependencies to manage at runtime
- Performance-critical paths — Enables link-time optimization (LTO) across library boundaries
- Embedded systems — Single binary with no dynamic loader available
- Small libraries — Overhead of shared library machinery not justified
The linker is smart about static libraries: it only pulls in object files that resolve undefined symbols. If your executable uses algebra.cpp functions but not statistics.cpp, the statistics code is excluded from the final binary.
Shared Libraries
A shared library (also called dynamic library) is loaded at runtime by the operating system's dynamic linker. Multiple programs can share a single copy of the library in memory, reducing total system resource usage.
# CMakeLists.txt — Creating a shared library
cmake_minimum_required(VERSION 3.20)
project(NetworkLib VERSION 2.3.1 LANGUAGES CXX)
# Create a shared library
add_library(netlib SHARED
src/socket.cpp
src/http_client.cpp
src/dns_resolver.cpp
)
# Set public include directories
target_include_directories(netlib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# Set version properties
set_target_properties(netlib PROPERTIES
VERSION ${PROJECT_VERSION} # libnetlib.so.2.3.1
SOVERSION ${PROJECT_VERSION_MAJOR} # libnetlib.so.2 symlink
)
# Link executable against the shared library
add_executable(web_client src/main.cpp)
target_link_libraries(web_client PRIVATE netlib)
On Linux, this produces three filesystem entries:
# Shared library files on Linux after build
ls -la lib/
# libnetlib.so -> libnetlib.so.2 (development symlink)
# libnetlib.so.2 -> libnetlib.so.2.3.1 (SOVERSION symlink)
# libnetlib.so.2.3.1 (actual library file)
Library Versioning with SOVERSION
The VERSION and SOVERSION properties control the symlink structure on Unix systems. The SOVERSION represents the ABI version — increment it when you make binary-incompatible changes:
# CMakeLists.txt — SOVERSION for ABI compatibility
cmake_minimum_required(VERSION 3.20)
project(Codec VERSION 3.2.0 LANGUAGES CXX)
add_library(codec SHARED src/encoder.cpp src/decoder.cpp)
set_target_properties(codec PROPERTIES
# Full version: libcodec.so.3.2.0
VERSION 3.2.0
# ABI version: libcodec.so.3 (symlink)
# Increment SOVERSION only on ABI-breaking changes
SOVERSION 3
)
Platform-Specific Shared Library Files
| Platform | Extension | Example |
|---|---|---|
| Linux | .so | libnetlib.so.2.3.1 |
| macOS | .dylib | libnetlib.2.3.1.dylib |
| Windows | .dll + .lib | netlib.dll + netlib.lib (import lib) |
Object Libraries
An object library compiles source files into object files but does not archive or link them. It acts as a logical grouping of compiled objects that can be incorporated into other targets. Object libraries avoid the overhead of creating an actual archive file.
# CMakeLists.txt — Object library for code reuse
cmake_minimum_required(VERSION 3.20)
project(GameEngine VERSION 1.0 LANGUAGES CXX)
# Create object library — compiles sources but doesn't create .a or .so
add_library(engine_core OBJECT
src/renderer.cpp
src/physics.cpp
src/audio.cpp
)
target_include_directories(engine_core PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
# Use objects in a shared library
add_library(engine SHARED $<TARGET_OBJECTS:engine_core> src/engine_api.cpp)
# Use same objects in a static library (no recompilation!)
add_library(engine_static STATIC $<TARGET_OBJECTS:engine_core> src/engine_api.cpp)
# Or link directly (CMake 3.12+)
add_executable(game src/main.cpp)
target_link_libraries(game PRIVATE engine_core)
Object Library Use Cases
- Compile once, use in multiple targets — Build both static and shared variants from the same objects
- Position-independent code control — Compile with
-fPIConce, use in both shared libraries and executables - Internal modularization — Split large targets into logical groups without creating actual library files
target_link_libraries() with object libraries directly. Before 3.12, you had to use the $<TARGET_OBJECTS:name> generator expression exclusively.
Module Libraries
A module library is a shared library that is loaded at runtime via dlopen() (Unix) or LoadLibrary() (Windows) rather than linked at build time. It's the mechanism behind plugin architectures.
# CMakeLists.txt — Plugin system with MODULE libraries
cmake_minimum_required(VERSION 3.20)
project(PluginHost VERSION 1.0 LANGUAGES CXX)
# The host application that loads plugins
add_executable(host src/host.cpp src/plugin_loader.cpp)
target_include_directories(host PRIVATE include)
# A plugin — loaded at runtime, NOT linked at build time
add_library(plugin_reverb MODULE plugins/reverb.cpp)
target_include_directories(plugin_reverb PRIVATE include)
# Plugins don't have the "lib" prefix on all platforms
set_target_properties(plugin_reverb PROPERTIES
PREFIX "" # Output: plugin_reverb.so (not libplugin_reverb.so)
SUFFIX ".plugin" # Custom extension: plugin_reverb.plugin
)
# Another plugin
add_library(plugin_echo MODULE plugins/echo.cpp)
set_target_properties(plugin_echo PROPERTIES PREFIX "" SUFFIX ".plugin")
SHARED library can be linked to at build time with target_link_libraries(). A MODULE library cannot — it's designed exclusively for runtime loading. On most platforms they produce the same file format, but the semantic distinction matters for CMake's dependency tracking.
Interface Libraries
An interface library has no compiled source files. It exists purely to propagate usage requirements — include directories, compile definitions, link dependencies — to consuming targets. This is the canonical pattern for header-only libraries.
# CMakeLists.txt — Header-only library using INTERFACE
cmake_minimum_required(VERSION 3.20)
project(JsonParser VERSION 1.0 LANGUAGES CXX)
# No source files! Header-only library
add_library(json_parser INTERFACE)
# All properties use INTERFACE scope (propagated to consumers)
target_include_directories(json_parser INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# Require C++17 from consumers
target_compile_features(json_parser INTERFACE cxx_std_17)
# Propagate a dependency
target_compile_definitions(json_parser INTERFACE JSON_PARSER_VERSION=1)
# Consumer automatically gets include paths and C++17 requirement
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE json_parser)
Interface libraries are also useful for creating "meta targets" that bundle multiple dependencies:
# CMakeLists.txt — Meta-target bundling dependencies
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0 LANGUAGES CXX)
# Bundle multiple dependencies into one logical target
add_library(platform_deps INTERFACE)
target_link_libraries(platform_deps INTERFACE
Threads::Threads
${CMAKE_DL_LIBS}
$<$<PLATFORM_ID:Linux>:rt>
)
add_executable(server src/server.cpp)
target_link_libraries(server PRIVATE platform_deps)
flowchart LR
subgraph "Library Types"
STATIC["STATIC
.a / .lib
Linked at build time"]
SHARED["SHARED
.so / .dylib / .dll
Linked at load time"]
OBJECT["OBJECT
No archive file
Object files only"]
MODULE["MODULE
.so / .dll
dlopen() at runtime"]
INTERFACE["INTERFACE
No output file
Headers + properties only"]
end
subgraph "Usage"
EXE[Executable]
end
STATIC -->|"code copied into"| EXE
SHARED -->|"referenced at runtime"| EXE
OBJECT -->|"objects merged into"| EXE
MODULE -.->|"loaded dynamically"| EXE
INTERFACE -->|"properties propagated"| EXE
BUILD_SHARED_LIBS
When add_library() is called without an explicit type (STATIC/SHARED), CMake checks the BUILD_SHARED_LIBS variable to decide. This lets users choose at configure time whether they want static or shared libraries:
# CMakeLists.txt — Letting users choose library type
cmake_minimum_required(VERSION 3.20)
project(FlexLib VERSION 1.0 LANGUAGES CXX)
# Provide an option (defaults to OFF = static)
option(BUILD_SHARED_LIBS "Build shared libraries instead of static" OFF)
# No explicit STATIC or SHARED — respects BUILD_SHARED_LIBS
add_library(flexlib
src/core.cpp
src/utils.cpp
)
target_include_directories(flexlib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE flexlib)
Users choose at configure time:
# Build with static libraries (default)
cmake -B build -DBUILD_SHARED_LIBS=OFF
# Build with shared libraries
cmake -B build -DBUILD_SHARED_LIBS=ON
add_library() to give consumers flexibility. Only hard-code STATIC or SHARED when the library fundamentally requires one type (e.g., a plugin must be MODULE, a header-only library must be INTERFACE).
Linking Semantics
The target_link_libraries() command accepts three visibility keywords that control how dependencies propagate through the dependency graph:
| Keyword | Compiles with? | Links with? | Propagates to consumers? |
|---|---|---|---|
PRIVATE | Yes | Yes | No |
PUBLIC | Yes | Yes | Yes |
INTERFACE | No | No | Yes |
# CMakeLists.txt — Linking visibility in practice
cmake_minimum_required(VERSION 3.20)
project(Networking VERSION 1.0 LANGUAGES CXX)
# Low-level crypto library
add_library(crypto STATIC src/crypto/aes.cpp src/crypto/sha256.cpp)
target_include_directories(crypto PUBLIC include/crypto)
# TLS library uses crypto internally but doesn't expose it
add_library(tls STATIC src/tls/handshake.cpp src/tls/record.cpp)
target_include_directories(tls PUBLIC include/tls)
target_link_libraries(tls
PUBLIC Threads::Threads # Consumers also need threads
PRIVATE crypto # Crypto is an implementation detail
)
# HTTP library builds on TLS (exposes TLS in its API)
add_library(http STATIC src/http/client.cpp src/http/server.cpp)
target_include_directories(http PUBLIC include/http)
target_link_libraries(http
PUBLIC tls # HTTP API exposes TLS types
PRIVATE ZLIB::ZLIB # Compression is internal
)
# Application links HTTP — automatically gets Threads transitively
add_executable(web_app src/main.cpp)
target_link_libraries(web_app PRIVATE http)
# web_app links: http, tls, Threads::Threads
# web_app does NOT link: crypto, ZLIB (they're PRIVATE)
Transitive Dependencies Explained
When target A links PUBLIC to B, and target C links to A, then C automatically inherits B's include directories and link libraries. This is transitive dependency propagation — CMake's most powerful feature for large projects:
flowchart TD
APP["web_app
(executable)"]
HTTP["http
(static lib)"]
TLS["tls
(static lib)"]
THREADS["Threads::Threads"]
CRYPTO["crypto
(static lib)"]
ZLIB["ZLIB::ZLIB"]
APP -->|"PRIVATE"| HTTP
HTTP -->|"PUBLIC"| TLS
HTTP -->|"PRIVATE"| ZLIB
TLS -->|"PUBLIC"| THREADS
TLS -->|"PRIVATE"| CRYPTO
style APP fill:#3B9797,color:#fff
style HTTP fill:#16476A,color:#fff
style TLS fill:#16476A,color:#fff
style THREADS fill:#132440,color:#fff
style CRYPTO fill:#666,color:#fff
style ZLIB fill:#666,color:#fff
In the diagram above, web_app only directly links http — but it transitively receives tls and Threads::Threads through PUBLIC links. The PRIVATE dependencies (crypto, ZLIB) do not propagate.
PUBLIC. If it's only used in your .cpp implementation files, use PRIVATE. If your library doesn't use it at all but wants to pass it on, use INTERFACE.
Symbol Visibility
By default, shared libraries on Linux/macOS export all symbols, while Windows exports none. This inconsistency leads to portability issues. Modern CMake provides tools to enforce consistent visibility across platforms.
CXX_VISIBILITY_PRESET
Set the default visibility to hidden, then explicitly export only the public API:
# CMakeLists.txt — Controlling symbol visibility
cmake_minimum_required(VERSION 3.20)
project(ImageLib VERSION 2.0 LANGUAGES CXX)
add_library(imagelib SHARED
src/loader.cpp
src/transform.cpp
src/codec.cpp
)
# Hide all symbols by default (consistent with Windows behavior)
set_target_properties(imagelib PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
target_include_directories(imagelib PUBLIC include)
GenerateExportHeader
CMake's GenerateExportHeader module creates a portable export macro header that works on all platforms:
# CMakeLists.txt — Generate export macros automatically
cmake_minimum_required(VERSION 3.20)
project(ImageLib VERSION 2.0 LANGUAGES CXX)
include(GenerateExportHeader)
add_library(imagelib SHARED
src/loader.cpp
src/transform.cpp
)
set_target_properties(imagelib PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
# Generates imagelib_export.h with IMAGELIB_EXPORT macro
generate_export_header(imagelib
EXPORT_MACRO_NAME IMAGELIB_EXPORT
EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/include/imagelib_export.h
)
target_include_directories(imagelib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
$<INSTALL_INTERFACE:include>
)
Then use the generated macro in your public headers:
// include/imagelib/loader.h — Using the export macro
#pragma once
#include "imagelib_export.h" // Generated by CMake
// IMAGELIB_EXPORT resolves to:
// __declspec(dllexport) on Windows when building the DLL
// __declspec(dllimport) on Windows when consuming the DLL
// __attribute__((visibility("default"))) on GCC/Clang
class IMAGELIB_EXPORT ImageLoader {
public:
ImageLoader();
~ImageLoader();
bool load(const char* path);
int width() const;
int height() const;
private:
struct Impl; // Private implementation (not exported)
Impl* pImpl;
};
// Non-exported helper (stays hidden)
namespace detail {
void internal_decode(const unsigned char* data, int size);
}
__declspec(dllexport), no symbols are exported from a DLL at all — the resulting .lib import library will be empty. Always use GenerateExportHeader for cross-platform shared libraries.
RPATH and Install Names
When you build an executable that links a shared library, the OS dynamic linker needs to find that library at runtime. RPATH embeds a search path directly into the binary so it knows where to look.
# CMakeLists.txt — RPATH configuration for portable installs
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0 LANGUAGES CXX)
add_library(applib SHARED src/applib.cpp)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE applib)
# --- RPATH settings for installed binaries ---
# Use full RPATH during build (for running from build tree)
set(CMAKE_BUILD_RPATH_USE_ORIGIN TRUE)
# Set RPATH for installed binaries
set(CMAKE_INSTALL_RPATH
"$ORIGIN/../lib" # Linux: relative to executable
)
# On macOS, use @rpath
if(APPLE)
set(CMAKE_INSTALL_RPATH "@executable_path/../lib")
set(CMAKE_MACOSX_RPATH TRUE)
endif()
# Don't strip RPATH from installed binaries
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
# Install rules
install(TARGETS myapp RUNTIME DESTINATION bin)
install(TARGETS applib LIBRARY DESTINATION lib)
RPATH strategies vary by platform:
| Platform | Mechanism | Relative Syntax |
|---|---|---|
| Linux | ELF RPATH/RUNPATH | $ORIGIN/../lib |
| macOS | LC_RPATH load command | @executable_path/../lib, @loader_path/../lib, @rpath |
| Windows | DLL search order | Same directory as .exe, then PATH |
$ORIGIN (Linux) or @executable_path (macOS) for relocatable installations. This allows users to install your application anywhere without breaking library resolution. Windows searches the executable's directory first, so just co-locate DLLs with the .exe.
Library Versioning
Proper library versioning communicates ABI compatibility to users and package managers. CMake supports this through target properties:
# CMakeLists.txt — Complete library versioning setup
cmake_minimum_required(VERSION 3.20)
project(DataLib VERSION 4.2.1 LANGUAGES CXX)
add_library(datalib SHARED
src/parser.cpp
src/serializer.cpp
src/validator.cpp
)
set_target_properties(datalib PROPERTIES
# Full version: libdatalib.so.4.2.1
VERSION ${PROJECT_VERSION}
# ABI version: libdatalib.so.4 (major version = ABI contract)
SOVERSION ${PROJECT_VERSION_MAJOR}
# macOS: set compatibility and current version
MACOSX_RPATH TRUE
)
# On the filesystem (Linux):
# libdatalib.so -> libdatalib.so.4 (dev symlink)
# libdatalib.so.4 -> libdatalib.so.4.2.1 (SOVERSION symlink)
# libdatalib.so.4.2.1 (real file)
#
# Programs link against libdatalib.so.4 — any 4.x.y is compatible
The versioning scheme follows semantic versioning at the ABI level:
- Major (SOVERSION) — Increment when binary interface changes (removed functions, changed signatures, changed class layout)
- Minor — Increment when new functions/classes added without breaking existing ones
- Patch — Increment for bug fixes with no API/ABI changes
# Inspect library version information on Linux
readelf -d libdatalib.so.4.2.1 | grep -i "soname"
# 0x000000000000000e (SONAME) Library soname: [libdatalib.so.4]
# On macOS, use otool
otool -L libdatalib.4.2.1.dylib
# libdatalib.4.dylib (compatibility version 4.0.0, current version 4.2.1)
Exercises
Create a project with a math library containing 5+ functions. Build it as both STATIC and SHARED. Link an executable to each variant and compare:
- The size of the executable when linked statically vs dynamically
- Run
ldd(Linux) orotool -L(macOS) on each to see dependencies - Use
nm -Dto inspect exported symbols from the shared library
Create a three-layer project:
libbase— provides aBaseclasslibmiddle— depends PUBLIC on libbase, addsMiddleclassapp— links only to libmiddle
Verify that app can use Base without directly linking libbase. Then change the link to PRIVATE and observe the compilation error. Understand why PUBLIC propagation is essential when types appear in your public headers.
Build a shared library that works on both Linux and Windows:
- Use
GenerateExportHeaderto create an export macro - Set
CXX_VISIBILITY_PRESETto hidden - Export only 2-3 public classes/functions
- Verify with
nm -D(Linux) that only exported symbols are visible - Keep internal helper functions hidden
Create a plugin host application:
- Define a plugin interface header with a
create_plugin()factory function - Build 2-3 plugins as MODULE libraries
- Write a host that scans a directory and
dlopen()s each plugin - Verify that plugins cannot be linked at build time (only loaded at runtime)
Conclusion & Next Steps
Libraries are the fundamental building blocks of modular C++ software. In this part you've learned:
- STATIC — Archive of objects linked at build time, producing self-contained executables
- SHARED — Dynamically linked at load time, shared across processes, versioned with SOVERSION
- OBJECT — Compile-once objects without creating an archive file
- MODULE — Shared libraries for runtime loading (plugins)
- INTERFACE — Header-only or meta targets propagating usage requirements
- Linking semantics — PUBLIC/PRIVATE/INTERFACE control transitive propagation
- Symbol visibility —
GenerateExportHeadercreates portable export macros - RPATH — Embed relative library search paths for relocatable installs
add_library(), target_link_libraries(), and GenerateExportHeader in the official CMake documentation.