Writing Portable C Across Different Pointer Sizes
Writing portable software in C across architectures with varying pointer sizes—such as 16-bit microcontrollers, 32-bit embedded systems, and 64-bit servers—requires strict adherence to data abstraction, standardized integer types, and architecture-neutral programming practices. This article explains how developers decouple pointer logic from fixed-width assumptions, select appropriate types from standard headers, manage structure alignment, and format output safely to ensure code compiles and runs reliably regardless of the underlying memory model.
Use Standard Integer Types for Pointer Arithmetic
A common portability pitfall is casting a pointer to an
int or long. In modern computing, data models
vary significantly:
- ILP32:
int,long, and pointers are 32 bits. - LP64 (most Unix/Linux 64-bit systems):
intis 32 bits, whilelongand pointers are 64 bits. - LLP64 (64-bit Windows):
intandlongare 32 bits, whilelong longand pointers are 64 bits.
Because sizeof(long) does not reliably match pointer
width across platforms, C99 introduced <stdint.h>,
which provides:
intptr_t: A signed integer type guaranteed to hold a pointer value without truncation.uintptr_t: An unsigned integer type guaranteed to hold a pointer value without truncation.
If an address must be stored or manipulated as an integer (for
instance, during low-level memory mapping or hashing), developers must
use uintptr_t instead of basic integer types.
Standardize Sizes
and Offsets with <stddef.h>
Using basic types for memory allocation and buffer management leads
to overflow bugs or truncation on architectures with larger memory
spaces. C provides dedicated types in <stddef.h>:
size_t: The unsigned integer type representing the size of any object in bytes (the result of thesizeofoperator). It adapts dynamically to the memory addressing limit of the target architecture.ptrdiff_t: The signed integer type used to represent the result of subtracting one pointer from another within the same array.
Using unsigned int instead of size_t can
cause buffer-length limits or overflow vulnerabilities on 64-bit
targets.
Avoid Binary Serialization of In-Memory Pointers
Because pointer sizes differ across targets, raw in-memory structures cannot be written directly to disk or transmitted over a network if interoperability between different architectures is required.
To maintain portability:
- Never transmit raw pointers: Serialize only semantic identifiers, offsets, or array indices.
- Account for struct padding and alignment: A
structure containing a pointer will vary in total size and alignment
boundaries between 32-bit and 64-bit systems. Use explicit packing
(
#pragma packor attributes) cautiously, or preferably, serialize fields explicitly using fixed-width types likeuint32_toruint64_t.
Use Correct Format Specifiers
Printing pointer values or memory sizes using incorrect format specifiers causes undefined behavior due to stack mismatching during variadic function calls.
- Use
%pfor printing pointer addresses (casting the argument to(void *)). - Use
%zufor variables of typesize_t. - Use
%tdfor variables of typeptrdiff_t. - Use macros from
<inttypes.h>(such asPRIxPTRorPRIdPTR) when formattinguintptr_torintptr_t.
Enforce Compiler Checks and Static Analysis
Modern compilers can detect pointer-width incompatibilities during compilation:
- Enable strict warnings such as
-Wall -Wextra -Wconversion -Wpointer-to-int-castin GCC and Clang. - Use static assertions via
_Static_assert(orstatic_assertin C11 and later) to validate critical architectural invariants at compile time rather than relying on runtime checks. - Compile and test code against multiple cross-compilation toolchains (e.g., targeting both 32-bit ARM and 64-bit x86_64) within continuous integration pipelines to catch pointer truncation errors early.