Use of #define to alias structure members

1k views Asked by At

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?

3

There are 3 answers

3
Mark Benningfield On

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.

0
Lundin On

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.

3
Schwern On

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.