cmake#100#将资源文件编译进可执行程序的方法

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等等文件作为资源嵌入我们的可执行程序中。