当前位置: C语言 -- 基础 -- 预处理指令

预处理指令(四)

四、二进制资源包含

资源是可以从编译环境中访问的数据源。二进制资源包含(binary resource inclusion)是指将二进制资源直接包含到程序中。这样可以将资源与程序一体化,避免资源丢失。

使用#embed预处理指令可以实现二进制资源包含。

embed形参是嵌入形参序列中的单个预处理器形参。它有一个实现资源宽度(implementation resource width),实现资源宽度是实现定义的、以位(bit)为单位的定位资源大小;同时还有一个资源宽度(resource width),其应是以下两种情况之一:

-- 如果存在limit嵌入形参,根据limit嵌入形参计算得到的位数。

//假设test.bin资源大于4字节,资源宽度等于4字节。
#embed "test.bin" limit(4)

-- 实现资源宽度。

//资源宽度等于实现资源宽度,即test.bin资源大小。
#embed "test.bin"

embed形参序列是空格分隔的预处理器形参列表,这些参数可以修改#embed预处理指令的替换结果。

#embed "test.bin" limit(4) if_empty(0)

ISO/IEC 9899:2024标准对#embed预处理指令作了一些限制:

-- #embed指令应标识一个资源,该资源可由实现根据提供的嵌入形参作为二进制数据序列处理。

#embed "test.bin"

-- 未在ISO标准中指定的嵌入形参应有实现定义;实现定义的嵌入形参可能会改变#embed指令的语义。

//gnu::offset是GCC编译器提供的非标准拓展嵌入形参。
#embed "test.bin" gnu::offset(4)

-- 资源宽度为0的资源视为空资源。

//假设test1.bin为空,资源宽度为0。
#embed "test1.bin"

//假设test2.bin不为空,但资源宽度仍为0。
#embed "test2.bin" limit(0)

-- 嵌入元素宽度(embed element width)应为以下两种情况之一:

① 由实现定义的嵌入形参确定的、大于0的整数常量表达式。

CHAR_BIT

表达式(资源宽度)%(嵌入元素宽度)的值应为0。

:如果将包含的二进制资源当作一个数组,数组中每个元素占用的位数就是嵌入元素宽度。上述约束有助于确保数据在特定环境中既不会被填充值填充,也不会被截断;同时确保数据在使用memcpy函数时具有可移植性(使用该函数从数据初始化的字符数组进行复制操作。)。)


#embed指令的扩展结果是一个由整数常量表达式列表构成的标记序列。列表中整数常量表达式对应的标记组使用逗号分隔。标记序列既不能以逗号开头,也不能以逗号结尾。如果整数常量表达式列表为空,则标记序列为空。#embed指令会被其扩展结果替换;并且如果存在嵌入参数,则可能包含额外的或者替代的标记序列。


#embed预处理指令具有以下三种语法格式:

第一种格式:

#embed <h-char-sequence> embed-parameter-sequenceopt new-line

其中embed是指令名;h-char-sequence是头文件字符序列,头文件字符序列中的字符可以是源字符集中除换行符和>字符外的任何字符;embed-parameter-sequence是嵌入形参序列;new-line是换行符。

该格式指令将在实现定义的位置搜索< >内的特定序列标识的资源。如何搜索命名资源将由实现定义。


第二种格式:

#embed "q-char-sequence" embed-parameter-sequenceopt new-line

其中embed是指令名;q-char-sequence是引号字符序列(quote character sequence),引号字符序列中的字符可以是源字符集中除换行符和"字符外的任何字符;embed-parameter-sequence是嵌入形参序列;new-line是换行符。

该格式指令将在实现定义的位置搜索" "内的特定序列标识的资源。如何搜索命名资源将由实现定义。如果不支持这种搜索或者搜索失败,该指令会被重新处理,就像读取以下格式的指令一样:

#embed <h-char-sequence> embed-parameter-sequenceopt new-line

其中的序列与原始指令中的序列完全相同(包括原始指令中可能存在的>字符)。


第三种格式:

#embed pp-tokens new-line

其中embed是指令名;pp-tokens是预处理标记;new-line是换行符。

#embed后的预处理标记像在普通文本中一样处理。每个当前定义为宏名的标识符会被其替换列表替换。完成所有替换后,结果指令应与前面两种格式中的一种匹配。< >内或者" "内预处理标记序列构成单个头文件名预处理标记的方法将由实现定义。

#define TEST "test.bin"
...
#embed TEST

相邻字符串字面量不会串联成单个字符串字面量;因此宏展开后生成两个相邻字符串字面量的指令都是无效指令。

:串联相邻字符串字面量发生在编译阶段步骤6,即预处理后。)

#define TEST "test"".bin"
...
#embed TEST   //非法。

该格式#embed指令与条件包含预处理指令结合可提高代码的可移植性。

#if defined(_WIN32) || defined(_WIN64)
  #define VERSION "windows_version.bin"
#elif defined(__unix__)
  #define VERSION "unix_version.bin"
#else
  #define VERSION "default_version.bin"
#endif
...
#embed VERSION

#embed指令扩展后序列中整数常量表达式的值通过实现定义的数据资源映射方式来确定。每个整数常量表达式的值在[0,2嵌入元素宽度-1]区间范围内,如果同时满足以下两个条件:

整数常量表达式列表用于初始化元素类型与unsigned char类型兼容的数组,或者用于初始化元素类型与char类型兼容的数组(这种情况下char类型不能表示负值。)。

嵌入元素宽度等于CHAR_BIT

初始化后数组元素的内容就像编译时资源的二进制数据使用fread函数读取到数组中一样。

const unsigned char arr[] = { #embed "test.bin" };

上述代码可能会违反(资源宽度)%(嵌入元素宽度)的值应为0这一约束条件,因为存在这样一种可能:实现定义的8位资源宽度可能无法被嵌入元素宽度整除(例如:在一个CHAR_BIT16位的系统中。)。这种情况下实现应发出诊断信息,从而有助于提高代码的可移植性。


#embed指令的扩展结果可以是一个逗号分隔的整数常量表达式列表、单个整数常量表达式或者为空,因此其扩展结果也可以初始化非数组对象。

//初始化对象的值域范围为[0,2嵌入元素宽度-1]。

//普通变量初始化时大括号可以保留,也可以省略。
int i1 = #embed "test.bin";
int i2 = { #embed "test.bin" };

//结构变量初始化时大括号不能省略。
struct {
  int i;
  double d;
} s = { #embed "test.bin" };

#embed指令旨在将资源中的二进制数据转换为整数常量表达式序列,并在可能的情况下保留资源位流(bit stream)的值。对于二进制资源包含,ISO标准鼓励实现使用一种与实现定义的源文件包含搜索路径类似、但有所区别的机制。

实现应考虑程序编译时和程序执行时的位序(bit order)和字节序(byte order),以便更准确地表示#embed指令包含资源的二进制数据。这样能最大限度的保证:如果在编译时通过#embed指令包含的资源与执行时访问的资源相同,通过类似fread函数写入连续存储空间的数据,将与使用#embed指令包含内容初始化的字符数组实现逐位完全相等。

/*同一资源编译时读取和执行时读取的比较。*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
  //编译时读取资源。
  const unsigned char arr_t[] = { #embed "test.bin" };  //存储读取资源。

  //执行时读取资源。
  size_t size = sizeof(arr_t);
  unsigned char arr_e[size];  //存储读取资源。

  //打开文件。
  FILE *pFile = fopen("test.bin", "rb");
  if(!pFile)
  {
    perror("Error");
    exit(EXIT_FAILURE);
  }

  unsigned char *ptr = arr_e;
  //读取资源。
  if(fread(ptr, sizeof(unsigned char), size, pFile) != size)
  {
    fclose(pFile);
    perror("Error");
    exit(EXIT_FAILURE);
  }

  //关闭文件。
  fclose(pFile);

  //同一资源编译时读取和执行时读取的比较。
  if(memcmp(arr_t, arr_e, size) == 0)
  {
    puts("The bit sequence and bit order are equal.");
  }
  else
  {
    puts("The bit sequence and bit order are not equal.");
  }
  
  return 0;
}