3

This is a subjective question, so I will accept 'there is no answer' but read fully as this is specifically on a system where the code is safety critical.

I've adopted some embedded C code for a safety critical system, where the original author has (in random places) used syntax like this:

#include <stdio.h>

typedef struct tag_s2 {
    int a;
}s2;

typedef struct tag_s1 {
  s2 a;
}s1;

s1 inst;

#define X_myvar inst.a.a

int main(int argc, char **argv)
{
   X_myvar = 10;
   printf("myvar = %d\n", X_myvar + 1);
   return 0;
}

Effectively using a #define to alias and obscure a deep structure member. Mostly two or three, but occasionally four deep. BTW: This is a simple example, the real code is far more complicated but I can't publish any part of that here.

The use of this is not consistent, in some places the aliased variable is used directly other by it's alias, some parts of code are not aliased.

IMO this is bad practice as it obscures the code with no gain reducing maintainability and readability, leading to future errors and misunderstanding.

If the style was 100% consistent then perhaps I would be more happy with it.

However, being safety critical a change is costly. So not wanting to fix 'wot aint broke' I am open to other arguments.

Should I fix it or leave well alone?

Is there any guidance (e.g. Generic C, MISRA or DO178B style guides) that would have an opinion on this?

4
  • Is it really called X_myvar or is there a more descriptive name?
    – Schwern
    Commented Apr 28, 2019 at 18:59
  • To be honest in many places the new name is (IMO) not very descriptive and looks too similar to other constants or global variables (of which the are also too many). So far as I can see the only reason for this was to save typing, which is not a good reason. I can't give specific examples. But I could suggest that the original name is far more descriptive of what it's doing.
    – Jay M
    Commented Apr 28, 2019 at 21:44
  • TBH if this is software that even remotely touches passenger safety, you should raise the red flag with your management now. This code is proven in use, but it looks like it is a proven-in-use mess and I don't see how you could rewrite or extend the system starting with source code transformation by hand. Be aware that you could go to jail if you ignore base practises. Commented Apr 29, 2019 at 19:43
  • @Vroomfondel Don't worry. This is not an aircraft/train/car application (I'm under NDA so I can't say who or what it's for). I am fully aware of the legal implications of writing poor/untested code (even if I did not write it). That is why I raised this question. Otherwise it's just my opinion vs the original author. The company that own the code have been informed of the issue.
    – Jay M
    Commented Apr 30, 2019 at 7:58

3 Answers 3

3

Yeah you should get rid of it. Obscure macros are dangerous.

It was common in older C to avoid spelling out deep nesting to do things like

#define a inst.a

In which case you only had to type inst.a instead of inst.a.a. Although this is questionable practice, macros like these were used to repair a shortcoming in the language, namely the lack of anonymous structs. Modern C supports that from C11 though. We can use anonymous structures to get rid of unnecessarily nested structs:

typedef struct {
  struct
  { 
    int a;
  };
}s1;

But MISRA-C:2012 doesn't support C11 so that might not be an option.

Another trick you can use to get rid of long names is something like this:

int* x = &longstructname.somemember.anotherstruct.x; 
// use *x from here on

x is a local variable with limited scope. That's much more readable than the obscure macros and it gets optimized away in the machine code.

2

However, being safety critical a change is costly. So not wanting to fix 'wot aint broke' I am open to other arguments.

It's paradoxical death spiral that the most critical code gets the least attention because people are afraid to change it.

That you are hesitant to make a simple, rote refactoring to this code tells me the code either has no tests or you don't trust the tests. When you're afraid to improve code because you might break it, that delays improvements to the code. You're likely to do the smallest possible thing which will make the code even more brittle and unsafe.

I'd advise the first thing is to get some tests in place along with a staging environment for trials. Then all changes become safer. There might be some gafes initially while you find all the weird and dangerous things this code is doing, but that's what the staging area is for. In the medium and long term everyone will improve this code faster and with more confidence. Making code easier and safer to change allows it to be made easier and safer to change; the spiral then goes up, not down.


The technique of making a macro seem like a single variable is a technique I've seen before in the Perl 5 code base. It is written more in C macros than in C. For example, here's a bit of manipulating the Perl call stack.

#define SP sp
#define MARK mark
#define TARG targ

#define PUSHMARK(p) \
    STMT_START {                                                      \
        I32 * mark_stack_entry;                                       \
        if (UNLIKELY((mark_stack_entry = ++PL_markstack_ptr)          \
                                           == PL_markstack_max))      \
        mark_stack_entry = markstack_grow();                      \
        *mark_stack_entry  = (I32)((p) - PL_stack_base);              \
        DEBUG_s(DEBUG_v(PerlIO_printf(Perl_debug_log,                 \
                "MARK push %p %" IVdf "\n",                           \
                PL_markstack_ptr, (IV)*mark_stack_entry)));           \
    } STMT_END

#define TOPMARK S_TOPMARK(aTHX)
#define POPMARK S_POPMARK(aTHX)

#define INCMARK \
    STMT_START {                                                      \
        DEBUG_s(DEBUG_v(PerlIO_printf(Perl_debug_log,                 \
                "MARK inc  %p %" IVdf "\n",                           \
                (PL_markstack_ptr+1), (IV)*(PL_markstack_ptr+1))));   \
        PL_markstack_ptr++;                                           \
} STMT_END
#define dSP     SV **sp = PL_stack_sp
#define djSP        dSP
#define dMARK       SV **mark = PL_stack_base + POPMARK
#define dORIGMARK   const I32 origmark = (I32)(mark - PL_stack_base)
#define ORIGMARK    (PL_stack_base + origmark)

#define SPAGAIN     sp = PL_stack_sp
#define MSPAGAIN    STMT_START { sp = PL_stack_sp; mark = ORIGMARK; } STMT_END

#define GETTARGETSTACKED targ = (PL_op->op_flags & OPf_STACKED ? POPs : PAD_SV(PL_op->op_targ))
#define dTARGETSTACKED SV * GETTARGETSTACKED

These are macros upon macros upon macros. The Perl 5 source is riddled with them. There is a lot of opaque magic happening there. Some of them need to be macros to allow assignment, but many could be inline functions. Despite being part of a public API they are indifferently documented in part because they are macros and not functions.

This style is very clever and useful if you're already very familiar with the Perl 5 source code. For everyone else it has made the Perl 5 internals extremely difficult to work with. While some compilers will provide stack traces for macro expansions, others will only report on the expanded macro leaving one scratching their head what the hell const I32 origmark = (I32)(mark - PL_stack_base) is because it never appears in your source.

Like many macro hacks, while the technique is very clever it is also mind-bending and unfamiliar to many programmers. Mind-bending is not what you want in safety critical code. You want simple, boring code. That alone is the simplest argument to replace it with well named getter and setter functions. Trust the compiler to optimize them.


A good example of this is GLib which carefully uses well-documented function-like macros to make generic data structures. For example, adding a value to an array.

#define             g_array_append_val(a,v)

While this is a macro, it acts and is documented like a function. It's macro solely as a mechanism to create a safe, type generic array. It hides no variables. You can safely use it without ever being aware it's a macro.


In conclusion, yes, change it. But instead of simply replacing X_myvar with inst.a.a consider creating functions that continue to provide encapsulation.

void s1_set_a( s1 *s, int val ) {
    s->a.a = val;
}

int s1_get_a( s1 *s ) {
    return s->a.a;
}

s1_set_a(&inst, 10);
printf("myvar = %d\n", s1_get_a(&inst) + 1);

The internals of s1 are hidden making it easier to change the internals later (for example, changing s1.a to a pointer to save memory). What variable you're working with is clear making the overall code easier to understand. The function names provide a clear explanation of what's happening. Because they're functions they have an obvious place for documentation. Trust the compiler to know how best to optimize it.

3
  • I like your logic. What you say about being afriad to change it is very true. Testing it is costly and time consuming. There are some test harnesses but because of the nature of the product it's not easy to test in the lab. (Something I want to change).
    – Jay M
    Commented Apr 28, 2019 at 21:48
  • Though a good idea, I don't think accessor functions will work in this case, I am very space constrained (it's a small embedded systems where the memory is limited) and there are a lot of these type of aliases (almost all commonly used structure members) I really don't undestand why somebody would put stuff in structures then want to access them as globals.....
    – Jay M
    Commented Apr 28, 2019 at 21:50
  • @JasonMorgan "I am very space constrained"... Trust the compiler to know how best to optimize. It might choose to inline the accessor functions (you can even give it an inline hint, though it's questionable how much modern compilers pay attention to that). Or you might get smaller code by using function calls instead of repeating the same code over and over again with a macro. Maybe try -Os or your equivalent. I'd recommend giving it a shot. As for the testing being costly and time consuming, yes, improving the test harness to make testing easier will help. Good luck.
    – Schwern
    Commented Apr 28, 2019 at 22:35
1

From a maintenance perspective, yes, this is definitely code that needs to be fixed.

However, it is only from that perspective that the code needs to be fixed. It does not harm program correctness, and if the code is correct as-is, that is the paramount consideration.

That's why code like this should never be fixed unless a thorough unit test and regression test regimen is already in place. You should only fix code like this if you can be certain that you don't break correctly-functioning code in the process.

3
  • I'm veering towards fixing it, but in a limited way as certain areas of the code change. Adding a simple minor change to a major one does no seem so risky as the bigger risk is the new code not the old (refactored) code. Change for changes sake seems too much.
    – Jay M
    Commented Apr 28, 2019 at 21:53
  • 1
    Yes, that would be good time to do this sort of fix, but keep in mind that without a good test regimen, even a "it's just a minor fix" can really bite you on the ass. Commented Apr 29, 2019 at 14:39
  • I know, that is why I'm reluctant to fix it without very good cause.
    – Jay M
    Commented Apr 30, 2019 at 8:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.