TRT
Sign in

Foundations · Module 2 · Lesson 9 of 10

Portable C, POSIX, and Platform Boundaries

Learn which guarantees come from the C language, which come from POSIX, and which depend on Linux or a CPU architecture so you can write code that is portable on purpose.

FreeAbout 14 minutes

By the end you can

  • Distinguish ISO C, POSIX, operating-system, and CPU-ABI guarantees
  • Recognize implementation-defined, unspecified, and undefined behavior
  • Decide when to isolate platform-specific code behind a small interface
  • Use feature checks and documentation instead of assuming one machine's behavior is universal

Builds on Storage Classes, Scope, and Linkage.

01 · A question first

You compile a program on Linux and it works. On another operating system, `fork()` is missing, `sizeof(long)` changes, and a bitfield has a different layout. Which parts of your program were guaranteed by C, and which parts were only true on your original platform?

Make a guess, then step through the trace below to check it.

02 · Watch it run

Classifying a program feature by its source of guaranteestep 1 of 3
int count = 7;size_t bytes = sizeof count;#ifdef __linux__int fd = open("data.bin", O_RDONLY);#endif
  1. guarantee 0count: ISO C(looked at in this step)
  2. guarantee 1sizeof count: ISO C, implementation-defined result
  3. guarantee 2open: not yet compiled

The declaration and sizeof operator are defined by the ISO C language, although the exact size of int is implementation-defined.

Machine work: compile ISO C expression; select implementation-defined int width

03 · The idea

Start with the problem

C is portable, but not every C program is portable. strlen, memcpy, and sizeof belong to the C language and its standard library. fork, epoll, and pthread_create belong to POSIX or a particular operating system. Register names, stack alignment, and calling conventions may belong to a CPU ABI.

The important skill is not avoiding platform code. Systems programs need it. The skill is knowing which promise you are relying on and keeping that dependency in a visible place.

Four layers of guarantees

Think of a C program as four layers:

  1. ISO C defines the language, object model, expressions, and standard library.
  2. POSIX adds Unix-style files, processes, signals, threads, and sockets.
  3. The operating system adds behavior specific to Linux, macOS, or another kernel.
  4. The CPU ABI defines calling conventions, binary formats, register rules, and alignment details.

An API's location in this stack determines how widely it can be reused. malloc is ISO C. mmap is POSIX. epoll is Linux-specific. The System V AMD64 ABI is specific to a family of Unix-like systems and x86-64 processors.

Three kinds of surprising behavior

Implementation-defined behavior is chosen by the compiler or platform and documented. The width of int is an example. Your program may use it, but it should not silently assume one answer.

Unspecified behavior allows one of several results without requiring the implementation to document which result it chooses. Do not build correctness on a choice the language does not promise.

Undefined behavior gives the implementation no requirements. Reading past an array, using a pointer after its object lifetime, or overflowing a signed integer can make the compiler assume the impossible never happens. A result that “worked in a debug build” is not a guarantee.

Isolating platform code

Keep the policy of a program separate from the mechanism of a platform:

c
/* file_store.h */
int store_read(const char *path, void *buffer, size_t capacity);

The rest of the program can call store_read without knowing whether the implementation uses ISO C streams, POSIX file descriptors, or a test double. Put the platform implementation in one source file and document its required operating system.

Use compiler feature checks for optional behavior, but do not use them as a substitute for understanding:

c
#if defined(__linux__)
/* Linux implementation */
#else
/* Portable fallback or a clear build error */
#endif

Prefer standard types when the requirement is about a property rather than a platform name. Use size_t for object sizes, ptrdiff_t for pointer differences, and fixed-width types from <stdint.h> when a protocol or file format requires exactly 32 or 64 bits. Use <inttypes.h> format macros when printing those values. This makes intent visible and prevents a reader from assuming that an int or long has the same width everywhere.

Portability also includes behavior, not only compilation. Two systems can disagree because of text encoding, path syntax, line endings, byte order, time zones, or filesystem permissions. Define these choices in the API: decide whether a path is a UTF-8 byte sequence, whether a timestamp is UTC, and whether a file operation follows symbolic links. A precise contract is easier to port than undocumented assumptions.

Read documentation in layers

When you meet an unfamiliar API, ask:

  1. Is it part of ISO C, POSIX, the operating system, or the CPU ABI?
  2. What header declares it and what feature-test macro enables it?
  3. What does it return on failure, and how is the error reported?
  4. Which ownership, lifetime, alignment, and thread-safety rules apply?

For example, malloc returns ISO C allocated storage and uses a null pointer to report failure. open is a POSIX API that returns a file descriptor and reports failure with -1 plus errno. epoll is Linux-specific and has Linux-specific readiness semantics. Treating all three as “a function that returns something” hides important differences.

Build for the target you mean

A compiler flag can make a dependency explicit. Use a standard selection such as -std=c17, warnings such as -Wall -Wextra, and feature-test macros required by the platform documentation. Cross-compilation should name the target architecture and operating system rather than relying on the host's defaults. If a feature is required, fail during configuration with a useful message instead of compiling a partial implementation that fails later.

Keep the portable core testable on its own. Platform adapters can have a small integration test that runs only on the supported system. This gives two clear feedback loops: the language-level behavior is portable, while the platform behavior is verified where it is intended to run.

Write the assumption beside the code, not only in a team document.

Where this is used

This boundary becomes essential in the next modules. Core lessons use portable C where possible. The POSIX I/O, process, thread, and networking modules intentionally use Unix APIs. The Expert runtime lessons additionally depend on an x86-64 ABI. Naming those boundaries lets learners remember the concept instead of memorizing accidental details from one machine.

04 · Prove it

Question 1 of 3 · answer from memory

Which guarantee does `sizeof(int)` provide?

Something wrong with this lesson? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save