上一篇中,我们只用单个源文件构建可执行文件。实际项目中往往会有多个源文件,通常分布在不同子目录中。这种实践有助于理顺项目的源代码结构,支持模块化、代码重用和关注点分离,还可以简化并加速项目的重新编译。本篇将展示如何将源代码编译成库,以及如何链接这些库。
提示:本示例在 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 提供了一个机制来处理这个问题,我们将在后面的章节中解释它如何工作。