Skip to content

Designing and Distributing Libraries

A library is not complete when it builds inside its own source tree. Consumers need a deliberate source API, binary boundary, install layout, dependency contract, and compatibility policy.

Separate public and private material

text
report/
├── include/report/report.h
├── src/report.c
├── tests/
└── CMakeLists.txt

Public headers should include everything their declarations require, avoid private paths, minimize macro pollution, and remain usable from intended language modes. Private headers can live under src and need not be installed.

For a C-compatible header included from C++:

c
#ifdef __cplusplus
extern "C" {
#endif

struct report;
struct report *report_create(void);
void report_destroy(struct report *report);

#ifdef __cplusplus
}
#endif

Use opaque handles when layout stability and invariants matter. Supply create/destroy pairs so allocation stays within the owning runtime.

Static and shared variants have different contracts

Static consumers incorporate selected code at link time and may need transitive link libraries. Shared consumers depend on exported symbols and loader discovery. Building both from one target can require position-independent code, export macros, and distinct naming conventions.

Do not expose all symbols by accident. Hidden-by-default visibility with an explicit API macro narrows ABI commitments. On Windows the macro expands differently while building versus consuming a DLL.

API versioning is not ABI versioning

Adding a function to a C API may preserve existing binary consumers; changing a public struct or calling convention may not. Many C++ changes alter mangled symbols or instantiated layout, and standard-library types couple consumers to a library ABI.

Semantic versioning communicates intent but cannot itself guarantee ABI stability. State whether upgrades require consumer recompilation. Use ABI comparison tools where binary compatibility is a product requirement.

Install an actual package

A conventional installation includes:

  • public headers under an include prefix;
  • libraries in platform library/bin locations;
  • runtime binaries where the loader/installer expects;
  • CMake config and exported-target files;
  • optionally pkg-config metadata;
  • licenses and documentation.

CMake package configs should expose namespaced targets:

cmake
find_package(Report CONFIG REQUIRED)
target_link_libraries(consumer PRIVATE Report::report)

The target carries usage requirements. Consumers should not need to know the installed archive filename or reconstruct dependency flags.

Test from outside the source tree

After installation into a staging prefix, configure a tiny separate consumer using only documented discovery. This catches:

  • missing installed headers;
  • build-tree paths leaked into exports;
  • absent transitive dependencies;
  • incorrect static/shared definitions;
  • package version-file errors;
  • runtime deployment failures.

Unit tests linked directly to the source target cannot reveal all of these packaging defects.

Package metadata should be relocatable

Avoid embedding one machine's absolute paths. Use install-relative paths and CMake's package helpers. A package manager may relocate its store or deploy an archive to another prefix.

Optional prompts

Explain: Why should a library provide its own destruction function for an opaque C handle?

Answer: It preserves ownership and allocator/runtime matching, and lets the implementation change representation without exposing size or cleanup details.

Explain: Why is a separate consumer test necessary?

Answer: In-tree tests inherit source paths and target state that can hide broken installation metadata and missing public requirements.

Further reference