Using Shared Libraries

Adding support for user-configurable library types and an initial introduction to target installation.

Greg Filak
Published

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 is ON, it creates a SHARED library.
  • If BUILD_SHARED_LIBS is OFF (or not defined), it creates a STATIC 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.

Building a Shared Library

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:

  1. 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.
  2. The targets that these rules apply to. We're applying this rule only to the GreeterApp target, so only a single argument follows TARGETS in this case. We could add more if needed.
  3. The DESTINATION keyword informs CMake that the next argument will specify where we want our files installed to.
  4. The "${CMAKE_SOURCE_DIR}/bin" argument will cause our files to be installed in a directory called bin (binary), which is a common convention. The use of the CMAKE_SOURCE_DIR prefix specifies that this bin 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. The cmake --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 our CMakeLists.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 between STATIC and SHARED 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).
Next Lesson
Lesson 24 of 25

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.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate