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:
In this example,make
is used as the build tool.
The advantage ofmake
is that it is simple/straightforward to used, and preinstalled on most platforms. On MS Windows, use make for MSYS2/MinGW64 or for Cygwin.
The disadvantage is thatmake
is not completely platform independent itself (GNU vs. BSDmake
).
File dependency for the code examples is very basic, so it should be a simple task to use your preferred build tool instead (CMake
,Autotools
,Bazel
,Meson
etc).
Those tools may be better adapted to "platform independency", butmake
gives us the minimalistic approach we want to achieve.The compiler:
This code has been tested withgcc
andclang
.
The Best Practices are well-known: enable all warnings when compiling.
To make code portable between compiler versions, test with as many compiler versions as possible.
Using "compiler containers" is an easy way to run several compiler versions on the same machine.The C source code:
Here goes the focus for this repository.
We try to use common code for all platforms whenever possible.
The C language itself is quite portable, as long as we stick to the standards, avoiding GNU extensions, Win32 specific syntax, etc.
For UNIX-like platforms (Linux/BSD/Mac), system calls and API are generally quite similar between platforms, and not too hard to port between each other.
In most cases, MS Windows is the elephant in the room, with its own, quite different API (which is generally more "clumsy" than its UNIX equivalent, as we will see in the example code).
This makes it a natural choice to design portable code to be "UNIX-like" (as opposed of "Win32-like").Anyway, don't reinvent the wheel, especially for complex things; there are many portable C libraries out there.
GLib is a generic and cross-platform library,and is often used together with GTK for creating GUI:s.
For 2D graphics, there is Cairo, and for 3D graphics, there is Vulkan.
For portable multimedia libs, consider SDL or Allegro.Often, one of the big issues when porting source code, is to adapt UNIX code to Win32, due to the "clumsy" Win32 API.
For this, consider GnuWin32 and UnxUtils.
These projects are a bit outdated, but it's still worth reading the source code.The program execution:
An additional step to make code portable at compile-time, is to make it "run-time portable".
One approach for development is to create a small "base" or "bootstrap" application, which may be (almost) identical for all platforms.
The rest of the program may be made up of a set of "OS specific plugins".
Once the current OS has been detected, the main program conditionally loads the plugins for the current OS, using either dlopen() or LoadLibrary().
The build system: Make make
portable, but keep the Makefile simple
The build system is mainly made up of these files:
GNUmakefile
: GNUmake
syntax, read on Linux and MS Windows system, and also on Mac OS by default. May be used on FreeBSD withgmake
BSDmakefile
: BSDmake
syntax, read on FreeBSD. May optionally be read on Mac OS, usingbmake
.Makefile
: BSDmake
syntax. This file is needed, as NetBSD and OpenBSD do not recognizeBSDmakefile
as the default Makefile.Makefile.main
: Common rules. Included from the 3 previous file. No GNU/BSD specificmake
syntax may be used in this file.
The additional files Makefile.dist.*
and Makefile.icon
are less important, only included for completeness.
GNUmakefile
Using GNU
make
,GNUmakefile
is read instead of the defaultMakefile
.
GNU specificmake
syntax expressions, such asifeq
, may thus go intoGNUmakefile
.
This way, "platform dependendent" variables may be defined inGNUmakefile
, for example:ifeq ($(OS),Windows_NT) WIN32_LIBWSOCK32 = -lwsock32 WIN32_LIBREGEX = -lregex endif ifeq ($(OS),Solaris) SOLARIS_LIBSOCKET = -lsocket -lnsl endif
BSDmakefile
For FreeBSD users who prefer BSD
make
instead of GNUgmake
.
BSD specificmake
syntax expressions, such as.if
, may thus go intoBSDmakefile
..if defined(__FreeBSD__) FREEBSD_LIBBSDSPECIFIC = -lfreebsdspecific .endif
Makefile
Equivalent to
BSDmakefile
for OpenBSD and NetBSD users. BSDmake
syntax.Makefile.main
Keep the
Makefile.main
syntax simple. OnlyCC
,CFLAGS
, basic variables and common rules for all platforms.Example 1:
A socket application needs no additional linking flags, except on MS Windows, wich requires-lwsock32
.
When runningmake
on Linux, MacOS or MSYS2,GNUmakefile
is read, but does not match the conditionifeq ($(OS),Windows_NT)
.
When runningmake
on *BSD,GNUmakefile
is not even read.
In any case,$(WIN32_LIBWSOCK32)
evaluates to the empty string.
Only in the case ofWindows_NT
,$(WIN32_LIBWSOCK32)
evaluates to-lwsock32
:CFLAGS = -g -Wall -Wextra -Werror -pedantic -ansi MY_PORTABLE_APP1 = app1 OBJS = app1-portable.o app1-main.o $(MY_PORTABLE_APP1): $(OBJS) $(CC) $(CFLAGS) -o $(MY_PORTABLE_APP1) $(OBJS) $(WIN32_LIBWSOCK32)
Example 2:
A simple application, using theregex
library.
The source code is identical between platforms.
Once again, MS Windows needs the additional linker flag-lregex
:MY_REGEX_APP = regex1 $(OBJS) = $(MY_REGEX_APP) $(MY_REGEX_APP): $(OBJS) $(CC) $(CFLAGS) -o $(MY_REGEX_APP) $(OBJS) $(WIN32_LIBREGEX)
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:
Makefile.dist.gnu
Makefile.dist.bsd
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:
Makefile.icon.gnu
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
- Enable warnings:
-Wall -Wextra -Werror
Restrict extensions and features that may vary between platforms:
-pedantic -ansi
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 1:
One single C source code file for all platforms.
Platform-specific code is split by#ifdef
:s wherever needed:#ifdef <OS1> line of code for OS1 line of code for OS1 ... #elif defined <OS2> line of code for OS2 line of code for OS2 ... #else line of code for ANY_OTHER_OS line of code for ANY_OTHER_OS ... #endif line of common code line of common code ...
- Advantages:
- One single file for all platforms.
- No
#include
:s of other C cource code files, so the file scope ofstatic
functions doesn't change.
- Disadvantages:
- Less readable code, cluttered with many
#ifdef
:s. - Read and extract code for a specific platform is messy.
- Less readable code, cluttered with many
- Advantages:
Alternative 2:
One file with one single#ifdef
statement, and including one or more files for each platform (when needed):Header files: ------------- #ifdef <OS1> #include <header-OS1.h> #elif defined <OS2> #include <header1-OS2.h> #include <header2-OS2.h> #else #include <header-OTHER-OSS.h> #endif #include <header1-COMMON.h> #include <header2-COMMON.h> C source code: ------------- #ifdef <OS1> #include "source-code-OS1.c" #elif defined <OS2> #include "source-code-OS2.c" #include "more-source-code-OS2.c" #else #include "source-code-ANY-OTHER-OS.c" #endif
- Advantages:
- One single
#ifdef
per file makes the code more readable. - Read and extract code for a specific platform is easy.
For example,header-OS1.h
andsource-code-OS1.c
may be used "as is" in a project for that particular OS.
- One single
- Disadvantages:
- Using
#include "source-code.c"
for other C source code files is not recommended,
as this "broadens" the scope for functions declared asstatic
in the included files to include to joined.
To maintain this "broadened" scope as small as possible, keep C source code files small,
with few functions, and avoid nested#include "source-code.c"
.
- Using
- Advantages:
Alternative 2 is used in this repository.
Make it abstract
- Put any platform-dependent code inside its own function.
- Put that function in its own file (or together with other, platform dependent functions).
- Put common (platform-independent) code in separate files.
- Use many, small files, rather than big, monolithic ones.
- Use many, small functions, rather than big, monolithic ones.
- Put
main(int argc, char *argv[])
in its own file. This file should always be platform-independent.
Make code portable at compile-time:
- Alternative 1: one include file per OS
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.
- Alternative 2: "normalized" OS variable
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:
- An ELF executable, compiled for Linux, running on FreeBSD with Linux binary compatibility enabled.
- A Win32 (or MinGW64) PE executable running in an MSYS2 environment.
- A Win32 executable running on ReactOS.
- A Win32 executable running on Linux with Wine.
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:
- The uname(3) call, short for "unix name", may be used to obtain OS information, but only on UNIX-like platforms.
- The MS Windows Version Helper functions.
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:
label: Service Pack 3
patchlevel: 2
%%%
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
%%% 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:
- system(3)
- execlvp() %%%
- fork / exec /pipe
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
Links
DLL/SO:
Macros:
- https://caiorss.github.io/C-Cpp-Notes/Preprocessor_and_Macros.html
- https://sourceforge.net/p/predef/wiki/Home/
- https://sourceforge.net/p/predef/wiki/OperatingSystems/
- https://stackoverflow.com/questions/5919996/how-to-detect-reliably-mac-os-x-ios-linux-windows-in-c-preprocessor
- http://beefchunk.com/documentation/lang/c/pre-defined-c/precomp.html
Containers for compilers: