将二进制文件转换为 C 语言数组的 100 种方法

01 May 2023 3067 words 11 minutes BY-SA 4.0
develop c

How can I convert a binary file to the text declaring a C/C++ array with that content?

背景

标题党(英语:clickbait,又称钓鱼式标题、诱饵式标题),指网络中故意用较为夸张、耸动的文章标题以及缩略图以吸引网友点击观看文章、视频或帖子的人。

维基百科

有一个很大的 yuv 文件,如果直接 fread() 需要花费大量时间,特别是文件读写速度很慢的场景 (例如交叉编译时调试代码从一块开发板上读取电脑端的文件)。

以下内容绝对不与 Stackoverflow 同名问题 重复。

可以通过以下代码获取测试用的 yuv 文件:

ffmpeg -f rawvideo -pix_fmt yuv420p -s 1080x720 -i /dev/urandom -vframes 10 -f rawvideo 1080x720.yuv

可以通过以下代码检测生成的 yuv 文件和原始的 yuv 文件是否一致:

sha256sum out.yuv | cut -d' ' -f1 | diff <(sha256sum 1280x720.yuv | cut -d' ' -f1) -

方案

shell

Sharp shell programmers should take note of the following…

– Larry Wall Perl

需要安装一个软件把二进制文件转换为 8/10/16 进制编码。可以选择的选项包括:

以下用 od 和 16 进制编码作示范。

scripts/generate-yuv.h.sh:

#!/usr/bin/env sh
# dump yuv file to a C array to fasten fread()
echo "// Don't edit this file!
// generated by $0
#include <stdlib.h>
char yuv[] = {"
od -vAn -tx1 ${1:-1280x720.yuv} | sed 's/ /, 0x/g' | sed 's/$/,/g' | cut -c3-
echo '};
const size_t yuv_len = sizeof(yuv);
'

scripts/yuv.c:

#if 0
gcc "$0" -o a && exec a "$@"
#endif
#include <stdio.h>
#include <stdlib.h>
#include "yuv.h"
/**
 * compare hashes of output and original yuv to check if yuv.h is correct
 */
int main(int argc, char *argv[])
{
        fwrite(yuv, sizeof(yuv[0]), yuv_len, stdout);
        return EXIT_SUCCESS;
}
chmod +x scripts/generate-yuv.h.sh scripts/yuv.c
scripts/generate-yuv.h.sh 1280x720.yuv > yuv.h
scripts/yuv.c > out.yuv

点评:超级简单的方案,绝大部分 Linux 开发者在几分钟之内就能一边看着 od 的输出一边写出代码。不过 shell 脚本里的 4 次进程分叉使得性能差强人意。

perl

Practicing Perl Programmers should take note of the following…

– Larry Wall Perl

完全可以用专门的数据驱动编程语言把文本处理的进程分叉去掉。

scripts/generate-yuv.h.pl:

#!/usr/bin/env perl
exit if $#ARGV < 0;
$_ = `od -vAn -tx1 $ARGV[0]`;
exit 1 if $? != 0;
chomp;
s/ /, 0x/g;
s/$/,/gm;
s/^, /\t/gm;
$template = <<"EOF";
/**
 * Don't edit this file!
 * generated by $0
 */
#include <stdlib.h>
char yuv[] = {
$_
};
const size_t yuv_len = sizeof(yuv);
EOF
print $template;

__END__

=head1 NAME

generate-yuv.h.pl - dump yuv file to a C array to fasten fread()

点评:也是很简单的方案,做过数据驱动编程的开发者对这样的需求已经见怪不怪了。 同样几分钟就可以写好代码。性能瓶颈在 od 上。

C

Cerebral C and C++ programmers should take note of the following…

– Larry Wall Perl

这里直接推荐 Adobe 公司的 bin2c

$ bin2c yuv < /the/path/of/352x288.yuv > yuv.h
$ cat yuv.h
#include <stdlib.h>
const char yuv[] = "\
...
";
const size_t yuv_len = sizeof(yuv) - 1;

官方给出了速度比较:

xxd 13.9045 Mb/s

bin2c 547.613 Mb/s

类似的实现网上还有好几个。不一一点评了。

ld

As a result, you have many choices to control its behavior.

ld

要是只有以上几个普通方案,笔者倒真不至于浪费时间写这篇博文—— @harieamjari一个帖子 里给了一个令笔者高呼 awesome 的回答:

scripts/yuv.c

#if 0
gcc "$0" yuv.o -o a && exec a "$@"
#endif
#include <stdio.h>
#include <stdlib.h>
extern char _binary_out_yuv_start[];
extern char _binary_out_yuv_end[];

/**
 * compare hashes of output and original yuv to check if yuv.h is correct
 */
int main(){
        char *s = _binary_out_yuv_start, *e =_binary_out_yuv_end;
        fwrite(s, sizeof(s[0]), (size_t)(e - s), stdout);
        return EXIT_SUCCESS;
}
$ chmod +x scripts/yuv.c
$ ld -r -bbinary 1280x720.yuv -oyuv.o
$ nm yuv.o
XXXXXXXXXXXXXXXX D _binary_yuv_end
XXXXXXXXXXXXXXXX A _binary_yuv_size
0000000000000000 D _binary_yuv_start
$ scripts/yuv.c > out.yuv

总结

不止一种方法去做一件事。

– Larry Wall Perl

如果你愿意你真的可以找到 100 种方法——把 od 换成其它工具,使用 其他进制而非 16 进制,使用别的语言来实现这一功能。 当然这样的方法就算数量再多也远不如最后一种给笔者带来的震撼。 对开发工具的熟练掌握永远不是一件坏事。感谢阅读。

https://user-images.githubusercontent.com/32936898/199681341-1c5cfa61-4411-4b67-b268-7cd87c5867bb.png https://user-images.githubusercontent.com/32936898/199681363-1094a0be-85ca-49cf-a410-19b3d7965120.png https://user-images.githubusercontent.com/32936898/199681368-c34c2be7-e0d8-43ea-8c2c-d3e865da6aeb.png