QT可以将我们需要的txt,icon,png等文件作为资源文件编译进库或者可执行程序中,从而这些资源文件不用发布和部署,进而没有被篡改的风险。
如果我们不使用QT,要达到同样的效果,可以这样操作:比如有个data.txt文件,我们可以将data.txt文件转为C/C++的字符串数组,然后程序引用该数组,就如同读取文件一般。
将文件转为数组的方式一般有如下几种:
第一:在Linux系统上使用如xxd工具,使用命令 xxd -i data.txt > embedded_data.h 将文件转为数组文件;
第二:在Windows系统上我们可以写个如xxd(xxd的代码位于vim项目)一般的工具,执行文件转换;
第三:使用cmake原生的转换,将文件转为数组文件(当文件较大时,性能不如xxd等原生工具);
本文介绍第三种方法,我们编写一个通用的cmake函数,在Linux或者Windows上执行文件到数组的转换。
自定义转换函数的定义
自定义cmake函数,一般有两种使用方式:
第一:将该函数定义在CMakeLists.txt文件,该转换方法仅在配置阶段生成数组文件,编译阶段修改源文件,cmake不会因源文件变化而重新生成;
第二:将该函数单独定义在单独的文件,在cmake编译阶段,根据源文件的变化动态调用该函数生成数组文件;
本文我们重点间接第二种方法。
cmake转换函数定义如下,然后我们将下述内容保存为xxd.cmake文件(文件名称可自定义):
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| function(file_to_c_array INPUT_FILE OUTPUT_FILE ARRAY_NAME)
# 检查输入文件是否存在
if(NOT EXISTS "${INPUT_FILE}")
message(FATAL_ERROR "Does not Exist:${INPUT_FILE}")
endif()
# 生成MD5值
file(MD5 "${INPUT_FILE}" INPUT_FILE_MD5)
string(TOUPPER "${INPUT_FILE_MD5}" INPUT_FILE_MD5_HEX)
# 读取文件内容(二进制模式,避免换行符转换)
file(READ "${INPUT_FILE}" FILE_CONTENT BINARY)
# 获取文件字节长度
string(LENGTH "${FILE_CONTENT}" FILE_LEN)
# 生成数组代码
set(ARRAY_CODE "// FILE: ${INPUT_FILE}\n")
set(ARRAY_CODE "${ARRAY_CODE}// SIZE: ${FILE_LEN} Byte\n\n")
set(ARRAY_CODE "${ARRAY_CODE}#pragma once\n")
set(ARRAY_CODE "${ARRAY_CODE}#include <stdint.h>\n\n")
set(ARRAY_CODE "${ARRAY_CODE}const uint8_t ${ARRAY_NAME}_MD5[] = \"${INPUT_FILE_MD5_HEX}\"; \n\n")
set(ARRAY_CODE "${ARRAY_CODE}const uint8_t ${ARRAY_NAME}[] = \n{\n")
# 逐字节转换为十六进制(每16个字节换行,增强可读性)
set(COUNT 0)
math(EXPR COUNT_MAX "${FILE_LEN} -1")
set(LINE_CONTENT "")
foreach(IDX RANGE 0 ${COUNT_MAX})
# 提取单个字节
string(SUBSTRING "${FILE_CONTENT}" ${IDX} 1 BYTE)
# 转换为十六进制(0xXX 格式)
string(HEX "${BYTE}" BYTE_ASCII)
string(TOUPPER "${BYTE_ASCII}" BYTE_HEX)
# 处理 ASCII 转十六进制(兼容所有字节)
if(BYTE_HEX MATCHES "^[0-9A-F]+$")
set(BYTE_HEX "0x${BYTE_HEX}")
else()
# 非可打印字符直接转十六进制
string(REGEX REPLACE "(.)" "\\\\x\\1" BYTE_ESC "${BYTE}")
string(REPLACE "\\x" "" BYTE_HEX "${BYTE_ESC}")
set(BYTE_HEX "0x${BYTE_HEX}")
endif()
# 拼接一行的字节(每16个加逗号+换行)
set(LINE_CONTENT "${LINE_CONTENT} ${BYTE_HEX},")
math(EXPR COUNT "${COUNT} + 1")
if(COUNT EQUAL 16 OR IDX EQUAL ${COUNT_MAX})
set(ARRAY_CODE "${ARRAY_CODE} ${LINE_CONTENT}\n")
set(LINE_CONTENT "")
set(COUNT 0)
endif()
endforeach()
# 补充数组长度常量
set(ARRAY_CODE "${ARRAY_CODE}};\n\n")
set(ARRAY_CODE "${ARRAY_CODE}const size_t ${ARRAY_NAME}_len = ${FILE_LEN};\n")
# 写入输出文件(仅当内容变化时更新,避免重复编译)
if(EXISTS "${OUTPUT_FILE}")
file(READ "${OUTPUT_FILE}" EXISTING_CONTENT)
if(NOT "${EXISTING_CONTENT}" STREQUAL "${ARRAY_CODE}")
file(WRITE "${OUTPUT_FILE}" "${ARRAY_CODE}")
endif()
else()
file(WRITE "${OUTPUT_FILE}" "${ARRAY_CODE}")
endif()
message(STATUS "generatre: ${OUTPUT_FILE}/${ARRAY_NAME}/${FILE_LEN}")
endfunction()
if(DEFINED src AND DEFINED dest AND DEFINED name)
file_to_c_array(${src} ${dest} ${name})
else()
message(FATAL_ERROR "arg1 arg2 arg3")
endif()
|
注意:因HEX函数的原因,我们需要CMake版本在3.18之上。
自定义转换函数的使用方法
第一步,编写一个资源文件,如data.txt,我们将data.txt作为数组文件编译进可自行程序,其内容如下所示:
1
2
3
| LINE:1>> 112233ABC,XX,
LINE:2>> SDFDSF--111210AABCDEF
LINE:3>> 2331
|
第二步,编写一个程序文件,在程序中打印上述data.txt编译后的内容,其内容如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #include <stdio.h>
#include <string>
#include "embedded_data.h"
int main()
{
const uint8_t *zMD5 = embedded_data_MD5;
const uint8_t *zBuffer = embedded_data;
const size_t iBuffer = embedded_data_len;
std::string str = std::string((char*)zBuffer, iBuffer);
printf("md5 : %s\n", zMD5);
printf("length : %zu\n", iBuffer);
printf("string : \n%s\n", str.c_str());
return 0;
}
|
第三步,编写CMakeLists.txt文件,调用上述xxd.cmake,将data.txt编译成数组文件,并将测试程序编译为可执行程序,如下所示:
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
29
30
31
32
33
34
35
36
| SET(app_name databin)
CMAKE_MINIMUM_REQUIRED(VERSION 3.21 FATAL_ERROR)
PROJECT(${app_name} LANGUAGES CXX)
# 要转换的源文件(相对/绝对路径均可)
SET(RCSOURCE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/data.txt") # 二进制文件示例
# 输出的C数组文件(建议放到构建目录,避免污染源码)
SET(RCOUTPUT_FILE "embedded_data.h")
# 生成的数组名(自定义,比如 data_bin)
SET(RCOUTPUT_NAME "embedded_data")
ADD_CUSTOM_COMMAND(
OUTPUT ${RCOUTPUT_FILE}
COMMAND ${CMAKE_COMMAND}
-D src=${RCSOURCE_FILE} -D dest=${RCOUTPUT_FILE} -D name=${RCOUTPUT_NAME}
-P ${CMAKE_CURRENT_SOURCE_DIR}/xxd.cmake
DEPENDS ${RCSOURCE_FILE}
COMMENT "Generating source file from specification"
VERBATIM
)
# 使用add_custom_target创建生成目标
add_custom_target(generate_sources DEPENDS ${OUTPUT_FILE})
# -------------------------- 编译到项目 --------------------------
# 如果需要将数组文件加入项目编译,添加以下代码
add_executable(${app_name} main.cpp ${RCOUTPUT_FILE})
add_dependencies(${app_name} generate_sources)
# 让代码能include输出文件(添加输出目录到include路径)
target_include_directories(${app_name} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
|
上述文件的组织如下图所示:

第四步,编译和测试,使用如下命令:
1
2
3
| cmake -S . -B build -A x64
cmake --build build --config Release
|
执行过程如下图所示:

执行完毕之后,我们在build目录下会生成data.txt文件对应的数组文件embedded_data.h,如下图所示:

数组文件embedded_data.h的内容如下图所示:

第五步,运行生成的可执行文件,打印数组和data.txt对比一下内容是否一致,如下图所示:

可见,我们的数组与源文件是一致的,后续我们可以按此方法将mp3、txt、png、jpg等等文件作为资源嵌入我们的可执行程序中。