4

Let's assume I have a macro (more details about why, is below in the P.S. section)

void my_macro_impl(uint32_t arg0, uint32_t arg1, uint32_t arg2);

...

#define MY_MACRO(arg0, arg1, arg2)       my_macro_impl((uint32_t)(arg0), (uint32_t)(arg1), (uint32_t)(arg2))

The HW on which this macro is going to be used is little endian and uses 32bit architecture so that all the pointers are up to (and including) 32 bit width. My goal is to warn the user when it passes uint64_t or int64_t parameter by mistake.

I was thinking about using sizeof like this

#define MY_MACRO(arg0, arg1, arg2)       do \
                                         {  \
                                             static_assert(sizeof(arg0) <= sizeof(uint32_t));  \
                                             static_assert(sizeof(arg1) <= sizeof(uint32_t));  \
                                             static_assert(sizeof(arg2) <= sizeof(uint32_t));  \
                                             my_macro_impl((uint32_t)(arg0), (uint32_t)(arg1), (uint32_t)(arg2));  \
                                         } while (0)

But the user can use MY_MACRO with a bit-field and then my code fails to compile:

error: invalid application of 'sizeof' to bit-field

Question: Is there an option to detect at the compilation time if the size of the macro argument larger than, let's say, uint32_t?


P.S.

The MY_MACRO is going to act similarly to printf in a real-time embedded environment. This environment has a HW logger which may receive up to 5 parameters, each parameter should be 32 bits. The goal is to preserve the standard format as for printf. The format strings are parsed offline and the parser is well aware that every parameter is 32 bits, so it will cast it based on the %... from the format string. Possible usages are below.

Not desired usage:

uint64_t time = systime_get();
MY_MACRO_2("Starting execution at systime %llx", time); // WRONG! only the low 32 bits are printed. I want to detect it and fail the compilation.

Expected usage:

uint64_t time = systime_get();
MY_MACRO_3("Starting execution at systime %x%x", (uint32_t)(time >> 32), (uint32_t)time); // OK! 
Alex Lop.
  • 6,810
  • 1
  • 26
  • 45
  • 1
    Would using `sizeof 0 + (arg0)` make any difference? – melpomene Sep 22 '19 at 13:01
  • @melpomene This one might do the difference assuming 32 bit architecture (https://godbolt.org/z/PwI9mZ). – Alex Lop. Sep 22 '19 at 18:01
  • @melpomene ...The only downside is that it gives a warning when passed argument is an array `warning: sizeof on pointer operation will return size of 'int *' instead of 'int [30]' [-Wsizeof-array-decay]` – Alex Lop. Sep 22 '19 at 18:10

3 Answers3

5

The following approach may work for this need:

#define CHECK_ARG(arg)                  _Generic((arg), \
                                                 int64_t  : (arg),  \
                                                 uint64_t : (arg),  \
                                                 default  : (uint32_t)(arg))

Then, the MY_MACRO can be defined as

#define MY_MACRO(a0, a1, a2)       do \
                                   {  \
                                       uint32_t arg1 = CHECK_ARG(a0);  \
                                       uint32_t arg2 = CHECK_ARG(a1);  \
                                       uint32_t arg3 = CHECK_ARG(a2);  \
                                       my_macro_impl(arg1, arg2, arg3);\
                                   } while (0)

In such case, when passing for example uint64_t, a warning is fired:

warning: implicit conversion loses integer precision: 'uint64_t' (aka 'unsigned long long') to 'uint32_t' (aka 'unsigned int') [-Wshorten-64-to-32]

Note:

Other types like double, 128/256 bit types can be handled similarly.

Appropriate warnings should be enabled.

EDIT:

Inspired by Lundin's comment and answer, the proposed above solution can easily be modified to a portable version which will cause compilation error and not just a compiler warning.

#define CHECK_ARG(arg)          _Generic((arg),         \
                                         int64_t  : 0,  \
                                         uint64_t : 0,  \
                                         default  : 1)

So the MY_MACRO can be modified to

#define MY_MACRO(a0, a1, a2)       do \
                                   {  \
                                       _Static_assert(CHECK_ARG(a1) && \
                                                      CHECK_ARG(a2) && \
                                                      CHECK_ARG(a3),   \
                                                      "64 bit parameters are not supported!"); \
                                       my_macro_impl((uint32_t)(a1), (uint32_t)(a2), (uint32_t)(a3)); \
                                   } while (0)

This time, when passing uint64_t parameter MY_MACRO(1ULL, 0, -1), the compilation fails with error:

error: static_assert failed due to requirement '_Generic((1ULL), long long: 0, unsigned long long: 0, default: 1) && (_Generic((0), long long: 0, unsigned long long: 0, default: 1) && _Generic((-1), long long: 0, unsigned long long: 0, default: 1))' "64 bit parameters are not supported!"

Alex Lop.
  • 6,810
  • 1
  • 26
  • 45
  • Inasmuch as the OP specified detection of `int64_t` and `uint64_t` only (not, say, wide extension types that the compiler might offer), this looks good to me. It does require C11, though. – John Bollinger Sep 22 '19 at 12:49
  • @JohnBollinger Right. 128/256 bit types can be handled in the same way. And yes, c11 is required for `_Generic`. – Alex Lop. Sep 22 '19 at 12:54
  • How does this solve anything? It relies on a specific gcc warning. The _Generic doesn't add anything, it explicitly lets 64 bit pointers through, and then lets various other wrong types through as well. – Lundin Sep 23 '19 at 10:36
  • @Lundin Please read the P.S. section. The purpose is to implement a logger and allow it to print pointers/integers similarly to the `printf` method. The pointers on this HW are 32 bits so it maybe a pointer to anything (uint64_t, function, array) as long as the HW logger will have its full value. – Alex Lop. Sep 23 '19 at 10:52
  • Yeah I saw it. Your code here doesn't achieve what you ask for. The _Generic doesn't do anything except filtering out one particular gcc warning, which is by no means mandatory for the compiler to give. It would be better if the _Generic failed for incorrect types, portably forcing the user to fix their code. – Lundin Sep 23 '19 at 12:23
  • I posted an alternative answer that seems to be more in line(?) with what you ask for. – Lundin Sep 23 '19 at 14:08
  • 1
    I think I can see _five_ problems with this solution. See https://godbolt.org/z/SOO76h. **#1** It doesn't reject `double` args. **#2** gcc doesn't reject bitfields of width 33 bits. **#3** clang can incorrectly(?) reject a bitfield of width 1. **#4** Older versions of clang (e.g. 3.7 from 2015) will incorrectly not reject `const uint64_t`. **#5** Requires C11, so less portable. – Joseph Quinsey Sep 24 '19 at 17:29
1

The type of the ternary ?: expression is the common type of its second and third arguments (with integer promotion of smaller types). So the following version of your MY_MACRO will work in a 32-bit architecture:

static_assert(sizeof(uint32_t) == sizeof 0, ""); // sanity check, for your machine

#define MY_MACRO(arg0, arg1, arg2) \
    do {  \
        static_assert(sizeof(0 ? 0 : (arg0)) == sizeof 0, "");  \
        static_assert(sizeof(0 ? 0 : (arg1)) == sizeof 0, "");  \
        static_assert(sizeof(0 ? 0 : (arg2)) == sizeof 0, "");  \
        my_macro_impl((uint32_t)(arg0), (uint32_t)(arg1), (uint32_t)(arg2));  \
    } while (0)

Moreover, this solution should work with all versions of C and C++ (with, if necessary, a suitable definition of static_assert).

Note this macro, like the OP's original, has function semantics in that the arguments are evaluated only once, unlike for example the notorious MAX macro.

Joseph Quinsey
  • 9,553
  • 10
  • 54
  • 77
  • 1
    That'a an interesting approach however this won't work if the arguments are not of arithmetic type (for example pointer): `error: invalid argument type 'void *' to unary expression`. Unary + can only be applied on arithmetic types. – Alex Lop. Sep 22 '19 at 17:11
  • **Update:** The answer has been revised to use the ternary operator. My original answer suggested using unary `+` for promotion, but the OP pointed out that this doesn't work on pointers. A quick fix would be to use the binary `+` to add `0`. But that would fail on `void *` pointers, according to the strict rules. (Some compilers, such as gcc, permit this, but then you would need to add pragmas to the macro definition to suppress warning messages.) – Joseph Quinsey Sep 22 '19 at 18:29
  • Be aware that `?:` comes with implicit type promotion - the types of the 2nd and 3rd arguments are converted according to "the usual arithmetic conversions", meaning that the `?:` forces the input to become `int` in case of small integer types. So if you feed this macro something like `uint8_t` or `_Bool`, it won't be spotted. In addition (as pointed out), it is non-portable to 8- or 16-bit systems where `int` has a different size than `int32_t`. – Lundin Sep 23 '19 at 14:12
  • 1
    You were right about the issues with the bitfields. It is very tricky to used with `_Generic`. Using your approach with integer promotion should work for all cases. – Alex Lop. Sep 25 '19 at 12:50
1

Question: Is there an option to detect at the compilation time if the size of the macro argument larger than, let's say, uint32_t?

The only way to do this portably, is by generating a compiler error with _Generic. If you want the error to be pretty and readable, you feed the result of _Generic to _Static_assert, so that you can type out a custom string as compiler message.

Your specification seems to be this:

  • Everything must be compile-time checks.
  • The macro can get 1 to 5 parameters of any type.
  • Only int32_t and uint32_t are allowed types.

This means that you have to write a variadic macro and it must accept 1 to 5 parameters.


Such a macro can be written like this:

#define COUNT_ARGS(...) ( sizeof((uint32_t[]){__VA_ARGS__}) / sizeof(uint32_t) )

#define MY_MACRO(...)                                                           \
  _Static_assert(COUNT_ARGS(__VA_ARGS__)>0 && COUNT_ARGS(__VA_ARGS__)<=5,       \
                 "MY_MACRO: Wrong number of arguments");                        

COUNT_ARGS creates a temporary compound literal of as many objects as you give the macro. If they are wildly incompatible with uint32_t you might get compiler errors/warnings here already. If not, COUNT_ARGS will return the number of arguments passed.


With that out of the way, we can do the actual, portable type check of each item in the variable argument list. To check the type of one single item with _Generic:

#define CHECK(arg) _Generic((arg), uint32_t: 1, int32_t: 1, default: 0)

Then pass the result of that on to _Static_assert. However, for 5 arguments we would need to check 1 to 5 items. We can "chain" a number of macros for this purpose:

#define CHECK(arg) _Generic((arg), uint32_t: 1, int32_t: 1, default: 0)

#define CHECK_ARGS1(arg1,...) CHECK(arg1)
#define CHECK_ARGS2(arg2,...) (CHECK(arg2) && CHECK_ARGS1(__VA_ARGS__,0))
#define CHECK_ARGS3(arg3,...) (CHECK(arg3) && CHECK_ARGS2(__VA_ARGS__,0))
#define CHECK_ARGS4(arg4,...) (CHECK(arg4) && CHECK_ARGS3(__VA_ARGS__,0))
#define CHECK_ARGS5(arg5,...) (CHECK(arg5) && CHECK_ARGS4(__VA_ARGS__,0))

Each macro checks the first argument passed to it, then forwards the rest of them, if any, to the next macro. The trailing 0 is there to shut up ISO C warnings about rest arguments required for variadic macros.

We can bake the calls to these into a _Static_assert that calls the proper macro in the "chain" corresponding to the number of arguments:

_Static_assert(COUNT_ARGS(__VA_ARGS__) == 1 ? CHECK_ARGS1(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 2 ? CHECK_ARGS2(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 3 ? CHECK_ARGS3(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 4 ? CHECK_ARGS4(__VA_ARGS__,0) :    \
               COUNT_ARGS(__VA_ARGS__) == 5 ? CHECK_ARGS5(__VA_ARGS__,0) : 0, \
               "MY_MACRO: incorrect type in parameter list " #__VA_ARGS__);   \

Full code with examples of use:

#include <stdint.h>

#define COUNT_ARGS(...) ( sizeof((uint32_t[]){__VA_ARGS__}) / sizeof(uint32_t) )

#define CHECK(arg) _Generic((arg), uint32_t: 1, int32_t: 1, default: 0)

#define CHECK_ARGS1(arg1,...) CHECK(arg1)
#define CHECK_ARGS2(arg2,...) (CHECK(arg2) && CHECK_ARGS1(__VA_ARGS__,0))
#define CHECK_ARGS3(arg3,...) (CHECK(arg3) && CHECK_ARGS2(__VA_ARGS__,0))
#define CHECK_ARGS4(arg4,...) (CHECK(arg4) && CHECK_ARGS3(__VA_ARGS__,0))
#define CHECK_ARGS5(arg5,...) (CHECK(arg5) && CHECK_ARGS4(__VA_ARGS__,0))

#define MY_MACRO(...)                                                           \
do {                                                                            \
  _Static_assert(COUNT_ARGS(__VA_ARGS__)>0 && COUNT_ARGS(__VA_ARGS__)<=5,       \
                 "MY_MACRO: Wrong number of arguments");                        \
  _Static_assert(COUNT_ARGS(__VA_ARGS__) == 1 ? CHECK_ARGS1(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 2 ? CHECK_ARGS2(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 3 ? CHECK_ARGS3(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 4 ? CHECK_ARGS4(__VA_ARGS__,0) :    \
                 COUNT_ARGS(__VA_ARGS__) == 5 ? CHECK_ARGS5(__VA_ARGS__,0) : 0, \
                 "MY_MACRO: incorrect type in parameter list " #__VA_ARGS__);   \
} while(0)


int main (void)
{
//MY_MACRO();                          // won't compile, "empty initializer braces"
//MY_MACRO(1,2,3,4,5,6);               // static assert "MY_MACRO: Wrong number of arguments"
  MY_MACRO(1);                         // OK, all parameters int32_t or uint32_t
  MY_MACRO(1,2,3,4,5);                 // OK, -"-
  MY_MACRO(1,(uint32_t)2,3,4,5);       // OK, -"-
//MY_MACRO(1,(uint64_t)2,3,4,5);       // static assert "MY_MACRO: incorrect type..."
//MY_MACRO(1,(uint8_t)2,3,4,5);        // static assert "MY_MACRO: incorrect type..."
}

This should be 100% portable and doesn't rely on the compiler giving extra diagnostics beyond what's required by the standard.

The old do-while(0) trick is there to allow compatibility with icky-style brace formatting standards such as if(x) MY_MACRO(1) else. See Why use apparently meaningless do-while and if-else statements in macros?

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • The point is that it is ok to have arguments of 32 bits and less (8, 16)... 64 bits causes an issue because only the low 32 bits are written to the hardware. So the desired behavior is to fail only for 64 bits arguments – Alex Lop. Sep 23 '19 at 14:16
  • @AlexLop. This can be easily adapted to allow or block any types (including pointer types, structs, arrays etc). Simply rewrite the `CHECK` macro. – Lundin Sep 23 '19 at 14:17