A six step process for migrating embedded C into a C++ object-oriented framework

Dirk Braun

May 14, 2008

Dirk Braun

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.

< Previous
Page 2 of 4
Next >

Loading comments...

Parts Search Datasheets.com

KNOWLEDGE CENTER