c语言宏高级用法,宏定义X-MACRO的高级应用(高阶版,含源码)

1. 结构体序列化问题

通常情况下,对于模块或者设备之间通信,往往是以字节流的方式来传输,而模块内部却要将这些字节流按某种数据结构来处理。这就存在,如何将数据结构(结构体)转换(序列化)成字节流,已经如何将字节流转换(反序列化)成数据结构的方法。

这不是很简单么?

tStrcut s;

unsigned char buff[100];

memcpy(buff, &s, sizeof(s)); // serial

memcpy(%s, buff, sizeof(s)); // de-serial

似乎,这也没什么毛病,非要挑毛病,那也只是结构体对齐的一点点问题。对于我们追求卓越的程序猿,还有什么更好的方法呢?

1.1 使用X-MACRO来构建

之前,我们学习了X-MACRO,何不用一下?

首先,我们定义一个宏:

#define EXPAND_STAR \

EXPAND_STAR_MEMBER(x, int) \

EXPAND_STAR_MEMBER(y, int) \

EXPAND_STAR_MEMBER(z, int) \

EXPAND_STAR_MEMBER(radius, double)

然后,在用这个宏定义结构体:

typedef struct

{

#define EXPAND_EXPAND_STAR_MEMBER(member, type) type member;

EXPAND_STAR

#undef EXPAND_EXPAND_STAR_MEMBER

} starStruct;

那么这个序列化和反序列化方法可以这样:

void serialize_star(const starStruct *const star, unsigned char *buffer)

{

#define EXPAND_STAR_MEMBER(member, type) \

memcpy(buffer, &(star->member), sizeof(star->member)); \

buffer += sizeof(star->member);

EXPAND_STAR

#undef EXPAND_STAR_MEMBER

}

void deserialize_star(starStruct *const star, const unsigned char *buffer)

{

#define EXPAND_STAR_MEMBER(member, type) \

memcpy(&(star->member), buffer, sizeof(star->member)); \

buffer += sizeof(star->member);

EXPAND_STAR

#undef EXPAND_STAR_MEMBER

}

如何打印结构体元素呢?元素中有int和double,打印方式是不一样的,于是,我们可以这样做:

#define print_int(val) printf("%d", val)

#define print_double(val) printf("%g", val)

void print_star(const starStruct *const star)

{

/* print_##type will be replaced with print_int or print_double */

#define EXPAND_EXPAND_STAR_MEMBER(member, type) \

printf("%s: ", #member);                   \

print_##type(star->member);                 \

printf("\n");

EXPAND_STAR

#undef EXPAND_STAR_MEMBER

}

1.2 用#include来改善

我是不满足于此的,至少,我对宏定义后面的反斜杠\看着不顺眼,总是有种强迫症要把它删掉。仔细想想,宏定义的各种奇技淫巧,实际上有部分内容是可以放在头文件(或者被一个#include)的文件。如以下内容是可以放在一个叫xmacro.def的文件:

// xmacro.def

EXPAND_STAR_MEMBER(x, int)

EXPAND_STAR_MEMBER(y, int)

EXPAND_STAR_MEMBER(z, int)

EXPAND_STAR_MEMBER(radius, double)

#undef EXPAND_STAR_MEMBER

什么?.def后缀也行?如果你真的有这样的疑问,你肯定没认真看我的《基于C99规范,最全C语言预处理知识总结》,不仅可以命名成xmacro.def,我还可以将它命名成xmacro.fxxk你信不信?

好了,在使用这些宏定义的.c文件中,这样用:

typedef struct

{

#define EXPAND_EXPAND_STAR_MEMBER(member, type) type member;

#include "xmacro.def"

} starStruct;

其他的EXPAND_STAR和#undef EXPAND_STAR_MEMBER都可以替换成#include "xmacro.def"。

在这个案例中,这个更改似乎没看出什么好处。别急,后面再举一个例子给你感受下include的威力!

对于序列化的这例子,满意么?No!还有可以优化的地方。

来,从上面的打印元素的函数print_star入手,这次要发挥两个井号##的作用。

#define FORMAT_(type) FORMAT_##type

#define FORMAT_int   "%d"

#define FORMAT_double "%g"

void print_star(const starStruct *const star)

{

/* FORMAT_(type) will be replaced with FORMAT_int or FORMAT_double */

#define EXPAND_EXPAND_STAR_MEMBER(member, type) \

printf("%s: " FORMAT_(type) "\n", #member, star->member);

#include "star.def"

}

是不是很好玩?还有呢,继续搞下去。

1.3 用可变参数__VA_ARGS__来改善

你知道宏定义中的不定参数...怎么玩么?

#define EXPAND_STAR \

EXPAND_STAR_MEMBER(x, int) \

EXPAND_STAR_MEMBER(y, int) \

EXPAND_STAR_MEMBER(z, int) \

EXPAND_STAR_MEMBER(radius, double)

可以改成:

#define EXPAND_STAR(_, ...) \

_(x, int, __VA_ARGS__) \

_(y, int, __VA_ARGS__) \

_(z, int, __VA_ARGS__) \

_(radius, double, __VA_ARGS__)

这个__VA_ARGS__就是针对...的,详细使用方法见《基于C99规范,最全C语言预处理知识总结》。

1.4 源码

那么以上的代码可以更改成:

#include

#include

/*

Generic

*/

#define STRUCT_MEMBER(member, type, dummy) type member;

#define SERIALIZE_MEMBER(member, type, obj, buffer)     \

memcpy(buffer, &(obj->member), sizeof(obj->member)); \

buffer += sizeof(obj->member);

#define DESERIALIZE_MEMBER(member, type, obj, buffer)   \

memcpy(&(obj->member), buffer, sizeof(obj->member)); \

buffer += sizeof(obj->member);

#define FORMAT_(type) FORMAT_##type

#define FORMAT_int "%d"

#define FORMAT_double "%g"

/* FORMAT_(type) will be replaced with FORMAT_int or FORMAT_double */

#define PRINT_MEMBER(member, type, obj) \

printf("%s: " FORMAT_(type) "\n", #member, obj->member);

/*

starStruct

*/

#define EXPAND_STAR(_, ...) \

_(x, int, __VA_ARGS__) \

_(y, int, __VA_ARGS__) \

_(z, int, __VA_ARGS__) \

_(radius, double, __VA_ARGS__)

typedef struct

{

EXPAND_STAR(STRUCT_MEMBER, )

} starStruct;

void serialize_star(const starStruct *const star, unsigned char *buffer)

{

EXPAND_STAR(SERIALIZE_MEMBER, star, buffer)

}

void deserialize_star(starStruct *const star, const unsigned char *buffer)

{

EXPAND_STAR(DESERIALIZE_MEMBER, star, buffer)

}

void print_star(const starStruct *const star)

{

EXPAND_STAR(PRINT_MEMBER, star)

}

2. EEPROM、FLASH等Memory条目结构化访问

2.1 Memory访问问题

像EEPROM、Flash(包含MCU内部的CodeFlahs、DataFlash,以及专门的外设NorFlash、NandFlash)这样的存储空间,有Block、Section等概念。MCU访问其时,需要按照某种约定去访问,也就是说,不能想在任意一个位置写任意长度的数据。例如,如果DataFlash的一次写单位是8个字节的一个block,那么MCU就必须每次写8字节倍数大小的数据内容,而不能随意写内容到一个block中的特定位置。这些就导致在软件设计上诸多不便。

那么,我们是否有方法,将这些内存空间预先规划好,上层软件通过特定接口访问这些分好区域的空间?

2.2 一般想法

遇到这个问题,通常我们惯性地想,将指定内容到指定地址不就可以了?

以一个block为8字节大小的DataFlash举例,其起始地址为0x00008000。

#define DATA1_ADDR0x00008000

#define DATA2_ADDR0x00008008

#define DATA3_ADDR0x00008010

#define DATA4_ADDR0x00008020

然后再限定每个数据的长度

#define DATA1_LEN0x00008000

#define DATA2_LEN0x00008008

#define DATA3_LEN0x00008010

#define DATA4_LEN0x00008020

似乎,这也能玩,但是每次改起来不嫌累么?而且也不好维护啊,换一个IC怎么搞?

2.3 使用X-MACRO

按照上面的结构体系列化例子的方法,我们尝试使用X-MACRO将Memory结构化。

以下是一些设想:

将Memory的物理地址映射到自定义逻辑地址

逻辑地址按Memory的Block对齐,逻辑地址从0开始

用户数据按逻辑地址分配

应用接口按实际内容大小操作

底层接口根据逻辑地址对齐读写Memory

#define ALL_ENTRIES()\

ENTRY(L_ADDR_DATA1, 8)\

ENTRY(L_ADDR_DATA2, 8)\

ENTRY(L_ADDR_DATA3, 16)\

ENTRY(L_ADDR_DATA4, 16)

然后通过ENTRY定义逻辑地址

#define ENTRY(addr, len)addr

再定义长度

#define ENTRY(addr, len)len

呃……好像不对劲,这个逻辑地址还是要人为定义才行。

发散下思维,我们或许可以将ENTRY(addr, len)中的addr定义为enum,当做是条目ID,而逻辑地址通过其他方式实现(结构体?)。

那,我们将上面的内容改一下:

#define ALL_ENTRIES()\

ENTRY(ID_DATA1, 8)\

ENTRY(ID_DATA2, 8)\

ENTRY(ID_DATA3, 16)\

ENTRY(ID_DATA4, 16)

第一步,定义ID

#undef ENTRY

#define ENTRY(id, len)id,

typedef enum

{

ALL_ENTRIES()

MEM_ID_MAX

}MEM_ID;

第二步,定义逻辑地址(定义结构体)

我们可以,用一个结构体来map其内存逻辑地址

#undef ENTRY

#define ENTRY(id, len)unsigned char mem_##id[len],

struct MemAddrAlign

{

ALL_ENTRIES()

};

第三步,定义逻辑地址(定义逻辑地址数组)

再通过结构体的元素offset来取其元素的位置

#define offsetof(s,m) ((int)((char*)&((s*)0)->m))

#undef ENTRY

#define ENTRY(id, len)offsetof(struct MemAddrAlign,mem_##c),

const unsigned int mem_entry_logic_addr[MEM_ID_MAX]=

{

ALL_ENTRIES()

};

第四步,定义数据长度

#undef ENTRY

#define ENTRY(id, len)len,

const unsigned int mem_entry_size[MEM_ID_MAX]=

{

ALL_ENTRIES()

};

最后,我们就可以通过数据块定义的ID(即enum)来对mem_entry_logic_addr和mem_entry_size的取下标访问来操作Memory了。

等等,这里好像还有个问题,ALL_ENTRIES()里面定义的长度不是按block 8字节对齐,怎么办?毕竟每个Entry都要求定义成8字节的倍数,似乎并不友好。

2.4 方法改进

不知你是否留意,Excel里面有个公式叫ROUNDUP,它可以将数值向上舍入,类似四舍五入的方法。

类似地,我们也可以做一个类似这样的方法,将数值定义为8的整数倍,如ROUNDUP(5)的值是8,ROUNDUP(8)的值是8,ROUNDUP(15)的值是16等等,如何实现?

#define BASE_SIZE         8

#define MEM_ROUNDUP(size)     ((size+BASE_SIZE-1)/BASE_SIZE*BASE_SIZE)

于是,我们将上面的“第二步,定义逻辑地址(定义结构体)”改一下:

#undef ENTRY

#define ENTRY(id, len)unsigned char mem_##id[len],

struct MemAddrAlign

{

ALL_ENTRIES()

};

总算好了,我们就可以方便操作memory了。例如读数据:

BOOL mem_entry_read(uint32 id, uint8* buff)

{

BOOL ret = FALSE;

if(id < MEM_ID_MAX && NULL != buff)

{

ret = mem_drv_read(mem_entry_index[id], buff, mem_entry_size[id]);

}

return ret;

}

其中的逻辑地址何长度分别为mem_entry_index[id]、mem_entry_size[id],而这个函数只有两个参数,ID和数据缓冲区。其余的通过ID对应的Entry里的预定于参数获得。

对于某些Memory外设,如果block并不是限制写入的单位,只是擦除单位,其实我们也可以扩展一下这个API,允许它从逻辑地址的某个偏移开始读写,但是逻辑地址还是要按照block大小对齐。当然,更多的逻辑处理需要在Memory的driver函数完成,如mem_drv_read。

BOOL mem_entry_read_offset(uint32 id, uint32 offset, uint8* buff, uint32 len)

{

BOOL ret = FALSE;

if(id < MEM_ID_MAX && NULL != buff)

{

uint32 addr = mem_entry_index[id];

uint32 id_length = mem_entry_size[id];

if(len + offset > id_length || len == 0)

{

ret = FALSE;

}

else

{

ret = mem_drv_read(addr + offset, buff, len);

}

}

return ret;

}

到这一步,算是大功告成了,但是我们是追求完美的,我觉得还有改善的地方。

2.5 终极完善

从上面的过程看,我们对ALL_ENTRIES()里面的内容进行了4个步骤解析(详见上面的第一二三四步)。我们是否可以通过include的多次包含来处理。即将一些定义的东西放在一个头文件(如*.def)而用另一个C文件多次包含它,实现以上的四个步骤预编译解析?就类似上文的1.2 用#include来改善一样,但是我们要在其基础上再完善,我们需要完美极致!

实际上,这个原理很简单,如果读完下面的解析还不理解,可以下载完整源码自行调试分析:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值