RT-Thread的内核对象模型是一种非常有趣的面向对象实现方式。由于C语言更为面向系统底层,操作系统核心通常都是采用C语言和汇编语言混合编写而成。C语言作为一门高级计算机编程语言,一般被认为是一种面向过程的编程语言:程序员按照特定的方式把要处理事物的过程一级级分解成一个个子过程。面向对象源于人类对世界的认知多偏向于类别模式,根据世界中不同物品的特性分门别类的组织在一起抽象并归纳,形成各个类别的自有属性。在计算机领域一般采用一门新的,具备面向对象特征的编程语言实现面向对象的设计,例如常见的编程语言C++,Java,Python等。那么RTThread既然有意引入对象系统,为什么不直接采用C++来实现? 这个需要从C++的实现说起,用过C++的开发人员都知道,C++的对象系统中会引入很多未知的东西,例如虚拟重载表,命名粉碎,模板展开等。对于一个需要精确控制的系统,这不是一个很好的方式,假于它人之手不如握入己手!面向对象有它非常优越的地方,取其精华(即面向对象思想,面向对象设计),也就是RT-Thread内核对象模型的来源。RT-Thread实时操作系统中包含一个小型的,非常紧凑的对象系统,这个对象系统完全采用C语言实现。
C语言的对象化模型:采用C语言实现的关键是如何运用C语言本身的特性来实现上述面向对象的特征。
1、封装,隐藏内部实现;
封装是一种信息隐蔽技术,它体现于类的说明,是对象的重要特性。封装使数据和加工该数据的方法(函数)封装为一个整体,以实现独立性很强的模块,使得用户只能见到对象的外特性(对象能接受哪些消息,具有那些处理能力),而对象的内特性(保存内部状态的私有数据和实现加工能力的算法)对用户是隐蔽的。封装的目的在于把对象的设计者和对象者的使用分开,使用者不必知晓行为实现的细节,只须用设计者提供的消息来访问该对象。在C语言中,大多数函数的命名方式是动词+名词的形式,例如要获取一个semaphore,会命名成take_semaphore,重点在take这个动作上。在RT-Thread系统的面向对象编程中刚好相反,命名为rt_sem_take,即名词+动词的形式,重点在名词上,体现了一个对象的方法。另外对于某些方法,仅局限在对象内部使用,它们将采用static修辞把作用范围局限在一个文件的内部。通过这样的方式,把一些不想让用户知道的信息屏蔽在封装里,用户只看到了外层的接口,从而形成了面向对象中的最基本的对象封装实现。
一般属于某个类的对象会有一个统一的创建析构过程。在RT-Thread中这些分为两类(以semaphore对象为例):
• 对象内存数据块已经存在,需要对它进行初始化 – rt_sem_init;
• 对象内存数据块还未分配,需要创建并初始化 – rt_sem_create。
可以这么认为,对象的创建(create)是以对象的初始化(init)为基础的,创建动作相比较而言多了个内存分配的动作。相对应的两类析构方式:
• 由rt_sem_init初始化的semaphore对象 – rt_sem_detach;
• 由rt_sem_create创建的semaphore对象 – rt_sem_delete.
2、继承,复用现有代码;
继承性是子类自动共享父类之间数据和方法的机制。它由类的派生功能体现。一个类直接继承其它类的全部描述,同时可修改和扩充。继承具有传递性。继承分为单继承(一个子类只有一父类)和多重继承(一个类有多个父类,当前RT-Thread的对象系统不能支持)。类的对象是各自封闭的,如果没继承性机制,则类对象中数据、方法就会出现大量重复。继承不仅支持系统的可重用性,而且还促进系统的可扩充性。
1 类似的实现代码如下程序清单: 2 3 /* 父类 */ 4 struct parent 5 { 6 int a, b; 7 char *str; 8 }; 9 10 /* 继承于父类的子类 */ 11 struct child 12 { 13 struct parent p; 14 int a, b; 15 }; 16 17 /* 操作示例函数*/ 18 void func() 19 { 20 struct child obj, *obj_ptr; /* 子类对象及指针 */ 21 struct parent *parent_ptr; /* 父类指针 */ 22 obj_ptr = &obj; 23 24 /* 取父指针 */ 25 parent_ptr = (struct parent*) &obj; 26 27 /* 可通过转换过类型的父类指针访问相应的属性 */ 28 parent_ptr->a = 1; 29 parent_ptr->b = 5; 30 /* 子类属性的操作 */ 31 obj_ptr->a = 10; 32 obj_ptr->b = 100; 33 }
在上面代码中,注意child结构中第一个成员p,这种声明方式代表child类型的数据中开始的位置包含一个parent类型的变量。在函数func中obj是一个child对象,正像这个结构类型指示的,它前面的数据应该包含一个parent类型的数据。在第25行的强制类型赋值中parent_ptr指向了obj变量的首地址,也就是obj变量中的p对象。好了,现在parent_ptr指向的是一个真真实实的parent类型的结构,那么可以按照parent的方式访问其中的成员,当然也包括可以使用和parent结构相关的函数来处理内部数据,因为一个正常的,正确的代码,它是不会越界访问parent结构体以外的数据。
经过这基本的结构体层层相套包含,对象简单的继存关系就体现出来了:父对象放于数据块的最前方,代码中可以通过强制类型转换获得父对象指针。
3、多态,改写对象行为;
对象根据所接收的消息而做出动作。同一消息为不同的对象接受时可产生完全不同的行动,这种现象称为多态性。利用多态性用户可发送一个通用的信息,而将所有的实现细节都留给接受消息的对象自行决定,如是,同一消息即可调用不同的方法。例如:RT-Thread系统中的设备:抽象设备具备接口统一的读写接口。串口是设备的一种,也应支持设备的读写。但串口的读写操作是串口所特有的,不应和其他设备操作完全相同,例如操作串口的操作不应应用于SD卡设备中。多态性的实现受到继承性的支持,利用类继承的层次关系,把具有通用功能的协议存放在类层次中尽可能高的地方,而将实现这一功能的不同方法置于较低层次,这样,在这些低层次上生成的对象就能给通用消息以不同的响应。
RT-Thread对象模型采用结构封装中使用指针的形式达到面向对象中多态的效果,例如:
1 /* 抽象父类 */ 2 struct parent 3 { 4 int a; 5 6 /* 反映不同类别属性的方法 */ 7 void (*vfunc)(int a); 8 } 9 10 /* 抽象类的方法调用 */ 11 void parent_vfunc(struct parent *self, int a) 12 { 13 assert(self != NULL); 14 assert(slef->vfunc != NULL); 15 /* 调用对象本身的虚拟函数 */ 16 self->vfunc(a); 17 } 18 19 /* 继承自parent的子类 */ 20 struct child 21 { 22 struct parent p; 23 int b; 24 }; 25 26 /* 子类的构造函数 */ 27 void child_init(struct child* self) 28 { 29 struct parent* parent; 30 31 /* 强制类型转换获得父类指针 */ 32 parent = (struct parent*) self; 33 assert(parent != NULL); 34 35 /* 设置子类的虚拟函数 */ 36 parent->vfunc = child_vfunc; 37 } 38 /* 子类的虚拟函数实现 */ 39 static void child_vfunc(struct child* self, int a) 40 { 41 self->b = a + 10; 42 }
在RT-Thread内核对象中分为两类:静态内核对象和动态内核对象。
静态内核对象通常放在RW(用于放置带初始值的全局变量)或ZI(用于放置未初始化或初始化0的全局变量)段中,在系统启动后在程序中初始化,由系统在栈区自动分配内存空间;而动态内核对象则是从堆区创建的,然后手动分配内存及初始化。