1

I try to use va_arg to retrieve next argument in function. It works well for all types (including char*) but char:

void test1(char t,...) {
  va_list args;
  va_start(args, t);
  if(t=='c') Serial.println(va_arg(args, char));
  else if(t=='n') Serial.println(va_arg(args, int));
  va_end(args);  
}

Test:

int n = 42;
char c = '?';

test1('n', n); // prints 42
test1('c', c); // prints nothing!

Can you verify/explain it? The code runs on Arduino Uno, 9600 bauds.

Jan Turoň
  • 31,451
  • 23
  • 125
  • 169
  • 1
    The va_arg is one of the things from C I'd never used in C++. What's even purpose of this. Looks more like [XY problem](https://meta.stackexchange.com/a/66378) – KIIV Feb 12 '20 at 07:47

2 Answers2

6

Arguments smaller than int are promoted to int before being passed to variadic functions, and such functions should thus retrieve the arguments as type int.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • You were right, I found the reference [here](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf) (7.16.1.1) and the correct code is `if(t=='c') Serial.println((char)va_arg(args, int));` - seems that AVR uses big endian int representation (unlike x86), so 'c' is encoded as 0x00 0x63, so in my original code the first byte is extracted into result and the zero effectively truncates the string, which is exactly what I observed. – Jan Turoň Feb 12 '20 at 08:35
  • @JanTuroň It should be using Little Endian I believe, so that's not why. – Lundin Feb 12 '20 at 08:44
  • @JanTuroň I posted a more detail answer explaining why the code doesn't work. It is simply undefined behavior - it has nothing to do with AVR endianess. – Lundin Feb 12 '20 at 11:34
  • +1. An intelligently designed `va_arg` could do this promotion under the hood, saving the user from having to know this. Just saying/ranting. :| – Petr Skocik Feb 12 '20 at 11:48
  • 1
    @PSkocik There are lots of cases of UB in the standard that the committee could easily fix and make well-defined. Though for va_arg & friends they take up a significant part of the UB summary annex, so in this specific case I'm leaning towards "broken beyond repair". – Lundin Feb 12 '20 at 14:50
  • @PSkocik: Such accommodations may be possible on many systems, but I can't say with any confidence that they would be possible on all. – supercat Feb 12 '20 at 15:12
  • @PSkocik: A much more useful feature would have been to allow a variadic function prototype to specify a type for all floating-point arguments, a common size for all integer arguments, and a common type for all pointer arguments. The `long double` type could have been much more useful if that were done, since `printf` could specify the use of `%f` for any floating-point type that wasn't wrapped in a macro, and `%Lf` for a floating-point value wrapped in an "as long double" macro. – supercat Feb 12 '20 at 15:15
  • @supercat On any system with `__typeof`, accommodating type promotion in `va_arg` is trivial: `#define promoting_va_arg(ap,type) va_arg(ap, __typeof(1?(type){0}:(type){0}))` – Petr Skocik Feb 12 '20 at 18:44
1

Variadic functions come with an oddball special rule for implicit type promotion known as the default argument promotions.

C17 6.5.2.2/7

The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments.

Ellipsis being ...

The default argument promotions are normally just used when using old style non-prototype functions. They are therefore defined as:

C17 6.5.2.2/6

If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions.

In your case, char counts as an integer type and the above means that it will get integer promoted when passed to a variadic function.

Binary this means that ASCII ? = 0x3F gets promoted to int. AVR uses 16 bit little endian so it gets stored as 0x3F 0x00 in memory. The problem does not lie there.

Rather, when you try to use va_arg on the wrong type, you invoke undefined behavior. This is stated in the documentation for va_arg:

C17 7.16.1.1/2

If there is no actual next argument, or if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined

So the only possible solution is to change the code to if(t=='c') Serial.println(va_arg(args, int));.


Unrelated to your question, using variadic functions and stdio.h on a 8 bit MCU is very bad practice. Not only are these function dangerous, they will also consume lots of flash and RAM.

Also, for embedded systems, you should always use stdint.h instead of the default types of C.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • I find variadic formatting output functions can be useful on embedded systems, though I prefer to use my own rather than `printf` family. For many applications, my preferred function uses byte values 0x80-0xFF as format specifiers which are more compact and easier to parse than `printf` arguments, and also include options to insert things like decimal points, which makes it possible to show decimal fractions in a UI while only having to do integer math. – supercat Feb 12 '20 at 15:23
  • @supercat Kind of like how fishing with dynamite can be useful, just not very safe :) MISRA-C and all other coding standards I know of ban variadic functions. – Lundin Feb 12 '20 at 15:27
  • Were it not for the desire to use the same calling convention for variadic and unprototyped functions when practical, variadic functions could have been implemented in a way that would have allowed an ABI-agnostic "get a pointer to the next argument, if any, and report its type" [all structures and all unions would be lumped together] function, while making the code for most call sites more compact than with existing approaches, thus allowing suitably-written variadic functions to be implemented safely. – supercat Feb 12 '20 at 16:54
  • @supercat Be that as it may, the bottom line is that embedded systems should be deterministic, not variadic. – Lundin Feb 12 '20 at 17:41
  • Why couldn't a function be both variadic and deterministic? The caller, who knows how many arguments there are, would be responsible for building a length-prefixed data structure and passing a pointer to it (much of the building process could be taken care of in a compiler-supplied subroutine to avoid bulking up the client code). Such a design would have broken any code which tried to call variadic functions without prototypes (which is why implementations didn't do things that way) but it could otherwise have been both safe and code-space efficient. – supercat Feb 12 '20 at 18:56