利用 CMake 在 C\C++ 中嵌入资源

在程序中嵌入资源是很常见的需求,资源可以是 GLSL Shader、LICENSE/EULA 文本、预定义公钥、图标图片等等。特定的平台和SDK里,会有特定的接口和工具来导入管理这些资源,比如 VC 的rc、QT 的 qrc,或者直接使用文件读写、dlopen/dlsym 来加载外部资源。对于C\C++而言,可以直接将资源转换成字符串字面量或数组,嵌入到程序中使用,本文即讲解如何在 cmake 构建中方便的嵌入各种资源。

嵌入文本资源可以在源文件中写字面量,但是字符转义和格式调整会比较麻烦,二进制资源可以使用 xxd / hexdump 工具来转成数组,但这增加额外依赖。其实利用 CMake 本身的指令语法,就可以 实现上述的资源嵌入。主要思路如下:

  1. 将资源文件纳入工程中,利用 cmake 获取文件大小、文件名以及文件内容;
  2. 利用 string(MAKE_C_IDENTIFIER...) 转换文件名为合法的 C 符号,后面将会使用该 符号作为资源字节数组名;
  3. 利用 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文件包含实际定义。 在大项目中,可以将资源统一在一个子模块中,模块最终输出成静态库或动态库,其他需要使用资源的模块可以链接该模块来获取资源。

示例工程源码下载.

自动使能 CAN 总线

Linux 上 CAN 总线接口以网络设备形式呈现,默认系统启动时 CAN 总线处于 DOWN 模式,需要手动使用 ip 命令配置并使能 UP。但也可以使用系统 network 的 interfaces 式文件来实现 CAN 总线自动化配置和激活。

以 can0 总线设备为例,如果设备为非可插拔(固定)总线,则可以新建 /etc/network/interfaces.d/can 文件,写入如下内容(假设使用 500k 波特率):

1
2
3
4
5
auto can0
iface can0 inet manual
        pre-up /sbin/ip link set $IFACE type can bitrate 500000 restart-ms 10
        up /sbin/ip link set $IFACE up
        down /sbin/ip link set $IFACE down

重启,然后使用 ip -details link show can0 来查看总线是否激活。

对于可插拔总线,比如 USB 转换的 CAN 总线,需要修改 auto can0 为 allow-hotplug can0:

1
2
3
4
5
allow-hotplug can0
iface can0 inet manual
        pre-up /sbin/ip link set $IFACE type can bitrate 500000 restart-ms 10
        up /sbin/ip link set $IFACE up
        down /sbin/ip link set $IFACE down

这样当 CAN 设备插入时,系统会自动配置并激活。

Linux To Go 系统优化

本文主要讲述如何优化配置 U 盘上安装的 Debian/Linux 系统,使其适配各种硬件主机并优化 U 盘上性能。

在高速 U 盘上安装 Linux 系统随身携带使用是很方便的事情,这跟 LiveCD 模式不同,所有的系统配置和文件都是随盘保存,不会随着重启重置,也不会因为更换机器而产生差异。

硬件及系统选择

U 盘硬件直接影响系统性能,主要影响指标为容量以及主控性能。

建议使用 U 盘容量大于等于 128G,不管是文件系统大小还是从用户数据考量,过小的容量很容易造成磁盘满载,而 U 盘满载会导致 IO 等待 GC 处理而卡顿,影响体验和 U 盘寿命。

而 U 盘主控同样影响,尤其是主控的 4K 随机读写性能。市面上大多数 U 盘都是连续读写速度还不错,但是小文件 4K 性能很渣,而操作系统大多数都是受制于 4K 读写性能。这里推荐三星的三款 U 盘,Bar Plus,Fit Plus,Duo Plus,这三款 4K 性能都很优异(4K IO > 10MB/s)。

系统镜像方面,推荐使用最新内核 (5.6 以上)的系统,这样可以支持最新的硬件平台而保证在各个硬件上启动,而且新的内核已知问题更少。这里推荐使用 Debian testing(bullseye),镜像建议使用附带非自由驱动的 firmware-debian-testing-dvd DVD 镜像。

分区及挂载优化

U 盘相对于 SSD,存在天然的短板(读写性能差,没有磨损平衡等),所以选择合适的文件系统并适当配置,可以提升不少性能和寿命优化。

本文推荐 U 盘系统将根 / 和用户目录 /home 独立分区,并且采用 btrfs 文件系统。独立分区可以后期更换系统而保留用户文件数据,使用 btrfs 可以获得一些先进的文件系统特性,但是使用 btrfs 文件系统可能存在不稳定的情况,所以推荐安装的系统都尽量使用最新的内核(越新的内核中 btrfs 已知问题越少)。

在安装完系统后,建议修改 /etc/fstab 中 btrfs 文件系统的挂载参数如下:

1
defaults,noatime,compress=lzo,ssd_spread,autodefrag

其中:

  • noatime. 默认为 relatime,每次读取文件会更新文件访问时间而增加 IO,修改为 noatime 后可以大幅提升文件系统性能。
  • compress=lzo. 启用文件系统透明压缩(lzo 算法),可以缩小文件系统占用磁盘空间(在 / 上大概可以获得 30% 压缩,在 /home 上大约可以获得 20% 的压缩),提升 IO 吞吐量(因为压缩),而代价仅略微增加 CPU 消耗(btrfs压缩算法有 3 种,默认为zlib,压缩效果好,但是占用 CPU 高,lzo压缩快,CPU 占用低,zstd 在两者之间,但是 grub 目前还不支持 zstd 压缩 )。
  • ssd_spread. 启用 btrfs 为低速 SSD 设备的优化策略。
  • autodefrag. 启用在线整理,可以后台自动优化文件系统布局。

需要注意的是,在系统安装时,默认 btrfs 是不激活透明压缩的,在修改 /etc/fstab 后,透明压缩也只对新生成的文件生效,想要对已有文件进行压缩处理,需要执行如下指令:

1
2
sudo btrfs filesystem defragment -v -r /
sudo btrfs filesystem defragment -v -r /home/

想要粗略知道 btrfs 文件系统大小和占用磁盘大小,可以执行如下指令:

1
2
sudo btrfs filesystem show /
sudo btrfs filesystem show /home

源及驱动安装

针对 Debian testing 系统的更新源建议使用清华大学开源镜像站的源(国内):

1
2
3
4
5
6
7
# /etc/apt/sources.list
deb http://mirrors.tuna.tsinghua.edu.cn/debian/ testing main contrib non-free
# deb-src http://mirrors.tuna.tsinghua.edu.cn/debian/ testing main contrib non-free
deb http://mirrors.tuna.tsinghua.edu.cn/debian/ testing-updates main contrib non-free
# deb-src http://mirrors.tuna.tsinghua.edu.cn/debian/ testing-updates main contrib non-free
deb http://mirrors.tuna.tsinghua.edu.cn/debian-security testing-security main
# deb-src http://mirrors.tuna.tsinghua.edu.cn/debian-security testing-security main

建议在系统中安装如下额外驱动包,以适配广泛的平台硬件:

1
2
3
4
5
6
sudo apt-get install  \
        firmware-amd-graphics \
        firmware-brcm80211 \
        firmware-iwlwifi \
        firmware-misc-nonfree \
        firmware-realtek