0%

cmake 迷你系列(六)target_xxx_xxx 中的传播机制

引言

我们在使用 target 相关指令时,它们多有一个共同类型的参数 PRIVATE、INTERFACE、PUBLIC 那么这几个参数到底是什么意思呢?本文将为大家揭晓。

传播机制

为了说明清楚,我们以一个 demo 开始,首先给出项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
test 
|---libA
|---include
|---libA.h
|---privateHeaderA.h
|---libA.c
|---CMakeLists.txt
|---libB
|---include
|---libB.h
|---privateHeaderB.h
|---libB.c
|---CMakeLists.txt
|---libC
|---include
|---libC.h
|---subModule
|---sub.h
|---sub.c
|---privateHeaderC.h
|---libC.h
|---CMakeLists.txt
|---libD
|---libD.h
|---CMakeLists.txt

|---main.c
|---CMakeLists.txt

库分析

  • libA

    1
    2
    3
    4
    5
    6
    7
    8
    #ifndef TEST_LIBA_H
    #define TEST_LIBA_H

    #include "libB.h"

    void libA();

    #endif //TEST_LIBA_H

    在 libA 对外提供的头文件中,引入了 libB 库的头文件 libB.h。同时声明了一个函数 void libA() 我们在 libA.c 中实现该函数:

    1
    2
    3
    4
    5
    6
    7
    8
    #include <stdio.h>
    #include "libA.h"
    #include "privateHeaderA.h"

    void libA()
    {
    printf("this info from libA and the private data is %s\n", PRIVATE_HEADER_A);
    }

    以上代码中,我们引入了 privateHeaderA.h 头文件:

    1
    2
    3
    4
    5
    6
    #ifndef TEST_PRIVATEHEADERA_H
    #define TEST_PRIVATEHEADERA_H

    #define PRIVATE_HEADER_A "PRIVATE_HEADER_A"

    #endif //TEST_PRIVATEHEADERA_H

    内容很简单,只是定义一个宏而已。该头文件不对外暴露,一会我们通过 CMakeLists.txt 文件就可以看到。我们在 CMakeLists.txt 中定义该库的内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    add_library(libA STATIC libA.c)

    target_include_directories(libA
    PUBLIC include
    PRIVATE .
    )

    target_link_libraries(libA
    PUBLIC libB
    )
    target_compile_definitions(libA PRIVATE -DTEST_LIB_A)
    • 1 行,我们添加一个静态库 libA。
    • 3 行,我们指定 libA 的链接头文件,其中 include 下的头文件定义为 PUBLIC 作为对外开放的头文件,而当前路径下的头文件(privateHeaderA.h)定义为 PRIVATE 也就是不对外开放。
    • 8 行,libA 链接了 libB,这里定义为 PUBLIC,为什么是 PUBLIC 是因为 libA 对外提供的头文件 libA.h 中引入了 libB 的头文件 libB.h,所以,必须把 libB 传递给需要链接到 libA 的那一端。否则会提示无此头文件。
    • 11 行,给 libA 库添加一个宏,并将它定义为 PRIVATE,稍后测试该宏的可见性。
  • libB

    我们看一下 libB 的头文件内容:

    1
    2
    3
    4
    5
    6
    #ifndef TEST_LIBB_H
    #define TEST_LIBB_H

    void libB();

    #endif //TEST_LIBB_H

    在 libB 的头文件中,我们只声明了一个函数 libB()。我们看下 libB() 的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    #include <stdio.h>

    #include "libB.h"
    #include "privateHeaderB.h"
    #include "libC.h"

    void libB()
    {
    printf("this info from libB and it's private data is %s\n", PRIVATE_HEADER_B);
    libC();
    #if TEST_LIB_C
    puts("libB can access marco from C");
    #endif
    }

    从以上代码中我们看出,libB.c 引入了私有的 privateHeaderB.h 头文件以及 libC.h 头文件,并在代码里调用了 libC.h 中的函数 libC(),并在接下来测试了 libC 定义的 PUBLIC 宏。从以上代码我们可以分析出,libB.c 隐藏了调用库 libC 的细节。privateHeaderB.h 也十分简单,仅仅定义一个宏:

    1
    2
    3
    4
    5
    6
    #ifndef TEST_PRIVATEHEADERB_H
    #define TEST_PRIVATEHEADERB_H

    #define PRIVATE_HEADER_B "PRIVATE_HEADER_B"

    #endif //TEST_PRIVATEHEADERB_H

    我们看下它的 CMakeLists.txt 文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    add_library(libB STATIC libB.c)

    target_include_directories(libB
    PUBLIC include
    PRIVATE .
    )

    target_link_libraries(libB PRIVATE libC)

    target_compile_definitions(libB PUBLIC -DTEST_LIB_B)

    我们着重看下 8 行,libB 以 PRIVATE 的方式链接了 libC,因为 libB 中只是 libB.c 源码使用了 libC 的代码,libB 对外暴露的头文件并没有暴露 libC 的相关信息,所以 libB 没有必要向外部传递 libC 的信息,所以这里用 PRIVATE 关键字。另外 10 行,向 libB 添加了 TEST_LIB_B 宏,并设置可见性为 PUBLIC。

  • libC

    我们接下来看下 libC 库,该库不依赖任何库,只是单纯的提供功能。我们看下它的头文件:

    1
    2
    3
    4
    5
    6
    #ifndef TEST_LIBC_H
    #define TEST_LIBC_H

    void libC();

    #endif //TEST_LIBC_H

    对外的头文件只是提供一个 libC() 函数。以下是它的实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <stdio.h>

    #include "libC.h"
    #include "privateHeaderC.h"
    #include "sub.h"

    void libC()
    {
    printf("this info from libC and it's private data is %s\n", PRIVATE_HEADER_C);
    sub();
    #ifdef TEST_LIB_C
    puts("libC's test macro has been opened");
    #endif
    }

    这里 libC() 函数调用了它子模块中的一个函数 sub(),同时添加一个宏判断。privateHeaderC.h 头文件依旧很简单:

    1
    2
    3
    4
    5
    6
    #ifndef TEST_PRIVATEHEADERC_H
    #define TEST_PRIVATEHEADERC_H

    #define PRIVATE_HEADER_C "PRIVATE_HEADER_C"

    #endif //TEST_PRIVATEHEADERC_H

    sub.h 内容也只是声明一个函数:

    1
    2
    3
    4
    5
    6
    #ifndef TEST_SUB_H
    #define TEST_SUB_H

    void sub();

    #endif //TEST_SUB_H

    sub.c 的实现:

    1
    2
    3
    4
    5
    6
    7
    #include <stdio.h>
    #include "sub.h"

    void sub()
    {
    puts("the info from sub function of libC");
    }

    最后我们看下它的 CMakeLists.txt 文件:

    1
    2
    3
    4
    5
    6
    7
    8
    add_library(libC STATIC libC.c subModule/sub.c)

    target_include_directories(libC
    PUBLIC include
    PRIVATE . subModule
    )

    target_compile_definitions(libC PUBLIC -DTEST_LIB_C)

    我们把 libC.c、subModule\sub.c 共同编译成库 libC,暴露 include 下的头文件,将当前路径以及 subModule 下的头文件隐藏掉。最后我们定义了一个 PUBLIC 级别的宏给库 libC。

  • libD

    libD 是一个特殊的库,它只有头文件,没有源码。

    1
    2
    3
    4
    5
    6
    #ifndef TEST_LIBD_H
    #define TEST_LIBD_H

    #define LIB_D "LIB_D is only a header"

    #endif //TEST_LIBD_H

    在它的头文件里,我们自定义个宏 LIB_D 。我们看下它的 CMakeLists.txt 文件:

    1
    2
    3
    4
    5
    6
    7
    add_library(libD INTERFACE)

    target_include_directories(libD
    INTERFACE libD.h
    )

    target_compile_definitions(libD INTERFACE -DTEST_LIB_D)

    注意添加库时,我们这里指定了 INTERFACE 关键字,同时在链接头文件时,也指定了 INTERFACE。在最后我们定义的宏也必须是 INTERFACE。也就是说,当库的可见级别为 INTERFACE 时,所有的链接可见性都必须是 INTERFACE。这其实是说的通的,因为只有向外暴露接口时才会是 INTERFACE。如果保函源码了,那它就得是 PUBLIC 级别,否则就只能是 PRIVATE 了。

main 函数

既然库都已经准备好了,我们最后的重点就是 main 函数了。首先我们先看下它的 CMakeLists.txt 文件:

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

set(CMAKE_C_STANDARD 11)

add_executable(test main.c)

target_link_libraries(test PRIVATE libA libD)

add_subdirectory(libA)
add_subdirectory(libB)
add_subdirectory(libC)
add_subdirectory(libD)

我们只需要关注第 8 行,我们给 test 可执行目标以 PRIVATE 级别链接了 libA 和 libD。也就是说 test 不会暴露任何信息给它的依赖方。我们看下 main.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include "libA.h"
#include "libD/libD.h"

int main()
{
libA();
libB();
printf("libD is %s\n", LIB_D);

#ifdef TEST_LIB_A
puts("can access marco from A");
#endif

#ifdef TEST_LIB_B
puts("can access marco from B");
#endif

#if TEST_LIB_C
puts("can access marco from C");
#endif

#if TEST_LIB_D
puts("can access marco from D");
#endif
return 0;
}

输出

1
2
3
4
5
6
7
8
9
this info from libA and the private data is PRIVATE_HEADER_A
this info from libB and it's private data is PRIVATE_HEADER_B
this info from libC and it's private data is PRIVATE_HEADER_C
the info from sub function of libC
libC's test macro has been opened
libB can access marco from C
libD is LIB_D is only a header
can access marco from B
can access marco from D

分析

  • 因为链接了 libA,所以 libA() 函数可以直接调用,输出第 1 行内容。
  • 由于 libA 对外提供的头文件 libA.h 引入了 libB.h 头文件,同时 libA 以 PUBLIC 方式链接库 libB,所以,main.c 同样可以调用 libB(),并输出第 2 行。
  • 由于 libB() 函数内部调用了 libC 库的函数 libC()libC() 函数有调用了 sub(),同时 libC() 又进行了宏判断。所以输出了 3 ~ 5 行内容。libB 对 libC 中定义的 PUBLIC 宏进行了定义测试,导致输出了第 6 行内容。
  • main 链接了 libD,所以可以读取 libD 头文件中的宏信息,于是输出第 7 行。
  • 接下来是宏输出,因为 libA 的宏可见性为 PRIVATE,所以 main 不可见。libB 的宏设置为 PUBLIC,而 libB 又是通过 libA 暴露给 main 的,所以第 8 行得以输出。虽然 libC 的宏也是 PUBLIC,但是它被 libB 隐藏掉了。那么 main 自然是访问不了 libC 的宏的,但是 libB 可以访问。最后 libD 中定义的 INTERFACE 宏可以顺利被 main 访问,输出第 9 行。

总结

根据以上测试,如果我们定义库 A 以及库 B。

  • 如果库 A 只有在自己的源码中使用了库 B 的功能且库 A 对外提供的头文件不包含任何库 B 中的信息时,我们用 PRIVATE 进行链接库 B。
  • 如果库 A 在它对外暴露的头文件中包含了库 B 中的头文件,同时库 A 的源码里也引入了库 B 中的实现,那么库 A 必须以 PUBLIC 方式链接库 B(当其他库链接库 A 时,将库 B 暴露给它)。
  • 如果库 A 只在它对外提供的头文件中引入了库 B 的头文件,库 A 的代码并没有引入(也就是库 A 的源码也没有引入它对外提供的头文件)或者库 B 本身就是一个 head-only 库,那么必须使用 INTERFACE 进行链接。

参考