Table of Contents
Overview
I’m going to try my best to describe a method of programming in which we will create a “module” in C, very similar to what a class would provide in C++. This method was taught to me and I’ve found it very helpful at allowing a user to instantiate a piece of code N number of times (like new in C++) as well as abstract the contents of that module (private: in C++). This tutorial will be referencing the code here, which is available for use, which will fill an array with constrained random numbers, and provide stats on that data set. It’s worth noting I use the terms “Handle” and “Module” interchangeably throughout.
Header File
The consumer of this module will ultimately care about the header file, as that’s what exposes/provides the functions to interact with the module. There are 2 main aspects of the header file that I’m going to go over, the first being the init parameters.
Init Parameters
Think of the init parameter structure as a requirement to use the module. You must fill it out, based on your needs, and you must provide it to the module. It basically tells the module what to do/how to operate. Below is the init parameters for this module. It’s important to comment each item in the struct so the user knows generally what to do with it, or what is expected.
typedef struct {
uint32_t numEntries; //number of entries available in the array
void *data; //pointer to array of data
int32_t upperbound; //maximum value of any given random number
int32_t lowerbound; //minimum value of any given random number
bool zeroInitialize; //indicates whether user wants array cleared out first
} h_HandleInitParams_t;
Functions
There are 2 functions that will actually setup the module (
bool h_HandleInit(void *_pHandle, h_HandleInitParams_t *pInitParams);
bool h_HandleGenerate(void *_pHandle, uint32_t numRandoms);
uint32_t h_GetNumEntries(void *_pHandle);
int32_t h_GetMinEntry(void *_pHandle);
int32_t h_GetMaxEntry(void *_pHandle);
int32_t h_GetAvg(void *_pHandle);
Source File
The source file is where we will actually implement the guts of the handle, including its handle structure, and all the functions that go with it. Let’s start with the handle structure.
Handle Structure
As you see below, there are very similar elements to the
//Primary handle for this library, hidden in the C
//file so the user isn't aware of its contents
typedef struct {
bool initialized;
uint32_t numEntries; //number of entries available in the array
void *data; //pointer to array of data
int32_t upperbound; //maximum value of any given random number
int32_t lowerbound; //minimum value of any given random number
int32_t min; //minimum generated value in the list
int32_t max; //maximum generated value in the list
int32_t avg; //average across all the values in the list
} h_HandleParams_t;
It’s important to note that the user must allocate space/memory for the handle….but how can they if they don’t know
#define HANDLE_SIZE 40
We also ensure that this define always matches the size of the structure with this in the source file:
M_CompileTimeAssert(HANDLE_SIZE==sizeof(h_HandleParams_t));
Handle Initialization
I won’t go through every function in the handle source (remember, all source is available here), but it’s important to detail what’s going on in the init function.
bool h_HandleInit(void *_pHandle, h_HandleInitParams_t *pInitParams)
{
if(_pHandle == NULL || pInitParams == NULL)
return false;
h_HandleParams_t* pHandle = (h_HandleParams_t*)_pHandle;
pHandle->data = pInitParams->data;
pHandle->numEntries = pInitParams->numEntries;
pHandle->lowerbound = pInitParams->lowerbound;
pHandle->upperbound = pInitParams->upperbound;
pHandle->min = INT32_MAX; //start off at the highest possible value to avoid false data
pHandle->max = INT32_MIN; //start off at the lowest possible value to avoid false data
pHandle->avg = 0;
return true;
}
Void *???
Yes, the function input parameter for the handle is just a plain old void *, so the user doesn’t have to know what the structure of the handle really is, it just knows the size of continuous data it needs to provide. This is called abstraction.
Handle->element = Init->Element
Most of the handle’s init function simply copies from the init parameter element into the handle element. This seems like a lot of work, but you’ll see later why this is so powerful.
Accessor Functions
For the sake of time, I’ll detail one of the accessor functions.
int32_t h_GetAvg(void *_pHandle)
{
h_HandleParams_t* pHandle = (h_HandleParams_t*)_pHandle;
return pHandle->avg;
}
It shouldn’t be surprising that the meat of this function just simply returns the average,
Testing
Memory
In this particular module/handle, we need 2 blocks of memory, one for the handles (one or more), and one for the random values the handle will generate.
#define MAX_NUM_HANDLES 10
int32_t Rvals[MAX_NUM_HANDLES][MAX_SIZE];
uint8_t HandleMem[MAX_NUM_HANDLES][HANDLE_SIZE];
Handle Init
Below is just an example of the handle initialization. With this we set the
//initialize all the handles first
for(uint32_t h = 0; h < MAX_NUM_HANDLES; h++)
{
printf("Handle Library Test\n");
h_HandleInitParams_t initParams;
void *pHandle = HandleMem[h];
initParams.data = Rvals[h];
initParams.numEntries = Range(1, MAX_SIZE);
initParams.zeroInitialize = true;
initParams.lowerbound = 12345;
initParams.upperbound = 235643;
h_HandleInit(pHandle, &initParams);
}
Usage
The first thing I do under test is to make sure the module zeroed out the data array (since I set
//run libraries
for(uint32_t h = 0; h < MAX_NUM_HANDLES; h++)
{
printf("Running Handle %d\n", h);
void *pHandle = HandleMem[h];
//validate that the library zeroed out the array entries
for (uint32_t i = 0; i < h_GetNumEntries(pHandle); i++)
{
assert(Rvals[h][i] == 0);
}
h_HandleGenerate(pHandle, h_GetNumEntries(pHandle));
for (uint32_t i = 0; i < h_GetNumEntries(pHandle); i++)
{
printf("Rval %d: is %d\n", i, Rvals[h][i]);
}
}
Why go through all the trouble?
The benefits to this methodology are:
- Abstraction – the user doesn’t need to know all the details of the module, it just knows what it needs, and can retrieve results through accessor functions (very much like a C++ class).
- Scalability –
,GetNumEntries,h_GetMinEntry,h_GetMaxEntryexist once. I mean literally, there is only one block of instruction memory for each one, no matter how many handles you make. Also, scaling to more or less handles is a matter of adding an element or removing one from the handle memory.h_GetAvg
- State Preservation – Each handle retains its state, so you can execute or call on it at any time and it inherently picks up where it left off. In this particular example that is less useful, but imagine adding a state variable and an action you pass in. The handle would update and preserve whatever state it changed to.
Conclusion
Honestly, the benefits of this methodology isn’t obvious until you start using it, and more importantly, once you start scaling up a module across N number of elements and add or update functions. Hopefully this can make its way into your style of how you write C modules; I’ve found it to be useful and use it as a go-to method of writing code.