1、概述
1.1、 C++语言的特点
C语言自诞生以来已被广泛应用于系统和应用开发。比如Google的微内核操作系统fuchsia就是用C实现的,ARM的嵌入式操作系统ARMmbed也主要基于C实现。在应用开发方面,C被广泛用于GUI、游戏引擎、图形引擎、浏览器引擎、数据库等的开发。
C++语言的广泛使用,得益于其如下特点:
(1)支持面向对象编程,封装、继承、多态等机制使编程更加高效。
(2)兼容C,支持面向过程编程及驱动开发。
(3)标准库支持丰富的文件和数据结构操作。
(4)性能优异。
1.2、C++在嵌入式RTOS上的应用
随着MCU芯片处理能力的增强,嵌入式设备的图形显示、模式识别、脚本解析等能力不断赋能设备拓展应用边界。同时,随着应用的不断拓展,对嵌入式设备提出了更高的要求。通常,嵌入式系统采用C语言开发,但GUI、AI算法等复杂应用采用C开发,为此在RTOS上支持C语言的需求变得越来越强烈。
有些RTOS封装系统接口为上层应用提供了自定义的C类,但由于这些类不符合C标准,基于这些类开发的应用缺乏可移植性。另一方面,当使用外部开源软件时,需要进行适配,若软件比较复杂,适配工作量比较大,更为灾难性的是,自定义的类由于不够全,往往很难满足上层软件的需要。所以,最可行的方法还是要支持标准C++库。
本文主要阐述了基于GCC工具链在RTOS上支持C++的两个关键部分:
- RTOS上对C++初始化的支持;
- RTOS上适配C++库;
说明:本篇文章基于物联网操作系统AliOS Things上C++11实践总结而成,已在智能音箱等场景中应用。
2、C++初始化支持
对C语言而言,只能用常量或常量表达式初始化全局变量,比如不允许调用函数初始化,也不允许用另一个全局变量初始化。也即是说,全局变量的值在编译时就确定了。另外对于全局数组,其长度在编译时也确定了。
编译器将未赋初值的全局变量放在bss段,有初始值的全局变量放在data段(只读数据放到rodata段)。当RTOS启动时,bss段全部清为0,从程序镜像中读取data段的内容并写入到对应data段的内存,这样就完成了所有全局变量的初始化。
引入C++后,有了对象的概念,这个时候RTOS启动过程中的初始化就不再是清内存、拷贝内存那么简单了。对象内部的空间需要调用new分配,比如虚函数表、一些容器的内部存储空间。同时,对象初始化过程中需要调用父类的构造函数。这些都无法在编译时确定。
C处理这个问题的办法是:把所有C源文件中需要在初始化时调用的函数的地址集中放到一个表中,RTOS在初始化时遍历该函数表并调用每一个函数,以此完成C++对象的初始化。
RTOS启动时,初始化C++对象的伪代码如下:
for (f = __ctors_start__; f < __ctors_end__; f++) {
(*f)();
}
3、在RTOS上适配C++库
在RTOS上实现对C的初始化支持后,下载一个芯片厂商提供的工具链,配置一下编译选项,C似乎可以跑起来,但其实存在诸多问题。
芯片厂商提供的要么是基于linux的工具链,要么是基于裸机的工具链(bare-metal)。在RTOS上显然只能选bare-metal工具链。所谓bare-metal,其含义是无操作系统平台,其线程模式为single,即无多线程并发。在这种模式下,不支持C的多线程,比如不支持mutex、thread、condition_variable等类,同时C库内部实现中不考虑多线程互斥。所以这种模式下的工具链用在RTOS上,一方面功能不全,另一方面存在稳定性隐患,尤其在多核平台上多线程并发问题将变得严重。因此,为了真正实现对C++11的全量支持,需针对RTOS进行适配。
3.1、C++库的依赖关系
GCC工具链中集成了一个C++库,其依赖关系如下:
上图中三个依赖部分说明如下:
- C把C库中的符号导入到std命名空间提供给上层应用使用,同时C内部机制的实现也有赖于C库,比如输出流依赖于puts、putchar等接口。当然,C库也要实现多线程支持,这个有机会另外开辟一篇进行阐述,这里就不展开了。
- libgcc库提供了一些较底层的接口与机制,比如C++的线程变量基于libgcc库提供的线程变量管理机制。
- C适配层实现了C与RTOS的对接,这部分因底层OS的不同而不同。所以,ARM等芯片厂商或编译器厂商提供的针对嵌入式的工具链往往是裸机平台的,因为RTOS数量众多,无法一一满足。针对特定RTOS的多线程版本工具链需RTOS厂商自己定制。
3.2、适配
适配主要涉及类型与接口两部分,具体可参考./gcc/libgcc/gthr.h文件。
3.2.1、适配的接口说明如下:
C++内部类型 |
说明 |
__gthread_t |
线程类型 |
__gthread_key_t |
线程内部变量 |
__gthread_once_t |
单次执行 |
__gthread_mutex_t |
互斥信号量 |
__gthread_recursive_mutex_t |
支持递归的互斥信号量 |
__gthread_cond_t |
条件变量 |
__gthread_time_t |
时间 |
3.2.2、适配的接口如下
C++内部接口 |
说明 |
__gthread_active_p |
返回1表示支持多线程,那么C++库内部的实现将考虑线程间临界资源保护 |
__gthread_create |
创建线程 |
__gthread_join |
等待目标线程结束 |
__gthread_detach |
线程状态置为detached |
__gthread_equal |
判断是否为同一个线程 |
__gthread_self |
获得当前线程 |
__gthread_yield |
让出CPU |
…… |
…… |
3.2.3、适配说明
用typedef把C内部类型定义为RTOS的类型,基于RTOS的接口实现上述适配接口,便完成了C库的适配。如果RTOS上已经完成了对posix接口的支持,那么适配就比较方便了。示例如下:
//__gthread_t类型定义
typedef pthread_t __gthread_t;
//__gthread_create接口实现
static inline int
__gthread_create (__gthread_t *__threadid, void *(*__func) (void*), void *__args)
{
return pthread_create (__threadid, NULL, __func, __args);
}
3.3、配置与重编C++库
在编译C++库时,需配置为使能多线程模式,主要配置项如下:
--enable-threads=posix
其实使能该选项只是触发了配置脚本检查是否支持多线程,若配置脚本执行过程中判断系统不支持多线程,最终编译出来的库还是单线程的。比如,配置脚本中对__GTHREADS_CXX0X宏是否定义进行了判断,若该宏未定义则使能多线程失败。
完成适配与配置后,重编工具链即可生成多线程版本的C库。编译完成后可查看cconfig.h文件,确认使能的C++特性。以_GLIBCXX_USE_SCHED_YIELD宏为例,若没有生成该宏,那么thread类yield()函数实现为空函数、
4、后记
得益于C++良好的封装机制,用C++写的代码比用C写的代码bug率低很多。但硬币的另一面是,调试难度增加了。
即便开启了编译优化,C语言的一行语句与汇编的对应关系也相对比较清楚。但C++由于其复杂的机制,一行简单的赋值语句往往会对应十几条、甚至几十条汇编。这需要RTOS的维测能力提供高效的调试支持。
开发者技术支持
如需更多技术支持,可加入钉钉开发者群,或者关注微信公众号
更多技术与解决方案介绍,请访问阿里云AIoT首页https://iot.aliyun.com/