hmijail hmijail - 3 months ago 9
C Question

Can a C function have dual use depending on a parameter being volatile or not?

I'm developing on C99 for an embedded environment using GCC.
I made a small library to deal with circular buffers and queues. It implements and works on instances of a basic struct which contains a buffer and the needed metadata.

struct circbuf_u8_st {
const uint8_t buffer_num_elems;
uint8_t head;
uint8_t tail;
uint8_t * buffer;
};


Sometimes the
struct
is used as a
volatile
global, since it is used for communication between an interrupt routine which generates data and the main loop which consumes the data.

But sometimes the
struct
is used as a non-
volatile
local, for example when one part of the main loop generates data to be consumed later on the same main loop.

Having the
struct
being sometimes
volatile
and sometimes not means that any function dealing with the
struct
needs two versions, for the
volatile
and non-
volatile
parameter. That is a maintenance problem: any change made in one of the versions has to be repeated in the other version. For example,

void circbufu8_reset(struct circbuf_u8_st *cb);
void circbufu8_v_reset(struct circbuf_u8_st volatile *cbv);


I could just go with the
volatile
version of everything, since that will be always correct. But that also implies a pessimization for the non-
volatile
case that I would like to avoid.

So, a possible solution would be to declare a
union
with
volatile
/non
volatile
members, and declare the
struct
members to have the type of that
union
.

union un_dual_volatile_u8 {
uint8_t volatile v;
uint8_t nv;
};


That would help to get rid of the 2-versions-of-each-function problem. But, does it really help? What semantics would (could) such a function have? My understanding is that the compiler will have to use the strictest semantics needed by the
union
, so in fact this would just be a needlessly complicated version of the everything-
volatile
option, with the same pessimization.

So, the questions are:


  • am I right that the union won't help?

  • is there then any way to avoid the function duplication and the pessimization?

  • would C++ help in this kind of situation? (would function overloading respect the
    volatile
    and non-
    volatile
    semantics? or maybe using generics?)



(looking at the compiler output is not a valid answer; I'm looking for a standard-based justification that can be relied upon)

EDIT: I removed the C++ tag, since the C++ part of the question was more out of curiosity. And of course C11's generics make the problem easier to solve, but the goal is to solve this in C99.

Answer

(Self-answering because of lack of non-C++ answers)

In C99 the only way to have the overloaded-function-like behaviour like this is through preprocessor tricks. The union wouldn't work, at the very least because the semantics of such a union are undecided in GCC itself, have been for some years and don't seem to be changing. In that bug report and discussion looks like GCC developers aren't really sure about what to interpret from the standard… so I took that as a very strong suggestion to abandon this whole angle of attack, even if I changed compilers.

So finally I went with generalized preprocessor "templates" which declared and initialized functions, structures, etc with parameterized types - for example, a uint16 in one case and a volatile uint8 in another. For example, for the circular buffer:

#define CIRCBUF_TYPE_DECLARE(TYPE, TYPELABEL, QUALIF, QUALIFLABEL)          \
    struct circbuf##TYPELABEL##QUALIFLABEL##_st {                           \
        const TYPE buffer_num_elems;                                        \
        TYPE QUALIF head;                                                   \
        TYPE QUALIF tail;                                                   \
        uint8_t QUALIF * buffer;                                            \
    };                                                                      \
                                                                            \
    typedef struct circbuf##TYPELABEL##QUALIFLABEL##_st circbuf##TYPELABEL##QUALIFLABEL;

And then invoked like so:

CIRCBUF_TYPE_DECLARE(uint8_t, _u8, volatile, _v)

For this kind of thing, Jens Gustedt's P99 preprocessor library for C was a great inspiration. Later I found that this kind of "preprocessor templating" is also used in Linux's kernel source code… So even though I rolled my own versions probably with time I would have tried using one of the existing implementations. However, I switched jobs before I had the time for that.

For those interested, I wrote a blog post with the full discussion on the C99-standard-based justification of why the volatile+non-volatile union is legal and yet how the semantics are still not clear.

Comments