Daly(翻译, 有删减)
Lars Wirzenius http://liw.iki.fi/liw/texts/cpp-trick.html
介绍
一般教科书或者编程指南中都建议减少宏的使用,从我的经验看,大量使用宏是错误的来源,经常会使人陷入混乱。
但当我
1997
年参与
HiBase
项目时学习到这个技巧后,使我很震惊。我发现在一些任务里,这个技巧可以避免大量相似结构体的内存分配,释放,初始化等问题。当然他不是万金油,但在特定的程序,这种技巧非常实用。
(
译者:通信协议中有大量不同种类但结构相似的数据包,如果为每一种包都定义一个结构体,并且对应不同的初始化
,
释放等函数,程序会变得很复杂庞大,管理也很麻烦。对于面向对象的语言
(Java/C++)
可以为每一种包定义一个类,通过继承,管理各种包的操作。但是
C
就不行
)
问题提出
假设通信协议中有下面两种数据包,一个是鉴权包,一个是确认包
struct AuthPacket {
char *username;
char *password;
};
struct AckPacket {
int status;
char *error_text;
};
两个结构是未经二进制编码的包结构,方便程序员管理,赋值。分别函数去操作是一件痛苦且容易出错的事,单是初始化就要为每一种包写一遍。如果一个新字段加进来,那么对应的初始化,释放,输出全部要改写,任何一项工作都很容易导致bug. (译者:如果不写函数直接对内部成员操作更糟,一是到处有重复的代码,二是难以保持数据的一致性,程序规模大了以后很难跟踪错误)
解决方案
利用预编译定义一种新的”语言”去描述包。
PACKET(Auth,
STRING(username)
STRING(password)
)
PACKET(Ack,
INTEGER(status)
STRING(error_text)
)
描述放到头文件,这里用packet-desc.h
(在wap网关软件kannel中是放到.def文件)。要定义一个实际的数据类型,可以这样:
struct Packet {
int type;
#define INTEGER(name) int name;
#define STRING(name) char *name;
#define PACKET(name, fields) /
struct name { fields } name;
#include "packet-desc.h"
};
编译器展开以后就是这个样子:
struct Packet {
int type;
struct Auth {
char *username;
char *password;
} Auth;
struct Ack {
int status;
char *error_text;
} Ack;
};
除了包类型定义,还可以利用这些宏定义enum类型,内存管理,IO操作等。
讨论
在宏定义中,特别注意分号,逗号。这些都要非常小心。比起缺点,这种技巧带来的优点更明显。(译者: 协议软件中的数据包通常用到联合体(union),当要解析或者封装包的时候,如用传统方法,整个程序到处都是switch...case..结构。如果一个字段或者数据包结构改变,所有的switch...case...都得修改。大量地方修改同一项的结果是,容易导致不一致!)
关于本技巧的应用,可参见开源Wap网关Kannel源代码中对PDU的处理部分
例子代码
cpp-trick.h
#if !defined(PACKET) || !defined(INTEGER) || !defined(STRING)
#error Not all necessary macros were defined.
#endif
PACKET(Auth,
STRING(username)
STRING(password)
)
PACKET(Ack,
INTEGER(status)
STRING(error_text)
)
#undef PACKET
#undef INTEGER
#undef STRING
cpp-trick.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void panic(char *msg) {
fprintf(stderr, "%s/n", msg);
exit(EXIT_FAILURE);
}
void *xmalloc(size_t size) {
void *p;
p = malloc(size);
if (p == NULL)
panic("Out of memory");
return p;
}
void *xstrdup(char *str) {
char *copy;
copy = xmalloc(strlen(str) + 1);
strcpy(copy, str);
return copy;
}
int read_integer(int *p) {
if (fread(p, sizeof(int), 1, stdin) == 0)
return 0;
return 1;
}
char *read_string(void) {
int len;
char *str;
if (read_integer(&len) == 0)
return NULL;
str = xmalloc(len + 1);
if (fread(str, len, 1, stdin) != 1)
return NULL;
str[len] = '/0';
return str;
}
void write_integer(int i) {
if (fwrite(&i, sizeof(i), 1, stdout) != 1)
panic("Write error.");
}
void write_string(char *str) {
int len;
len = (int) strlen(str);
write_integer(len);
if (fwrite(str, len, 1, stdout) != 1)
panic("Write error.");
}
typedef enum {
#define INTEGER(name)
#define STRING(name)
#define PACKET(name, fields) name,
#include "cpp-trick.h"
} PacketType;
char *type_name(PacketType type) {
#define INTEGER(name)
#define STRING(name)
#define PACKET(name, fields) /
if (type == name) return #name;
#include "cpp-trick.h"
return "unknown";
}
typedef struct Packet {
PacketType type;
#define INTEGER(name) int name;
#define STRING(name) char *name;
#define PACKET(name, fields) struct name { fields } name;
#include "cpp-trick.h"
} Packet;
Packet *packet_create(PacketType type) {
Packet *packet;
packet = xmalloc(sizeof(Packet));
packet->type = type;
#define INTEGER(name) p->name = 0;
#define STRING(name) p->name = NULL;
#define PACKET(name, fields) /
{ struct name *p = &packet->name; fields }
#include "cpp-trick.h"
return packet;
}
void packet_destroy(Packet *packet) {
#define INTEGER(name)
#define STRING(name) free(p->name); p->name = NULL;
#define PACKET(name, fields) /
{ struct name *p = &packet->name; fields }
#include "cpp-trick.h"
free(packet);
}
void packet_dump(Packet *packet) {
printf("Dumping packet %p:/n", (void *) packet);
printf(" Type: %s/n", type_name(packet->type));
#define INTEGER(name) printf(" %s: %d/n", #name, p->name);
#define STRING(name) printf(" %s: %s/n", #name, p->name);
#define PACKET(name, fields) /
if (packet->type == name) /
{ struct name *p = &packet->name; fields }
#include "cpp-trick.h"
printf("End of dump./n");
}
Packet *packet_read(void) {
Packet *packet;
int type;
if (read_integer(&type) == 0)
return NULL;
packet = packet_create(type);
#define INTEGER(name) read_integer(&p->name);
#define STRING(name) p->name = read_string();
#define PACKET(name, fields) /
if (type == name) { struct name *p = &packet->name; fields }
#include "cpp-trick.h"
return packet;
}
void packet_write(Packet *packet) {
write_integer(packet->type);
#define INTEGER(name) write_integer(p->name);
#define STRING(name) write_string(p->name);
#define PACKET(name, fields) /
if (packet->type == name) /
{ struct name *p = &packet->name; fields }
#include "cpp-trick.h"
}
int main(int argc, char **argv) {
Packet *packet;
if (argc != 2) {
fprintf(stderr, "Usage: %s [read|write]/n", argv[0]);
return EXIT_FAILURE;
}
if (strcmp(argv[1], "read") == 0) {
while ((packet = packet_read()) != NULL) {
packet_dump(packet);
packet_destroy(packet);
}
} else {
packet = packet_create(Auth);
packet->Auth.username = xstrdup("guest");
packet->Auth.password = xstrdup("demo");
packet_write(packet);
packet_destroy(packet);
packet = packet_create(Ack);
packet->Ack.status = 123;
packet->Ack.error_text = xstrdup("Access denied.");
packet_write(packet);
packet_destroy(packet);
}
return 0;
}