嵌入式编程模块化—— 君子协定
共 7987字,需浏览 16分钟
·
2020-12-06 21:48
【说在前面的话】
将模块视作一个黑盒子:模块的设计者不用向外透露黑盒子的实现细节;同时模块的使用者也无法看到黑盒子的内部。
模块的设计者和模块的使用者完全通过“接口”来进行约定和沟通。这里所有的接口约定都是通过接口头文件来进行描述和传递的。
接口(及接口头文件)遵循“最小信息公开原则”,即,任何跟使用模块所提供的服务无关的、或者非必要的(可有可无)信息都应该从接口头文件中删除。
如何隐藏模块的实现,或者说隐藏源代码;
接口头文件中数据结构的保护,或者说如何阻止用户绕开模块所提供的API而直接访问关键结构体的内部(私有)成员;
对于第一条来说,我们只需要把模块编译成library,连同接口头文件一起提供给客户使用就可以做到;而对于第二条要想实现起来却并非那么简单——虽然我们常常说C语言可以通过结构体来模拟类的概念,但它却无法像C++的类那样提供对私有(private)和受保护(protected)成员的隐藏。换句话说,在实践“最小信息公开原则”的时候,如果用户调用服务的时候,确实需要用到结构体(这个结构体是最小信息),如何防止结构体的定义信息被“非法使用”,就成了一个切实的难题。
为了让后续的讨论更为清晰,我们不妨具体的定义一下我们的任务:
只允许用户使用结构体的大小和对齐信息——这样用户可以自由的定义变量,或是通过malloc这样的函数进行动态分配; 以某种“通过实际手段强制了的君子协定”的形式——仅在语法层面——阻止用户直接访问结构体的成员。
【什么是掩码结构体】
typedef struct byte_queue_t byte_queue_t;
struct byte_queue_t {
uint8_t *pchBuffer;
uint16_t hwSize;
uint16_t hwHead;
uint16_t hwTail;
uint16_t hwCount;
};
typedef struct byte_queue_cfg_t {
uint8_t *pchBuffer;
uint16_t hwSize;
} byte_queue_cfg_t;
extern
byte_queue_t * byte_queue_init(byte_queue_t *ptObj, byte_queue_cfg_t *ptCFG);
extern
bool byte_queue_enqueue(byte_queue_t *ptObj, uint8_t chByte);
extern
bool byte_queue_dequeue(byte_queue_t *ptObj, uint8_t *pchByte);
extern
uint_fast16_t byte_queue_count(byte_queue_t *ptObj);
typedef struct byte_queue_t byte_queue_t;
struct __byte_queue_t {
uint8_t *pchBuffer;
uint16_t hwSize;
uint16_t hwHead;
uint16_t hwTail;
uint16_t hwCount;
};
struct byte_queue_t {
uint8_t chMask[sizeof(struct __byte_queue_t)];
};
这里,我们实际上是给原来的类型重命名为__byte_queue_t,并建立了一个内部只使用数组来“滥竽充数”的替身——也就是我们所说的掩码结构体。
typedef struct byte_queue_t byte_queue_t;
struct __byte_queue_t {
uint8_t *pchBuffer;
uint16_t hwSize;
uint16_t hwHead;
uint16_t hwTail;
uint16_t hwCount;
};
struct byte_queue_t {
uint8_t chMask[sizeof(struct __byte_queue_t)]
__attribute__((aligned(
__alignof__(struct __byte_queue_t)
)));
};
至此,掩码结构体 byte_queue_t 拥有了和原本的结构体 struct __byte_queue_t 一样的尺寸和对齐;同时还在“语法”层面阻止了用户直接访问结构体成员的可能(当然,这也只能防君子不防小人),我们原本设立的两个目标都已成功达成。然而,聪明的你会在脑海里浮现出一个疑问——要想掩码结构体能正常工作,上述信息都必须放置到接口头文件中,难道用户是傻子,看不到结构体 __byte_queue_t 么?
typedef __name __name;
struct __
__VA_ARGS__ \
}; \
struct __name { \
uint8_t chMask[sizeof(struct __
__attribute__((aligned( \
__alignof__(struct __
))); \
};
/* 这只是一个为未来预留的语法糖 */
struct __
(struct __
借助上述宏,我们可以将接口头文件 byte_queue.h 中代码简化为:
...
declare_class(byte_queue_t)
def_class(byte_queue_t,
uint8_t *pchBuffer;
uint16_t hwSize;
uint16_t hwHead;
uint16_t hwTail;
uint16_t hwCount;
)
end_def_class(byte_queue_t)
...
而模块源代码中,则可以使用 class_internal() 来获取原本的结构体类型:
...
...
bool byte_queue_enqueue(byte_queue_t *ptObj, uint8_t chByte)
{
/* initialise "this" (i.e. ptThis) to access class members */
class_internal(ptObj, ptThis, byte_queue_t);
...
if ( (this.hwHead == this.hwTail)
&& (0 != this.hwCount)) {
//! queue is full
return false;
}
...
}
【如何使用PLOOC来简化开发】
从Github上下载最新的 release 版本。
解压缩后重命名目录为 PLOOC,并复制到你的目标工程中
在你的工程中添加对PLOOC目录的引用
在工程配置中打开对 C99 的支持,如果可能,直接开启 C11和GNU扩展的支持:
如果你使用的是 gcc, clang 或是 arm compiler 6,你还需要打开对微软扩展的支持(-fms-extensions)并屏蔽一些恼人且无害的 warning:
-fms-extensions -Wno-microsoft-anon-tag -Wno-empty-body
NOTE:如果你使用的是 arm compiler 6,在开启微软扩展以后,还需要额外定义一个宏 _MSC_VER 来避免底层库中的一些不必要的编译错误。
至此,我们就完成了 PLOOC 在你工程中的部署。
打开接口头文件 byte_queue.h 并在靠近结构体定义的地方其中添加以下内容:
/*! \NOTE: Make sure #include "plooc_class.h" is close to the class definition
*/
__<模块名称>_CLASS_IMPLEMENT
__<模块名称>_CLASS_INHERIT__
...
...
/*! \NOTE: Make sure #include "plooc_class.h" is close to the class definition
*/
...
/* 头文件的尾部 */
/*! \note it is very important to undef those macros */
在 byte_queue.h 里定义目标类:
//! \name class byte_queue_t
//! @{
declare_class(byte_queue_t)
def_class(byte_queue_t,
private_member(
uint8_t *pchBuffer;
uint16_t hwSize;
)
protected_member(
uint16_t hwHead; //!< head pointer
uint16_t hwTail; //!< tail pointer
uint16_t hwCount; //!< byte count
)
)
end_def_class(byte_queue_t) /* do not remove this for forward compatibility */
//! @}
打开 byte_queue.c,在文件的最开始通过定义宏 __BYTE_QUEUE_CLASS_IMPLEMENT 来标记自己“类主人”的身份,当然,别忘记包含自己的接口头文件:
在 byte_queue.c 中,如果某个函数(类的方法)试图访问类的成员,则应该首先借助 class_internal() 来“脱下马甲”。方法跟前文一样,这里就不再赘述。
【后记】
扫描下方微信,加作者微信进技术交流群,请先自我介绍喔。 推荐阅读:
嵌入式编程专辑 Linux 学习专辑 C/C++编程专辑 Qt进阶学习专辑
如果你喜欢我的思维,欢迎订阅 裸机思维