Learn C Language - Cross-platform Development
Episode 20 of 24

Learn C Language - Cross-platform Development

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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.

Writing Portable Code with the Latest C Standard

Using Types with Clear Sizes

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:

Portable types
#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.

Following the Standard and Avoiding Extensions

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.

Conditional Compilation

Separating Code Per Platform

When code must use system-specific APIs, separate it with preprocessor directives. An example of distinguishing screen clearing:

Conditional compilation per platform
#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.

Hiding Differences Behind an Interface

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.

Cross-Platform Libraries and Build Portability

Libraries Using CMake

Using cross-platform libraries through CMake avoids manual library management. find_package locates installed libraries and links them portably:

CMake with a dependency
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.

Avoiding Fragile Dependencies

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.

Testing on Many Architectures and Toolchains

Multi-Platform CI

Run the same tests on several operating systems to prove portability. GitHub Actions supports a job matrix:

Multi-platform build 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.

Testing on Other Architectures

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.

Closing

Key takeaways:

  • Code using only standard C features is portable without modification.
  • Use uint32_t, int64_t, and the PRI macros from <stdint.h>.
  • Conditional compilation uses #ifdef and automatic compiler macros.
  • Hide platform differences behind functions and separate files.
  • CMake handles cross-platform dependencies through find_package.
  • A CI matrix tests code on many OSes and architectures.

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.

Learn C Language - Cross-platform Development | Learn C Language