1

I am writing a MISRA compliant code that runs on a microcontroller, and this program must be written in the C language. I would like to design my software according to the object-oriented design. However, the C-language lacks OOP support. Therefore, I decided to create "classes" and "packages" through C-files and Folders, respectively. Currently, all legacy function names were having the following names: <Module_Name>_f_<Function_Name>_<Return type>.

This function naming convention works as long as there is only a single Module. However, if I add SubModules or even SubSubModules, then the function name might get confusing. For example, having Module, Submodule, and a SubSubModule might end up in one the following function names:

  1. <Module_Name><SubModule_Name><SubSubModule_Name>_f_<Function_Name>_<Return type>

  2. <Module_Name>_<SubModule_Name>_<SubSubModule_Name>_f_<Function_Name>_<Return type>

  3. <Module_Name>_f_<SubModule_Name>_<SubSubModule_Name>_<Function_Name>_<Return type>

    ...

What would be a good name for such functions, and their respective C-files? I would like to have a naming convention that one can read and still understand the "class"/"package" structure?

To make it more clear, we can take more concrete example with the following file structure containing Folders and C-files:

Module (Folder)
  - SubModule_1 (Folder)
     - SubSubModule_1_1.c
     - SubSubModule_1_2.c
     - SubSubSubModule_1_2_1.c (Maybe also put in a seperate Sub-Folder?)
     - SubSubSubModule_1_2_2.c (Maybe also put in a seperate Sub-Folder?)
     ...

  - SubModule_n (Folder)
     - SubSubModule_n_1.c
     - SubSubModule_n_2.c
     ...

The above file structure might look like this in an OOP pseudocode:

class Module:
  begin Module;

    # Field Declarations
    SubModule_1 subModule_1_Instance;
    SubModule_2 subModule_2_Instance;
    ...
    # Function declarations
    Module_f_<Function_Name>_<return type>;
    ...

  end Module;


class SubModule_1:
  begin SubModule_1;

    # Field Declarations
    SubSubModule_1_1 subSubModule_1_1_Instance;
    SubSubModule_1_2 subSubModule_1_2_Instance;
    ...
    # Function declarations
    ModuleSubModule1_f_<Function_Name>_<return type>;
    OR 
    Module_SubModule1_f_<Function_Name>_<return type>;
    OR
    Module_f_SubModule1_f_<Function_Name>_<return type>;
    ...

  end SubModule_1;


class SubSubModule_1_1:
  begin SubSubModule_1_1;

    # Function declarations
    ModuleSubModule1SubModuleSubModule11_f_<Function_Name>_<return type>;
    OR 
    Module_SubModule1_SubModule11_f_<Function_Name>_<return type>;
    OR
    Module_f_SubModule1_SubModule11__f_<Function_Name>_<return type>;
    ...

  end SubSubModule_1_1;

So for the SubSubModule_1_1, I might end up with:

  1. ModuleSubModule1SubModuleSubModule11_f_<Function_Name>_<return type>;
  2. Module_SubModule1_SubModule11_f_<Function_Name>_<return type>;
  3. Module_f_SubModule1_SubModule11__f_<Function_Name>_<return type>;

Is there maybe a better way to name those functions? I am looking forward to Your replays/alternatives. Thank you in advance.

SimpleThings
  • 147
  • 2
  • 12
  • 3
    If you're writing code for a microcontroller then you you probably need to pay some attention to keeping code complexity under control. Do you really think you need a three- or four-level deep hierarchy? I would be shooting for *one*. – John Bollinger Jul 01 '20 at 21:38
  • I'd recommend that module and sub modules names have a length limit, say 6, thus `Graphx_Line_Draw()`. I see little value to encoding the `f` nor the _return type_. BTW, how do you code return type `unsigned long long` or `struct abc`? – chux - Reinstate Monica Jul 01 '20 at 21:42
  • @Reinstate Monica The reason for encoding `f` is to differentiate between variable and function names. For example, a global variable could be called `_g__`. Similarly, for the return types, it is mandatory to have it contained in a function name. I am allowed to only use platform-specific data types (MISRA): `uint8`, `uint16`, `uint32`. This is to avoid having function return values being assigned to the mismatching data types. Imagine, assigning 16 bit return value which represents an engine speed to a 8 bit value. You will get a garbage value. – SimpleThings Jul 01 '20 at 22:15
  • @John Bollinger Could you please tell me why exactly you think that the code complexity for the microcontroller code should be less? If you are concerned with the uCs resources, then no need to worry, because it is a powerful 32-bit uC. The question is more how to make code more structured and readable. Currently, the legacy code has 3-level hierarchy, i.e. Module Name (Main folder) together with various SubFolders and their appropriate C/header files within them. – SimpleThings Jul 01 '20 at 22:22
  • 1
    @SimpleThings I already understand the _reason_ for [Hungarian like notation](https://en.wikipedia.org/wiki/Hungarian_notation). I disagree to its value/usefulness vs. its cost. A good compiler will warn about narrowing assignments (the trees) so I don't need naming to help me there. I find the very verbose names take away from the higher level understanding (the forest), something the coding teams needs. IAC, code to your group's coding standards - just hope the naming convention does not get carried away. – chux - Reinstate Monica Jul 01 '20 at 22:36
  • @SimpleThings Why you need OOPS concept in C ? In my opinion do structured programming in C. Otherwise you may end up inviting new bugs and wasting valuable time in fixing it. SimpleThings make the life simple : ) – Babajan Jul 02 '20 at 05:18
  • @SimpleThings It isn't very meaningful to discuss the number of required abstraction layers without a real world use-case. More abstraction layers doesn't automatically mean more structured/readable, it could as well mean the opposite. So nobody here can really tell if you need 3-4 layers or not, but it's indeed quite rare. – Lundin Jul 02 '20 at 07:06
  • @Reinstate Monica I agree with all points you mentioned. The compiler shall issue a warning about narrowing assignments. However, imagine this situation `typedef unsigned char uint2_t; // 2-bit type typedef unsigned char uint4_t; // 4-bit type uint2_t twoBitVar; uint4_t fourBitVar; twoBitVar = fourBitVar;` WIll most of the compilers actually warn about this assignment? After all, for both variables the underlying types are `unsigned char`. – SimpleThings Jul 02 '20 at 20:59
  • @Babajan I want to make code more readable and understandable. Currently, there is only 1 level of abstraction. So if I am reading a function name which belongs to one of the submodules, and this function is called in the `Module`, then from the function name I have no idea to which `SubModule` it belongs. The legacy code has a lot of submodules. – SimpleThings Jul 02 '20 at 21:06
  • @Babajan You are right, I should avoid overengineering and thus introducing new troubles. I think I will use the naming of functions `_f__`. Anything with more hierarchy (Submodules) might be an overkill. – SimpleThings Jul 02 '20 at 21:10
  • You could also go a similar way as in AUTOSAR. (e.g. AUTOSAR_SWS_BSWGeneral.pdf). And btw. even AUTOSAR actually stays away from function/variable prefix/infix/suffixes to differ between variables or functions. But they do use like _u8 or _s8 mnemonics on function names for different argument/return types of the same ("template-like") function. And they suggest only the component prefix, without going too deep in hierarchy, plus an optional vendor/api-infix in case of multiple instances of different driver implementations .. e.g. CanTrcv_1_TJA1043 vs CanTrcv_1_TJA1145 instead of CanTrcv – kesselhaus Jul 04 '20 at 14:29

2 Answers2

4

Sticking to an OO design is almost always a good idea, but you need to boil down OO to the things that matter. Namely:

  • Autonomous objects that only know of their designated purpose and know nothing about unrelated things.

    For example in an embedded system, your SPI driver shouldn't and needn't know anything about the LCD you are using, even though you are communicating with the LCD through SPI.

  • Private encapsulation that hides information away to reduce complexity, tight coupling and namespace collisions.

  • In some cases, inheritance.

    For example if you are writing a portable HAL that should function the same no matter the underlying microcontroller hardware. (Like for example a SPI driver.)

All of the above OO can be achieved in C and the language directly or indirectly has language support for it. There's misc other concepts like "RAII", which are handy but not necessary. Unfortunately we can't get automatically called constructors/destructors in C, so we have to live with calling them explicitly.

The main thing to concider when doing OO in C (and other languages) is to do it on a file level. The header file should contain the public interface - everything that the caller needs to know, that you would normally have declared public in a language with keyword support. Each header file contains a corresponding .c file containing the private implementation details.

It's a good idea to have a strict naming policy like in your examples, so that the caller knows where a certain function belongs. The functions belonging to the SPI driver spi.h should be named spi_init, spi_transceive and so on, with the source code prefix first.

Not sure if I like the SubSubModule idea though, seems a bit burdensome. Also, in an embedded system there should be just so many cases where you actually need inheritance, it is a bit of a rare beast rather than the main attraction in most programs. Often it can rather be a sign of poor design and over-engineering with far too many abstraction layers. It's also important to never let your inheritance API be set in stone. Don't hesitate to change it later on, when you discover new requirements that weren't considered during the initial design.

Regarding private encapsulation, C supports that through the static keyword. Functions declared static in the .c file are truly private and can't be accessed from other files. It doesn't work quite as well for variables though. You can use static file scope variables as a "poor man's private", that's in fact how it is done most of the time in embedded systems. static variables have some limitations though: they force the object to become a "singleton pattern" with only one instance possible. Which is fine if you only need one instance of the SPI driver, but what if the MCU comes with 5 different SPI peripherals, all behaving identically?

As a side note, static variables aren't thread-safe in larger, multi-process/multi-thread programs. Could become relevant in case of RTOS.

It is however possible to take OO one step further in C, by using the concept known as opaque type / opaque pointers. Examples. This allows you to create multi-instance classes, fully encapsulated, or optionally with some public parts. It can be used to model inheritance and polymorphism, by letting the first object of the inherited class contain a struct instance of its parent. Function pointers enable "virtual" inherited functions, where calling a function through a base class pointer invokes the corresponding function in the caller.

An object declared as opaque through pointers to incomplete type cannot be allocated by the caller, they can only declare pointers to them. From the caller's perspective they work essentially just the same as abstract base classes in C++. You will have to encapsulate the object allocation inside the init function (constructor). This is a bit of a disadvantage in low-end embedded systems, since sanity demands that we don't use malloc there. Instead memory allocation will have to be done through a fixed maximum size static memory pool. Examples: Static allocation of opaque data types

From a MISRA-C perspective, they actually encourage the use of opaque type since MISRA-C:2012 (Dir 4.8).

Do not over-use opaque type though. It makes perfect sense for things like HAL on top of drivers, portable code, protocol handling etc. But not so much for hiding away non-portable, application-specific logic, which doesn't benefit from abstraction layers since you won't be able to re-use or port it anyway.

Overall, program design is highly qualified work. It takes lots of experience to get it done properly. Add too much abstraction and you end up in over-engineered, meta-programming hell. Add too little and you end up in spaghetti-programming, tight-coupling hell.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • Regarding using `static` keyword for "private"/C-File variables, why is it considered as "poor man's private"? `Opaque` pointers seem like a good idea. I would have to ask my colleagues if we are actually allowed to use `malloc`. Is there a 3rd alternative to declare a variable truely private? The application I am working on is safety-critical, it controls an automotive powertrain. Therefore, some of the `SubModule` are handling transmission, clutch, brake pedal signals. Thus, I was looking for a good function naming convention to indicate to which `SubModule` a function belongs to. – SimpleThings Jul 03 '20 at 19:43
  • @SimpleThings Mainly because it is single instance. You _could_ create multiple instances of the driver by using arrays or passing along a reference to the hardware peripheral to every function - that's actually quite common practice. Regarding malloc, forget about it, it's banned in _all_ embedded systems but particularly in MISRA-C and safety-critical ones. If I have to tell you that you can't use it you shouldn't be coding powertrain programs. Use static memory pools as in the link I gave you. For a safety-critical system you will have a fixed number of instances determined at compile time. – Lundin Jul 03 '20 at 19:48
  • The comment about asking the team was more of to show politness and respect for your comment which seemed to me as suggesting using `malloc`. Moreover, you put the effort into your answers. I was definitely never intdenting to use `malloc` and thus pointers at all. I do not want to be a responsible person when a segmentation fault happens, or heap overruns the stack =). That is why I wanted to have a good naming convention. While you answers are very informative and useful, they did not touch the main topic of my question, and that is the function naming convention. – SimpleThings Jul 03 '20 at 20:59
  • @SimpleThings You should name the functions with a prefix so that the caller sees in which module they belong. How to do that in the smoothest way possible, without creating very long identifiers, isn't easy to say without a specific case in mind. Generally though, it is probably enough to name them after the base class. Very common case: you have a HAL, spi.h, you'll have a function such as `spi_init`, and it is made "pure virtual" - meaning no definition exists in spi.c and the implementation is left entirely to the inherited class. So the inherited class will use `spi_`prefix too. – Lundin Jul 04 '20 at 11:05
  • What you describe is actually the current state in the legacy code. So instead of `spi` let me call my base class `controller`. The current function names are called as `controller_f__...` The `controller` does not calculate any algorithm but periodically calls functions of other `SubModules` such as `transmission`, `clutch` and `brake` pedal, etc. Currently, just looking from the function name, it is really hard to know to which `SubModule` it belongs. E.g. function called `controller_f_readTemp` does not tell me anything of `SubModule`. Thus, I wanted to include this info. – SimpleThings Jul 04 '20 at 21:33
  • @SimpleThings It's kind of the point that the caller code calls `spi_rec` instead of `mcuxyz_spi_rec`, so that the caller code turns portable too, when possible. With modern IDEs, finding declarations is easy - so the naming is not mainly to locate the identifier, but for the benefit of caller code readability: you'll have `adc_read(buf); spi_rec(buf);` etc rather than `read(buf); rec(buf)` where you even can't tell which function that reads from the ADC and which function that reads from SPI. – Lundin Jul 06 '20 at 06:20
  • I might have used a confusing name. The `controller` does not refer to the actual MCU, but rather to software module that I am responsible for. So if inside the `controller` I call two functions belonging to other files (`adc.h`, `spi.h`): `controller_f_read(buf)` and `controller_f_put(buf)`, then just by reading their names, I do not know which of them belongs to either `adc` or `spi`. If I write `controller_adc_read(buf)`, then it is clear. Additionally, I wanted to have `controller` in the prefix of a name, to show to a reader that this function belongs to my specific module. – SimpleThings Jul 08 '20 at 09:51
1

The concept missing from this discussion is the "this" pointer to have instance-specific data. It's implicit in C++, but must be explicit in C.

For example, in a hypothetical module NSMotionController.c:

typedef struct NSMotionControllerStruct {
   float speed__m_s;
} NSMotionController_t;

float NSMotionController_SpeedGet__m_s(NSMotionController_t const * const this) {
     return this->speed__m_s;
}
bool NSMotionController_Initialize(NSMotionController_t * const this, float const speedCurrent__m_s) {
     this->speed__m_s = speedCurrent__m_s;
     return true;
}

We can use this like so:

int main(int argc, char ** argv) {
    NSMotionController_t motionControllerInstance1;
    NSMotionController_Initialize(motionControllerInstance1, 1.0f);
    NSMotionController_t motionControllerInstance2;
    NSMotionController_Initialize(motionControllerInstance1, 2.0f);
    printf("speed1: %.1f\n", NSMotionController_SpeedGet__m_s(&motionControllerInstance1));
    printf("speed2: %.1f\n", NSMotionController_SpeedGet__m_s(&motionControllerInstance2));
}

As far as naming, I use a two-letter namespace ("NS" above) since C doesn't support namespaces idiomatically. I use the module name, then an underscore to start the method name. I use two underscores to separate a units suffix ("__m_s" above indicates "meters per second").

For polymorphism, you can use function pointers. So, augmenting our example with function pointers:

typedef float (*NSMotionControllerInterface_SpeedGet__m_s_t)(void const * const this);

typedef struct NSMotionControllerStruct {
    NSMotionControllerInterface_SpeedGet__m_s_t SpeedGet__m_s;
    float speed__m_s;
} NSMotionController_t;

float NSMotionController_SpeedGet__m_s(void const * const this) {
    NSMotionController_t const * const motionThis = (NSMotionController_t const *) this;
    return motionThis->speed__m_s;
}
bool NSMotionController_Initialize(NSMotionController_t * const this, float const speedCurrent__m_s) {
    this->SpeedGet__m_s = NSMotionController_SpeedGet__m_s;
    this->speed__m_s = speedCurrent__m_s;
    return true;
}

int main(int argc, char ** argv) {
    NSMotionController_t motionControllerInstance1;
    NSMotionController_Initialize(motionControllerInstance1, 1.0f);
    NSMotionController_t motionControllerInstance2;
    NSMotionController_Initialize(motionControllerInstance1, 2.0f);
    printf("speed1: %.1f\n", motionControllerInstance1.SpeedGet__m_s(&motionControllerInstance1));
    printf("speed2: %.1f\n", motionControllerInstance2.SpeedGet__m_s(&motionControllerInstance2));
}

Rather than using polymorphism on a single function, though, you can gather them up in a struct and pass that to other modules.

typedef float (*NSMotionControllerInterface_SpeedGet__m_s_t)(void const * const this);
typedef bool (*NSMotionControllerInterface_SpeedSet__m_s_t)(void const * const this, float const speedNew__m_s);
typedef struct NSMotionControllerInterfaceStruct {
    NSMotionControllerInterface_SpeedGet__m_s_t SpeedGet__m_s;
    NSMotionControllerInterface_SpeedSet__m_s_t SpeedSet__m_s;
} NSMotionControllerInterface_t;

typedef struct NSMotionControllerStruct {
    NSMotionControllerInterface_t interface;
    float speed__m_s;
} NSMotionController_t;

float NSMotionController_SpeedGet__m_s(void const * const this) {
    NSMotionController_t const * const motionThis = (NSMotionController_t const *) this;
    return motionThis->speed__m_s;
}

bool NSMotionController_SpeedSet__m_s(void const * const this, float const speedNew__m_s) {
    NSMotionController_t const * const motionThis = (NSMotionController_t const *) this;
    motionThis->speed__m_s = speedNew__m_s;
    return true;
}

bool NSMotionController_Initialize(NSMotionController_t * const this, float const speedCurrent__m_s) {
    this->interface.SpeedGet__m_s = NSMotionController_SpeedGet__m_s;
    this->interface.SpeedSet__m_s = NSMotionController_SpeedSet__m_s;
    this->speed__m_s = speedCurrent__m_s;
    return true;
}

int main(int argc, char ** argv) {
    NSMotionController_t motionControllerInstance1;
    NSMotionController_Initialize(motionControllerInstance1, 1.0f);
    NSMotionController_t motionControllerInstance2;
    NSMotionController_Initialize(motionControllerInstance1, 2.0f);

    NSMotionControllerInterface_t * const interface1 = motionControllerInstance1.interface;
    NSMotionControllerInterface_t * const interface2 = motionControllerInstance2.interface;
    printf("speed1: %.1f\n", interface1->SpeedGet__m_s(&interface1));
    printf("speed2: %.1f\n", interface2->SpeedGet__m_s(&interface2));

    interface1->SpeedSet__m_s(&interface1, 5.0f);
    printf("speed1 (faster): %.1f\n", interface1->SpeedGet__m_s(&interface1));

    /* Example of passing abstract interface */
    NSGroundControl_t groundControl;
    NSGroundControl_Initialize(&groundControl, interface1);
}

In short, never use statics when you can avoid it. This will also help unit testing, which I imagine is next (or hopefully first) if you're working in a MISRA environment.

Scott S
  • 341
  • 4
  • 11
  • I like your function naming convetion and the idea of using "this" pointer. However, I might not benefit from polymorphism. I just realized that the word `Module` I used, is maybe confusing. I think the `Module` maybe corresponds to your `namespace`. Because my `Module` does not calculate any algorithm. `Module` is basically called from a periodic RTOS task, and in turn `Module` calls other `SubModules` such as `transmission`, `clutch pedal`, `brake pedal`, etc. Maybe `Module_Trans_f_readTemp_u16` would be a good name for a transmission submodule function. Unit testing is mandatory. – SimpleThings Jul 03 '20 at 20:02
  • Whelp, if you're unit testing you're likely to need polymorphism for mocking. Though I'm using gtest which is C++-based, so mocks are C++ OO, so I write my own mocks for hardware (e.g., serial-connected peripherals, ADCs). There may be a better C-based unit test framework with better support for C mocks. – Scott S Jul 06 '20 at 17:25
  • Hey, to do unit testing the TESSY tool has been utilized. And so far it has worked OK for the C code. – SimpleThings Jul 08 '20 at 09:34
  • @SimpleThings How useful is TESSY's mock support? – Scott S Jul 20 '20 at 22:59
  • I am not maybe the best person to answer this question, as I have never used the tool myself. I really do not know about how good its mock support is. I am only specifying the test specification for my functions to the testing team. However, the good thing is that the tool tells you how much code and branch coverage did your test cases cover. And it can give you hints to increase the coverage. – SimpleThings Jul 22 '20 at 20:00
  • @SimpleThings Nice! Good to know. – Scott S Jul 24 '20 at 16:30