ODeV framework The Blog, a place where thoughts run free...

Objects, inheritance and polymorphism in the framework

The Object Oriented (OO) programming paradigm has introduced a fundamental change in the way we design a program. A change needed to solve the increasing complexity, in term of code lines, of a program.
Note: the term Object Oriented was used first by the M.I.T. Artificial Intelligence Group in the 1960, but it was Alan Kay to coin the term “object-oriented programming” at grand school between the 1966 and 1967.

“I made up the term ‘object-oriented’, and I can tell you I didn’t have C++ in mind.”
Alan Kay, OPSLA 1997

The ODeV framework, even if is written in C (a procedural language), uses some of the OO concepts:

  • Code encapsulation (object to protect the data structure and make the code more modular)
  • Inheritance (to reuse code already developed as it is)
  • Polymorphism (to make the code extensible)

For the implementation of the above concepts, I was inspired by the C++ programming language. I will use the code from the datalog_stwin project as an example in order to give a context to the discussion.

Code encapsulation


This is an important OOP concept that binds together the data and the functions to manipulate the data. By assuming that you can access the data only through the functions exported by an object’s class, it makes the code safer from unwanted misuse. This idea, also, leads us to the important OOP concept of data hiding, which is implemented in C++ with the class keyword, its syntax and its semantics.
We cannot declare a class in C, but we can use the idea by following few code practices:
  1. To group all the data of the class (called members of the class) in a struct.
  2. To use as first parameter of all functions of a class (called methods of the class) a pointer to the class object. This pointer, by convention is called _this in ODeV. In this way a method accesses a specific instance of the class, that is an object.
  3. If we want to hide the class members, we use a typedef in the header file and declare the class members in the .c file.

As example let’s give a look at the IIS3DWBTask class.

iis3dwbtask_class_1


This class extends the AManagedTaskEx abstract class and it implements the IEventSrc interface. In the ODeV framework I implement each application level class or low-level driver class in three files:

  • IIS3DWBTask.h: the header file exporting the public API of the class.
  • IIS3DWBTask_vtbl.h: the header file containing the declaration of all the virtual functions implemented by the class (I will come back to this file later).
  • IIS3DWBTask.c: the source file with the class methods implementation, both public and private, as well as the definition of the class members.

This is the header file IIS3DWBTask.h.

1. #ifndef IIS3DWBTASK_H_
2. #define IIS3DWBTASK_H_
3.
4. #ifdef __cplusplus
5. extern "C" {
6. #endif
7.
8.
9. #include "systp.h"
10. #include "syserror.h"
11. #include "AManagedTaskEx.h"
12. #include "AManagedTaskEx_vtbl.h"
13. #include "SPIBusIF.h"
14. #include "SensorsCommonParam.h"
15. #include "iis3dwb_reg.h"
16. #include "SensorEventSrc.h"
17. #include "SensorEventSrc_vtbl.h"
18. #include "queue.h"
19.
20.
21. #define IIS3DWB_CFG_MAX_LISTENERS 2
22.
23. /**
24. * Create type name for _IIS3DWBTask.
25. */
26. typedef struct _IIS3DWBTask IIS3DWBTask;
27.
28.
29. // Public API declaration
30. //***********************
31.
32. /**
33. * Allocate an instance of IIS3DWBTask.
34. *
35. * @return a pointer to the generic obejct ::AManagedTaskEx if success,
36. * or NULL if out of memory error occurs.
37. */
38. AManagedTaskEx *IIS3DWBTaskAlloc();
39.
40. /**
41. * Get the SPI interface for the sensor task.
42. *
43. * @param _this [IN] specifies a pointer to a task object.
44. * @return a pointer to the SPI interface of the sensor.
45. */
46. SPIBusIF *IIS3DWBTaskGetSensorIF(IIS3DWBTask *_this);
47.
48. /**
49. * Get the ::IEventSrc interface for the sensor task.
50. * @param _this [IN] specifies a pointer to a task object.
51. * @return a pointer to the ::IEventSrc interface of the sensor.
52. */
53. IEventSrc *IIS3DWBTaskGetEventSrcIF(IIS3DWBTask *_this);
54.
55.
56. // Inline functions definition
57. // ***************************
58.
59.
60. #ifdef __cplusplus
61. }
62. #endif
63.
64. #endif /* IIS3DWBTASK_H_ */

Let me share some comments. At line 26 I create the type name IIS3DWBTask for the class. In this way the members of the class are not visible when another part of the application code wants to use this sensor. We are “recommending” the user (that is another developer) to use only the class methods. This is an example of the code practice 3.

At lines 38, 46 and 53 we see that all the public API have _this as first parameter (in this case it is also the only one), with the only exception of the IIS3DWBTaskAlloc() method. The allocator is a special case. It is used to allocate in the RAM an instance (to instantiate) of the IIS3DWBTask class, so it doesn’t have _this as parameter, but it returns a pointer to the new allocated object. The strategy to allocate an object depends on each class and this gives flexibility to the framework. For this sensor class I use the singleton design pattern: there is only one instance of the class in the application. Note that a new allocated object is not initialized, that means it is not ready to be used, yet. This is an example of the code practice 2.

In the source file IIS3DWBTask.c there is the definition of the class members:

65. /**
66. * IIS3DWBTask internal structure.
67. */
68. struct _IIS3DWBTask {
69. /**
70. * Base class object.
71. */
72. AManagedTaskEx super;
73.
74. // Task variables should be added here.
75.
76. /**
77. * SPI IF object used to connect the sensor task to the SPI bus.
78. */
79. SPIBusIF m_xSensorIF;
80.
81. /**
82. * Specifies sensor parameters used to initialize the sensor.
83. */
84. SensorInitParam m_xSensorCommonParam;
85.
86. /**
87. * Specifies the sensor ID to access the sensor configuration inside the sensor DB.
88. */
89. uint8_t m_nDBID;
90.
91. /**
92. * Synchronization object used to send command to the task.
93. */
94. QueueHandle_t m_xInQueue;
95.
96. /**
97. * Buffer to store the data read from the sensor
98. */
99. uint8_t m_pnSensorDataBuff[IIS3DWB_SAMPLES_PER_IT * 7];
100.
101. /**
102. * ::IEventSrc interface implementation for this class.
103. */
104. IEventSrc *m_pxEventSrc;
105.
106. /**
107. * Specifies the time stamp in tick.
108. */
109. uint32_t m_nTimeStampTick;
110.
111. /**
112. * Used during the time stamp computation to manage the overflow of the hardware timer.
113. */
114. uint32_t m_nOldTimeStampTick;
115.
116. /**
117. * Specifies the time stamp linked with the sensor data.
118. */
119. uint64_t m_nTimeStamp;
120. };

All the members of the class are declared inside one structure, struct _ IIS3DWBTask at line 68. This is an example of the code practice 1.
An advantage is to reduce the number of global variables in the application. Moreover, this implementation of the class makes easy to move the IIS3DWBTask in another project because the dependencies of the file are not much. In fact, if we look at the include graph in the following image, we note that this class depends only on few other classes and interfaces other than the ODeV framework.

IIS3DWBTask_include_graph

For example, if we try to copy the three files that define the IIS3DWBTask class into another ODeV_f project we should have few compiler errors due to the missing files highlighted in the previous image. In a project with more the 1000 files we can say that IIS3DWBTask depends only on few files (code modularity).

Ineritance

This is an important concept of the OOP that allows classes to be arranged in a hierarchy that represents the “is-type-of” relationship.
For example, looking back to the class diagram image above, the IIS3DWBTask class extends (or we can also say that it inherits from) the AManagedTaskEx class. This means that IIS3DWBTask is a type of AManagedTaskEx and so an instance of the sensor class must behave also like a managed task object. In practice this means that I can use a pointer to an IIS3DWBTask object in all methods that have a pointer to a AManagedTaskEx as parameter. For example consider the method ACAddTask(ApplicationContext *this, AManagedTask *pTask) of the ApplicationContext class that we use to add a managed task to the application context during the application initialization (SysLoadApplicationContext(ApplicationContext *pAppContext)). Even if the second parameter is a pointer to a AManagedTask object, we can use a pointer to our IIS3DWBTask object:

121. sys_error_code_t SysLoadApplicationContext(ApplicationContext *pAppContext) {
122. assert_param(pAppContext);
123. sys_error_code_t xRes = SYS_NO_ERROR_CODE;
124.
125. s_pxIIS3DWBObj = IIS3DWBTaskAlloc();
125. xRes = ACAddTask(pAppContext, (AManagedTask*)s_pxIIS3DWBObj);
126.
127. return xRes;
128. }

Note that at line 125 I cast the pointer to AManagedTask to avoid the compiler warning… after all we are using a C compiler and not a C++ one ;-)

The inheritance is a powerful way to reuse existing code. For example, even if the IIS3DWBTask class export a very simple public API it implements and it is used by the framework as a more complex object, as a managed task extended. It is able to perform a power mode switch, it interacts with the system during startup to perform the initialization in the proper way, and so on. That means that our application class, IIS3DWBTask, is well integrated with the framework because it inherits these capabilities from its superclass, AManagedTaskEx, and it does not have to re-implement all the protocol code to manage the interaction with the INIT task.

In the ODeV framework I use single inheritance that means a class, like IIS3DWBTask, can inherit from only one super class, AManagedTaskEx in this case. This is different from C++ language that support multiple inheritance, a powerful syntax but that introduce a lot of complexity.
The implementation of the inheritance used in the framework is called, in literature, inheritance by composition: the first member of a class data structure must be a variable of the base class, like in the code line 72.

So, for example the following image show the class diagram for the IIS3DWBTask class and the declaration of the class data structure:

odev_obj_model_uml_class_diagram

If we look at the binary code generated by the compiler when we instantiate an object of type IIS3DWBTask, then we will find something like this where all the members of the object are organized in memory according the class (struct) declaration:


odev_obj_model_bin_layout

With this binary layout when we use a pointer to an object of the subclass IIS3DWBTask, we can access all the members of the full object, but if we cast that pointer into a pointer to an object of one of the base classes, it works fine and the visibility is restricted to only the base class members, as expected:

odev_obj_model_inheritance_ex

The base class, AManagedTaskEx, is an abstract class, that means it has some virtual methods, and this introduce the other section of this post.

Polymorphism



One of the key features of class inheritance is that a pointer to a derived class is type-compatible with a pointer to its base class. Polymorphism is the art of taking advantage of this simple but powerful and versatile feature. In C++, polymorphism causes a member function to behave differently based on the object that calls/invokes it. There are different types of polymorphism, in ODeV framework I implemented the polymorphism trough the virtual functions implemented with the virtual tables.

Let’s go back to our example to try to understand the implementation of the virtual functions. Because the base class AManagedTask has, as first member, a virtual pointer vptr, then all the derived managed tasks inherit the virtual pointer. It is a pointer to a table of function pointers (code line 129).

odev_obj_model_vptr_1

129. struct _AManagedTask_vtbl {
130. sys_error_code_t (*HardwareInit)(AManagedTask *_this, void *pParams);
131. sys_error_code_t (*OnCreateTask)(AManagedTask *_this, TaskFunction_t *pvTaskCode, const char **pcName, unsigned short *pnStackDepth, void **pParams, UBaseType_t *pxPriority);
132. sys_error_code_t (*DoEnterPowerMode)(AManagedTask *_this, const EPowerMode eActivePowerMode, const EPowerMode eNewPowerMode);
133. sys_error_code_t (*HandleError)(AManagedTask *_this, SysEvent xError);
134. };

A virtual function in the base class AManagedTask is implemented using the virtual table, like, for example the AMTHardwareInit() method (code line 135).

135. SYS_DEFINE_INLINE
136. sys_error_code_t AMTHardwareInit(AManagedTask *_this, void *pParams) {
137. return _this->vptr->HardwareInit(_this, pParams);
138. }

The sub class IIS3DWBTask can override a virtual function by modifying the corresponding pointer in the class virtual table (code lines 143, 151).

139. /**
140. * IIS3DWBTask virtual table.
141. */
142. static const AManagedTaskEx_vtbl s_xIIS3DWBTask_vtbl = {
143. IIS3DWBTask_vtblHardwareInit,
144. IIS3DWBTask_vtblOnCreateTask,
145. IIS3DWBTask_vtblDoEnterPowerMode,
146. IIS3DWBTask_vtblHandleError,
147. IIS3DWBTask_vtblForceExecuteStep,
148. IIS3DWBTask_vtblOnEnterPowerMode
149. };
150.
151. sys_error_code_t IIS3DWBTask_vtblHardwareInit(AManagedTask *_this, void *pParams) {
152. assert_param(_this);
153. sys_error_code_t xRes = SYS_NO_ERROR_CODE;
154. IIS3DWBTask *pObj = (IIS3DWBTask*)_this;
155.
156. IIS3DWBTaskConfigureIrqPin(pObj, FALSE);
157.
158. return xRes;
159. }

In this way, when we use the virtual function of the base class, the method that is invoked depends on the actual type of the pointer _this.

AMTHardwareInit((AManagedTask*)pxMyIIS3DWBTaskObj, NULL);

odev_obj_model_vptr_2


Now, if we want to modify the implementation of the AMTHardwareInit() provided by the IIS3DWBTask, we can simply define a sub class and provide a new implementation for the virtual function. We do not need to modify the source code of the in the file IIS3DWBTask.c.

A bit of naming convention



The virtual functions are powerful tools, so I would like to identify at a glance what are the virtual functions implemented by a class. Of course, I can give a look at the virtual table of the class. But a class can have more than one virtual table if it implements one or more interface other than subclassing a base class. The strategy that I used in ODeV_f is:
  • To declare all the virtual functions implemented by a class in a separate file called [CLASS_NAME]_vtbl.h (eg. IIS3DWBTask_vtbl.h)
  • To add the ‘_vtbl’ marker between the class name and the name of the method that implement a virtual function (eg. IIS3DWBTask_vtblHardwareInit)

blog comments powered by Disqus

We use cookies to personalize content and to provide a comment feature. To analyze our traffic, we use basic Google Analytics implementation with anonymized dat. If you continue without changing your settings, we'll assume that you are happy to receive all cookies on the stf12.org website. To understand more about the cookies please see this web page.