美文网首页
[082]破局Cmake中的PRIVATE,PUBLIC,INT

[082]破局Cmake中的PRIVATE,PUBLIC,INT

作者: 王小二的技术栈 | 来源:发表于2023-08-14 14:54 被阅读0次

    前言

    最近看了很多项目的代码,代码是用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之间的属性传递。

    六、参考文献

    https://chunleili.github.io/cmake/understanding-INTERFACE

    相关文章

      网友评论

          本文标题:[082]破局Cmake中的PRIVATE,PUBLIC,INT

          本文链接:https://www.haomeiwen.com/subject/dvhcmdtx.html