Step #1: Use the C++ compiler
Very often in SW-development it is a good idea to take small steps.
This eases tracking down the mistakes later on. So let's simply try to
switch to another compiler. In the Keil IDE we do this by simply
renaming the files from *.c to *.cpp. When we change timer.c to
timer.cpp and compile it we will see the stricter type checking of the
CPP compiler, which we resolve by doing type casts.
if
((g_pOnTimer[timerIdx] != NULL))
becomes
if
((g_pOnTimer[timerIdx] != (TimerCallbackPtr)NULL))
But on rebuilding the changed project a few more linker errors do
appear, too:
.\Simulator\Test.axf:
Error: L6218E: Undefined symbol TimerCreate (referred from hello.o).
hello.o refers to TimerCreate but timer.o exports
_Z11TimerCreatejPFvvE. The CPP-compiler allows function overloading (same function name with different
parameter lists). So it has to be able to distinguish between
functions that bear the same name. It does it by using name-decoration.
I guess that "_Z11" means something like CPP function that returns
an int and "jPFvvE" represents the parameter list. Unfortunately every
CPP compiler I have seen does this name-decoration in a different way.
So while calling into a library created by another compiler was
possible using pure C this is very rarely so using CPP.
Anyway, this name-decoration explains why a C-module cannot call CPP
functions. (Calling C functions from
CPP-modules is possible though.) While a C-module expects to
find a simple function "CreateTimer" as declared the function provided
has a decorated name.
This explains why the files will compile all right but linking
fails. Hence, any module that calls functions with decorated names must
also be a CPP-Module. In this example we're forced to convert main.c to
main.cpp.
Step #2: Create a simple class "
classes, code-reuse, object allocation
During this and the following steps I will work with a mixture of
object-oriented and plain C-code. This will work as long as I ensure
that I have only a single instance of the object. The reason for this
is " again " that I want to proceed in small steps.
Object orientation is analogous to the relationship between a cake
and its recipe. I can bake many cakes but I use the same recipe for
them all. The recipe is an analogy for code and I need it only once and
can instantiate it many times. The ingredients and state of each cake
that I bake are the properties of each instance of a cake-object. These
are the variables that describe the state of each instance of the
class.
One of the advantages of OO is code reuse. We have only one chunk of
code for all objects that there are. The distinct objects differ by
their individual state. So each object has its own data, but they all
share the same code.
From this follows that in every function the code has to know which
object it is working on. If you bake 5 cakes in parallel you should
know which one to add butter to. When programming this "which one" is
the hidden (this) pointer to the object to work on, that gets passed
into every function of a class.
In order to understand object-oriented programming it helps to ask:
How could I do OO in C? First, I would create a data structure that
contains all variables of the module. I would declare a variable of
that structure and delete former global variables. Then I equip all
functions with an additional first parameter " the pointer to this new
variable.
Finally I would change all these functions to access their data via
the new "this" pointer. (This is " by
the way " exactly how an early version of Keil's OO compiler worked. It
even generated intermediate C-files one could inspect.)
So we need object data. Just as ordinary variables can be declared
at compile-time or allocated dynamically, there are both options for
class objects, too. This is important because in many embedded systems
dynamic data allocation is not allowed or only in certain limits,
violates programming guidelines etc.
The reason is that safety critical systems that have to execute for
long periods cannot afford memory leaks or fragmentation. However, if
such rules apply, simply use static object instantiation as done in
this example.
 |
| Listing
3. Initial class interface |
Let's start by turning the timer into a class. As mentioned above,
during the intermediate steps of the C-to C++ migration part of the
implementation will be OO, while the rest remains "ordinary C". We can
do that because the OO implementation can still access global
variables. I start by creating a constructor and converting the "Init"
function. Listing 3 above
shows the new class-interface.
Lets declare a single instance of the class (global var in main.cpp):
Timer
myTimer = Timer(0);
This declares the object myTimer and will cause the constructor
Timer::Timer() to be called. But when? Somewhere in the startup code
i.e. before the first line of your own code is being executed! This
means that we cannot control exactly when the constructor of a
statically allocated class-object is being called.
If it is important to adhere to an initialization order during
system-init (e.g. reset hardware, then init) you need to split the
initialization into two parts and provide an extra Init() function (see Listing 4 below.)
 |
| Listing
4. Constructor and init function |
The initialization of the former TimerInit() function will now have
to be separated. The time independent initialization goes into the
constructor. The HW-Initialization is realized within the Init()
function of the new class, which I call at the right moment.