Question

Detect Operating Systems in the C Preprocessor

ccppcross-platformc-preprocessoros-detection

Question

For cross-platform C or C++ code that must compile on macOS, iOS, Linux, and Windows, how can the target operating system be detected reliably during preprocessing?

Short Answer

You will learn how C preprocessors select platform-specific code using compiler-defined macros. You will also learn why macOS and iOS require special handling, how to organize checks safely, and why platform detection is not part of standard C.

Concept

The C preprocessor runs before C code is compiled. It can include or exclude sections of a source file with directives such as #if, #elif, and #endif.

Compilers commonly define macros that describe their target platform. For example:

  • Windows targets define _WIN32.
  • Linux targets commonly define __linux__.
  • Apple targets define __APPLE__.

However, these macros are mostly compiler and platform conventions, not rules defined by the ISO C standard. A portable C program cannot use one standard macro that identifies every operating system.

Apple platforms need an extra step: both macOS and iOS are Apple targets, so both can define __APPLE__. After detecting an Apple target, include Apple's TargetConditionals.h header and inspect its TARGET_OS_* macros.

Platform checks matter when code genuinely depends on an operating-system feature, such as file paths, process APIs, dynamic libraries, or native user-interface integration. Keep these checks small and isolate them behind your own project interface when possible.

Mental Model

Think of preprocessing as packing instructions for several destinations.

  • A compiler target is the shipping label: Windows, Linux, or an Apple platform.
  • #if defined(...) is a sorting machine that sends the matching instructions into the final source file.
  • Code in non-matching branches is removed before the compiler checks it.

For Apple, __APPLE__ says only “this package goes somewhere in the Apple ecosystem.” TargetConditionals.h is the detailed address that tells you whether that destination is iOS or macOS.

Take Quiz

Syntax and Examples

Use defined(MACRO) inside #if or #elif to test whether a compiler-defined macro exists.

#if defined(_WIN32)
    /* Windows-specific code */
#elif defined(__linux__)
    /* Linux-specific code */
#elif defined(__APPLE__)
    /* An Apple platform; determine which one next. */
#else
    /* An unsupported or unknown platform. */
#endif

A practical cross-platform header can convert platform-specific macros into project-owned macros:

/* platform.h */

#if defined(_WIN32)
    #define PROJECT_WINDOWS 1

#elif defined(__APPLE__)
    #include <TargetConditionals.h>

    #if TARGET_OS_IPHONE
        #define PROJECT_IOS 1
    #elif TARGET_OS_OSX
        #define PROJECT_MACOS 1
    #else
        #error "Unsupported Apple platform"
    #endif

#elif defined(__linux__)
    #define PROJECT_LINUX 1

#else
    #error "Unsupported operating system"
#endif

Other source files can now use PROJECT_WINDOWS, PROJECT_IOS, PROJECT_MACOS, and PROJECT_LINUX rather than repeatedly checking compiler-specific names.

On Windows, use _WIN32 for both 32-bit and 64-bit Windows. Despite its name, _WIN32 is also defined by typical 64-bit Windows compilers. Use _WIN64 only when you specifically need to distinguish 64-bit Windows.

Step by Step Execution

Consider this program when compiled for an iOS target:

#include <stdio.h>

#if defined(_WIN32)
    #define PLATFORM_NAME "Windows"

#elif defined(__APPLE__)
    #include <TargetConditionals.h>

    #if TARGET_OS_IPHONE
        #define PLATFORM_NAME "iOS"
    #elif TARGET_OS_OSX
        #define PLATFORM_NAME "macOS"
    #endif

#elif defined(__linux__)
    #define PLATFORM_NAME "Linux"
#endif

int main(void) {
    printf("Building for %s\n", PLATFORM_NAME);
    return 0;
}

For an iOS build, preprocessing proceeds as follows:

  1. _WIN32 is not defined, so the Windows branch is discarded.
  2. __APPLE__ is defined, so the Apple branch is selected.
  3. TargetConditionals.h provides Apple target macros.
  4. TARGET_OS_IPHONE is true for an iOS target, so PLATFORM_NAME becomes "iOS".
  5. The macOS and Linux branches are discarded.
  6. The compiler receives code effectively equivalent to:
#include <stdio.h>
#define PLATFORM_NAME "iOS"

int main(void) {
    printf("Building for %s\n", PLATFORM_NAME);
    return 0;
}

Real World Use Cases

  • File-system behavior: choose Windows path handling or POSIX-oriented behavior where necessary.
  • Native APIs: include Windows headers on Windows and Apple framework headers only on Apple targets.
  • Dynamic libraries: use platform-specific library-loading implementations behind one shared interface.
  • Build diagnostics: stop compilation with #error when an unsupported platform is targeted.
  • Mobile versus desktop behavior: compile iOS-only integrations separately from macOS desktop integrations.
  • Testing and logging: report the compiled target platform in diagnostics or test output.

Prefer checking for a required capability when you can. For example, if you only need a compiler feature, test that feature rather than assuming every Linux compiler supports it in the same way.

Take Quiz

Real Codebase Usage

In production code, put platform detection in one dedicated header, often named platform.h or config_platform.h. Translate external compiler macros into stable project names.

/* platform.h */
#if defined(_WIN32)
    #define PROJECT_PLATFORM_WINDOWS 1
#elif defined(__linux__)
    #define PROJECT_PLATFORM_LINUX 1
#else
    #define PROJECT_PLATFORM_OTHER 1
#endif

Then keep platform-specific code behind a small interface:

/* system_paths.c */
#include "platform.h"

const char *temporary_directory(void) {
#if defined(PROJECT_PLATFORM_WINDOWS)
    return "C:\\Temp";
#elif defined(PROJECT_PLATFORM_LINUX)
    return "/tmp";
#else
    return "/tmp";
#endif
}

Useful project practices include:

  • Centralization: avoid scattering _WIN32 and __linux__ throughout the codebase.
  • Early failure: use #error for targets your project cannot support.
  • Capability checks: test for available headers, functions, or compiler extensions when that is the real requirement.
  • Build configuration: let CMake, Meson, Autoconf, or another build system detect libraries and features, then generate configuration macros.
  • Minimal branches: isolate native implementation files rather than wrapping large portions of application logic in preprocessor directives.

Common Mistakes

Assuming _WIN32 means only 32-bit Windows

This is incorrect:

#if defined(_WIN32)
    /* 32-bit Windows only -- incorrect assumption */
#endif

_WIN32 is normally defined for both 32-bit and 64-bit Windows targets. Test _WIN64 only if pointer size or another 64-bit-specific detail truly matters.

Treating __APPLE__ as macOS only

This is incomplete:

#if defined(__APPLE__)
    #define PLATFORM_NAME "macOS"  /* May also be iOS. */
#endif

On Apple targets, include TargetConditionals.h and test TARGET_OS_IPHONE or TARGET_OS_OSX.

Testing Apple macros before including Apple definitions

TARGET_OS_IPHONE comes from an Apple header. Do not assume it exists in a source file that has not included the relevant header.

#if TARGET_OS_IPHONE  /* May be undefined here. */
#endif

Instead, first identify Apple with __APPLE__, then include <TargetConditionals.h>.

Using nonstandard macro names without checking the compiler

Some compilers may provide aliases such as linux or __linux, but __linux__ is the commonly used spelling. Check your compiler's predefined macro documentation or inspect its macro output when supporting unusual toolchains.

Forgetting an unknown-platform branch

Without a final branch, unsupported builds may silently use the wrong fallback.

#else
    #error "This operating system is not supported"
#endif

Detecting an OS when a feature check is better

An OS check does not guarantee that a particular library, API version, or compiler extension exists. Use your build system or feature-test macros when the code depends on a specific capability.

Comparisons

NeedBest approachWhy
Select Windows-only code#if defined(_WIN32)Covers typical 32-bit and 64-bit Windows targets.
Select Linux-only code#if defined(__linux__)Common compiler-defined Linux target macro.
Select any Apple target#if defined(__APPLE__)Identifies Apple targets, but does not distinguish them.
Distinguish iOS and macOS#include <TargetConditionals.h> and TARGET_OS_*Apple-supported target classification.
Select 64-bit Windows specifically#if defined(_WIN64)Use only when 64-bit Windows differs materially.
Check whether an API existsBuild-system or feature checkMore accurate than guessing from the OS.

#ifdef NAME and #if defined(NAME) are similar for a simple existence test:

#ifdef _WIN32
    /* Valid */
#endif

#if defined(_WIN32)
    /* Also valid; easier to combine with other conditions. */
#endif

defined(...) is often preferred in complex conditions:

#if defined(__linux__) && defined(__x86_64__)
    /* Linux on x86-64 */
#endif
Take Quiz

Cheat Sheet

/* Windows: 32-bit or 64-bit */
#if defined(_WIN32)
#endif

/* 64-bit Windows only */
#if defined(_WIN64)
#endif

/* Linux */
#if defined(__linux__)
#endif

/* Apple platform, then refine the target */
#if defined(__APPLE__)
    #include <TargetConditionals.h>

    #if TARGET_OS_IPHONE
        /* iOS target */
    #elif TARGET_OS_OSX
        /* macOS target */
    #else
        /* Another Apple platform or unsupported target */
    #endif
#endif
  • Preprocessor platform macros are not standardized by ISO C.
  • Use defined(MACRO) to test whether a macro exists.
  • _WIN32 includes 64-bit Windows; _WIN64 means 64-bit Windows.
  • __APPLE__ is not enough to identify macOS versus iOS.
  • Keep detection in one project header and define project-owned macros.
  • Add #error for platforms the project does not support.
  • Prefer feature detection when your real question is “is this API available?”

FAQ

Is there a standard C macro for detecting the operating system?

No. ISO C does not define universal operating-system detection macros. These macros come from compilers and platform SDKs.

Why does 64-bit Windows define _WIN32?

The macro indicates the Windows API family rather than strictly the CPU bit width. Typical 64-bit Windows builds define both _WIN32 and _WIN64.

How do I distinguish macOS from iOS in C?

First test __APPLE__, include <TargetConditionals.h>, then test TARGET_OS_OSX for macOS or TARGET_OS_IPHONE for iOS.

Does __APPLE__ mean the code is running on an Apple device?

It describes the compilation target, not a runtime detection result. Preprocessor checks happen while building the program.

Can I use #ifdef __linux__ instead of #if defined(__linux__)?

Yes. Both test whether the macro is defined. #if defined(...) is convenient when combining several conditions.

Should every source file check _WIN32 and __linux__ directly?

Usually no. Put detection in a single project header and expose project-specific macros or platform-neutral functions.

How can I see macros defined by my compiler?

Most compilers have an option to output predefined macros. Consult the compiler documentation, especially when using a cross-compiler or an uncommon toolchain.

Take Quiz

Mini Project

Description

Create a small C program that reports the operating system it was compiled to target. The project demonstrates centralized preprocessor detection and the Apple-specific distinction between iOS and macOS.

Goal

Build a program that prints Windows, Linux, macOS, or iOS based on the compiler target.

Requirements

  • Create a header that determines one supported target platform at preprocessing time.
  • Use _WIN32 for Windows and __linux__ for Linux.
  • Use __APPLE__ and TargetConditionals.h to distinguish iOS from macOS.
  • Define one PROJECT_PLATFORM_NAME macro for the selected platform.
  • Produce a clear compilation error for unsupported targets.
Take Quiz

Keep learning

Related questions

View more