printf portability problems persist… possibly.

TL:DNR – You all probably already knew this, but I just learned about inttypes.h. (Well, not actually “just”; I found out about it last year for a different reason, but I re-learned about it now for this issue…)

I was today years old when I learned that there was a solution to a bothersome warning that most C programmers probably never face: printing ints or longs in code that will compile on 16-bit or 32/64-bit systems.

For example, this code works fine on my 16-bit PIC24 compiler and a 32/64-bit compiler:

int x = 42;
printf ("X is %dn", x);

long y = 42;
printf ("Y is %ldn", y);

This is because “%d” represents and “int“, whatever that is on the system — 16-bit, 32-bit or 64-bit — and “%ld” represents a “long int“, whatever that is on the system.

On my 16-bit PIC24 compiler, “int” is 16-bits and “long int” is 32-bits.

On my PC compiler “int” is 32-bits, and “long int” is 64-bits.

But int isn’t portable, is int?

As far as I recall, the C standard says an int is “at least 16-bits.” If you want to represent a 16-bit value in any compliant ANSI-C code, you can use int. It may be using 32 or 64 bits (or more?), but it will at least hold 16-bits.

What if you need to represent 32 bits? This code works fine on my PC compiler, but would not work as expected on my 16-bit system:

unsigned int value = 0xaabbaabb;

printf ("value: %u (0x%x) - ", value, value);

for (int bit = 31; bit >= 0; bit--)
{
    if ( (value & (1<<bit)) == 0)
    {
        printf ("0");
    }
    else
    {
        printf ("1");
    }
}
printf ("n");

On a 16-bit system, an “unsigned int” only holds 16-bits, so the results will not be what one would expect. (A good compiler might even warn you about that, if you have warnings enabled… which you should.)

stdint.h, anyone?

In my embedded world, writing generic ANSI-C code is not always optimal. If we must have 32-bits, using “long int” works on my current system, but what if that code gets ported to a 32-bit ARM processor later? On that machine, “int” becomes 32-bits, and “long” might be 64-bits.

Having too many bits is not as much of an issue as not having enough, but the stdint.h header file solves this by letting us request what we actually want to use. For example:

#include <stdio.h>
#include <stdint.h> // added

int main()
{
    uint32_t value = 0xaabbaabb; // changed
    
    printf ("value: %u (0x%x) - ", value, value);
    
    for (int bit = 31; bit >= 0; bit--)
    {
        if ( (value & (1<<bit)) == 0)
        {
            printf ("0");
        }
        else
        {
            printf ("1");
        }
    }
    printf ("n");

    return 0;
}

Now we have code that works on a 16-bit system as well as a 32/64-bit system.

Or do we?

There is a problem, which I never knew the solution to until recently.

printf ("value: %u (0x%x) - ", value, value);

That line will compile without warnings on my PC compiler, but I get a warning on my 16-bit compiler. On a 16-bit compiler, “%u” is for printing an “unsigned int”, as is “%x”. But on that compiler, the “uint32_t” represents a 32-bit value. Normal 16-bit compilers would probably call this an “unsigned long”, but my PIC24 compiler has its own internal variable types, so I see this in stdint.h:

typedef unsigned int32 uint32_t;

On the Arduino IDE, it looks more normal:

typedef unsigned long int uint32_t;

And a “good” compiler (with warnings enabled) should alert you that you are trying to print a variable larger than the “%u” or “%x” handles.

So while this works fine on my 32-bit compiler…

// For my 32/64-bit system:
uint32_t value32 = 42;
printf ("%u", value32);

…it gives a warning on the 16-bit ones. To make it compile on the 16-bit compiler, I change it to use “%lu” like this:

// For my 16-bit system:
uint32_t value32 = 42;
printf ("%lu", value32);

…but then that code will generate a compiler warning on my 32/64-bit system ;-)

There are some #ifdefs you can use to detect architecture, or make your own using sizeof() and such, that can make code that compiles without warnings, but C already solved this for us.

Hello, inttypes.h! Where have you been all my C-life?

On a whim, I asked ChatGPT about this the other day and it showed me define/macros that are in inttypes.h that take care of this.

If you want to print a 32-bit value, instead of using “%u” (on a 32/64-bit system) or “%lu” on a 16-bit, you can use PRIu32 which represents whatever print code is needed to print a “u” that is 32-bits:

#define PRIu32 "lu"

Instead of this…

uint32_t value = 42;
printf ("value is %u\n", value);

…you do this:

uint32_t value = 42;
printf ("value is %" PRIu32 "\n", value);

Because of how the C preprocessor concatenates strings, that ends up creating:

printf ("value is %lu\n", value); // %lu

But on a 32/64-bit compiler, that same header file might represent it as:

#define PRIu32 "u"

Thus, writing that same code using this define would produce this on the 32/64-bit system:

printf ("value is %u\n", value); // %u

Tada! Warnings eliminated.

And now I realize I have used this before, for a different reason:

uintptr_t
PRIxPTR

If you try to print the address of something, like this:

void *ptr = 0x1234;
printf ("ptr is 0x%x\n", ptr);

…you should get a compiler warning similar to this:

warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 2 has type ‘void *’ [-Wformat=]

%x is for printing an “unsigned int”, and ptr is a “void *”. Over the years, I made this go away by casting:

printf ("ptr is 0x%x\n", (unsigned int)ptr);

But, on my 32/64-bit compiler, the “unsigned int” is a 32-bit value, and %x is not for 32-bit values. Thus, I still get a warning. There, I would use “%lx” for a “long int”.

To make that go away, last year I learned about using PRIxPTR to represent the printf code for printing a pointer as hex:

printf ("pointer is 0x%" PRIxPTR "\n",

On my 16-bit compiler, it is:

#define PRIxPTR "lx"

This is because pointers are 32-bit on a PIC24 (even though an “int” on that same system is 16-bits).

On the 32/64-bit compiler (GNU-C in this case), it changes depending on if the system:

#ifdef _WIN64
...
#define PRIxPTR "I64x" // 64-bit mode
...
else
...
#define PRIxPTR "x" // 32-bit mode
...
#endif

I64 is something new to me since I never write 64-bit code, but clearly this shows there is some extended printf formatting for 64-bit values, versus just using “%x” for the default int size (32-bits) and “%lx” for the long size.

Instead of casting to an “(unsigned int)” or “(unsigned long int)” before printing, there is a special “uintptr_t” type that will be “whatever size a pointer is.

This gives me a warning:

printf ("ptr is 0x%" PRIxPTR "\n", (unsigned int)ptr);

warning: format ‘%lx’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘unsigned int’ [-Wformat=]

But I can simply change the casting of the pointer:

printf ("ptr is 0x%" PRIxPTR "\n", (uintptr_t)ptr);

You may have also noticed I still have a warning when declaring the pointer with a value:

void *ptr = 0x1234;

warning: initialization of ‘void *’ from ‘int’ makes pointer from integer without a cast [-Wint-conversion]

Getting rid of this is as simple as making sure the value is cast to a “void *”:

void *ptr = (void*)0x1234;

This is what happens when you learn C on a K&R compiler in the late 1980s and go to sleep for awhile without keeping up with all the subsequent standards, including one from 2023 that I just found out about while typing this up!

Per BING CoPilot…

  • C89/C90 (ANSI X3.159-1989): The first standard for the C programming language, published by ANSI in 1989 and later adopted by ISO as ISO/IEC 9899:1990.
  • C95 (ISO/IEC 9899/AMD1:1995): A normative amendment to the original standard, adding support for international character sets.
  • C99 (ISO/IEC 9899:1999): Introduced several new features, including inline functions, variable-length arrays, and new data types like long long int.
  • C11 (ISO/IEC 9899:2011): Added features like multi-threading support, improved Unicode support, and type-generic macros.
  • C17 (ISO/IEC 9899:2018): A bug-fix release that addressed defects in the C11 standard without introducing new features.
  • C23 (ISO/IEC 9899:2023): The latest standard, which includes various improvements and new features to keep the language modern and efficient.

The more you know…

Though, I assume all the younguns that grew up in the ANSI-C world already know this. I grew up when you had to write functions like this:

/* Function definition */
int add(x, y)
int x, y;
{
return x + y;
}

Now to get myself in the habit of never using “%u”, “%d”, etc. when using stdint.h types…

Until then…

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.