将C程序中的文本文件“ #include”作为char []


130

有没有办法在编译时将整个文本文件作为字符串包含在C程序中?

就像是:

  • file.txt:

    This is
    a little
    text file
  • main.c:

    #include <stdio.h>
    int main(void) {
       #blackmagicinclude("file.txt", content)
       /*
       equiv: char[] content = "This is\na little\ntext file";
       */
       printf("%s", content);
    }

获得在stdout上打印的小程序“这是一个小文本文件”

目前,我使用了一个骇人的python脚本,但是它非常丑陋,并且仅限于一个变量名,您能告诉我另一种方法吗?


看一下这里,将文件读入char []。/programming/410943/reading-a-text-file-into-an-array-in-c以下是一些使用C预处理器宏的技巧。http://gcc.gnu.org/onlinedocs/cpp/Macros.html
丹尼尔·怀特

3
为什么要这样做?为什么不在运行时读取文件?(答案:也许是因为很难知道文件在运行时在哪里,或者可能是因为应该只安装一个文件。)
Jonathan Leffler 2009年

或者,也许文本文件仅在编译时可用,例如源代码。
TMS

1
有时,您希望在开发时将数据作为单独的文件访问,但是将内容编译为二进制文件。示例是在无法访问本地存储的Arduino上运行Web服务器。您希望将html文件分开进行编辑,但是在编译时,它们需要作为字符串存在于源代码中。
Geordie

Answers:


134

我建议为此使用(unix util)xxd。你可以像这样使用它

$ echo hello world > a
$ xxd -i a

输出:

unsigned char a[] = {
  0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x0a
};
unsigned int a_len = 12;

18
请注意:xxd创建的char []不是以NULL结尾的!所以我做$ xxd -i <file.txt> file.xxd $ echo',0'>> file.xxd并在main.c中char char file_content [] = {#include“ file.xxd”};

2
我从不知道xxd。这很棒!

1
@eSKay:直接来自的输出xxd,如答案所示。数组的名称是输入文件名。如果您要管道输入数据而不是使用输入文件,则将获得十六进制值的列表(而不使用数组声明或len变量)。
Hasturkun

4
嵌入GLSL着色器时,这非常有用。
linello

5
将0x00终止添加到xxd的另一种方式产生了C代码:xxd -i file.txt | sed 's/\([0-9a-f]\)$/\0, 0x00/' > file.h
vleo

104

问题是关于C的,但是如果有人尝试使用C ++ 11来完成,那么由于新的原始字符串文字,只需对包含的文本文件进行很小的更改就可以完成:

在C ++中执行以下操作:

const char *s =
#include "test.txt"
;

在文本文件中执行以下操作:

R"(Line 1
Line 2
Line 3
Line 4
Line 5
Line 6)"

因此,文件的顶部必须只有一个前缀,文件的末尾必须带有一个后缀。您可以在两者之间做您想做的事情,只要您不需要字符序列,就不需要特殊的转义)"。但是,如果您指定自己的自定义分隔符,即使这样也可以使用:

R"=====(Line 1
Line 2
Line 3
Now you can use "( and )" in the text file, too.
Line 5
Line 6)====="

5
谢谢,我选择了这里提出的方法,将较长的sql片段嵌入到我的C ++ 11代码中。这样一来,我就可以将SQL整齐地分离到自己的文件中,并通过适当的语法检查,突出显示等对其进行编辑
。– YitzikC

1
这真的很接近我想要的。特别是用户定义的定界符。很有用。我确实想进一步:有没有一种方法可以从要包含的文件中完全删除前缀R“(和后缀)”?我尝试定义两个文件名为bra.in和ket.in的文件,并在其中添加了前缀和后缀,分别一并包含bra.in,file.txt和ket.in。但是编译器评估bra.in(这仅仅是R的内容“()之前包括下一个文件,以便它会抱怨请让我知道,如果有人知道如何从file.txt的得到的前缀和后缀乘坐感谢。。
TMS

我猜C ++不允许R“(<newline> #include ...)”吗?会是不错的文件被编译时摄取不要求不..任何编码任何....即直JSON或XML或CSV或什么
布赖恩·克里斯曼

如果将原始文字的文本1+R"...用作起始定界符而不是R"...,然后在之前添加换行符,则可以使原始文字的文本更具可读性Line 1。这会将表达式从数组转换为指针,但这在这里并不是真正的问题,因为您正在初始化指针而不是数组。
Ruslan

14

您有两种可能性:

  1. 利用编译器/链接器扩展将文件转换为二进制文件,并带有指向二进制数据开头和结尾的正确符号。看到这个答案:在GNU ld链接脚本中包含二进制文件
  2. 将文件转换为可以初始化数组的字符常量序列。请注意,您不能只执行“”并跨越多行。您将需要换行符(\),转义"符和其他字符才能使该功能起作用。只需编写一个小程序即可将字节转换为类似的序列'\xFF', '\xAB', ...., '\0'(或使用xxd其他答案描述的unix工具,如果有的话!)更容易:

码:

#include <stdio.h>

int main() {
    int c;
    while((c = fgetc(stdin)) != EOF) {
        printf("'\\x%X',", (unsigned)c);
    }
    printf("'\\0'"); // put terminating zero
}

(未经测试)。然后做:

char my_file[] = {
#include "data.h"
};

data.h是由哪里生成的

cat file.bin | ./bin2c > data.h

1
最后一行可能应读为“ cat file.bin | ./bin2c> data.h”或“ ./bin2c <file.bin> data.h”
Hasturkun 2009年

我使用codeproject.com/Tips/845393/…从二进制文件中创建了一个十六进制文件(在Windows上),然后使用了您的“ char my_file[] = { #include my_large_file.h };谢谢” 建议!
某处某人

bin2c一样bin2c从Debian的hxtools,提防
ThorSummoner

或者,如果这样,那么调用现在变得很奇怪:bin2c -H myoutput.h myinput1.txt myinputN.txt
ThorSummoner

9

好吧,受大民帖子启发,我测试了以下简单示例:

数据:

"this is test\n file\n"

test.c:

int main(void)
{
    char *test = 
#include "a.data"
    ;
    return 0;
}

gcc -E test.c输出:

# 1 "test.c"
# 1 "<built-in>"
# 1 "<command line>"
# 1 "test.c"

int main(void)
{
    char *test =
# 1 "a.data" 1
"this is test\n file\n"
# 6 "test.c" 2
    ;
    return 0;
}

因此它可以工作,但需要用引号引起来的数据。


这就是我在答案的最后一部分中提到的内容。
敏2009年

报价,或任何所谓的方式,请原谅我的英语
Ilya

这需要对数据进行C转义。我认为那不是该职位所要寻找的。如果它具有某种C转义文件内容的include宏,那就很好。
布莱恩·克里斯曼

8

我喜欢卡亚尔的回答。但是,如果您不想触摸输入文件,并且正在使用CMake,则可以在文件上添加分隔符字符序列。例如,以下CMake代码复制输入文件并相应地包装其内容:

function(make_includable input_file output_file)
    file(READ ${input_file} content)
    set(delim "for_c++_include")
    set(content "R\"${delim}(\n${content})${delim}\"")
    file(WRITE ${output_file} "${content}")
endfunction(make_includable)

# Use like
make_includable(external/shaders/cool.frag generated/cool.frag)

然后包含在c ++中,如下所示:

constexpr char *test =
#include "generated/cool.frag"
;

5

您可以使用objcopy

objcopy --input binary --output elf64-x86-64 myfile.txt myfile.o

现在,您有了一个目标文件,您可以将其链接到您的可执行文件中,其中包含来自的内容开头,结尾和大小的符号myfile.txt


1
您能告诉我们符号名称是什么吗?
Mark Ch

@MarkCh:根据文档,符号名称是从输入文件名生成的。
John Zwinck

我猜想这不适用于非x86-64机器,对吗?
ThorSummoner


2

您需要我的xtr实用程序,但可以使用bash script。这是我称为的脚本bin2inc。第一个参数是结果的名称char[] variable。第二个参数是的名称file。输出为C include file,文件内容编码为(以小写形式hex)为给定的变量名。的char arrayzero terminated,数据的长度被存储在$variableName_length

#!/bin/bash

fileSize ()

{

    [ -e "$1" ]  && {

        set -- `ls -l "$1"`;

        echo $5;

    }

}

echo unsigned char $1'[] = {'
./xtr -fhex -p 0x -s ', ' < "$2";
echo '0x00'
echo '};';
echo '';
echo unsigned long int ${1}_length = $(fileSize "$2")';'

您可以在此处获取XTR xtr(字符eXTRapolator)为GPLV3


2

如果您愿意采取一些肮脏的技巧,则可以利用原始字符串文字和#include某些类型的文件来发挥创意。

例如,假设我想在项目中包括一些用于SQLite的SQL脚本,并且想要突出显示语法,但是不想要任何特殊的构建基础结构。我可以拥有一个test.sql对SQLite有效的SQL 文件,并在其中--开始注释:

--x, R"(--
SELECT * from TestTable
WHERE field = 5
--)"

然后在我的C ++代码中,我可以拥有:

int main()
{
    auto x = 0;
    const char* mysql = (
#include "test.sql"
    );

    cout << mysql << endl;
}

输出为:

--
SELECT * from TestTable
WHERE field = 5
--

或包含来自test.py有效Python脚本的文件中的一些Python代码(因为#在Python中开始注释并且pass是空操作):

#define pass R"(
pass
def myfunc():
    print("Some Python code")

myfunc()
#undef pass
#define pass )"
pass

然后在C ++代码中:

int main()
{
    const char* mypython = (
#include "test.py"
    );

    cout << mypython << endl;
}

将输出:

pass
def myfunc():
    print("Some Python code")

myfunc()
#undef pass
#define pass

对于您可能想作为字符串包括在内的各种其他类型的代码,应该可以使用类似的技巧。我不确定这是否是个好主意。这是一种巧妙的技巧,但可能不是您在实际生产代码中想要的东西。周末黑客项目可能还可以。


我也使用这种方法将OpenGL着色器也放置在文本文件中!
yano

1

我在python3中重新实现了xxd,解决了所有xxd的烦恼:

  • const正确性
  • 字符串长度数据类型:int→size_t
  • 空终止(以防您可能需要)
  • C字符串兼容:unsigned放在数组上。
  • 较小的,可读的输出,就像您写过的一样:可打印的ascii原样输出;其他字节进行十六进制编码。

这是脚本,由其自身过滤,因此您可以看到它的作用:

pyxxd.c

#include <stddef.h>

extern const char pyxxd[];
extern const size_t pyxxd_len;

const char pyxxd[] =
"#!/usr/bin/env python3\n"
"\n"
"import sys\n"
"import re\n"
"\n"
"def is_printable_ascii(byte):\n"
"    return byte >= ord(' ') and byte <= ord('~')\n"
"\n"
"def needs_escaping(byte):\n"
"    return byte == ord('\\\"') or byte == ord('\\\\')\n"
"\n"
"def stringify_nibble(nibble):\n"
"    if nibble < 10:\n"
"        return chr(nibble + ord('0'))\n"
"    return chr(nibble - 10 + ord('a'))\n"
"\n"
"def write_byte(of, byte):\n"
"    if is_printable_ascii(byte):\n"
"        if needs_escaping(byte):\n"
"            of.write('\\\\')\n"
"        of.write(chr(byte))\n"
"    elif byte == ord('\\n'):\n"
"        of.write('\\\\n\"\\n\"')\n"
"    else:\n"
"        of.write('\\\\x')\n"
"        of.write(stringify_nibble(byte >> 4))\n"
"        of.write(stringify_nibble(byte & 0xf))\n"
"\n"
"def mk_valid_identifier(s):\n"
"    s = re.sub('^[^_a-z]', '_', s)\n"
"    s = re.sub('[^_a-z0-9]', '_', s)\n"
"    return s\n"
"\n"
"def main():\n"
"    # `xxd -i` compatibility\n"
"    if len(sys.argv) != 4 or sys.argv[1] != \"-i\":\n"
"        print(\"Usage: xxd -i infile outfile\")\n"
"        exit(2)\n"
"\n"
"    with open(sys.argv[2], \"rb\") as infile:\n"
"        with open(sys.argv[3], \"w\") as outfile:\n"
"\n"
"            identifier = mk_valid_identifier(sys.argv[2]);\n"
"            outfile.write('#include <stddef.h>\\n\\n');\n"
"            outfile.write('extern const char {}[];\\n'.format(identifier));\n"
"            outfile.write('extern const size_t {}_len;\\n\\n'.format(identifier));\n"
"            outfile.write('const char {}[] =\\n\"'.format(identifier));\n"
"\n"
"            while True:\n"
"                byte = infile.read(1)\n"
"                if byte == b\"\":\n"
"                    break\n"
"                write_byte(outfile, ord(byte))\n"
"\n"
"            outfile.write('\";\\n\\n');\n"
"            outfile.write('const size_t {}_len = sizeof({}) - 1;\\n'.format(identifier, identifier));\n"
"\n"
"if __name__ == '__main__':\n"
"    main()\n"
"";

const size_t pyxxd_len = sizeof(pyxxd) - 1;

用法(提取脚本):

#include <stdio.h>

extern const char pyxxd[];
extern const size_t pyxxd_len;

int main()
{
    fwrite(pyxxd, 1, pyxxd_len, stdout);
}

1

什么可能,如果你这样做的工作是:

int main()
{
    const char* text = "
#include "file.txt"
";
    printf("%s", text);
    return 0;
}

当然,您必须小心文件中的实际内容,确保没有双引号,所有合适的字符都被转义,等等。

因此,如果仅在运行时从文件加载文本,或将文本直接嵌入代码中,则可能会更容易。

如果您仍然希望将文本保存在另一个文件中,则可以在其中保存它,但是必须在其中将其表示为字符串。您将使用上面的代码,但其中没有双引号。例如:

file.txt

"Something evil\n"\
"this way comes!"

main.cpp

int main()
{
    const char* text =
#include "file.txt"
;
    printf("%s", text);
    return 0;
}

因此,基本上,您要在文本文件中包含C或C ++样式字符串。这将使代码更加整洁,因为文件开头没有那么多的文本。


3
这是个好主意,但不起作用,您可能会出错,因为文字包括换行符,或者#include部分将被读取为字符串并且不执行,如果执行则定为错误,如果不执行则定为错误。 。
Motti

1
@Motti:同意-书面上,语法上无效的C。这个想法很有趣-C预处理程序从逻辑上讲是一个单独的阶段-但是实践是,它不会起步,因为包含文件中的每一行都有以反斜杠等结尾
Jonathan Leffler

2
哼 在我看来,您不需要反斜杠,因为大多数编译器会将相邻的字符串连接在一起
EvilTeach

这个答案的意思是……如果就这么简单,我认为OP不会问这个问题!-1,因为此答案的出现会在某种程度上鼓励人们浪费时间尝试无法解决的问题。我认为,如果您将“可能有效的方法”更改为“仅供参考,则无效”
Mark Ch

@JonathanLeffler预处理程序运行后,取决于file.txt的格式,它应为有效的C或C ++。

0

即使可以在编译时完成(我一般也不认为可以这样做),文本也很可能是经过预处理的标头,而不是逐字记录文件的内容。我希望您必须在运行时从文件中加载文本,或进行讨厌的剪切粘贴操作。


0

Hasturkun使用xxd -i选项的答案非常好。如果要将转换过程(文本->十六进制包含文件)直接合并到构建中,则hexdump.c工具/库最近添加了与xxd的-i选项类似的功能(它没有提供完整的标头-您需要提供char数组定义-但这具有让您选择char数组名称的优点):

http://25thandclement.com/~william/projects/hexdump.c.html

它的许可证比xxd更“标准”,而且非常自由-使用CMidLists.txt和scheme.c文件可以看到使用它在程序中嵌入init文件的示例:

https://github.com/starseeker/tinyscheme-cmake

将生成的文件包含在源代码树和捆绑实用程序中都有优缺点-如何处理它取决于项目的特定目标和需求。hexdump.c打开此应用程序的捆绑选项。


0

我认为单独使用编译器和预处理器是不可能的。gcc允许这样做:

#define _STRGF(x) # x
#define STRGF(x) _STRGF(x)

    printk ( MODULE_NAME " built " __DATE__ " at " __TIME__ " on host "
            STRGF(
#               define hostname my_dear_hostname
                hostname
            )
            "\n" );

但不幸的是,这不是:

#define _STRGF(x) # x
#define STRGF(x) _STRGF(x)

    printk ( MODULE_NAME " built " __DATE__ " at " __TIME__ " on host "
            STRGF(
#               include "/etc/hostname"
            )
            "\n" );

错误是:

/etc/hostname: In function init_module’:
/etc/hostname:1:0: error: unterminated argument list invoking macro "STRGF"

我看着,就像你叫我一样。我没有在您的答案中看到任何新信息(其他答案中没有的信息),没有提及将/etc/hostname生成机器名称嵌入字符串的方法,即使该方法可行,它也不会由于Mac OS X没有文件,因此可移植/etc/hostname。请注意,使用以下划线开头和大写字母开头的宏名称,是使用为实现保留的名称,即Bad Thing™。
乔纳森·莱夫勒

0

为什么不将文本链接到程序并将其用作全局变量!这是一个例子。我正在考虑使用它在可执行文件中包含Open GL着色器文件,因为GL着色器需要在运行时为GPU编译。


0

我遇到了类似的问题,对于小文件,上述约翰内斯·绍布(Johannes Schaub)的解决方案对我来说就像一个魅力。

但是,对于更大的文件,编译器的字符数组限制会遇到问题。因此,我编写了一个小型编码器应用程序,将文件内容转换为大小相等的块(可能填充零)的2D字符数组。它产生带有2D数组数据的输出文本文件,如下所示:

const char main_js_file_data[8][4]= {
    {'\x69','\x73','\x20','\0'},
    {'\x69','\x73','\x20','\0'},
    {'\x61','\x20','\x74','\0'},
    {'\x65','\x73','\x74','\0'},
    {'\x20','\x66','\x6f','\0'},
    {'\x72','\x20','\x79','\0'},
    {'\x6f','\x75','\xd','\0'},
    {'\xa','\0','\0','\0'}};

其中4实际上是编码器中的变量MAX_CHARS_PER_ARRAY。然后,可以轻松地将带有结果C代码的文件(例如“ main_js_file_data.h”)内联到C ++应用程序中,例如:

#include "main_js_file_data.h"

这是编码器的源代码:

#include <fstream>
#include <iterator>
#include <vector>
#include <algorithm>


#define MAX_CHARS_PER_ARRAY 2048


int main(int argc, char * argv[])
{
    // three parameters: input filename, output filename, variable name
    if (argc < 4)
    {
        return 1;
    }

    // buffer data, packaged into chunks
    std::vector<char> bufferedData;

    // open input file, in binary mode
    {    
        std::ifstream fStr(argv[1], std::ios::binary);
        if (!fStr.is_open())
        {
            return 1;
        }

        bufferedData.assign(std::istreambuf_iterator<char>(fStr), 
                            std::istreambuf_iterator<char>()     );
    }

    // write output text file, containing a variable declaration,
    // which will be a fixed-size two-dimensional plain array
    {
        std::ofstream fStr(argv[2]);
        if (!fStr.is_open())
        {
            return 1;
        }
        const std::size_t numChunks = std::size_t(std::ceil(double(bufferedData.size()) / (MAX_CHARS_PER_ARRAY - 1)));
        fStr << "const char " << argv[3] << "[" << numChunks           << "]"    <<
                                            "[" << MAX_CHARS_PER_ARRAY << "]= {" << std::endl;
        std::size_t count = 0;
        fStr << std::hex;
        while (count < bufferedData.size())
        {
            std::size_t n = 0;
            fStr << "{";
            for (; n < MAX_CHARS_PER_ARRAY - 1 && count < bufferedData.size(); ++n)
            {
                fStr << "'\\x" << int(unsigned char(bufferedData[count++])) << "',";
            }
            // fill missing part to reach fixed chunk size with zero entries
            for (std::size_t j = 0; j < (MAX_CHARS_PER_ARRAY - 1) - n; ++j)
            {
                fStr << "'\\0',";
            }
            fStr << "'\\0'}";
            if (count < bufferedData.size())
            {
                fStr << ",\n";
            }
        }
        fStr << "};\n";
    }

    return 0;
}

0

这个问题使我很恼火,而xxd在我的用例中不起作用,因为当我尝试编写脚本时,它使变量名为__home_myname_build_prog_cmakelists_src_autogen之类的东西,因此我制作了一个实用程序来解决这个确切的问题:

https://github.com/Exaeta/brcc

它生成一个源文件和头文件,并允许您显式设置每个变量的名称,以便随后可以通过std :: begin(arrayname)和std :: end(arrayname)使用它们。

我将其合并到我的cmake项目中,如下所示:

add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/binary_resources.hpp ${CMAKE_CURRENT_BINARY_DIR}/binary_resources.cpp
  COMMAND brcc ${CMAKE_CURRENT_BINARY_DIR}/binary_resources RGAME_BINARY_RESOURCES_HH txt_vertex_shader ${CMAKE_CURRENT_BINARY_DIR}/src/vertex_shader1.glsl
  DEPENDS src/vertex_shader1.glsl)

我想通过细微的调整也可以使其适用于C。


-1

在xh中

"this is a "
"buncha text"

在main.c中

#include <stdio.h>
int main(void)
{
    char *textFileContents =
#include "x.h"
    ;

    printf("%s\n", textFileContents);

    return 0
}

应该做的工作。


对于多行,您需要添加\ n,以便:“ line 1 \ n”“ line 2 \ n”
Superfly Jon

这有点误导,显然这需要准备文本文件以添加引号和\ n字符,在一般情况下不起作用
Mark Ch
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.