11. Libraries, ABI & Packaging¶
What & why¶
Sooner or later your code becomes a library someone else consumes: another team, another project, the open-source world. C++ has no single package manager, and binary compatibility is fragile. This lesson covers building static and shared libraries, controlling symbol visibility, what the ABI is and how to avoid breaking it, making your CMake project consumable, distributing through vcpkg and Conan, and C++20 modules.
Static vs. shared libraries¶
Static (.a, .lib) |
Shared (.so, .dylib, .dll) |
|
|---|---|---|
| Linked | into the executable at build time | loaded at runtime |
| Deployment | one self-contained binary | library must be present on the target system |
| Updates | rebuild the app | replace the library (if ABI-compatible) |
| Binary size | larger executables | shared across processes |
| Pitfalls | duplicate symbols, link order | ABI compatibility, symbol visibility, load paths |
add_library(geo src/geo.cpp) # STATIC or SHARED decided by BUILD_SHARED_LIBS
add_library(geo STATIC src/geo.cpp)
add_library(geo SHARED src/geo.cpp)
Let consumers choose with -DBUILD_SHARED_LIBS=ON rather than hard-coding.
Symbol visibility¶
On Linux/macOS, every non-static function in a shared library is exported by default. That bloats the symbol table, slows loading, prevents optimizations, and turns internals into de facto API. On Windows, nothing is exported unless marked __declspec(dllexport).
Hide everything by default; export deliberately:
set_target_properties(geo PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
)
include(GenerateExportHeader)
generate_export_header(geo) # creates geo_export.h with GEO_EXPORT
target_include_directories(geo PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>)
#include "geo_export.h"
class GEO_EXPORT Polygon { // exported
public:
double area() const;
};
GEO_EXPORT double distance(Point a, Point b);
namespace detail {
double helper(double); // not exported
}
The ABI¶
The API is the source-level contract (what compiles). The ABI (application binary interface) is the binary-level contract: calling conventions, name mangling, object layout, vtable layout, exception handling. A shared library can be swapped without recompiling its users only if its ABI is unchanged.
Things that break ABI¶
- Adding, removing, or reordering data members of a class (changes size/offsets)
- Adding or reordering virtual functions (changes vtable layout)
- Changing a function's parameters, return type,
const-ness, ornoexcept(in some cases) - Changing a class's base classes
- Changing inline functions or templates that users compiled into their code
- Changing default template arguments, enum underlying types, alignment
- Switching compilers or standard libraries with incompatible ABIs (e.g. libstdc++ vs libc++, MSVC debug vs release runtime), or
_GLIBCXX_USE_CXX11_ABI
Things that are usually safe¶
- Adding new non-virtual member functions
- Adding new free functions and new classes
- Changing function bodies that aren't inline
Techniques for stable ABIs¶
- Pimpl (lesson 9): the public class holds only a pointer; members can change freely.
- Avoid exposing standard library types across the boundary if consumers may use a different standard library build. A C API boundary is the most portable of all:
// geo_c.h — extern "C" API: stable, callable from C, Python (ctypes), Rust, ...
#ifdef __cplusplus
extern "C" {
#endif
typedef struct geo_polygon geo_polygon; // opaque handle
geo_polygon* geo_polygon_create(void);
void geo_polygon_add_point(geo_polygon*, double x, double y);
double geo_polygon_area(const geo_polygon*);
void geo_polygon_destroy(geo_polygon*);
#ifdef __cplusplus
}
#endif
- Versioned inline namespaces:
namespace geo { inline namespace v2 { ... } }— v1 and v2 symbols get different mangled names, turning ABI mismatches into link errors instead of crashes. - Semantic versioning + SONAME: bump the major version when the ABI breaks.
Tools like abi-compliance-checker and libabigail (abidiff) compare two builds and report breaks.
Header-only libraries¶
Everything in headers (templates, inline functions). Easiest to consume — just add an include path — at the cost of compile time and no ABI boundary at all (users always recompile).
add_library(mathx INTERFACE)
target_include_directories(mathx INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_features(mathx INTERFACE cxx_std_20)
Making a CMake project consumable¶
The goal: after installing, other projects write
Layout¶
geo/
├── CMakeLists.txt
├── cmake/geoConfig.cmake.in
├── include/geo/polygon.hpp # public headers, under a project-named directory
└── src/polygon.cpp # private sources and headers
CMakeLists.txt¶
cmake_minimum_required(VERSION 3.24)
project(geo VERSION 1.2.0 LANGUAGES CXX)
add_library(geo src/polygon.cpp)
add_library(geo::geo ALIAS geo) # same name for in-tree and installed use
target_compile_features(geo PUBLIC cxx_std_20)
target_include_directories(geo PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
install(TARGETS geo EXPORT geoTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT geoTargets
NAMESPACE geo::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/geo
)
configure_package_config_file(cmake/geoConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/geoConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/geo
)
write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/geoConfigVersion.cmake
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/geoConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/geoConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/geo
)
cmake/geoConfig.cmake.in¶
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# find_dependency(fmt) # re-find your PUBLIC dependencies here
include("${CMAKE_CURRENT_LIST_DIR}/geoTargets.cmake")
check_required_components(geo)
Try it¶
cmake -S geo -B geo/build && cmake --build geo/build
cmake --install geo/build --prefix $HOME/.local/geo
cmake -S app -B app/build -DCMAKE_PREFIX_PATH=$HOME/.local/geo
Also make it work via FetchContent / add_subdirectory — the geo::geo alias ensures consumers write the same target_link_libraries either way. Guard tests and examples with if(PROJECT_IS_TOP_LEVEL) so they don't build when you're a dependency.
Package managers¶
vcpkg¶
Consumer side, manifest mode — vcpkg.json next to your CMakeLists.txt:
{
"name": "app",
"version": "0.1.0",
"dependencies": ["fmt", "catch2", { "name": "cpr", "features": ["ssl"] }]
}
Pin versions with a builtin-baseline. To publish, add a port (a portfile.cmake + vcpkg.json) to the vcpkg registry or to your own registry.
Conan¶
conan install . --output-folder=build --build=missing
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake
Conan also manages binary packages per compiler/settings combination, which matters for large dependencies. To publish, write a conanfile.py recipe and upload to ConanCenter or your own remote.
C++20 modules¶
Modules replace textual #include with compiled interfaces: faster builds, no macro leakage, no include-order bugs.
// geo.cppm — module interface unit
export module geo;
import std; // C++23 standard library module (where supported)
export namespace geo {
struct Point { double x, y; };
double distance(Point a, Point b);
}
double geo::distance(Point a, Point b) {
return std::hypot(a.x - b.x, a.y - b.y);
}
// main.cpp
import geo;
import std;
int main() {
std::println("{}", geo::distance({0, 0}, {3, 4}));
}
cmake_minimum_required(VERSION 3.28)
project(geo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
add_library(geo)
target_sources(geo PUBLIC FILE_SET CXX_MODULES FILES geo.cppm)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE geo)
Status (2026): supported by recent MSVC, Clang, and GCC with CMake 3.28+ and the Ninja generator; import std; needs newer toolchains and extra CMake configuration. Mixed ecosystems (header-based dependencies, older compilers) are still common, so most libraries ship headers and offer modules as an option.
Checklist for a library release¶
- [ ] Public headers under
include/<project>/; nothing private exposed. - [ ] Namespaced targets (
geo::geo) that work viafind_package,FetchContent, andadd_subdirectory. - [ ] Hidden visibility by default; explicit exports.
- [ ] Semantic version; SOVERSION bumped on ABI breaks; ABI checked in CI.
- [ ] Tests and examples only built when top-level.
- [ ] CI on GCC, Clang, and MSVC; sanitizers; warnings as errors.
- [ ] License, README with a minimal usage example, changelog.
- [ ] A vcpkg port and/or Conan recipe.
Common mistakes¶
- Exporting everything from shared libraries.
- Breaking ABI in a minor release — adding a data member to a public class.
- Passing STL types across DLL boundaries built with different runtimes/settings.
- Absolute paths in exported targets — use
$<BUILD_INTERFACE>/$<INSTALL_INTERFACE>. - Global CMake settings (
CMAKE_CXX_FLAGS,include_directories) that leak into consumers. - Building tests when consumed as a subproject.
- Forgetting
find_dependencyfor public dependencies in the config file.
Exercises¶
- Turn the intermediate link checker's
linkcheck_libinto an installable package withfind_package(linkcheck CONFIG)support. Consume it from a separate project. - Build it as a shared library with hidden visibility. Use
nm -D --defined-only(Linux) ordumpbin /exports(Windows) to list exported symbols before and after. - Add a data member to an exported class, rebuild only the library, and run an old executable against it. What happens? Then convert the class to pimpl and repeat.
- Write an
extern "C"wrapper for the link checker and call it from Python viactypes. - Add a
vcpkg.jsonto a project with two dependencies, and pin a baseline. - Convert a small header + source pair into a C++20 module and build it with CMake 3.28+ and Ninja.
Recap¶
- Static libraries link into the binary; shared libraries load at runtime and need ABI care.
- Hide symbols by default; export explicitly with generated export macros.
- ABI breaks: layout changes, vtable changes, inline/template changes. Use pimpl, C APIs, inline namespaces, and SOVERSIONs.
- Make CMake projects consumable: namespaced targets, install rules, exported config files.
- vcpkg and Conan manage dependencies; publish ports/recipes for your libraries.
- C++20 modules improve build times and isolation; adoption is growing but mixed.