CMake 系列(三):构建和链接静态库与动态库

上一篇中,我们只用单个源文件构建可执行文件。实际项目中往往会有多个源文件,通常分布在不同子目录中。这种实践有助于理顺项目的源代码结构,支持模块化、代码重用和关注点分离,还可以简化并加速项目的重新编译。本篇将展示如何将源代码编译成库,以及如何链接这些库。

提示:本示例在 CMake 3.5 或更高版本中有效,并已在 GNU/Linux、macOS 和 Windows 上测试通过。

具体实施

回看第一个例子,这里不再为可执行文件提供单个源文件,而是引入一个类,用来包装要打印到屏幕上的消息。更新后的 hello-world.cpp:

#include "Message.hpp"

#include <cstdlib>
#include <iostream>

int main() {
  Message say_hello("Hello, CMake World!");
  std::cout << say_hello << std::endl;

  Message say_goodbye("Goodbye, CMake World");
  std::cout << say_goodbye << std::endl;

  return EXIT_SUCCESS;
}

Message 类包装了一个字符串,并提供了重载的 << 操作,它包含两个源码文件:Message.hpp 头文件与 Message.cpp 源文件。Message.hpp 中的接口包含以下内容:

#pragma once

#include <iosfwd>
#include <string>

class Message {
public:
  Message(const std::string &m) : message_(m) {}

  friend std::ostream &operator<<(std::ostream &os, Message &obj) {
    return obj.printObject(os);
  }

private:
  std::string message_;

  std::ostream &printObject(std::ostream &os);
};

Message.cpp 的实现如下:

#include "Message.hpp"

#include <iostream>
#include <string>

std::ostream &Message::printObject(std::ostream &os) {
  os << "This is my very nice message: " << std::endl;
  os << message_;

  return os;
}

这里有两个文件需要编译,所以 CMakeLists.txt 必须进行修改。本例中,先把它们编译成一个库,而不是直接编译成可执行文件:

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

project(recipe-03 LANGUAGES CXX)

add_library(message
  STATIC
    Message.hpp
    Message.cpp
  )

add_executable(hello-world hello-world.cpp)

target_link_libraries(hello-world message)

本节引入了两个新命令:

  • add_library(message STATIC Message.hpp Message.cpp):让 CMake 将指定源文件编译并打包成名为 message 的静态库;
  • target_link_libraries(hello-world message):将可执行文件 hello-world 链接到刚生成的 message 库。

我们可以像之前的示例一样配置并构建项目:

$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .

编译成功后,构建目录中会包含 libmessage.a 静态库(GNU/Linux 上)和 hello-world 可执行文件。

CMake 接受的其他值也可以作为 add_library() 的第二个参数,我们来看下后面会用到的值:

  • STATIC:创建静态库,即把编译得到的目标文件打包成归档文件,供链接其他目标时使用(GNU/Linux 上为 .a 文件,Windows 上为 .lib 文件);
  • SHARED:创建动态库(共享库),在运行时动态加载(GNU/Linux 上为 .so 文件,Windows 上为 .dll 文件);静态链接与动态链接在底层的完整原理,本站《Linux 系列导读》的「链接与共享库」阶段有更深入的展开;
  • OBJECT:创建对象库,即只编译生成目标文件集合,不打包成归档文件,可以直接作为其他目标的输入。

CMake 还能生成特殊类型的库,它们不会在构建系统中产生输出,但对于组织目标之间的依赖关系和构建需求非常有用:

  • IMPORTED:表示从外部项目导入的库,CMake 不会编译它,只需声明其位置即可在项目中使用;
  • INTERFACE:不产生任何输出文件,只向依赖它的目标传递使用要求(如头文件搜索路径、编译宏定义等)。

后面的章节中,我们会使用 target_sources 来汇集源码,而不是像本例中这样直接把源文件集合交给 add_library()。Craig Scott 撰写过一篇精彩的博文,其中对 target_sources 命令的具体使用有详细介绍。

使用对象库

现在展示 OBJECT 库的使用,修改 CMakeLists.txt 如下:

cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

project(recipe-03 LANGUAGES CXX)

add_library(message-objs
  OBJECT
    Message.hpp
    Message.cpp
  )

# this is only needed for older compilers
# but doesn't hurt either to have it
set_target_properties(message-objs
  PROPERTIES
    POSITION_INDEPENDENT_CODE 1
  )

add_library(message-shared
  SHARED
    $<TARGET_OBJECTS:message-objs>
  )

add_library(message-static
  STATIC
    $<TARGET_OBJECTS:message-objs>
  )

add_executable(hello-world hello-world.cpp)

target_link_libraries(hello-world message-static)

首先,用 add_library(message-objs OBJECT Message.hpp Message.cpp) 定义一个对象库。此外,需要保证编译的目标文件与位置无关,这可以通过 set_target_properties() 命令设置 message-objs 目标的相应属性来实现。

注意:在某些平台和/或使用较老的编译器时,可能需要显式地为目标设置 POSITION_INDEPENDENT_CODE 属性。

现在,可以使用这个对象库来生成静态库(message-static)和动态库(message-shared)。要注意引用对象库的生成器表达式语法:$<TARGET_OBJECTS:message-objs>。生成器表达式是 CMake 在生成阶段(即配置之后)构造的,用于生成特定于配置的构建输出,我们将在后面的章节中深入介绍。最后,将 hello-world 可执行文件链接到消息库的静态版本。

生成同名的两个库

是否可以让 CMake 生成同名的两个库?换句话说,它们可以都叫 message,而不是 message-static 和 message-shared 吗?这需要修改这两个目标的属性:

add_library(message-shared
  SHARED
    $<TARGET_OBJECTS:message-objs>
  )

set_target_properties(message-shared
  PROPERTIES
    OUTPUT_NAME "message"
  )

add_library(message-static
  STATIC
    $<TARGET_OBJECTS:message-objs>
  )

set_target_properties(message-static
  PROPERTIES
    OUTPUT_NAME "message"
  )

能否链接到 DSO?

我们可以链接到 DSO(动态共享对象)吗?这取决于操作系统和编译器。

这是为什么呢?要正确生成并使用 DSO,需要程序员限制符号的可见性。这要在编译器的帮助下实现,而不同的操作系统和编译器对此有不同的约定。CMake 提供了一个机制来处理这个问题,我们将在后面的章节中解释它如何工作。

系列导航


   转载规则


《CMake 系列(三):构建和链接静态库与动态库》 由 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
CMake 系列(四):用条件控制编译 CMake 系列(四):用条件控制编译
前面几篇的示例比较简单,CMake 的执行流是线性的:从一组源文件到单个可执行文件,或者再生成静态库/动态库。为了确保完全控制构建项目、配置、编译和链接所涉及的所有步骤的执行流,CMake 提供了自己的语言。本篇将探索条件结构 if-els
2022-04-05
下一篇 
CMake 系列(二):切换构建生成器 CMake 系列(二):切换构建生成器
CMake 是一个构建系统生成器,可以使用单个 CMakeLists.txt 为不同平台上的不同工具集配置项目。您可以在 CMakeLists.txt 中描述构建系统必须执行的操作,以配置并编译代码;基于这些指令,CMake 将为所选的构建
2022-03-22
  目录