Code of the Day
AdvancedSystems programming patterns

Writing portable C

Write C that compiles and runs correctly across platforms — feature-test macros, C standards, compiler extensions, and portability pitfalls.

CAdvanced10 min read
By the end of this lesson you will be able to:
  • Explain the difference between C89, C99, C11, and C17
  • Use _POSIX_C_SOURCE to enable POSIX extensions
  • Apply feature-test macros to access platform-specific APIs safely
  • Identify common portability pitfalls around integer sizes and endianness

C code that works on Linux today may fail on macOS, Windows, or an embedded target — or even on the same platform with a different compiler version. Writing portable C means knowing which features are part of the standard, which are POSIX extensions, and which are compiler-specific.

C language standards

StandardYearKey additions
C89/C901989/1990The original ANSI/ISO C
C991999// comments, VLAs, stdint.h, bool, designated initialisers, restrict
C112011_Alignas, _Generic, <threads.h>, _Atomic, _Static_assert
C172018Bug fixes to C11; no new features
C232023nullptr, _BitInt, #embed, typeof

Request a specific standard with gcc: -std=c99, -std=c11, -std=c17, or -std=c23. The default in recent gcc is roughly C17 with GNU extensions.

For maximum portability, target C99 (-std=c99). For modern features, target C11 (-std=c11). Most production code targets one of these.

POSIX extensions and feature-test macros

Standard C does not include POSIX APIs: fork, pipe, pthreads, sigaction, openat, etc. To use them, define feature-test macros before any #include (or via -D on the command line):

#define _POSIX_C_SOURCE 200809L  /* POSIX.1-2008 */
#include <unistd.h>
#include <pthread.h>

Or in the Makefile:

CFLAGS += -D_POSIX_C_SOURCE=200809L

Common feature-test macros:

MacroWhat it enables
_POSIX_C_SOURCE=200809LPOSIX.1-2008 (most POSIX APIs)
_XOPEN_SOURCE=700X/Open (superset of POSIX)
_GNU_SOURCEGNU extensions (Linux-specific, non-portable)
_DEFAULT_SOURCEglibc default (POSIX + BSD)

Do not use _GNU_SOURCE in code that must run on macOS or BSD — use _POSIX_C_SOURCE instead.

Exact-width integer types

int is not 32 bits everywhere. Use <stdint.h> for exact sizes:

#include <stdint.h>

int32_t  x = -1000000;   /* exactly 32 bits, signed */
uint64_t y = 18000000000ULL; /* exactly 64 bits, unsigned */
uint8_t  byte = 0xFF;    /* exactly 8 bits */

For sizes that just need to be "at least N bits", use the minimum-width types: int_least32_t, uint_least16_t.

For the fastest type with at least N bits: int_fast32_t.

Endianness

Multi-byte integers are stored differently on different architectures:

  • Little-endian (x86, x86-64, ARM default): least significant byte first.
  • Big-endian (some ARM modes, SPARC, PowerPC): most significant byte first.

This matters when reading binary file formats or network packets:

#include <arpa/inet.h> /* POSIX: network byte order conversion */

uint32_t host_value = 12345678;
uint32_t network_value = htonl(host_value); /* host to network (big-endian) */
uint32_t back = ntohl(network_value);        /* network to host */

htonl/ntohl (host to network long / network to host long) and htons/ntohs (for uint16_t) convert between host byte order and network byte order (big-endian). Always use them for network data.

Checking at compile time: _Static_assert

#include <stdint.h>

_Static_assert(sizeof(int) == 4, "Expected 32-bit int");
_Static_assert(sizeof(void *) == 8, "Expected 64-bit pointer");

_Static_assert (C11) causes a compile error if the condition is false, with the given message. Use it to document and enforce portability assumptions at the point where they matter.

Compiler-specific attributes

When using compiler-specific features, guard them:

#if defined(__GNUC__) || defined(__clang__)
  #define LIKELY(x)   __builtin_expect(!!(x), 1)
  #define UNLIKELY(x) __builtin_expect(!!(x), 0)
#else
  #define LIKELY(x)   (x)
  #define UNLIKELY(x) (x)
#endif

This lets you use __builtin_expect for branch prediction hints while keeping the code compilable on non-GCC compilers.

Compile on at least two platforms. The single most effective portability practice: compile your code with both gcc and clang (-Wall -Wextra -pedantic -std=c11) and fix all warnings. Clang's diagnostic messages are often more precise. Divergence between the two compilers reveals assumptions baked into your code.

Where to go next

Next: the mini shell challenge — you will build a minimal interactive shell that combines fork, exec, waitpid, pipes, and signal handling into a complete systems program.

Finished reading? Mark it complete to track your progress.

On this page