Object-Orientated C
Although C is not generally thought of as an object-orientated language, it’s flexibility does allow object-orientated style code to be written, albeit with slightly more verbose syntax than a “object-orientated” language such as C++. Why would you want to do this? Well, there are a number of reasons:
- Organization: We humans tend to think of the world in terms of objects and their interactions. Writing code in the same style can make it easier to understand. It helps bundle data and functions that operate on that data into the same files, making them easier to maintain.
- Testing: Inheritance along with dependency injection can allow you to write code which is easier to test (more on this below).
- Flexibility: Writing code in an object-orientated style naturally makes it easy to create many instances of objects, which can be harder in imperative languages when you rely on file local or global variables.
- Clear Data Ownership: Using an OO style means your are more likely to bundle data into
structs
and less likely to rely on file local or global variables that are defined outside of any function in a.c
file. This is a good thing. File local or global data makes it harder to understand what happens when functions are called, and makes it easier to introduce bugs such as multiple threads accessing the same data without synchronisation. When data is bundled into objects, you are forced to pass the object around to all functions that need it, making developers aware of what data is used where.
Object-orientated code is a broad term for a number of different coding ideas. These include:
- Encapsulation. An object encapsulates all of it’s data and functions (called methods in the object-orientated world).
- Abstraction. The user of an object can only see and use it’s public interface, and has no knowledge of it’s internal workings.
- Polymorphism.
- Inheritance.
The above points can be implemented in C with varying levels of success and simplicity.
Also miss out on compiler-enforced encapsulation and abstraction.
Defining the Basic Object
In C, structures (typedef struct
) can be used to represent an “object”. Below is C code which defines a very simple object, which only holds data and contains no functions. Think of a struct
in C being the equivalent of a class
in dedicated OO-languages.
We would then create an instance of our object with the code:
Note that by itself, the above code is not very object-orientated, as even non-OO styled C would make extensive use of structures. This is where methods come in (functions which belong to an object).
Methods
A method is just a name for a function which belongs to an object. When we say “belong” we mean that the function can manipulate the data or call other methods of a particular object instance.
In stronger OO languages, methods are declared as part of a class, and a pointer to the current instance of the object (the keyword this
) is automatically passed into each method. We don’t have such luxuries in C, and so have to declare our methods as normal functions, and pass in the pointer to the object instance explicitly. You would normally want to do this as the first argument for consistency. Here we just call it obj
.
The first (and only, in this case) argument passed into this “method” is a pointer to instance of a ComplexNum
object that you wish to operate on. All non-static methods of an object will have this as their first argument.
Wait, What About A Constructor?
In OO-universe, a constructor is a special method which is run automatically when a new instance of an object is created. Unfortunately, in C, there is no way to enforce a method to run upon creation of our struct
object (o.k., yes you could wrap the creation of a struct inside a macro which also called a method).
A simple way to have constructor-like behaviour is to create an Init()
method.
The downside of this is that we have to remember to call it every time we create a new complexNum_t
object.
Inheritance and Polymorphism
Inheritance is the ability to define a new object/class which is a modified (extended) version of an existing object/class. Inheritance allows polymorphism, which is the ability to treat different objects in a similar way. One great use case for inheritance/polymorphism for embedded C is to make code testable. Inheritance can be used to abstract hardware specific implementations out from code you want to test on, say Linux, which doesn’t have access to GPIO and other hardware peripherals (which is done on real hardware through special memory mapped registers).
This is a placeholder for the reference: fig-gpio-inheritance-diagram shows how the basic inheritance structure for our example GPIO driver.
This is a placeholder for the reference: fig-gpio-real-vs-testing-swimlane-diagram shows how the real application and testing application would start-up, create the appropriate GPIO object and pass it to the module.
Notice how the module does not depends on either child class, meaning that it run both on a real hardware or in a testing environment on Linux just fine. A different main()
function, is used to the build the embedded application compared to the testing application. This allows each to create an instance of their respective GPIO class. This GPIO instance is then passed to the module (this is known as dependency injection). When passing it, you can either just pass the .base
portion of the struct
, or cast the entire struct
to the base class type (this works because the .base
portion is the first member of the struct
). The module will then call get()
and set()
, which due to polymorphism, will call different functions depending on whether it is a GpioReal
or GpioTest
instance.
We begin by defining the base “class” (a struct
because we are using C) in GpioBase.h
:
Since we are planning on using this as an interface, we don’t actually want anyone every creating an instance of GpioBase
(it exists solely to be extended from to make child classes). So we don’t actually need to define a constructor for it, nor any set()
or get()
methods. However, I did create a constructor, just to be consistent across all “classes”. I did however make sure that assert()
’s are raised if the base class methods are called.
We can then define the GpioReal
class in GpioReal.h
, which inherits from GpioBase
:
Notice how we have added a new member to the struct
, pinNumber
. This is needed because this class will need to pass the pin number when calling the hardware-specific GPIO driver functions. We have also declared a constructor function for this class.
We can then define the methods for this class in GpioReal.c
:
Note how the first thing we do in the set()
and get()
methods is to cast the self
pointer from the base type to the child type. We can safely do this because we know that the self
pointer passed in will always be of the type of the child class (since it must have been initialized with a constructor function from that class). Casting it gives us access to the child class members, which we would need to get hardware specific data so we can make the right HAL calls (or direct register manipulation if you’re feeling up to it!).
For a more real-world example, let’s pretend you were writing a GpioReal
class for running on the Zephyr framework/RTOS. You would likely want to make the GpioReal_init()
take in a pointer to the Zephyr GPIO device from the device tree (struct gpio_dt_spec
) and save it to the GpioReal struct
. You could then make the appropriate call in the set()
method (gpio_pin_set_dt(&self->gpioDtSpec, value)
), and similarly in the get()
method (return gpio_pin_get_dt(&self->gpioDtSpec)
).
Now we’ve finished our GpioReal
class, let’s write the GpioFake
class in GpioFake.h
and GpioFake.c
.
Note that we extend the class with a new variable, state
, which keeps track of the GPIO state, since we don’t have any hardware to do this for us. We also add two variables to keep track of how many times the set()
and get()
methods are called.
And now the implementation of the methods:
And our trifecta of Gpio
classes is complete! We can now use them.
Notice that functionWhichAcceptsAnyGpio()
accepts and operates on the GpioBase
interface. This function is an example of how you can write code that will work on both the real embedded hardware and a Linux testing environment. This function could be in it’s own file and not import GpioReal.h
nor GpioFake.h
. Depending on what child class is passed in, different set()
and get()
methods will be called (this is polymorphism).
This is the output from running the code:
Embedding the Base Class In The Child Class
As mentioned above, the base class has to be a named member in the child class struct (named base
in this case). This is ok, but does mean that we have to type self->base
when calling methods on the base class.
If you want to get around this, add the -fms-extension
compiler flag. Luckily this flag is supported by most compilers, including GCC (the flag stands for “Microsoft extensions”, and this embedding feature was first done by Microsoft). This will allow you to write:
To embed the base class, you write out the type, but do not give it a name. With the -fms-extension
flag, you can then access the base class methods directly on the child class instance:
You can see a full working example of this code on GitHub at gbmhunter/c-inheritance-example.
Interfacing To Imperative Code
You may be happy-as-Larry, writing all your C code in an OO style. But what happens when you want to interface with third-party (or previously written) code which is written in the standard C “imperative” style.
The example I will use is based around the PSoC family of microcontrollers. When you create a new UART for the microcontroller (via the graphical schematic editor in the PSoC Creator IDE), the PSoC libraries provide an associated set of functions to control the UART (e.g., if you had named the UART component CyUart1
, then you would be given functions such as CyUart1_Start()
, CyUart2_Read()
, e.t.c). This functions are not written in an OO-style.
The solution I propose is to create your own object-orientated UART driver, which wraps provides an interface from the imperative-style PSoC UART functions to the rest of your OO code.
Here is an complete, working example:
Footnotes
-
University of Missouri-St. Louis. C++ Reserved Words. Retrieved 2024-11-11, from https://www.umsl.edu/~lawtonb/224/oview2a.html. ↩