序列化

通常情况下,需要将复杂的数据结构发送到另一个程序,或从另一个程序接收数据,尤其是当该程序可能运行在不同的架构上,或者数据结构的版本可能不同。例如,一个程序可能会在退出时将其状态保存到文件中,并在重新启动时读取这些状态。

send 函数通常会首先将一个魔术标识符和版本号写入文件或网络套接字,然后依次写入所有的数据成员(即串行化)。如果遇到可变长度的数组(如字符串),它将首先写入长度信息,然后是数据,或者先写入数据,最后是一个特殊的终止符。数据格式通常为 XML 或二进制格式;在二进制格式的情况下,htonl() 等宏可能会派上用场。

receive 函数与之几乎完全相同:它会逐个读取所有项。可变长度数组要么通过先读取计数然后读取数据来处理,要么通过读取数据直到遇到特殊的终止符为止。

由于这两个函数通常遵循与数据(结构体)声明相同的模式,因此如果它们能够从一个通用定义中生成就会非常方便。

X-Macros

X-Macros 利用预处理器强制编译器多次编译相同的文本。 有时会多次包含一个特殊的文件(扩展名为 .def)。例如,variables.def 文件可能如下所示:

INT(value)
INT(shift)

在这种情况下,C 代码会像这样:

...
#define INT(var) int var;
#include "variables.def"
#undef INT
...
printf("version=1\n");
#define INT(var) printf(#var "=%d\n", var);
#include "variables.def"
#undef INT
...

如果不希望多次包含一个单独的文件,可以使用另一个宏。例如:

#define VARIABLES INT(value) \
                  INT(shift)

然后,#include 可以用宏调用来替代。

使用这种方法,还可以将其他可以操作这些值列表的宏的名称传递给宏。例如:

#define VAR_LIST(_) _(value) \
                    _(shift)
...
#define VAR_INT_DECL(var) int var;
VAR_LIST(VAR_INT_DECL)
...
printf("version=1\n");
#define VAR_INT_PRINTF(var) printf(#var "=%d\n", var);
VAR_LIST(VAR_INT_PRINTF)
...

这种方法不需要重新定义宏,并且可以使代码更易于理解和维护。

X-Macros 还特别适用于保持字符串和枚举类型之间的映射同步。

带版本控制的序列化

假设我们想要在上面的示例中添加一些额外的变量,但仍希望程序能够读取旧版本(版本 1)的文件。那么我们需要在列表处理的宏中添加一个版本参数和一个默认值参数:

#define VAR_LIST(_) _(value,1,0) \
                    _(shift,1,0) \
                    _(mask,2,0xffff)
...
int inputVer;
#define VAR_INT_DECL(var,varVer,default) int var;
VAR_LIST(VAR_INT_DECL)
...
scanf("version=%d", &inputVer);
#define VAR_INT_SCN(var,varVer,default) if (varVer <= inputVer) scanf(#var "=%d", &var); else var = default;
VAR_LIST(VAR_INT_SCN)
...
printf("version=2\n"); /* 总是输出最新版本 */
#define VAR_INT_PRT(var,varVer,default) printf(#var "=%d\n", var);
VAR_LIST(VAR_INT_PRT)
...

这里,我们首先定义了 VAR_LIST,它为每个变量添加了版本号和默认值参数。然后通过检查版本号,程序决定是否读取对应的值,或者使用默认值。这种方式使得序列化可以向前兼容,并且支持在后续版本中增加新变量。

最后修改: 2025年01月12日 星期日 13:31