Skip to content

Object-Orientated C

Published On:
Feb 4, 2016
Last Updated:
Feb 4, 2016

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.

typedef struct {
double real;
double imag;
} ComplexNum;

We would then create an instance of our object with the code:

void main () {
ComplexNum myComplexNum;
myComplexNum.real = 3.4;
myComplexNum.imag = 5.7;
}

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.

double ComplexNum_GetMagnitude(ComplexNum const * obj) {
return sqrt(obj->real * obj->real + obj->imag * obj->imag);
}

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.

void ComplexNum_Init(complexNum_t * obj) {
obj->real = 0.0;
obj->imag = 0.0;
}

The downside of this is that we have to remember to call it every time we create a new complexNum_t object.

void main() {
complexNum_t myComplexNum;
// We have to remember to call the "constructor"
// If we forget, bad things could happen
ComplexNum_Init(&myComplexNum);
}

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.

A diagram showing how 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 (through special memory mapped registers).

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.

A swimlane diagram showing the difference between real hardware and a testing environment.

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:

GpioBase.h
// Forward declaration
typedef struct GpioBase GpioBase;
struct GpioBase {
void (*set)(GpioBase *self, uint8_t value);
uint8_t (*get)(GpioBase *self);
};
void GpioBase_Init(GpioBase *self);

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.

GpioBase.c
#include "GpioBase.h"
// Private function declarations
void GpioBase_set(GpioBase *self, uint8_t value);
uint8_t GpioBase_get(GpioBase *self);
void GpioBase_Init(GpioBase *self) {
self->set = GpioBase_set;
self->get = GpioBase_get;
}
void GpioBase_set(GpioBase *self, uint8_t value) {
printf("GpioBase_set() called. This should never happen!\n");
assert(0);
}
uint8_t GpioBase_get(GpioBase *self) {
printf("GpioBase_get() called. This should never happen!\n");
assert(0);
return 0;
}

We can then define the GpioReal class in GpioReal.h, which inherits from GpioBase:

GpioReal.h
#include "GpioBase.h"
typedef struct {
GpioBase base;
uint32_t pinNumber;
} GpioReal;
void GpioReal_init(GpioReal *self, uint32_t pinNumber);

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:

GpioReal.c
#include "GpioReal.h"
void GpioReal_set(GpioBase *self, uint8_t value);
uint8_t GpioReal_get(GpioBase *self);
void GpioReal_init(GpioReal *self, uint32_t pinNumber) {
// Initialize the base class
GpioBase_Init(&self->base);
// Override the set and get methods
self->base.set = GpioReal_set;
self->base.get = GpioReal_get;
// Initialize the pin number
self->pinNumber = pinNumber;
}
void GpioReal_set(GpioBase *self, uint8_t value) {
GpioReal *gpioReal = (GpioReal *)self;
printf("Real GPIO set() called. This is where you would make hardware calls.\n");
// gpio_set(gpioReal->pinNumber, value);
}
uint8_t GpioReal_get(GpioBase *self) {
GpioReal *gpioReal = (GpioReal *)self;
printf("Real GPIO get() called. This is where you would make hardware calls.\n");
// return gpio_get(gpioReal->pinNumber);
return 0;
}

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.

GpioFake.h
#include "GpioBase.h"
typedef struct {
GpioBase base;
uint8_t state;
uint32_t numSetCalls;
uint32_t numGetCalls;
} GpioFake;
void GpioFake_init(GpioFake *self);

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:

GpioFake.c
#include "GpioFake.h"
void GpioFake_set(GpioBase *self, uint8_t value);
uint8_t GpioFake_get(GpioBase *self);
void GpioFake_init(GpioFake *self) {
// Initialize the base class
GpioBase_Init(&self->base);
// Override the set and get methods
self->base.set = GpioFake_set;
self->base.get = GpioFake_get;
// Initialize the test state
self->state = 0;
self->numSetCalls = 0;
self->numGetCalls = 0;
}
void GpioFake_set(GpioBase *self, uint8_t value) {
GpioFake *gpioFake = (GpioFake *)self;
gpioFake->state = value;
gpioFake->numSetCalls++;
printf("Fake GPIO set() called. For testing, this is where you would keep track how many times the GPIO is set, when it was called, and what value.\n");
}
uint8_t GpioFake_get(GpioBase *self) {
GpioFake *gpioFake = (GpioFake *)self;
gpioFake->numGetCalls++;
printf("Fake GPIO get() called. This is where you would keep track how many times the GPIO is read, when, and you could fake return values.\n");
return gpioFake->state;
}

And our trifecta of Gpio classes is complete! We can now use them.

main.c
#include "GpioBase.h"
#include "GpioFake.h"
#include "GpioReal.h"
// This function accepts either a GpioReal or a GpioTest!
// This function does not depend on any specific GPIO nor hardware calls
// (i.e. it could be in it's own file and not import GpioReal.h or GpioTest.h)
void functionWhichAcceptsAnyGpio(GpioBase *gpio) {
// Set the GPIO
gpio->set(gpio, 1);
// Read back the GPIO
gpio->get(gpio);
}
int main() {
// Create a instance of GpioReal. 13 is the pin number.
GpioReal gpioReal;
GpioReal_init(&gpioReal, 13);
functionWhichAcceptsAnyGpio(&gpioReal.base);
// Create a instance of GpioFake
GpioFake gpioFake;
GpioFake_init(&gpioFake);
functionWhichAcceptsAnyGpio(&gpioFake.base);
return 0;
}

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:

Terminal window
gbmhunter@geoff-laptop:~/personal/c-inheritance-example/build$ ./c-inheritance-example
Real GPIO set() called. This is where you would make hardware calls.
Real GPIO get() called. This is where you would make hardware calls.
Fake GPIO set() called. For testing, this is where you would keep track how many times the GPIO is set, when it was called, and what value.
Fake GPIO get() called. This is where you would keep track how many times the GPIO is read, when, and you could fake return values.

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:

typedef struct {
GpioBase; // NOTE: No name!
uint32_t pinNumber;
} GpioReal;

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:

void GpioReal_init(GpioReal *self, uint32_t pinNumber) {
GpioBase_Init(&self->base);
self->set = GpioReal_set; // No base prefix needed!
self->get = GpioReal_get; // No base prefix needed!
self->pinNumber = pinNumber;
}

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:

#include <stdio.h>
#include <stdint.h>
//========== MOCK CYPRESS API FOR UART ================//
void CyUart1_Start() {
printf("CyUart1_Start() called.\r\n");
}
void CyUart1_Stop() {
printf("CyUart1_Stop() called.\r\n");
}
void CyUart1_Read() {
printf("CyUart1_Read() called.\r\n");
}
void CyUart1_Write() {
printf("CyUart1_Write() called.\r\n");
}
//=============== OUR UART DRIVER ================//
typedef struct {
void (* CyUart_Start)(void);
void (* CyUart_Stop)(void);
uint8_t (* CyUart_Read)(void);
void (* CyUart_Write)(uint8_t byte);
} uart_t;
void Uart_Init(uart_t * obj,
void (* CyUart_Start)(void),
void (* CyUart_Stop)(void),
uint8_t (* CyUart_Read)(void),
void (* CyUart_Write)(uint8_t byte)) {
obj->CyUart_Start = CyUart_Start;
obj->CyUart_Stop = CyUart_Stop;
obj->CyUart_Read = CyUart_Read;
obj->CyUart_Write = CyUart_Write;
}
void Uart_Start(uart_t * obj) {
obj->CyUart_Start();
}
void Uart_Stop(uart_t * obj) {
obj->CyUart_Stop();
}
uint8_t Uart_Read(uart_t * obj) {
return obj->CyUart_Read();
}
void Uart_Write(uart_t * obj, uint8_t byte) {
obj->CyUart_Write(byte);
}
#define GET_CY_UART_FUNCTIONS(prefix) \
prefix##_Start, \
prefix##_Stop, \
prefix##_Read, \
prefix##_Write
//=============== MAIN PROGRAM ================//
int main(void) {
uart_t myUart;
Uart_Init(&myUart, GET_CY_UART_FUNCTIONS(CyUart1));
Uart_Start(&myUart);
uint8_t byte = Uart_Read(&myUart);
Uart_Write(&myUart, 0x2A);
Uart_Stop(&myUart);
return 0;
}

Footnotes

  1. University of Missouri-St. Louis. C++ Reserved Words. Retrieved 2024-11-11, from https://www.umsl.edu/~lawtonb/224/oview2a.html.