0%

cmake 迷你系列(二)创建你自己的库

引言

上一篇文章 【开启】 我们从零开始搭建起来了一个简单的 hello world 程序。并在文章的结尾,我们写了一个工具类,用于相加两个数,那么本章就围绕这个工具类展开。

你也可以封装库

我们回顾一下项目结构:

1
2
3
4
5
6
test
|---src
|---main.c
|---utils.c
|---utils.h
|---CMakeLists.txt

我们直接把 utils.c 同 main.c 打包到了一起,如果我们想把 utils 打包成库,供其他程序使用,应该怎么做呢?

静态库

由于我们要单独打库,所以我们需要修改一下我们的编译规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.21)
project(test VERSION 0.0.1 LANGUAGES C)

set(CMAKE_C_STANDARD 11)

set(MAIN_SOURCE
${PROJECT_SOURCE_DIR}/src/main.c
)

set(UTILS_SOURCE
${PROJECT_SOURCE_DIR}/src/utils.c
)

add_library(utils STATIC ${UTILS_SOURCE})
#add_executable(test ${MAIN_SOURCE})

我们分析一下以上的修改:

  1. 6 行,我们使用 set 命令取代之前的 aux_source_directory(),因为我们这次需要把 main 函数跟库分开打。这里 set 指定将 main.c 保存到了 MAIN_SOURCE 变量下,后续只要 ${MAIN_SOURCE} 即可获取,另外注意到 PROJECT_SOURCE_DIR 这个变量,该变量则代表了当前项目的根路径。
  2. 10 行,我们以同样的方式指定了库的源码程序,并存到了 UTILS_SOURCE 变量中。
  3. 14 行,我们使用 add_library 来告诉编译器,我们要生成库,名字是 utils,生成为静态库,参与编译的源码为 UTILS_SOURCE 指定的。当然,static 关键字可以不指定,默认情况下生成的也是静态库。
  4. 15 行,我们把 test 执行文件注释掉,因为它需要静态库,但是此时还没有编译出来,如果不注释掉,当编译的时候会报以下错误:
1
2
3
4
5
6
7
8
Undefined symbols for architecture x86_64:
"_add", referenced from:
_main in main.c.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make[2]: *** [test] Error 1
make[1]: *** [CMakeFiles/test.dir/all] Error 2
make: *** [all] Error 2

不过不要紧,后续我们再解决这个问题。

  1. 执行以下命令进行编译:
1
2
3
cmake -S . -B build
cd build
make

那么在 build 路径下就生成了 libutils.a 这个文件,也就是我们需要的静态库文件。

使用静态库

有了静态库文件之后,我们怎么使用它呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cmake_minimum_required(VERSION 3.21)
project(test VERSION 0.0.1 LANGUAGES C)

set(CMAKE_C_STANDARD 11)

set(MAIN_SOURCE
${PROJECT_SOURCE_DIR}/src/main.c
)

set(UTILS_SOURCE
${PROJECT_SOURCE_DIR}/src/utils.c
)

find_library(UTILS_LIB NAMES utils PATHS ${PROJECT_SOURCE_DIR}/build)
#add_library(utils STATIC ${UTILS_SOURCE})

add_executable(test ${MAIN_SOURCE})
target_link_libraries(test ${UTILS_LIB})

我们着重看一下 14 行以及 18 行。其中 14 行告诉编译器,应该去哪里找库文件,UTILS_LIB 用于存放库信息,NAMES 跟 PATHS 分别代表了库的名字以及查找路径,注意我们生成的库都以 lib + 源码文件的名字开头,如本例中 utils 编译出来的库叫做 libutils.a 。但是我们使用 find_library 查找时,名字可以不用加 lib。
找到库之后,我们需要将该库链接到我们的目标程序,这也是 target_link_libraries 所做的工作。该命令则是告诉编译器将 ${UTILS_LIB} 库链接到 test 这个目标程序。
接下来就可以启动或者编译我们的源程序了!

📚 Tips

target_link_libraries 必须要放到 add_executable 指令的下边,也就是说,必须要先生成目标文件,才可以为目标文件创建链接属性。除了 target_link_libraries 命令外,link_libraries(${UTILS_LIB}) 也可以链接,如果我们的库需要全局链接,那么可以使用该命令(并且只有放到根路径中的 CMakeLists.txt 才会生效),否则还是建议使用 target_link_libraries。除此之外还有另外一个不再推荐的指令 target_link_libraries() 。

动态库

创建好静态库之后,我们这次创建动态库,方法很简单,只将 add_library(utils STATIC ${UTILS_SOURCE}) 修改为 add_library(utils SHARED ${UTILS_SOURCE}) 即可。

删除 build 路径,重新编译,当动态库创建好之后,启动我们的项目,提示:

1
2
3
4
make[3]: *** No rule to make target `../build/libutils.a', needed by `test'.  Stop.
make[2]: *** [CMakeFiles/test.dir/all] Error 2
make[1]: *** [CMakeFiles/test.dir/rule] Error 2
make: *** [test] Error 2

奇怪了,我们生成的动态库明明是 libutils.dylib,为什么又去找 libutils.a 了呢?我们回想一下,谁来保存我们的库路径?当然是 find_library(UTILS_LIB NAMES utils PATHS ${PROJECT_SOURCE_DIR}/build) 这个指令中的 UTILS_LIB 变量。既然我们生成了新的库,但是它还去找老的静态库,也就是说,这个变量被缓存了。那么怎么解决这个问题呢?很简单,只需要清掉这个缓存就可以了,我们在 find_library(UTILS_LIB NAMES utils PATHS ${PROJECT_SOURCE_DIR}/build) 上添加另外一条指令 unset(UTILS_LIB CACHE),再启动我们的程序,发现可以正常运行了。