Master CMake for Building Static and Dynamic Libraries

Master CMake for Building Static and Dynamic Libraries

1. Task

Explain the process of building static and dynamic libraries with CMake in a simple and understandable way using examples.

Tasks:

  1. Create a static library and a dynamic library, providing the HelloFunc function for use in other programs, which outputs the string ‘Hello World!’ to the terminal.

  2. Install header files and shared libraries.

  3. Write a program to use the constructed shared library.

2. Preparation

(1) Create a directory named t3 to store the projects involved in this section.

mkdir t3

(2) Create a shared library.

cd t3
mkdir lib

(3) Create a CMakeLists.txt file in the t3 directory with the following content:

PROJECT(HELLOLIB)
ADD_SUBDIRECTORY(lib)

(4) In the lib directory, create two source files hello.c and hello.h.

Content of hello.c:

#include "hello.h"
void hello_func(void) 
{
	printf("Hello World!\n");
	return;
}

Content of hello.h:

#ifndef HELLO_H_
#define HELLO_H_ (1)
#include <stdio.h>
void hello_func(void);
#endif

(5) Create a CMakeLists.txt file in the lib directory with the following content:

SET(LIBHELLO_SRC hello.c)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})

3. Compile the Shared Library

Using an out-of-source build method, create a build directory and execute the following in the build directory:

mkdir build
cd build
cmake ..
make

At this point, you should see a libhello.so in the lib directory, which is the shared library.

If you want to specify the location of libhello.so, you can modify the ADD_SUBDIRECTORY(lib) directive in the main project CMakeLists.txt file to specify a compilation output location or add SET(LIBRARY_OUTPUT_PATH <path>) in lib/CMakeLists.txt to specify a new location.

4. ADD_LIBRARY Directive

Syntax:

ADD_LIBRARY(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL]
sources...)

You do not need to write out libhello.so; just specify hello, and the cmake system will automatically generate libhello.X.

There are three types:

  1. SHARED, dynamic library.

  2. STATIC, static library.

  3. MODULE, effective in systems using dyld; if dyld is not supported, it is treated as SHARED.

The EXCLUDE_FROM_ALL parameter means that this library will not be built by default unless there are dependencies from other components or it is built manually.

5. Compile the Static Library

The static library name should match the dynamic library name, except the suffix is .a. To add a static library, use this command:

ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})

Note that if you add the above content based on the previous dynamic library compilation, you will find that the static library is not built, and only a dynamic library is generated. This is because the target name hello cannot be duplicated, so the static library build directive is ineffective. If you change the above hello to hello_static:

ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})

you can build a libhello_static.a static library.

However, this result is not what we want; we need static and dynamic libraries with the same name. Since the target name is unique, we cannot achieve this with the ADD_LIBRARY directive. At this point, we need to use another directive, SET_TARGET_PROPERTIES.

5.1. SET_TARGET_PROPERTIES Directive

The basic syntax of SET_TARGET_PROPERTIES is:

SET_TARGET_PROPERTIES(target1 target2 ...
PROPERTIES prop1 value1
prop2 value2 ...)

This directive can be used to set the output name, and for dynamic libraries, it can also specify the dynamic library version and API version.

In this example, we need to add the following line to lib/CMakeLists.txt:

SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello") 

Thus, we can obtain both libhello.so and libhello.a libraries simultaneously.

The final content of lib/CMakeLists.txt:

SET(LIBHELLO_SRC hello.c)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})
ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})
SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")

5.2. GET_TARGET_PROPERTY Directive

The corresponding directive to SET_TARGET_PROPERTY is GET_TARGET_PROPERTY, with the basic syntax:

GET_TARGET_PROPERTY(VAR target property)

The specific usage is as follows, add to lib/CMakeLists.txt:

GET_TARGET_PROPERTY(OUTPUT_VALUE hello_static OUTPUT_NAME)

MESSAGE(STATUS "This is the hello_static OUTPUT_NAME:"${OUTPUT_VALUE})

If this property is not defined, it will return NOTFOUND.

After the build, you will find that libhello.a has been built and is located in the build/lib directory, but libhello.so has disappeared. The reason for this issue is that cmake tries to clean up other libraries using the same name when building a new target; thus, when building libhello.a, it cleans up libhello.so.

To avoid this problem, you can use SET_TARGET_PROPERTIES to define the CLEAN_DIRECT_OUTPUT property.

Add to lib/CMakeLists.txt:

SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)
SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

Now, if we build again, we will find that both libhello.so and libhello.a are generated in the build/lib directory.

6. Dynamic Library Version Number

According to the rules, dynamic libraries should include a version number. We can take a look at the system’s dynamic libraries, which generally are:

libhello.so.1.0
libhello.so -> libhello.so.1
libhello.so.1 -> libhello.so.1.0

To implement the dynamic library version number, you need to use the SET_TARGET_PROPERTIES directive.

The specific usage is as follows:

SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1) 

VERSION refers to the dynamic library version, and SOVERSION refers to the API version.

By adding the above directive to lib/CMakeLists.txt and rebuilding, in the build/lib directory, you will generate:

libhello.so.1.2 libhello.so.1 -> libhello.so.1.2 libhello.so -> libhello.so.1

7. Install Shared Library and Header Files

In the above example, we need to install libhello.a, libhello.so.x, and hello.h to the system directory to allow others to develop and use them. In this example, we will install the shared library hello to the <prefix>/lib directory and hello.h to the <prefix>/include/hello directory.

Using the INSTALL directive learned in the previous section, add the following directives to lib/CMakeLists.txt:

INSTALL(TARGETS hello hello_static LIBRARY DESTINATION lib ARCHIVE DESTINATION lib)
INSTALL(FILES hello.h DESTINATION include/hello)

Note that the static library must use the ARCHIVE keyword, and install using the following command:

cmake -DCMAKE_INSTALL_PREFIX=/usr ..
make
sudo make install

Thus, the header files and shared libraries can be installed in the system directories /usr/lib and /usr/include/hello.

[ 50%] Built target hello
[100%] Built target hello_static
Install the project...
-- Install configuration: ""
-- Installing: /usr/lib/libhello.so.1.2
-- Installing: /usr/lib/libhello.so.1
-- Installing: /usr/lib/libhello.so
-- Installing: /usr/lib/libhello.a
-- Installing: /usr/include/hello/hello.h

If an error occurs:

CMake Error: cmake_symlink_library: System Error: Operation not supported
CMake Error: cmake_symlink_library: System Error: Operation not supported
make[2]: *** [lib/CMakeFiles/hello_dynamic.dir/build.make:85: lib/libhello.so.1.2] Error 1
make[2]: *** Deleting file 'lib/libhello.so.1.2'
make[1]: *** [CMakeFiles/Makefile2:130: lib/CMakeFiles/hello_dynamic.dir/all] Error 2
make: *** [Makefile:130: all] Error 2

It indicates that the make command might be run in a Windows directory shared via hgfs, and generating so files requires running in a pure Linux environment. For example, copy t3 to ~/t3, and clear the contents of the corresponding build directory, then execute:

cmake .. 
make

8. Use External Shared Libraries and Header Files

8.1. Preparation

(1) Create a directory named t4; all resources for this section will be stored in the t4 directory.

mkdir t4

(2) Create a src directory in the t4 directory.

cd t4
mkdir src

(3) Write a source file main.c in the src directory with the following content:

#include "hello.h"
int main(void) 
{
	hello_func();
	return 0;
}

(4) Write the main project file CMakeLists.txt in the t4 directory.

PROJECT(NEWHELLO)
ADD_SUBDIRECTORY(src)

(5) Write src/CMakeLists.txt in the t4/src directory.

ADD_EXECUTABLE(main main.c)

8.2. Introduce Header File Search Path

After completing the preparation work, direct building will fail because the header file “hello.h” cannot be found. The error output is:

cmake/t4/src/main.c:1:19: error: hello.h: No such file or directory

To allow our project to find hello.h, we need to introduce a new directive, INCLUDE_DIRECTORIES, with the complete syntax:

INCLUDE_DIRECTORIES([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)

This directive can be used to add multiple specific header file search paths to the project, separated by spaces. If the path contains spaces, you can enclose it in double quotes. The default behavior is to append to the existing header file search paths. You can control the search path addition method using two ways:

CMAKE_INCLUDE_DIRECTORIES_BEFORE: set this cmake variable to on to place the added header file search paths before existing paths. Using AFTER or BEFORE parameters also controls whether to append or place at the front.

Now, let’s add a header file search path in src/CMakeLists.txt:

INCLUDE_DIRECTORIES(/usr/include/hello)

Enter the build directory and rebuild. At this point, the error of not finding hello.h has disappeared, but a new error has occurred:

CMakeFiles/main.dir/main.o: In function 'main':
main.c:(.text+0x5): undefined reference to 'hello_func'
collect2: error: ld returned 1 exit status
src/CMakeFiles/main.dir/build.make:96: recipe for target 'src/main' failed
make[2]: *** [src/main] Error 1
CMakeFiles/Makefile2:97: recipe for target 'src/CMakeFiles/main.dir/all' failed
make[1]: *** [CMakeFiles/main.dir/all] Error 2
make: *** [Makefile:90: recipe for target 'all' failed

This is because we did not link to the shared library libhello.

8.3. Add Shared Library to Target

Now, the task we need to complete is to link the target file to libhello. Here, we need to introduce two new directives: LINK_DIRECTORIES and TARGET_LINK_LIBRARIES.

The complete syntax of LINK_DIRECTORIES is:

LINK_DIRECTORIES(directory1 directory2 ...)

This directive is straightforward; it adds non-standard shared library search paths. For example, if both shared libraries and executable binaries exist within the project, you need to specify the paths for these shared libraries during compilation. In this example, we do not use this directive.

The complete syntax of TARGET_LINK_LIBRARIES is:

TARGET_LINK_LIBRARIES(target library1 <debug | optimized> library2 ...)

This directive can be used to add required shared libraries for the target. In this example, it is an executable file, but it can also be used to add shared library links for libraries you write.

To solve the previous undefined reference error for HelloFunc, we need to add the following directive to src/CMakeLists.txt:

TARGET_LINK_LIBRARIES(main hello)

It can also be written as:

TARGET_LINK_LIBRARIES(main libhello.so)

Here, hello refers to the previously constructed shared library libhello.XX.

Enter the build directory and rebuild.

cmake ..
make

At this point, you will obtain an executable program main linked to libhello, located in the build/src directory. The result of running main is:

$ ./main
Hello World!

Let’s check the linking situation of main:

$ ldd src/main 
        linux-vdso.so.1 (0x00007ffd531fd000)
        libhello.so.1 => /usr/lib/libhello.so.1 (0x00007ff212afe000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff21270d000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ff212d00000)

You can clearly see that main is indeed linked to the shared library libhello, and it is linking to the dynamic library libhello.so.1.

9. Use External Static Libraries and Header Files

How to link to the static library? It’s simple:

Change the TARGET_LINK_LIBRARIES directive to:

TARGET_LINK_LIBRARIES(main libhello.a)

After rebuilding, let’s check the linking situation of main:

$ ldd src/main 
        linux-vdso.so.1 (0x00007ffc7a161000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff1da80c000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ff1dabfd000)

This indicates that main is indeed linked to the static library libhello.a.

10. Special Environment Variables

CMAKE_INCLUDE_PATH and CMAKE_LIBRARY_PATH. Be sure to note that these two are environment variables, not cmake variables.

The usage method is to set them using export in bash or set command in csh, or use CMAKE_INCLUDE_PATH=/home/include cmake … and so on.

These two variables are mainly used to support parameters like --extra-include-dir in previous autotools projects. If the header files are not stored in standard paths (/usr/include, /usr/local/include, etc.), these variables can be used to compensate.

For example, in this case, hello.h is stored in /usr/include/hello, so it cannot be found directly.

Earlier, we directly used the absolute path INCLUDE_DIRECTORIES(/usr/include/hello) to tell the project about this header file directory.

To make the program smarter, we can use CMAKE_INCLUDE_PATH to do so, using the bash method as follows:

 export CMAKE_INCLUDE_PATH=/usr/include/hello

Then replace INCLUDE_DIRECTORIES(/usr/include/hello) in the header file with:

FIND_PATH(myHeader hello.h)
IF(myHeader)
INCLUDE_DIRECTORIES(${myHeader})
ENDIF(myHeader)

FIND_PATH is used to search for file names in specified paths, for example:

FIND_PATH(myHeader NAMES hello.h PATHS /usr/include /usr/include/hello)

Here, we did not specify a path, but cmake can still help us find the path where hello.h is stored, because we set the environment variable CMAKE_INCLUDE_PATH.

If you do not use FIND_PATH, setting the CMAKE_INCLUDE_PATH variable will have no effect; you cannot expect it to directly add parameters -I < CMAKE_INCLUDE_PATH > to the compiler command.

Similarly, CMAKE_LIBRARY_PATH can be used in FIND_LIBRARY.

Since these variables are directly used for FIND_ directives, all cmake modules that use FIND_ directives will benefit.

Conclusion

  1. Build dynamic and static libraries using the ADD_LIBRARY directive.

  2. Use SET_TARGET_PROPERTIES to build both named dynamic and static libraries simultaneously.

  3. Control dynamic library versions using SET_TARGET_PROPERTIES.

  4. Use the INSTALL directive to install header files and dynamic/static libraries.

  5. Add non-standard header file search paths using the INCLUDE_DIRECTORIES directive.

  6. Add non-standard library file search paths using the LINK_DIRECTORIES directive.

  7. Add library linking for libraries or executable binaries using TARGET_LINK_LIBRARIES.

  8. Linking to static libraries using CMake.

There are many advanced topics in CMake that have not been discussed, such as compilation condition checks, compiler definitions, platform judgments, and how to work with pkgconfig, etc. The process of using cmake is essentially the process of learning the cmake language and writing cmake programs, and since it is a ‘cmake language’, it naturally involves variables, syntax, etc.

Welcome to follow my public account

Daily article updates

Master CMake for Building Static and Dynamic Libraries

Long press the image above to scan the QR code to follow

Leave a Comment