This episode makes C programs run on many platforms: writing portable code with the latest C standard, conditional compilation for Windows, Linux, and macOS, using cross-platform libraries with build portability, as well as testing on many architectures.

C's promise is portability: code written once, compiled anywhere. In reality, different operating systems have different APIs for the same thing. Episode 20 teaches how to honor that promise practically.
The foundation is following the C standard. Code that only uses standard features — including <stdint.h>, <string.h>, and the standard library — runs on every platform without modification. Differences only appear when touching system APIs.
For those differences, C provides conditional compilation, and for building, CMake handles platform differences centrally. Finally, testing on many architectures proves the portability promise with real evidence.
The basic int and long types can have different sizes across platforms. For portable code, use types from <stdint.h>, whose size and signedness are guaranteed:
#include <stdint.h>
#include <stdio.h>
int main(void) {
int64_t total = 0;
uint32_t id = 100;
printf("id: %" PRIu32 "\n", id);
return 0;
}int64_t and uint32_t are guaranteed to keep their sizes on every platform. Note the PRIu32 in the format string — this macro adapts the format specifier for platforms that use int or long. Using these macros avoids format warnings on 32-bit.
Compile with -std=c17 and -pedantic to make sure code only uses standard features. Functions like snprintf have been in the C99 standard and are portable. Avoid non-standard functions like strcpy_s and _sopen that only exist on Windows unless you use an abstraction layer.
When code must use system-specific APIs, separate it with preprocessor directives. An example of distinguishing screen clearing:
#ifdef _WIN32
#define BERSIHKAN "cls"
#else
#define BERSIHKAN "clear"
#endif
#include <stdio.h>
int main(void) {
printf("perintah: %s\n", BERSIHKAN);
return 0;
}The #ifdef _WIN32 block selects a definition based on the platform. _WIN32 is defined automatically by the MSVC and MinGW compilers, while POSIX platforms use the else branch. These macros are documented by the compiler and must not be defined manually.
Even better: hide platform differences behind your own functions. One header declares the interface, and separate implementation files per platform are chosen at build time. You'll see this pattern used by large libraries; CMake chooses which files to compile.
Using cross-platform libraries through CMake avoids manual library management. find_package locates installed libraries and links them portably:
cmake_minimum_required(VERSION 3.20)
project(app LANGUAGES C)
find_package(Threads REQUIRED)
add_executable(app main.c)
target_link_libraries(app PRIVATE Threads::Threads)find_package(Threads REQUIRED) finds the thread library under a portable name, and Threads::Threads links it without guessing the -pthread or -lpthread flags per platform. This is CMake's power: platform differences are resolved in one place.
Keep system dependencies minimal. The more system APIs you use directly, the more spots you must branch with #ifdef. Libraries like libuv or SDL provide cross-platform APIs for I/O, networking, and windowing, so you don't write #ifdef yourself.
Run the same tests on several operating systems to prove portability. GitHub Actions supports a job matrix:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}The matrix os: [ubuntu-latest, macos-latest, windows-latest] runs the same pipeline on three systems. Errors that only appear on one platform are found before users find them.
Besides operating systems, different architectures have different endianness and alignment. QEMU lets you run binaries for another architecture on your machine, and the cross toolchains from episode 19 build them. The combination validates code for ARM before deploying to a real target.
Warning
Run tests on all targets before release. Code that passes on Linux but fails on Windows is common; the differences include file byte order, path separators, and newline behavior.
Key takeaways:
<stdint.h>.In the next episode 21 we will discuss embedded and low-level systems — C for embedded and bare-metal programming, startup code, linker scripts, and memory maps, real-time constraints and deterministic behavior, up to embedded debugging with JTAG and simulators.