Achieving Template-like Behavior in C

Achieving Template-like Behavior in C

Leveraging C's preprocessor to achieve template-like behavior found in modern programming languages

Templates are a key feature in C++ that enable generic programming, making it possible to write flexible and reusable code. But what if you’re working in C, which doesn’t natively support templates?

In this quick tutorial, I’ll demonstrate how to use C’s preprocessor to imitate the behavior of C++’s and other modern programming languages templates. By leveraging macros and other preprocessor features, we’ll explore how to achieve a similar level of flexibility while working within C’s constraints.

Background and Motivation

A few years ago, early in my career, I applied for a role that required advanced C programming for highly efficient software. The team believed pure C was essential for their targets. As part of the application process, I was tasked with coding "generic" data structures. Taking the term "generic" literally, I created a template-like data structure using C’s preprocessor features.

Outline

  • Quick Review on C's Preprocessor Features

  • Queue as an Example for Templated Data Structure

  • Implementation

  • Testing the Template

  • Limitations and Final Thoughts

  • References

Quick Review on C's Preprocessor Features

First, quick refresher for C’s preprocessor and the Hash directives.

Macro Like Behavior (Simple Text Substitution or Function like behavior)

#define SQUARE(x) ((x) * (x))
int result = SQUARE(4); // Expands to ((4) * (4)) => result = 16

Stringification (Making it String “ #X “)

Sometimes, you may need to convert a macro argument into a string constant. Macro parameters are not replaced inside string constants by default, but you can achieve this using the # preprocessing operator. When a macro parameter is preceded by #, the preprocessor replaces it with the literal text of the argument, converting it into a string constant. Unlike regular parameter replacement, the argument is not macro-expanded before this conversion. This process is known as stringification.

#define STRINGIFY(x) #x
printf(STRINGIFY(Hello World)); // Output: "Hello World"

Concatenation (##)

Used to merge two tokens into one while expanding macros

#define CONCAT(a, b) a##b
int CONCAT(my, Var) = 42; // Expands to int myVar = 42;

C’s preprocessor offers many other features, such as file inclusion, conditionals, and more. However, the examples provided above are sufficient to serve the purpose while keeping this tutorial concise and easy to follow.

Queue as an Example for Templated Data Structure

The “C Template“ we are creating is for the famous data structure “Queue”.
Since Queue is a First In First Out (FIFO) buffer, the following is customary to define.

  • Definitions of the Queue type it self

  • Critical Section (Enter and Exit) (Optional)

  • EraseHard()

  • EraseSoft()

  • IsEmpty()

  • IsFull()

  • Enqueue()

  • Dequeue()

  • Peek()

Just like C++ templates, we will define the whole data structure in a header file. but I favored a different approach.

  • Defined the module with a concrete type in a .c file.

  • Hash-defined each method individually.

  • Defined the interfaces for the module.

  • Created the necessary macros to enable template functionality.

  • Included the .c file in a header file for ease of use and to maintain convention.

    • (Alternatively, the implementation can be placed directly in a .h file if preferred.)

Explanation of the archive view (file organization)

  • GCQueue = Generic Circular Queue

  • GCQueue_Cfg.h: holds the configurations of the template, Size, Overwrite when full or not, …. etcetera

  • GCQueue_Core.h: basically has one line “#include “GCQueue_Core.c“

  • GCQueue_Interface.h: Holds Queue’s Definitions?Constructor and the interfaces

Implementation

First lets “hash define” the queue’s type, you will notice that the expansion will occur on 2 steps and that’s due to how C’s preprocessor Macro expansion works. when you need a macro argument to be fully expanded before being used, you use an intermediate macro to achieve two-step expansion specially that we need the concatenation, scroll code to the right, to take place first then the second macro expansion.

Please notice the ‘\‘ used all over the type definition, ‘\‘ is used for multiline hash definitions

/*
 * Template to define queue_q_header + queue buffer with types varies between
 * signed/unsigned 8, 16, 32 and 64 bits variables
 */
#define DEFINE_GCQUEUE(TYPE)                    DEFINE_GCQUEUE_ABSTRACTOR(TYPE)

/*
 * This is a concatenation helper yet serve abstraction well, hence i changed the name
 */
#define DEFINE_GCQUEUE_ABSTRACTOR(TYPE)                                                                             \
typedef struct{uint32_t q_head; uint32_t q_tail; TYPE* data_buffer_ptr; const uint32_t q_capacity; uint32_t q_size; GCQ_Status_t gcq_status;}GCQ_##TYPE##_t;\
    PRIVATE_FUNCTIONS()                                                                                             \
    GCQUEUE_ERASEHARD(TYPE)                                                                                         \
    GCQUEUE_ERASESOFT(TYPE)                                                                                         \
    GCQUEUE_ISFULL(TYPE)                                                                                         \
    GCQUEUE_ISEMPTY(TYPE)                                                                                         \
    GCQUEUE_ENQUEUE(TYPE)                                                                                         \
    GCQUEUE_DEQUEUE(TYPE)                                                                                         \
    GCQUEUE_PEEK(TYPE)                                                                                             \

Second step is define the queue functions AND hash define it of course :).
Notice the GCQUEUE_ERASEHARD_CONC_HELPER to allow the expansion to occur on 2 steps again, which is an intermediary macro to help doing the concatenation first after first iteration of the expansion.

The same method will be used with other functions the same way, for the full code I have added the GitHub repository in the references section.

*
 * Hard erase
 */
#define GCQUEUE_ERASEHARD(TYPE)                    GCQUEUE_ERASEHARD_CONC_HELPER(TYPE)
#define GCQUEUE_ERASEHARD_CONC_HELPER(TYPE)                                                                         \
PUBLIC GCQ_Status_t GCQueue_##TYPE##_Hard_Erase(volatile GCQ_##TYPE##_t* const self);                             \
PUBLIC GCQ_Status_t GCQueue_##TYPE##_Hard_Erase(volatile GCQ_##TYPE##_t* const self)                             \
{                                                                                                                \
    GCQ_Status_t gcq_status = GCQ_ERROR_NUM;                                                                     \
                                                                                                                 \
    if((NULL != self) && (NULL != self->data_buffer_ptr))                                                        \
    {                                                                                                            \
        Enter_CriticalSection();                                                                                 \
        memset(self->data_buffer_ptr, QUEUE_ERASE_VALUE, (self->q_capacity) * (sizeof(self->data_buffer_ptr[0])));\
        self->q_tail = 0;                                                                                          \
        self->q_head = 0;                                                                                        \
        self->q_size = 0;                                                                                          \
        Exit_CriticalSection();                                                                                 \
        gcq_status = GCQ_OK;                                                                                     \
    }                                                                                                            \
    else                                                                                                         \
    {                                                                                                            \
        gcq_status = GCQ_DATA_BUFFER_NULL;                                                                       \
    }                                                                                                            \
    return gcq_status;                                                                                           \
}                                                                                                                \

Third, we need to create MACROS to instantiate the Queue with the desired TYPE

/* Pre -requisite: calling DEFINE_GCQUEUE() with the same type used for OBJECT_ADDRESS creation
 * Template to instantiate queue_q_header + queue buffer with types varies between
 * signed/unsigned 8, 16, 32 and 64 bits variables
 */
#define INSTANTIATE_GCQUEUE(TYPE, NAME, BUFFER_SIZE_IN_TYPE_SIZE)                                                 \
                INSTANTIATE_GCQUEUE_ABSTRACTOR(TYPE, NAME, BUFFER_SIZE_IN_TYPE_SIZE)

/*the abstractor is used to hide the instantiation implementation from the user,
 * not used as a concatenation helper
 */
#define INSTANTIATE_GCQUEUE_ABSTRACTOR(TYPE, NAME, BUFFER_SIZE_IN_TYPE_SIZE)                                     \
PRIVATE TYPE NAME##_data_buffer[BUFFER_SIZE_IN_TYPE_SIZE];                                                          \
PRIVATE volatile GCQ_##TYPE##_t NAME = {                                                                         \
    .q_head = 0,                                                                                                 \
    .q_tail = 0,                                                                                                 \
    .data_buffer_ptr = NAME##_data_buffer,                                                                         \
    .q_capacity = BUFFER_SIZE_IN_TYPE_SIZE,                                                                         \
    .q_size = 0,                                                                                                 \
    .gcq_status = GCQ_ERROR_NUM,                                                                                 \
};

Fourth, function calls

/*
 * APIs Abstractors
 */
#define GCQ_HARD_ERASE_ABSTRACTOR(TYPE, OBJECT_ADDRESS)                    GCQueue_##TYPE##_Hard_Erase(OBJECT_ADDRESS)
#define GCQ_SOFT_ERASE_ABSTRACTOR(TYPE, OBJECT_ADDRESS)                    GCQueue_##TYPE##_Soft_Erase(OBJECT_ADDRESS)
#define GCQ_IS_FULL_ABSTRACTOR(TYPE, OBJECT_ADDRESS)                     GCQueue_##TYPE##_IsFull(OBJECT_ADDRESS)
#define GCQ_IS_EMPTY_ABSTRACTOR(TYPE, OBJECT_ADDRESS)                     GCQueue_##TYPE##_IsEmpty(OBJECT_ADDRESS)
#define GCQ_ENQUEUE_ABSTRACTOR(TYPE, OBJECT_ADDRESS, DATA_ADDRESS)        GCQueue_##TYPE##_Enqueue(OBJECT_ADDRESS, DATA_ADDRESS)
#define GCQ_DEQUEUE_ABSTRACTOR(TYPE, OBJECT_ADDRESS, DATA_ADDRESS)        GCQueue_##TYPE##_Dequeue(OBJECT_ADDRESS, DATA_ADDRESS)
#define GCQ_PEEK_ABSTRACTOR(TYPE, OBJECT_ADDRESS, DATA_ADDRESS)            GCQueue_##TYPE##_Peek(OBJECT_ADDRESS, DATA_ADDRESS)

Finally, creating the interface that the user is going to use (GCQueue_Interface.h). Since this file is detected to share the Interfaces, I needed to create 2 types to interface with the Queue.

First Interface CREATE_GCQUEUE() to define the type and second to instantiate the queue itself

Second interface(s) resembles the API calls (function invocation), where you can feed it the type and name, needless to say the type and name needs to match the ones fed into CREATE_GCQUEUE(TYPE, NAME, BUFFER_SIZE_IN_TYPE_SIZE)

// File GCQueue_Interface.h
typedef enum{
    GCQ_EMPTY = 0,
    GCQ_FULL = 1,
    GCQ_OK = 2,
    GCQ_DATA_BUFFER_NULL = 3,
    GCQ_ENQUEUE_DATA_ADDRESS_NULL = 4,
    GCQ_DEQUEUE_DATA_ADDRESS_NULL = 5,
    GCQ_PEEK_DATA_ADDRESS_NULL = 6,
    GCQ_ERROR_NUM
}GCQ_Status_t;

/*
 * Queue Allocator
 */
#define CREATE_GCQUEUE(TYPE, NAME, BUFFER_SIZE_IN_TYPE_SIZE)                    \
        DEFINE_GCQUEUE(TYPE)                                                    \
        INSTANTIATE_GCQUEUE(TYPE, NAME, BUFFER_SIZE_IN_TYPE_SIZE)                \
/*
 * APIs
 */
#define GCQ_HARD_ERASE_API(TYPE, OBJECT_ADD)             GCQ_HARD_ERASE_ABSTRACTOR(TYPE, OBJECT_ADD)
#define GCQ_SOFT_ERASE_API(TYPE, OBJECT_ADD)             GCQ_SOFT_ERASE_ABSTRACTOR(TYPE, OBJECT_ADD)
#define GCQ_IS_FULL_API(TYPE, OBJECT_ADD)                GCQ_IS_FULL_ABSTRACTOR(TYPE, OBJECT_ADD)
#define GCQ_IS_EMPTY_API(TYPE, OBJECT_ADD)                GCQ_IS_EMPTY_ABSTRACTOR(TYPE, OBJECT_ADD)
#define GCQ_ENQUEUE_API(TYPE, OBJECT_ADD, DATA_ADD)      GCQ_ENQUEUE_ABSTRACTOR(TYPE, OBJECT_ADD, DATA_ADD)
#define GCQ_DEQUEUE_API(TYPE, OBJECT_ADD, DATA_ADD)      GCQ_DEQUEUE_ABSTRACTOR(TYPE, OBJECT_ADD, DATA_ADD)
#define GCQ_PEEK_API(TYPE, OBJECT_ADD, DATA_ADD)          GCQ_PEEK_ABSTRACTOR(TYPE, OBJECT_ADD, DATA_ADD)

Testing the Template

Testing can no longer be easy, you just need to do the following

  • Decide which TYPE your template will define
// File TestRunner.h
#define UTEST_QUEUE_BUFFER_SIZE                1
#define UTEST_TYPE_BEING_TESTED             int32_t
  • Create Instance with the desired Type of the template
// File TestRunner.h
CREATE_GCQUEUE(UTEST_TYPE_BEING_TESTED, gcqueue_utest, UTEST_QUEUE_BUFFER_SIZE);
  • Create your test, the tests shows the template in action, full tests can be checked
// File TestRunner.h
/*
 * Goal: 1- check whether the Hard erase is done in a proper way
 *
 * //Scenario_0
 * Arrange:
 * Act: call EraseHard_GCQueue(&gcqueue_utest_data_buffer)
 * Assert:
 * - gcq_status = Q_OK
 * - q_head = 0
 * - q_tail  = 0
 * - data_buffer_ptr[0 -> QUEUE_BUFFER_SIZE -1 ] = QUEUE_ERASE_VALUE
 *
 * //Scenario_1
 * Arrange:
 * Act: EraseHard_GCQueue(NULL)
 * Assert:
 * - gcq_status = GCQ_NULL_BUFFER
 *
 * //Scenario_2
 * Arrange: gcq_q_header_utest.data_buffer_ptr = NULL
 * Act:
 * Assert:
 * - TEST_ASSERT_NULL(gcq_q_header_utest.data_buffer_ptr)
 * - gcq_status = GCQ_NULL_BUFFER
 *
 */
TEST_F(GCQueueTest, TestHardEraseOfTheGCQueue)
{
    uint16_t iterator;
    GCQ_Status_t gcq_status;
/*
 * Test happy scenario
 */
    //Secenrio_0
    gcq_status = GCQ_HARD_ERASE_API(UTEST_TYPE_BEING_TESTED, &gcqueue_utest);
    ASSERT_EQ(GCQ_OK, gcq_status);
    ASSERT_EQ(gcqueue_utest.q_head, 0UL);
    ASSERT_EQ(gcqueue_utest.q_tail, 0UL);
    ASSERT_EQ(gcqueue_utest.q_size, 0UL);
    for(iterator = 0; iterator < UTEST_QUEUE_BUFFER_SIZE; iterator++)
    {
        ASSERT_EQ((UTEST_TYPE_BEING_TESTED)QUEUE_ERASE_VALUE, gcqueue_utest.data_buffer_ptr[iterator]);
    }
/*
 * Test sad scenario
 */
    //Scenario_1
    gcq_status = GCQ_HARD_ERASE_API(UTEST_TYPE_BEING_TESTED, NULL);
    ASSERT_EQ(GCQ_DATA_BUFFER_NULL, gcq_status);
    //Scenario_2
    gcqueue_utest.data_buffer_ptr = NULL;
    gcq_status = GCQ_HARD_ERASE_API(UTEST_TYPE_BEING_TESTED,NULL);
    ASSERT_EQ(GCQ_DATA_BUFFER_NULL, gcq_status);
}

Limitations and Final Thoughts

Never heard of a perfect solution, if you did please mention it in the comments section :D, this C style template is no exception, the following are some shortcomings:

  • Although it is easy to use, seeing an upper cased function call is not something I would like to see a lot in my code

  • Wherever the header is included, the whole implementation is added to the user’s file

  • No type checking hence less safe than in C++, you have to unit test all the bounds of all the TYPEs you might instantiate with the template

  • Debugging such code let’s say is not fun, hence emphasis on unit testing ;)

References

For the full code:
https://github.com/Samadony/Queue_Template_in_C

For More about Macro expansion, check GNUs documentation:
https://gcc.gnu.org/onlinedocs/gcc-4.8.5/cpp/Macros.html#Macros

I am using google test harness for testing, its reference:
https://google.github.io/googletest/