前言
最近看了很多项目的代码,代码是用cmake编译的,由于各种库之间链接关系错综复杂,加上PRIVATE,PUBLIC,INTERFACE属性值,我在添加代码的时候总会遇到稀奇古怪的编译的问题,网上看了很多文章,写的都不是很靠谱,正好看到一个b站视频讲的不错,解决了我很多疑惑,我又有了新的疑惑,折腾了一晚上终于把这个搞明白了,分享给大家。
一、原理
从 modern cmake(>=3.0) 开始,使用的范式从 director-oriented 转换到了 target-oriented。 这其中最重要的有三个概念:
target
target相应的properties
可见性
所谓target就是编译的目标,一般就三种:
静态库: 使用add_library()
动态库: 使用add_library() 指定SHARED关键字
可执行文件: 使用add_executable
所谓properties就是target的属性,最常见的有以下五种:
编译标志:使用target_complie_option
预处理宏标志:使用 target_compile_definitions
头文件目录:使用 target_include_directories
链接库:使用 target_link_libraries
链接标志:使用 target_link_options
所谓可见性就是上述这些属性在不同target之间的传递性。有三种:
PRIVATE
PUBLIC
INTERFACE
缺省值为PUBLIC
二、可见性的传递(非常重要)
每一个Target对于自身设置的不同属性处理
对于private的property,不会传递,只会自己用。
对于public的property,会传递,也自己用。
对于interface的property,会传递,但不会自己用
public和interface的属性是可传递属性
可见性的传递是依靠target_link_libraries,传递的规则如下:
假设如下链接关系
target_link_libraries(B XXX A)// XXX为private,public,interface
如果XXX为private,A的可传递属性变成B的private property
如果XXX为public,A的可传递属性变成B的public property
如果XXX为interface,A的可传递属性变成B的interface property
三、实战1
3.1 最简单的demo
interface_a.h
#ifndef CPP_INTERFACE_A_H
#define CPP_INTERFACE_A_H
int addA(int a, int b);
#endif //CPP_INTERFACE_A_H
interface_b.h
#ifndef CPP_INTERFACE_B_H
#define CPP_INTERFACE_B_H
int addB(int a, int b);
#endif //CPP_INTERFACE_B_H
interface_a.cpp
#include <stdio.h>
#include "interface_a.h"
int addA(int a, int b) {
printf("addA\n");
return a + b;
}
interface_b.cpp
#include <stdio.h>
#include "interface_b.h"
#include "interface_a.h"
int addB(int a, int b)
{
printf("addB\n");
return addA(a, b);
}
main.cpp
#include "interface_b.h"
#include <stdio.h>
int main()
{
printf("main\n");
addB(1, 2);
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(CPP)
set(CMAKE_CXX_STANDARD 17)
add_library(A libA/interface_a.c)
target_include_directories(A PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)
add_library(B SHARED libB/interface_b.c)
target_link_libraries(B A)
target_include_directories(B PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeB)
add_executable(CPP main.c)
target_link_libraries(CPP B)
用图来表示代码就如下,CPP调用B中addB,B中的addB调用addA
最后运行的结果
main
addB
addA
这例子简单吧,我们进一步来解读一下CMakeLists.txt,红色为传递过来的属性
查看对应的cmake的编译中间文件,可以进一步验证我们的判断,正好和对应的属性对应。
3.2 main中能否调用addA
可以看到CPP拥有target_include_directories(CPP PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA和target_link_libraries(CPP A)的属性
理论上来说肯定main.cpp可以调用addA
修改main.cpp
#include "interface_b.h"
#include "interface_a.h"
#include <stdio.h>
int main()
{
printf("main\n");
addA(1, 2);
addB(1, 2);
return 0;
}
成功运行
main
addA
addB
addA
3.3 将PUBLIC改成PRIVATE
如果我们对CMakeLists.txt做如下修改,请问上面main.c还能不能正常运行
cmake_minimum_required(VERSION 3.22)
project(CPP)
set(CMAKE_CXX_STANDARD 17)
add_library(A libA/interface_a.c)
target_include_directories(A PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)
add_library(B SHARED libB/interface_b.c)
target_link_libraries(B PRIVATE A)//改动的地方
target_include_directories(B PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeB)
add_executable(CPP main.c)
target_link_libraries(CPP B)
解读一下CmakeLists.txt,红色为传递过来的属性
和3.2中最大的差异就是CPP中includeA没了,那main.c肯定找不到#include "interface_a.h",所以会编译报错找不到头文件interface_a.h
运行结果果然和预料的一样。
/home/kobe/submits/CPP/main.c:2:10: fatal error: interface_a.h: No such file or directory
2 | #include "interface_a.h"
| ^~~~~~~~~~~~~~~
3.4 手动添加includeA
继续修改 CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(CPP)
set(CMAKE_CXX_STANDARD 17)
add_library(A libA/interface_a.c)
target_include_directories(A PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)
add_library(B SHARED libB/interface_b.c)
target_link_libraries(B PRIVATE A)
target_include_directories(B PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeB)
add_executable(CPP main.c)
target_link_libraries(CPP B)
target_include_directories(CPP PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)//修改的代码
解读一下CmakeLists.txt,红色为传递过来的属性,紫色是CPP额外加的属性
看到C自身属性添加了includeA,那头文件也有了,链接的时候,CPP链接B,B链接A,最后可以链接到一起,CPP应该可以使用addA了
运行结果果然可以
main
addA
addB
addA
四.实战2
4.1 Interface的作用
修改文件interface_b.cpp,移除B对A的addA的使用
#include <stdio.h>
#include "interface_b.h"
int addB(int a, int b)
{
printf("addB\n");
return a + b;
}
修改文件cmakelists.txt
cmake_minimum_required(VERSION 3.22)
project(CPP)
set(CMAKE_CXX_STANDARD 17)
add_library(A libA/interface_a.c)
target_include_directories(A PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)
add_library(B SHARED libB/interface_b.c)
target_link_libraries(B INTERFACE A)
target_include_directories(B PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeB)
add_executable(CPP main.c)
target_link_libraries(CPP B)
解读一下CmakeLists.txt,红色为传递过来的属性
因为CPP使用到A的接口和B的接口,B没有使用A的接口,所以按照上面的属性,A,B,CPP三个都可以正常编译运行
main
addA
addB
4.2 add_library(C INTERFACE) -- 比较特殊的用法
修改文件cmakelists.txt
cmake_minimum_required(VERSION 3.22)
project(CPP)
set(CMAKE_CXX_STANDARD 17)
add_library(A libA/interface_a.c)
target_include_directories(A PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includeA)
add_library(C INTERFACE)
target_include_directories(C INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/includeB)
add_library(B SHARED libB/interface_b.c)
target_link_libraries(B PUBLIC C INTERFACE A)
add_executable(CPP main.c)
target_link_libraries(CPP B)
这是种特殊的用法,就是创建一个虚拟的target C,add_library(C INTERFACE)不会编译出任何库和可执行文件,而且C的所有属性必须设置为INTERFACE
解读一下CmakeLists.txt,红色为传递过来的属性
最后也可以完美的运行!
这里C就是一个header-only的库,他的所有属性都是Interface的,不会编译出任何库,唯一作用就是将属性传递给link它的目标。
五、总结
按照1.原理和2.可见性的传递,对应每一个项目,用这样子的表格列出来每一个target对应的属性,也就可以了解到每一个target编译依赖的头文件以及库文件。记住以Target的视角来看待每一个属性,关注两个Target之间的link的属性,以及两个Target之间的属性传递。
网友评论