在程序中嵌入资源是很常见的需求,资源可以是 GLSL Shader、LICENSE/EULA 文本、预定义公钥、图标图片等等。特定的平台和SDK里,会有特定的接口和工具来导入管理这些资源,比如 VC 的rc、QT 的 qrc,或者直接使用文件读写、dlopen/dlsym 来加载外部资源。对于C\C++而言,可以直接将资源转换成字符串字面量或数组,嵌入到程序中使用,本文即讲解如何在 cmake 构建中方便的嵌入各种资源。
嵌入文本资源可以在源文件中写字面量,但是字符转义和格式调整会比较麻烦,二进制资源可以使用
xxd
/ hexdump
工具来转成数组,但这增加额外依赖。其实利用 CMake 本身的指令语法,就可以
实现上述的资源嵌入。主要思路如下:
- 将资源文件纳入工程中,利用 cmake 获取文件大小、文件名以及文件内容;
- 利用
string(MAKE_C_IDENTIFIER...)
转换文件名为合法的 C 符号,后面将会使用该
符号作为资源字节数组名;
- 利用
string(REGEX...)
转换文件内容为 HEX 编码,并导出到生成的 C 文件中;
再配合 CMake 中使用 protobuf/protobuf-c 文中的 add_custom_target
方法,可以实现只有资源文件修改后才会触发构建转换。
以示例工程为例,工程有3个文件,分别是
a_demo_assets.assets
,项目资源文件,其内容是 GPLv2 License 文本;
main.c
,项目主程序源码,内容是打印资源文件大小和内容;
CMakeLists.txt
,构建脚本;
main.c
源码内容如下,其中 a_demo_assets.assets.h
是资源生成的头文件,文件名与
资源文件名相关,头文件就包含两个常量定义 A_DEMO_ASSETS_ASSETS__SIZE
和 A_DEMO_ASSETS_ASSETS__DATA
,分别为资源字节大小和资源字节数组。
需要注意的是,转换的资源字节数组最后会多一个 0x00
字节,用来保证文本资源可以满足 C 风格字符串末尾截断。
1
2
3
4
5
6
7
8
9
10
|
#include <stdio.h>
#include "a_demo_assets.assets.h"
int main(int argc, char **argv)
{
printf("a_demo_assets size=%u\n", A_DEMO_ASSETS_ASSETS__SIZE);
printf("a_demo_assets data=\n");
fwrite(A_DEMO_ASSETS_ASSETS__DATA, A_DEMO_ASSETS_ASSETS__SIZE, 1, stdout);
return 0;
}
|
CMakeLists.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
cmake_minimum_required(VERSION 3.0)
project(demo_cmake_embed)
# 需要生成嵌入的文件
file (GLOB EMBED_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/*.assets"
)
# 输出文件目录
set (GEN_EMBED_OUTPUT_HDR_DIR
"${CMAKE_CURRENT_BINARY_DIR}/gen_inc")
set (GEN_EMBED_OUTPUT_SRC_DIR
"${CMAKE_CURRENT_BINARY_DIR}/gen_src")
file(MAKE_DIRECTORY ${GEN_EMBED_OUTPUT_HDR_DIR})
file(MAKE_DIRECTORY ${GEN_EMBED_OUTPUT_SRC_DIR})
# 依次处理文件
foreach(input_src ${EMBED_FILES})
# 配置输出文件名
file(SIZE ${input_src} embed_file_size)
get_filename_component(embed_file ${input_src} NAME)
set(gen_embed_file "${GEN_EMBED_OUTPUT_SRC_DIR}/${embed_file}.c")
set(gen_embed_file_header "${GEN_EMBED_OUTPUT_HDR_DIR}/${embed_file}.h")
# 清空输出文件
file(WRITE ${gen_embed_file} "")
file(WRITE ${gen_embed_file_header} "")
# for c compatibility
string(MAKE_C_IDENTIFIER ${embed_file} token)
# to upper case
string(TOUPPER ${token} token)
# read hex data from file
file(READ ${input_src} filedata HEX)
# convert hex data for C compatibility
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," filedata ${filedata})
# append data to output file
file(APPEND ${gen_embed_file}
"const unsigned char ${token}__DATA[] = {\n${filedata}0x00\n};\n"
"const unsigned long ${token}__SIZE = ${embed_file_size};\n")
file(APPEND ${gen_embed_file_header}
"extern const unsigned char ${token}__DATA[];\n"
"extern const unsigned long ${token}__SIZE;\n")
# 加入到生成文件列表
list(APPEND GEN_EMBED_FILES
${gen_embed_file}
${gen_embed_file_header}
)
endforeach()
add_custom_target(
embed_gen_files
DEPENDS ${GEN_EMBED_FILES}
)
include_directories(${GEN_EMBED_OUTPUT_HDR_DIR})
add_executable(${PROJECT_NAME} main.c ${GEN_EMBED_FILES})
add_dependencies(${PROJECT_NAME} embed_gen_files)
#target_link_libraries(${PROJECT_NAME} m)
|
在构建中,每个资源会对应生成头文件和C文件,头文件只包含大小和资源字节数组声明,C文件包含实际定义。
在大项目中,可以将资源统一在一个子模块中,模块最终输出成静态库或动态库,其他需要使用资源的模块可以链接该模块来获取资源。
示例工程源码下载.