C Portable

C portable code
Login

C portable code

Introduction

This repository is a collection of portable C source code snippets.
The code should run on Linux, BSD, Android, MS Windows, and Mac/iOS platforms.
(I don't have access to any Mac OSX nor an iOS device, so I haven't tested the code on those platforms.)

The focus here is set on portable source code, even if we also mention other aspects of portability, such as build system, compiler versions etc.

This repository is minimalistic by design.
The purpose is to show how to port small portions of code between platforms, to be used for copy-paste.
The purpose is NOT to be a portable library for anything and everything.

Make it all portable: The build system. The compiler. The C source code. The program execution.

The build system: Make make portable, but keep the Makefile simple

The build system is mainly made up of these files:

The additional files Makefile.dist.* and Makefile.icon are less important, only included for completeness.

The build system: Special targets

Here follow a few additional "special" make rules.
They are considered off-topic, and only included for completeness.

make dist

The dist rule describes how to prepare a package/installer for the current platform.
This differs very much between platforms, and even between packaging systems on the same platform (i.e. Linux).
This is why this rule is only a stub which prints the name of some package/installer systems for each platform.
Details:

make icon

The icon rule is used to associate an icon with an application.
This rule is only needed for MS Windows, which has the pecularity to embed the application icon within the executable.
The icon is compiled into an object file use the Resource Compiler.
It is not mandatory to embed an icon in a MS Windows application.
Anyway, if it is, the icon must be chosen and linked into the application at compile time, before invoking the linker.
That is why it must be consider as a make rule.
Except for MS Windows, this step could perfectly be part of the dist rule mentioned above.
(This makes MS Windows unique, once again. Doh.)

AFAIK, on all other platform, associating an icon with an application is always optional.
(Many text-mode executable never use an icon anyway.)

Most Linux and BSD desktop environments use the common standard set by Freedesktop.
See xdg-desktop-icon and xdg-desktop-resource.

Mac OS is another world, with its completely different steps to prepare an application for distribution.

Android uses a Manifest file to set the Application Icon

Details:

The compiler

I normally use these flags, which should be portable for both gcc and clang on all platforms:

    CFLAGS = -g -Wall -Wextra -Werror -pedantic -ansi
  1. Enable warnings: -Wall -Wextra -Werror
  2. Restrict extensions and features that may vary between platforms: -pedantic -ansi

    • -pedantic: "reject all programs that use forbidden extensions"
    • -ansi: "turns off certain features of GCC that are incompatible with ISO C90"

Compiler versions

This is another off-topic, but using "compiler containers" is quite an easy way to check if our code is "compiler version portable".
For example, using Dockers, we can test 4 different GCC versions from the command line (assumes that Docker is already installed):

    DOCKER_GCC="docker run --rm -v \"$PWD\":/usr/src/myapp -w /usr/src/myapp gcc"
    # Run 'make' using GCC version 6,7,8,9
    for c in 6 7 8 9; do \
        $DOCKER_GCC:$c make; \
    done
    # Compile a single C file using GCC version 6,7,8,9
    for c in 6 7 8 9; do \
        $DOCKER_GCC:$c gcc -o myapp myapp.c; \
    done

Another way is to change CC inside a Makefile:

    CC = docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp gcc:9 gcc

More about GCC and Clang Docker images:

The C source code

Two ways to split platform-specific code:

Alternative 2 is used in this repository.

Make it abstract

Make code portable at compile-time:

One include file per OS: myos-includes.h
A disadvantage of this approach is that using separate files for each platform, with a long list of platform-specific functions, may cause code for different platforms to "diverge".
Using small files with small functions tend to work better in the long run.

One "normalized" variable for the current OS: myos-defs.h
This alternative requires less code, as it deals with platform-specific quirks only when required (i.e. using #if (MYOS == MYOS_MSYS2) ...).

We will use Alternative 2 here.

Portable Makefiles (assumes using make to compile)

Platform-specific source code should obviously only be compiled for that target platform.

%%% https://stackoverflow.com/questions/714100/os-detecting-makefile %%%

Make code portable at run-time

Another, less common approach, to conditionally load and execute code at run-time, using dlopen() or LoadLibrary().
This is not commonly used, as the different file formats ELF, PE, and Mach-O are not compatible between each other, and thus not executable on other platforms anyway.

Examples of exceptions to this may be:

Design as plugins

Anyway, an application could be designed to load platform-dependent code depending on the detected platform at run-time.
Features could be added by as (nested) load-at-run-time-libraries, or plugins, from the main application.
If a feature/plugin hasn't been developed for a particular platform yet, the main application could return a message such as: Feature X not implemented for platform Y.
The feature/plugin may then be developed and added at a later stage, without changing the main application.

Using this "plugin approach" makes an application modular by design.

Detect OS version

To detect the OS at runtime, it would be convenient to have a "normalized", cross-platform function for that, let's say osinfo().
Existing API:s already gives us the OS info, but they are not cross-platform:

First, let's create a cross-platform application for each one, uname1 and version1.
Additional information such as detecting if we are running in a GUI or terminal environment would also be helpful.
(GUI would always be set to true for MS Windows and Mac OSX, but not always for Linux/*BSD systems.)
The goal is to merge all this "normalized information" into x_osinfo() and its demo program osinfo1.

Additional OS dependent information does not need to go into x_osinfo().
An example: Let's say that we want to use a particular regular expression in our code by calling regexec(), but a bug in the GNU/Linux version of regex (i.e. glibc) may crash our program, unless we are not running at least version 2.31 of glibc.
In this case, confstr(3) and _CS_GNU_LIBC_VERSION may give us that information.
Even if this is system wide information, this kind of information does not make sense on other platforms, so it is not included in x_osinfo().

%%% Examples of "normalized" information would be:

Detect OS version: uname1

The first example of cross-platform code is uname1.c.

To no surprise, uname(3) (short for "unix name"), may be used to obtain OS information on UNIX-like platforms.
Calling uname fills in the 5 members in the utsname struct:

    sysname  : Name of the operating system implementation.
    nodename : Network name of this machine.
    release  : Release level of the operating system.
    version  : Version level of the operating system.
    machine  : Machine hardware platform.

MS Windows has no such thing as uname or utsname, so check out uname-win32.c for the workaround.

%%% TODO %%% MOVE OUTPUT TO ITS OWN FILE

uname1: MSYS2

    system name = MSYS_NT-10.0
    node name   = Lenovo-PC
    release     = 2.11.2(0.329/5/3)
    version     = 2018-11-26 09:22
    machine     = x86_64

uname1: MS Windows / MinGW64

    system name = Windows
    node name   = Lenovo-PC
    release     = 18363
    version     = 10.0
    machine     = x86_64

uname1: FreeBSD

%%% TODO

uname1: OpenBSD

%%% TODO

uname1: NetBSD

%%% TODO

uname1: MacOSX

Detect MacOS version

%%% TODO

uname1: iOS

I have no access to an iOS device to test this.

uname1: Linux

    system name = Linux
    node name   = lenovovboxus
    release     = 4.4.0-176-lowlatency
    version     = #206-Ubuntu SMP PREEMPT Fri Feb 28 05:51:26 UTC 2020
    machine     = x86_64

uname1: Android / Termux

    system name = Linux
    node name   = localhost
    release     = 4.4.177-18057978
    version     = #1 SMP PREEMPT Fri Mar 27 23:41:18 KST 2020
    machine     = aarch64

Here we see a portability issue not related to MS Windows, an "Android curiosity":
We would like to see system name = Android, but instead we see system name = Linux.
There is no way to deduce from the output that this is Android.
We may connect to an Android/Termux shell and run uname -o or uname --operating-system:

    Android

How come?
For other options, the uname executable (part of coreutils) uses the utsname struct.
But when using -o, uname does no system calls, but simply prints the macro HOST_OPERATING_SYSTEM.
The HOST_OPERATING_SYSTEM macro is hardcoded to Android and only defined when building coreutils, so we cannot use this macro in our C source code.

The workaround for Android is to execute uname -o from within our source code, and parse the output (Android)).
That leads us to child processes and pipes.
See more how to Run a program inside another program

%%% Detect Windows version etc... %%%

Detect OS version: uname1

More detailed Windows version info

Detect OS version: version1

To get additional OS info for MS Windows, there is a set of MS Windows Version Helper functions.

Detect OS version: osinfo1

Run a program inside another program

%%% bla bla

There is:

To execute uname on Android bla bla %%%

https://stackoverflow.com/questions/13839935/forking-and-createprocess

Run a program inside another program

This implies starting a process

Processes

Cygwin ps

Links

DLL/SO:

Macros:

Containers for compilers: