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

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

比如:

  • 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);
    }
    

obtaining a little program that prints on stdout "This is a little text file"

At the moment I used an hackish python script, but it's butt-ugly and limited to only one variable name, can you tell me another way to do it?

84149 次浏览

也许吧的工作原理是:

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 + + 样式字符串。这将使代码更加简洁,因为在文件的开始部分没有大量的文本。

即使可以在编译时完成(我认为一般情况下不可以) ,文本也可能是预处理的标题,而不是文件内容的原文。我希望您必须在运行时从文件中加载文本,或者进行讨厌的剪切粘贴工作。

我建议使用(unix util) 谢谢。 你可以这样使用它

$ 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;

你有两种可能:

  1. 使用编译器/链接器扩展将文件转换为二进制文件,并使用适当的符号指向二进制数据的开始和结束。看看这个答案: 使用 GNU ld 链接器脚本包含二进制文件
  2. 将文件转换为可初始化数组的字符常量序列。请注意,您不能只是做“”和跨越多行。您将需要一个行继续字符(\)、转义字符 "和其他字符来实现这一功能。只需编写一个小程序,将字节转换成类似于 '\xFF', '\xAB', ...., '\0'的序列(或者使用由另一个答案描述的 unix 工具 xxd,如果您有的话!):

密码:

#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"
};

H 是由哪里生成的

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

好的,受到 代明的文章的启发,我测试了下面这个简单的例子:

A)数据:

"this is test\n file\n"

测试 c:

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

输出:

# 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;
}

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

在 x.h

"this is a "
"buncha text"

在主控室

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


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


return 0
}

应该可以完成任务。

你需要我的 xtr实用程序,但你可以做到这一点与 bash script。这个脚本我称之为 bin2inc。第一个参数是结果 char[] variable的名称。第二个参数是 file的名称。输出是 Cinclude 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

Hasturkun 使用 xxd-i 选项的答案非常好。如果你想直接将转换过程(text-> 十六进制包含文件)合并到你的构建中,那么我们可以使用 hexdump. c 工具/库最近添加了一个类似于 xxd 的-i 选项的功能(它没有提供完整的头文件——你需要提供字符数组定义——但是这个功能的优势在于可以让你选择字符数组的名称) :

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

它的许可证比 xxd“标准”得多,而且非常开放——在 CMakeLists.txt 和 scheme.c 文件中可以看到一个使用它在程序中嵌入 init 文件的例子:

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

在源代码树和捆绑实用程序中包含生成的文件有利也有弊——如何处理它取决于项目的具体目标和需求。C 打开此应用程序的捆绑选项。

我认为单独使用编译器和预处理器是不可能的。 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"

这个问题是关于 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)====="

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

我也有类似的问题,对于小文件来说,上面提到的 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;
}

你可以使用 objcopy:

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

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

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

  • 确保正确
  • 字符串长度数据类型: int → size _ t
  • 无效终止(如果您需要的话)
  • C 字符串兼容: 在数组上删除 unsigned
  • 更小的、可读的输出,正如您所写的那样: Printable 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);
}

我喜欢 Kayahr 的回答。但是,如果使用 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"
;

如果你愿意诉诸于一些肮脏的伎俩,你可以得到原始字符串文字和 #include为某些类型的文件创造性。

例如,假设我想在我的项目中包含一些针对 SQLite 的 SQL 脚本,我想得到一些语法突显,但是不想要任何特殊的构建基础结构。我可以有这个文件 test.sql,它是有效的 SQL for SQLite,其中 --开始一个注释:

--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
--

或者从一个有效的 Python 脚本 test.py文件中包含一些 Python 代码(因为 #在 Python 中启动注释,而 pass是 no-op) :

#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

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

这个问题让我很恼火,xxd 在我的用例中不起作用,因为当我尝试编写脚本时,它使得一个变量叫做 _ _ home _ myname _ build _ prog _ cmakelists _ src _ autogen,所以我创建了一个实用程序来解决这个问题:

Https://github.com/exaeta/brcc

它生成一个源文件和头文件,并允许您显式设置每个变量的名称,然后您可以通过 std: : start (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 语言。

如果您正在使用 CMake,您可能有兴趣编写如下 CMake预处理脚本:

Cmake/ConvertLayout.cmake

function(convert_layout file include_dir)
get_filename_component(name ${file} NAME_WE)
get_filename_component(directory ${file} DIRECTORY)
get_filename_component(directory ${directory} NAME)
string(TOUPPER ${name} NAME)
string(TOUPPER ${directory} DIRECTORY)


set(new_file ${include_dir}/${directory}/${name}.h)


if (${file} IS_NEWER_THAN  ${new_file})
file(READ ${file} content)


string(REGEX REPLACE "\"" "\\\\\"" content "${content}")
string(REGEX REPLACE "[\r\n]" "\\\\n\"\\\\\n\"" content "${content}")
set(content "\"${content}\"")
set(content "#ifndef ${DIRECTORY}_${NAME}\n#define ${DIRECTORY}_${NAME} ${content} \n#endif")
message(STATUS "${content}")


file(WRITE ${new_file} "${content}")


message(STATUS "Generated layout include file ${new_file} from ${file}")
endif()
endfunction()


function(convert_layout_directory layout_dir include_dir)
file(GLOB layouts ${layout_dir}/*)
foreach(layout ${layouts})
convert_layout(${layout} ${include_dir})
endforeach()
endfunction()

你的 CMakeLists.txt

include(cmake/ConvertLayout.cmake)
convert_layout_directory(layout ${CMAKE_BINARY_DIR}/include)
include_directories(${CMAKE_BINARY_DIR}/include)

在 c + + 的某个地方

#include "layout/menu.h"
Glib::ustring ui_info = LAYOUT_MENU;

下面是我在 Visual C + + 中使用的一个技巧。我添加了以下 Pre-Build 事件(其中 file.txt 是输入,file _ txt.h 是输出) :

@(
echo const char text[] = R"***(
type file.txt
echo ^^^)***";
) > file_txt.h

然后在需要的地方包含 file _ txt.h。

这并不完美,因为它在开始处加上 n,在结束处加上 n ^ ,但这不是一个需要处理的问题,我喜欢这个解决方案的简单性。如果有人可以提炼是为了摆脱额外的字符,这将是不错的。

我喜欢@Martin R 的回答,因为它没有触及输入文件,而是自动化了处理过程。为了改进这一点,我增加了自动分割超过编译器限制的大文件的能力。输出文件被写成一个由较小字符串组成的数组,这些字符串可以在代码中重新组合。由此产生的脚本,基于@Martin R 的版本,这里包含了一个例子:

Https://github.com/skillcheck/cmaketools.git

相关的 CMake 设置是:

make_includable( LargeFile.h
${CMAKE_CURRENT_BINARY_DIR}/generated/LargeFile.h
"c++-include" "L" LINE_COUNT FILE_SIZE
)

源代码是:

static std::vector<std::wstring> const chunks = {
#include "generated/LargeFile.h"
};


std::string contents =
std::accumulate( chunks.begin(), chunks.end(), std::wstring() );

可以对此使用程序集:

asm("fileData:    .incbin \"filename.ext\"");
asm("fileDataEnd: db 0x00");


extern char fileData[];
extern char fileDataEnd[];
const int fileDataSize = fileDataEnd - fileData + 1;