嵌入式开发中经常需要跨平台移植,但是不同平台的系统函数通常不一样,如果能封装一个平台适配层,将底层系统差异和上层业务代码隔离,移植起来将事半功倍。
系统层次结构如下所示:
对于下面常规的函数,封装起来很简单:
/* platform_a.h */
int platform_a_func(int value);
/* platform_b.h */
int platform_b_func(int value);
平台适配层头文件定义如下:
/* common.h */
int common_func(int value);
对于platform_a的适配如下:
/* common_platform_a.c */
int common_func(int value)
{
return platform_a_func(value);
}
对于platform_b的适配如下:
/* common_platform_b.c */
int common_func(int value)
{
return platform_b_func(value);
}
但是对于printf这种变参函数,假设两个平台提供的函数定义分别如下,应该如何适配呢?
/* platform_a.h */
int platform_a_printf(const char *format, ...);
/* platform_b.h */
int platform_b_printf(const char *format, ...);
像下面这样肯定是行不通的,因为变参不能这样传递:
/* common.h */
int common_printf(const char *format, ...);
/* common_platform_a.c */
int common_printf(const char *format, ...)
{
return platform_a_printf(format, ...);
}
/* common.h */
int common_printf(const char *format, ...);
/* common_platform_a.c */
int common_printf(const char *format, ...)
{
int len;
va_list ap;
va_start(ap, format);
len = platform_a_vprintf(format, ap);
va_end(ap);
return len;
}
但是有些平台并不提供类似vprintf的函数,也就是说可能根本就不存在platform_a_vprintf。 当然我们可以通过vsnpirntf函数先把变参收集到一个缓冲中,然后再调用系统函数platform_a_printf:
/* common.h */
int common_printf(const char *format, ...);
/* common_platform_a.c */
int common_printf(const char *format, ...)
{
char msg[MAX_MSG_LEN];
va_list ap;
va_start(ap, format);
vsnprintf(msg, MAX_MSG_LEN, format, ap); /* 截断情况下返回值大于MAX_MSG_LEN */
va_end(ap);
msg[MAX_MSG_LEN - 1] = 0;
return platform_a_printf("%s", msg);
}
这样是解决了我们的问题,但是引入了一个缓冲,多了一次内存拷贝,多了一次函数调用,是有代价的。 那么,有没有其他方法呢?
能否通过下面的方式在预编译期适配掉? 平台适配层头文件定义如下:
/* common.h */
#if defined(PLATFORM_A)
#define common_printf platform_a_printf
#elif defined(PLATFORM_B)
#define common_printf platform_b_printf
#else
#error "Please choose your platform!!!"
#endif
但是这样存在以下问题:
common.h
中添加int common_printf(const char *format, ...);
可解决该问题)另一种方式是头文件如下定义:
/* common.h */
int common_printf(const char *format, ...);
在编译依赖common.h的代码时,CFLAG中添加选项 -Dcommon_printf=platform_a_printf
。这种方式原理和上面的一样,都是在预编译期进行符号替换,不同之处是把平台相关的东西从代码中移到编译脚本中。
头文件如下定义:
/* common.h */
extern int (*common_printf)(const char *format, ...);
对于platform_a的适配如下:
/* common_platform_a.c */
int (*common_printf)(const char *format, ...) = platform_a_printf;
对于platform_b的适配如下:
/* common_platform_b.c */
int (*common_printf)(const char *format, ...) = platform_b_printf;
这样头文件和编译脚本中都不需要特殊处理,只需要在平台适配层做区分即可。
方法二由于是编译期就搞定的,无额外消耗,性能最优。 方法一性能最差,因为额外增加的操作太多。 方法三略次于方法二,因为多了一次寻址过程。详见下面的分析。 示例代码:
#include <stdio.h>
int platform_printf(const char *format, ...)
{
return printf("%s", format);
}
int (*common_printf)(const char *format, ...) = platform_printf;
int direct_call(void)
{
platform_printf("hello\n");
return 0;
}
int indirect_call(void)
{
common_printf("hello\n");
return 0;
}
int main()
{
direct_call();
indirect_call();
return 0;
}
armv7-a架构下的反汇编如下图所示,可以看到,间接调用比直接调用多了3条指令。 间接调用的时候首先把符号common_printf的地址0x1f64c加载到r3寄存器(右侧第4行和第5行),然后访问该地址的内容(右侧第6行),最后调用common_printf(右侧第9行)。
x86-64架构下的反汇编如下图所示,可以看到,间接调用比直接调用多了1条指令。