在 CMake 的世界里,将外部文本文件的内容“塞进” C++ 二进制文件,通常不是 CMake 的核心职责。CMake 的主要作用是管理构建过程,例如编译源代码、链接库以及安装文件。不过,CMake 提供了一些非常实用的机制,可以让我们间接地实现这个目标,并且以一种相当“优雅”的方式完成。这里的“优雅”意味着可维护、可配置,并且在项目结构清晰的前提下,能够自动化处理。
我们将探讨两种主流且优雅的方法来实现这个目标:
1. 使用 `configure_file` 和 C++ 预处理器:这是最常见也最“CMake 原生”的方式。
2. 使用 `file(READ ...)` 和一个简单的 C++ 程序/脚本:这种方式更加灵活,尤其适合处理较大的或需要更复杂解析的数据。
为了让整个过程清晰,我们假设你的项目结构如下:
```
my_project/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ └── embedded_data.h
└── data/
└── my_data.txt
```
我们的目标是将 `data/my_data.txt` 的内容嵌入到 `main.cpp` 中,通过 `embedded_data.h` 来访问。
方法一:利用 `configure_file` 和 C++ 预处理器
这种方法的核心思想是,在 CMake 构建过程中,读取文本文件的内容,然后将其注入到一个 C++ 头文件中,并使用预处理器宏(`define`)来表示这些数据。当 C++ 代码编译时,预处理器会将这些宏展开为实际的字符串字面量。
步骤 1:创建模板头文件 (`embedded_data.h.in`)
我们不直接创建一个 `embedded_data.h`,而是创建一个 `.h.in` 文件,这是一个包含占位符的模板。CMake 将会处理这个模板。
在 `src/embedded_data.h.in` 中添加如下内容:
```c++
ifndef EMBEDDED_DATA_H
define EMBEDDED_DATA_H
// 嵌入的文本数据内容将会在这里被定义为宏
// 可以是字符串字面量或者其他形式
define EMBEDDED_DATA_CONTENT "@EMBEDDED_DATA_CONTENT@"
define EMBEDDED_DATA_SIZE @EMBEDDED_DATA_SIZE@
// 如果需要更精细的控制,比如换行符,可以分段定义
// define EMBEDDED_DATA_LINE1 "@EMBEDDED_DATA_LINE1@"
// define EMBEDDED_DATA_LINE2 "@EMBEDDED_DATA_LINE2@"
endif // EMBEDDED_DATA_H
```
注意 `@[...@` 这样的占位符,CMake 的 `configure_file` 命令会查找这些占位符,并用实际值替换它们。
步骤 2:修改 `CMakeLists.txt`
在 `CMakeLists.txt` 中,我们需要执行以下操作:
1. 读取文本文件内容:使用 `file(READ ...)` 命令读取 `data/my_data.txt` 的内容到一个变量中。
2. 预处理文本内容:由于我们想将文本内容作为 C++ 字符串字面量使用,我们需要处理其中的特殊字符,特别是反斜杠 (``) 和双引号 (`"`),以及可能存在的换行符。CMake 提供了一些字符串操作函数。
3. 配置模板文件:使用 `configure_file()` 命令将模板文件 (`.h.in`) 复制到实际的头文件 (`.h`),并替换其中的占位符。
```cmake
设定项目名称和 CMake 版本
cmake_minimum_required(VERSION 3.10)
project(EmbedTxtData CXX)
1. 定义输入数据文件和输出头文件路径
set(INPUT_DATA_FILE "${CMAKE_CURRENT_SOURCE_DIR}/data/my_data.txt")
set(OUTPUT_HEADER_FILE "${CMAKE_CURRENT_BINARY_DIR}/embedded_data.h")
set(TEMPLATE_HEADER_FILE "${CMAKE_CURRENT_SOURCE_DIR}/src/embedded_data.h.in")
2. 读取文本文件内容
READ_ONLY_TEXT 会保留换行符,并将其读取为一个字符串
file(READ "${INPUT_DATA_FILE}" EMBEDDED_DATA_RAW_CONTENT)
3. 对读取的内容进行预处理,以符合 C++ 字符串字面量的要求
替换反斜杠为 \
替换双引号为 "
替换换行符为
string(REPLACE "\" "\\" EMBEDDED_DATA_PROCESSED_CONTENT "${EMBEDDED_DATA_RAW_CONTENT}")
string(REPLACE """ "\"" EMBEDDED_DATA_PROCESSED_CONTENT "${EMBEDDED_DATA_PROCESSED_CONTENT}")
注意:在configure_file中使用
可能会导致换行符被错误处理,更安全的方式是将其拆开定义或使用更直接的宏
如果数据很大,直接嵌入一个长字符串可能不是最佳选择,可以考虑分段定义。
为了演示简单,我们先假设可以处理换行符,如果不行,下面会提供替代方案。
简化的换行符处理(不完美,仅为演示):
string(REPLACE "
" "\n" EMBEDDED_DATA_PROCESSED_CONTENT "${EMBEDDED_DATA_PROCESSED_CONTENT}")
更健壮的处理方式是,在模板中直接嵌入字符串,CMake负责将换行符变成
所以我们不在此处替换换行符,而是让 configure_file 的 @EMBEDDED_DATA_CONTENT@ 宏能够直接接受多行字符串。
CMake 的 @VAR@ 宏替换,会将整个字符串内容粘贴过去。
4. 计算数据大小(可选,但通常有用)
set(EMBEDDED_DATA_SIZE ${EMBEDDED_DATA_RAW_CONTENT})
5. 使用 configure_file 配置头文件模板
将 TEMPLATE_HEADER_FILE 复制到 OUTPUT_HEADER_FILE
替换 @EMBEDDED_DATA_CONTENT@ 为 EMBEDDED_DATA_PROCESSED_CONTENT
替换 @EMBEDDED_DATA_SIZE@ 为 EMBEDDED_DATA_SIZE
configure_file(
"${TEMPLATE_HEADER_FILE}"
"${OUTPUT_HEADER_FILE}"
@ONLY @ONLY 表示只替换 @VAR@ 形式的变量,其他内容原样保留
)
6. 添加可执行文件,并指定源文件
add_executable(my_program src/main.cpp)
7. 将生成的头文件所在目录添加到头文件搜索路径
这样 C++ 代码就可以直接 include "embedded_data.h" 了
target_include_directories(my_program PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
8. (可选) 如果需要将嵌入的数据也作为资源文件安装,可以这样做
install(FILES "${OUTPUT_HEADER_FILE}" DESTINATION "include")
```
关于换行符和字符串字面量处理的更详细说明:
直接将 `data/my_data.txt` 的内容作为一个 C++ 字符串字面量放在 `define` 中,需要特别小心处理其中的双引号和反斜杠。例如,如果你的 `my_data.txt` 内容是:
```
This is line 1.
This line has a "quote".
This line has a backslash .
```
直接替换 `
` 会导致如下的宏:
```c++
define EMBEDDED_DATA_CONTENT "This is line 1.\nThis line has a "quote".\nThis line has a backslash \\."
```
这是一个有效的 C++ 字符串字面量。但如果数据量大,或者字符串中有很多特殊字符,手动处理会很繁琐。CMake 的 `configure_file` 本身并不直接处理 C++ 字符串的转义。它的作用是将文本粘贴过去。
所以,在 `CMakeLists.txt` 中,我们做了:
`string(REPLACE "\" "\\" ...)`:确保所有反斜杠都被转义成 `\`,这样在最终 C++ 字符串中就是一个反斜杠。
`string(REPLACE """ "\"" ...)`:确保所有双引号被转义成 `"`,这样在最终 C++ 字符串中就是一个双引号。
如果你的数据非常大,或者包含大量需要复杂转义的字符,这种方法可能会变得棘手。 更进一步,可以在模板头文件中使用多行字符串(如果你的 C++ 标准支持,例如 C++11 的原始字符串字面量 `R"(...)"`),或者将数据拆分到多个 `define` 中。
例如,将 `my_data.txt` 的每一行都定义为一个字符串宏,这在 `CMakeLists.txt` 中会比较复杂,可能需要循环读取文件行。但为了简洁和效率,通常我们会将原始数据块作为一个整体嵌入。
一个更直接的方式是,让 CMake 将原始文本内容直接插入到 `configure_file` 的占位符中,然后由 C++ 代码自己来处理如何解释它。
例如,在 `embedded_data.h.in` 中:
```c++
ifndef EMBEDDED_DATA_H
define EMBEDDED_DATA_H
// 嵌入的原始文本数据内容
// 在 C++ 中,你可以使用字符串字面量来包含它,或者使用其他方法解析
const char embedded_data_raw_ptr = R"RAW_STRING(
@EMBEDDED_DATA_CONTENT@
)RAW_STRING";
// 或者如果你需要一个方便访问的std::string
include
std::string embedded_data_string = R"RAW_STRING(
@EMBEDDED_DATA_CONTENT@
)RAW_STRING";
endif // EMBEDDED_DATA_H
```
然后,在 `CMakeLists.txt` 中,你只需要读取文件内容,并将其赋值给 `EMBEDDED_DATA_CONTENT` 变量即可,不需要进行转义处理:
```cmake
... (前面的 CMake 代码相同)
2. 读取文本文件内容
file(READ "${INPUT_DATA_FILE}" EMBEDDED_DATA_CONTENT) 直接读取,不做转义
4. 计算数据大小(可选,但通常有用)
set(EMBEDDED_DATA_SIZE ${EMBEDDED_DATA_CONTENT})
5. 使用 configure_file 配置头文件模板
configure_file(
"${TEMPLATE_HEADER_FILE}"
"${OUTPUT_HEADER_FILE}"
@ONLY
)
... (后面的 CMake 代码相同)
```
这样,在 `embedded_data.h` 生成后,`embedded_data_string` 变量(使用了 C++11 的原始字符串字面量 `R"(...)"`)将直接包含 `my_data.txt` 的原始内容,包括换行符、双引号等,无需任何转义。这是目前看来最优雅、最干净的方式。
步骤 3:编写 C++ 代码 (`main.cpp`)
```c++
include
include "embedded_data.h" // 包含我们生成的头文件
int main() {
std::cout << "Content from embedded data:" << std::endl;
std::cout << embedded_data_string << std::endl; // 访问嵌入的字符串
// 如果你定义了 EMBEDDED_DATA_CONTENT 宏(非原始字符串方式)
// std::cout << "Raw content defined as macro:" << std::endl;
// std::cout << EMBEDDED_DATA_CONTENT << std::endl;
std::cout << "Size of embedded data: " << EMBEDDED_DATA_SIZE << std::endl;
return 0;
}
```
构建与运行:
```bash
mkdir build
cd build
cmake ..
make
./my_program
```
你将看到 `my_data.txt` 的内容被打印出来。
方法二:使用 `file(READ ...)` 和一个自定义的 C++ 程序/脚本
这种方法更加灵活,尤其适用于以下情况:
数据量非常大,直接通过 `define` 引入可能导致 C++ 编译器的限制(尽管现代编译器很少有这个问题)。
需要对数据进行更复杂的处理或解析,例如将其转换为 C++ 结构体数组、枚举值等。
不想依赖 C++ 预处理器来嵌入数据。
这里的核心思想是:编写一个小的 C++ 程序,在 CMake 构建期间运行它,让它读取文本文件,然后生成一个 C++ 源文件(`.cpp`),这个源文件将包含数据,并提供一个访问接口(例如一个函数返回 `std::string` 或 `std::vector`)。
步骤 1:创建一个生成数据源的 C++ 程序 (`generate_data_source.cpp`)
这个程序将在 CMake 构建过程中被编译和运行一次。
在项目根目录(或 `tools/` 目录下)创建 `generate_data_source.cpp`:
```c++
include
include
include
include
include // For EXIT_SUCCESS, EXIT_FAILURE
// 函数:读取文件内容到 string
std::string readFileToString(const std::string& filePath) {
std::ifstream file(filePath);
if (!file.is_open()) {
std::cerr << "Error: Could not open file " << filePath << std::endl;
return "";
}
std::string content((std::istreambuf_iterator(file)),
std::istreambuf_iterator());
return content;
}
int main(int argc, char argv[]) {
if (argc != 4) {
std::cerr << "Usage: " << argv[0] << " " << std::endl;
return EXIT_FAILURE;
}
std::string inputTxtFile = argv[1];
std::string outputCppFile = argv[2];
std::string variableName = argv[3];
// 读取输入文本文件的内容
std::string fileContent = readFileToString(inputTxtFile);
if (fileContent.empty() && !inputTxtFile.empty()) { // 只有在文件存在但读取失败时才报错
return EXIT_FAILURE;
}
// 打开输出 C++ 文件
std::ofstream outFile(outputCppFile);
if (!outFile.is_open()) {
std::cerr << "Error: Could not open output file " << outputCppFile << std::endl;
return EXIT_FAILURE;
}
// 写入 C++ 代码,将文件内容包装成一个常量字符串
outFile << "include
";
outFile << "const char " << variableName << "_data = R"RAW_STRING(
";
outFile << fileContent; // 直接写入原始内容,RAW_STRING 会处理换行符等
outFile << "
)RAW_STRING";
";
outFile << "
size_t " << variableName << "_size = " << fileContent.length() << ";
";
outFile << "
const char get_" << variableName << "() {
";
outFile << " return " << variableName << "_data;
";
outFile << "}
";
outFile << "
size_t get_" << variableName << "_size() {
";
outFile << " return " << variableName << "_size;
";
outFile << "}
";
outFile.close();
std::cout << "Generated C++ source file: " << outputCppFile << std::endl;
return EXIT_SUCCESS;
}
```
步骤 2:修改 `CMakeLists.txt`
```cmake
cmake_minimum_required(VERSION 3.10)
project(EmbedTxtDataViaGen CXX)
1. 定义输入文件和输出文件
set(INPUT_DATA_FILE "${CMAKE_CURRENT_SOURCE_DIR}/data/my_data.txt")
set(GENERATOR_PROGRAM "${CMAKE_CURRENT_SOURCE_DIR}/generate_data_source.cpp")
set(GENERATED_SOURCE_FILE "${CMAKE_CURRENT_BINARY_DIR}/embedded_data_source.cpp")
set(EMBEDDED_DATA_VARIABLE_NAME "my_data") 给生成的数据起个名字
2. 编译数据生成器程序
add_executable(data_generator "${GENERATOR_PROGRAM}")
3. 使用 add_custom_command 创建一个自定义构建命令
这个命令会在构建 my_program 之前执行
add_custom_command(
OUTPUT "${GENERATED_SOURCE_FILE}" 这个命令的输出文件
DEPENDS "${INPUT_DATA_FILE}" "data_generator" 依赖于输入数据和生成器本身
COMMAND $ 要执行的命令
"${INPUT_DATA_FILE}" 传递给生成器的第一个参数:输入文本文件
"${GENERATED_SOURCE_FILE}" 第二个参数:输出的 C++ 源文件
"${EMBEDDED_DATA_VARIABLE_NAME}" 第三个参数:用于生成变量名的字符串
COMMENT "Generating C++ source file from text data..."
VERBATIM 确保命令字符串被正确传递
)
4. 添加主程序可执行文件
add_executable(my_program src/main.cpp)
5. 将生成的文件添加到主程序的源文件列表
target_sources(my_program PRIVATE "${GENERATED_SOURCE_FILE}")
6. 将生成的文件所在目录添加到头文件搜索路径(虽然这里我们直接在 .cpp 中定义,但如果数据通过 .h 访问,此步仍可能需要)
在这个例子中,我们直接在 .cpp 中定义了函数,所以不需要包含 .h 文件。
但如果我们想在 main.cpp 中调用 get_my_data(),则需要一个 .h 文件。
我们可以让 generate_data_source.cpp 也生成一个 .h 文件。
改进:同时生成头文件
set(GENERATED_HEADER_FILE "${CMAKE_CURRENT_BINARY_DIR}/embedded_data.h")
修改 generate_data_source.cpp 以生成 .h 文件
... (下面是 generate_data_source.cpp 的修改版本) ...
修改 CMakeLists.txt 以包含头文件
target_include_directories(my_program PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
7. (可选) 如果需要将生成的文件安装
install(FILES "${GENERATED_SOURCE_FILE}" "${GENERATED_HEADER_FILE}" DESTINATION "src")
```
修改 `generate_data_source.cpp` 以同时生成 `.h` 文件:
```c++
include
include
include
include
include // For EXIT_SUCCESS, EXIT_FAILURE
// 函数:读取文件内容到 string
std::string readFileToString(const std::string& filePath) {
std::ifstream file(filePath);
if (!file.is_open()) {
std::cerr << "Error: Could not open file " << filePath << std::endl;
return "";
}
std::string content((std::istreambuf_iterator(file)),
std::istreambuf_iterator());
return content;
}
int main(int argc, char argv[]) {
if (argc != 5) {
std::cerr << "Usage: " << argv[0] << " " << std::endl;
return EXIT_FAILURE;
}
std::string inputTxtFile = argv[1];
std::string outputCppFile = argv[2];
std::string outputHFile = argv[3];
std::string variableName = argv[4];
// 读取输入文本文件的内容
std::string fileContent = readFileToString(inputTxtFile);
if (fileContent.empty() && !inputTxtFile.empty()) { // 检查文件是否成功读取
return EXIT_FAILURE;
}
// 生成 C++ 源文件 (.cpp)
std::ofstream outFileCpp(outputCppFile);
if (!outFileCpp.is_open()) {
std::cerr << "Error: Could not open output C++ file " << outputCppFile << std::endl;
return EXIT_FAILURE;
}
outFileCpp << "include "" << outputHFile.substr(outputHFile.find_last_of('/') + 1) << ""
"; // 包含 .h 文件
outFileCpp << "const char " << variableName << "_data = R"RAW_STRING(
";
outFileCpp << fileContent;
outFileCpp << "
)RAW_STRING";
";
outFileCpp << "
size_t " << variableName << "_size = " << fileContent.length() << ";
";
outFileCpp << "
const char get_" << variableName << "() {
";
outFileCpp << " return " << variableName << "_data;
";
outFileCpp << "}
";
outFileCpp << "
size_t get_" << variableName << "_size() {
";
outFileCpp << " return " << variableName << "_size;
";
outFileCpp << "}
";
outFileCpp.close();
// 生成 C++ 头文件 (.h)
std::ofstream outFileH(outputHFile);
if (!outFileH.is_open()) {
std::cerr << "Error: Could not open output header file " << outputHFile << std::endl;
return EXIT_FAILURE;
}
outFileH << "ifndef EMBEDDED_" << variableName << "_H
";
outFileH << "define EMBEDDED_" << variableName << "_H
";
outFileH << "
include
";
outFileH << "
// Function prototypes for accessing embedded data
";
outFileH << "const char get_" << variableName << "();
";
outFileH << "size_t get_" << variableName << "_size();
";
outFileH << "
endif // EMBEDDED_" << variableName << "_H
";
outFileH.close();
std::cout << "Generated C++ source file: " << outputCppFile << std::endl;
std::cout << "Generated C++ header file: " << outputHFile << std::endl;
return EXIT_SUCCESS;
}
```
修改 `CMakeLists.txt` 来适应新的生成器:
```cmake
cmake_minimum_required(VERSION 3.10)
project(EmbedTxtDataViaGen CXX)
1. 定义输入文件和输出文件
set(INPUT_DATA_FILE "${CMAKE_CURRENT_SOURCE_DIR}/data/my_data.txt")
set(GENERATOR_PROGRAM "${CMAKE_CURRENT_SOURCE_DIR}/generate_data_source.cpp")
set(GENERATED_SOURCE_FILE "${CMAKE_CURRENT_BINARY_DIR}/embedded_data_source.cpp")
set(GENERATED_HEADER_FILE "${CMAKE_CURRENT_BINARY_DIR}/embedded_data.h") 输出的头文件
set(EMBEDDED_DATA_VARIABLE_NAME "my_data")
2. 编译数据生成器程序
add_executable(data_generator "${GENERATOR_PROGRAM}")
3. 使用 add_custom_command 创建一个自定义构建命令
add_custom_command(
OUTPUT "${GENERATED_SOURCE_FILE}" "${GENERATED_HEADER_FILE}" 命令输出多个文件
DEPENDS "${INPUT_DATA_FILE}" "data_generator"
COMMAND $
"${INPUT_DATA_FILE}"
"${GENERATED_SOURCE_FILE}"
"${GENERATED_HEADER_FILE}" 传递输出头文件的路径
"${EMBEDDED_DATA_VARIABLE_NAME}"
COMMENT "Generating C++ source and header files from text data..."
VERBATIM
)
4. 添加主程序可执行文件
add_executable(my_program src/main.cpp)
5. 将生成的文件添加到主程序的源文件列表
target_sources(my_program PRIVATE "${GENERATED_SOURCE_FILE}")
6. 将生成头文件所在的目录添加到头文件搜索路径
target_include_directories(my_program PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
```
步骤 3:编写 C++ 代码 (`main.cpp`)
```c++
include
include "embedded_data.h" // 包含我们生成的头文件
int main() {
std::cout << "Content from embedded data (via function):" << std::endl;
std::cout << get_my_data() << std::endl; // 调用生成器提供的函数
std::cout << "Size of embedded data: " << get_my_data_size() << std::endl;
return 0;
}
```
构建与运行:
```bash
mkdir build
cd build
cmake ..
make
./my_program
```
哪种方法更优雅?
方法一 (`configure_file` + 原始字符串字面量):
优点:非常简洁,几乎不需要额外的代码,直接利用 CMake 的强大功能。对于不包含复杂特殊字符的数据,或数据本身就是字符串的场景,非常方便。易于理解和维护。
缺点:如果数据本身包含大量 C++ 字符串字面量需要转义的字符,且模板头文件又不是用原始字符串字面量,那么在 CMakeLists.txt 中处理转义会很繁琐。
方法二 (`file(READ ...)` + 自定义生成器):
优点:极高的灵活性。可以根据需要生成任何 C++ 代码,例如将文本内容解析成结构体数组、常量枚举值,或者直接生成 C++ 头文件。对于复杂数据格式或需要自定义解析逻辑的场景是最佳选择。生成器程序本身也可以复用。
缺点:需要额外编写一个 C++ 程序来生成数据源文件,增加了项目的复杂度。构建过程会多一步编译生成器。
对于大多数情况,方法一(使用 `configure_file` 和原始字符串字面量)是最为优雅和推荐的。 它最大限度地利用了 CMake 的内置能力,保持了项目的简洁性。只有当你需要更复杂的逻辑或数据处理时,方法二才是必需的。
记住,“优雅”也包含“可维护性”。选择最能清晰表达意图,并且最易于未来修改和理解的方法。