Using Shared Libraries
Adding support for user-configurable library types and an initial introduction to target installation.
We've designed a modular build system using INTERFACE
, ALIAS
, and IMPORTED
targets. We have a clear dependency graph, and properties flow automatically from producers to consumers.
But so far, we've only been building static libraries. In this lesson, we'll build our first shared library.
User-Configurable Library Types
Right now, our greeter/CMakeLists.txt
defines our library like this:
greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
# add_library called without specifying the type
add_library(GreeterLib src/Greeter.cpp)
target_include_directories(GreeterLib PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
As we learned, when the type is omitted, add_library()
defaults to creating a STATIC
library. But what if we wanted to use a shared library instead? The most direct solution would be to simply add the SHARED
keyword to our add_library()
command:
add_library(GreeterLib SHARED src/Greeter.cpp)
Alternatively, we can leave our library type unspecified, and instead allow users to specify whether they want static or shared libraries by setting the BUILD_SHARED_LIBS
cache variable.
How BUILD_SHARED_LIBS
Works
BUILD_SHARED_LIBS
is a special variable recognized by CMake. When an add_library()
command is called without an explicit STATIC
, SHARED
, or INTERFACE
keyword, it checks the value of BUILD_SHARED_LIBS
:
- If
BUILD_SHARED_LIBS
isON
, it creates aSHARED
library. - If
BUILD_SHARED_LIBS
isOFF
(or not defined), it creates aSTATIC
library.
To expose this to the user, the convention is to add a friendly option()
to your root CMakeLists.txt
.
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
# Provide a user-facing option to control the library type
option(BUILD_SHARED_LIBS "Build libraries as shared" OFF)
project(Greeter VERSION "1.0")
add_subdirectory(greeter)
add_subdirectory(app)
We haven't changed the greeter/CMakeLists.txt
file at all. The add_library(GreeterLib ...)
call will now automatically respect this global setting. This small change means consumers can now control the library type directly from the command line without ever touching our build scripts.
Let's walk through the entire process to see the effect of this change.
Configure with BUILD_SHARED_LIBS
ON
From our build
directory, we'll re-configure the project, this time setting our new option to ON
:
cmake -DBUILD_SHARED_LIBS=ON ..
Build the Project
Now, we run the build command as usual.
cmake --build .
If you inspect your build/greeter
directory, you'll see that instead of a static library (.a
or .lib
), CMake has produced a shared library (.so
on Linux, .dll
on Windows).
The Runtime Linking Problem
Now, let's try to run our GreeterApp
(or GreeterApp.exe
) application from the build/app
directory:
./app/GreeterApp
As you may have guessed, our GreeterApp
now depends on a shared library that it probably can't find. As such, our program will likely error, or just appear to do nothing at all.
./app/GreeterApp: error while loading shared libraries: libGreeterLib.so: cannot open shared object file: No such file or directory
The operating system's loader saw that GreeterApp
needs libGreeterLib.so
, but it didn't know where to find it.
The loader only checks a few specific places for any shared library that our executable needs. The /build/greeter
directory where our shared library is placed after compilation is not one of them.
This is a classic deployment problem. Our build directory is a messy, intermediate workspace. It's not a clean, reliable environment for running our application. To solve this, we need a proper installation step.
Preparing for Distribution with install()
The install()
command is CMake's mechanism for taking the outputs from a messy build directory and arranging them into a clean, distributable layout. This "install" directory is what you would package into a .zip
file or an installer to give to other people.
Installation, packaging and distribution is an important topic that we'll cover in a dedicated chapter later in the course. But for now, let's walk through a quick example, as the installation process it sets some useful context.
The install()
command tells CMake what to do when we run the --install
build step. We'll cover how to run that soon, but lets first set up what should happen when we do.
Installing the Executable
In app/CMakeLists.txt
, we'll add an install rule for our executable:
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_executable(GreeterApp src/main.cpp)
target_link_libraries(GreeterApp GreeterLib)
# Install the final executable to a 'bin' directory
install(TARGETS GreeterApp DESTINATION "${CMAKE_SOURCE_DIR}/bin")
Our arguments are the following:
- The
TARGETS
keyword, informing CMake that the installation rule we are setting applies to targets, and that the next argument(s) will specify those targets. - The targets that these rules apply to. We're applying this rule only to the
GreeterApp
target, so only a single argument followsTARGETS
in this case. We could add more if needed. - The
DESTINATION
keyword informs CMake that the next argument will specify where we want our files installed to. - The
"${CMAKE_SOURCE_DIR}/bin"
argument will cause our files to be installed in a directory calledbin
(binary), which is a common convention. The use of theCMAKE_SOURCE_DIR
prefix specifies that thisbin
directory will always be in the root of our project.
Eventually, users should be able to specifiy where on their system the program should be installed, but we'll keep things simple for now. We'll learn much more about installation later in the course.
Installing the Library
Let's set the same install()
rules for our library:
greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_library(GreeterLib src/Greeter.cpp)
target_include_directories(GreeterLib PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
install(TARGETS GreeterLib
DESTINATION "${CMAKE_SOURCE_DIR}/bin"
)
Combined, these rules will place our executable file and shared library in the same location - within /bin
in our project root.
Setting the Runtime Library Search Path (rpath
)
Placing our shared library in the same location as our executable is enough to let Windows find it. When an .exe
file asks for a .dll
file, Windows will automatically search the same directory.
However, on Unix-like systems (including macOS and Linux), we need one more step. We'll set the INSTALL_RPATH
property on our GreeterApp
target. This property gets embedded into the executable, and gives Unix-like systems some additional hints about where that executable's dependencies might be.
The special "$ORIGIN"
path will be resolved to the directory the executable is in. This means the platform will look in that same location for any libraries we need, replicating Windows' behavior:
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_executable(GreeterApp src/main.cpp)
target_link_libraries(GreeterApp GreeterLib)
install(TARGETS GreeterApp DESTINATION "${CMAKE_SOURCE_DIR}/bin")
set_target_properties(GreeterApp PROPERTIES
INSTALL_RPATH "$ORIGIN"
)
The End-to-End Installation Workflow
Now let's see our new installation rules in action.
Configuring and Building
First, we configure and build our project as usual. Let's build the shared library version again.
cmake -DBUILD_SHARED_LIBS=ON ..
cmake --build .
Installing
Now, we execute the --install
step. As with the previous step, we'll run this from our ./build
directory, and use the .
command to tell CMake the output of our build is in this same directory:
cmake --install .
This command runs the install()
rules we defined. It finds the built artifacts in the current directory and its subdirectories, and copies them to the locations we requested in our DESTINATION
arguments.
Inspecting the Result
If you now look in the bin
directory, you should both our library and our executable. And, if we run the executable from that location, everything should work.
The following command assumes we're currently in the build directory, so ..
navigates up to our project root, and /bin
navigates to the bin directory in that location. Remember to add .exe
on Windows:
../bin/GreeterApp
Hello from the modular greeter library!
This was only a brief introduction to a very large topic, and our basic implementation isn't flexible or robust. For example:
- Users cannot control the installation directory. We've hardcoded the destination to the
/bin
directory in the project root. Thecmake --install
command even has a--prefix
option to let users set their location, and this flexibility is required to package and ship our project, but ourCMakeLists.txt
files doesn't support it. - If we revert back to a static library (such as by setting
-DBUILD_SHARED_LIBS=OFF
) then the library will still be copied over to our/bin
directory, even though it's no longer needed. - Installation isn't just for end-users wanting to run our executable. For example, it is also how we would prepare one of our libraries to be used by other developers. The implementation for that use case looks totally different to what we set up here.
We'll revisit installation and packaging later in the course, addressing all these limitations and much more.
Summary
This lesson covered some of the most important practical aspects of authoring and using libraries in a modern CMake project.
- Configurable Library Types: Use the
BUILD_SHARED_LIBS
option to allow users to easily switch betweenSTATIC
andSHARED
builds of your libraries. - Installation for Distribution: Use the
install()
command to gather your project's artifacts into a clean, distributable directory structure. This separates it from the temporary build tree and prepares it for packaging and shipping (which we'll cover later).
Build Configurations (Debug, Release, etc.)
Learn how CMake manages different build configurations like Debug and Release, the difference between generator types, and how to apply settings conditionally using modern techniques.