Creating Shared Libraries with CMake

Using CMake for dynamic linking of shared libraries

Ashwin Prasad
5 min readApr 27, 2024

This is not an article that will explain dynamic linking and static linking. For understanding the differences between the 2 types of linking mechanisms, refer to this link. Unlike my previous articles, this one aims to be a little practical rather than being theory-oriented. A very basic cmake knowledge is assumed.

What are we going to build?

Some things can only be understood by doing, and CMake is one of them. Here’s a brief description of what I’m going to create to demonstrate the usage of CMake:

Fig 1.1: Dependency Structure.
  1. A math library that does addition and subtraction
  2. A calculator library where the user can pass 2 integers and an operator based on which, different functions in the math library are called.
  3. A main executable that uses functions from the calculator library.

In the above example, both the Calculator library as well as the Math library are going to be shared libraries.

Project Structure

Everyone has their own way of organizing and structuring their code. With my limited experience, this is the way I like organising my C programs.

Fig 2.1 : Project Structure (ignore the temp.txt)

With addition to the structure shown in figure 2.1, every library as well as the root directory has a build directory from where the CMake commands are executed. For the libraries, these build directories will contain the shared object file (.so file) whereas for the main executable, the build directory will contain the main executable.
For the actual code, check out this link from my git repository.

Please check the code from my github repo before proceeding further.
link

Compiling into Shared Libraries

This is the core part of the article where we write the CMakeLists.txt to make out math_oper and calculator libraries act like shared libraries.

As you can see from the code, my main program calls the calculate function from the calculator library which in turn calls the add function from the math_oper library.

But how does the compiler know where to check for the declaration of calculate function when we write #include “calculator.h” and how does it know where the definition of the function is present. calculator.h does not seem to be a relative path nor does it seem to be an absolute path.

🎊 Enter CMake 🎊

Note: This can be done with gcc itself but there’s a reason CMake is preferred for big projects

Math_oper library

Since the root dependency of the project is the math_oper library, we’ll start there. let us analyse the CMakeList.txt of this library

cmake_minimum_required(VERSION 3.10)
project(math)
set(SOURCES src/math_oper.c)
add_library(math_oper SHARED ${SOURCES})
target_include_directories(math_oper PRIVATE include)

The magic happens in the third line. add_library() command from cmake takes the library name, library type and the source files belonging to the library, and then outputs a library. Since we want a dynamically linked library, type is specified as SHARED

Running cmake on this file will generate a libmath_oper.so file.

Calculator library

Since calculator is also a library, we are going to follow the exact same steps and use the add_library() command to create a shared library.

cmake_minimum_required(VERSION 3.10)
project(Calculator)
set(SOURCES src/calculator.c)
add_library(calculator SHARED ${SOURCES})
target_include_directories(calculator PUBLIC include)
find_library(MATH
NAMES math_oper
PATHS ../math_oper/build
)
target_link_libraries(calculator ${MATH})
target_include_directories(calculator PRIVATE ../math_oper/include)

But the additional handling required here is to make sure that the already created math_oper shared library is properly linked. To do this, find_library() and target_link_libraries() commands are used.

Since the math_oper library is present in a directory outside of the calculator directory, we need to use the find_library command to let cmake know where the .so file exists and the name of the library. The first argument “MATH” is the name that is used to refer the math_oper library in case it is found. This can be named anything according to the user’s wish.

Once the library is found, we tell cmake to link it to our calculator library using the target_link_libraries command. This command is pretty self explanatory.

Main Executable

Since our main executable is only using the calculator library, we only need to link it in the main CMakeList.txt file.

cmake_minimum_required(VERSION 3.10)
project(Main)
set(SOURCES src/main.c)
add_executable(main ${SOURCES})
target_include_directories(main PRIVATE
calculator/include
math_oper/include
)
find_library(CALCULATOR_LIB
NAMES calculator
PATHS calculator/build
)
target_link_libraries(main ${CALCULATOR_LIB})

We do the exact same thing here as we did for calculator cmakelist file.

Execution

Once all the files are set properly in place, we can proceed to execute the main program. But before that, we need to run cmake and generate the .so files for the calculator and math_oper libraries. So, starting from the inner most (most nested) dependency, we run the cmake and make commands and finally do the same for the main executable.

We have our main executable ready. running “./main” will run our program and the output will be as expected.

Let’s say my main program has become so large due to the addition of more functionalities and logic. Due to this, the compilation time might increase for the program. Now let’s also assume that I need to make a minor change in my math_oper library. Since we’ve used dynamic linked using shared obejcts, we do not have to recompile everything after making the changes. All we need to do is recompile the math_oper library and regenerate the corresponding .so file. It can be seen that the newly made changes have taken effect when we run the main executable. This is a serious advantage to dynamic linking as it makes working in modular teams more efficient.

Less compile time and more work

The memory required is also vastly reduced compared to static linking since only one common shared object will be used by all programs and the references will be resolved at run-time.

Final Thoughts

Dynamic linking, while it has its uses and advantages, it is also considered to be slighty slower than static linking since the unresolved symbols need to be resolved at runtime. While static linking is beneficial for the small percentage of applications that put a lot of emphasis on performance and speed, it is not possible to allow modular level compilation in static linking (all symbols will be resolved at compile time). This might not always be beneficial for libraries that are used in more than one place.

I think it would be a good idea to write your cmake files in such a way so as to allow dynamic linking for testing and static linking for deploying in production.

--

--

Ashwin Prasad

I write about things that intrigue me on any field of Computer Science, with more weightage to Machine Learning and Systems Programming