Tips for writing portable code
Most tips presented on this page are taken from the book [http://proquest.safaribooksonline.com/9780321246424 Cross-Platform Development in C++: Building Mac OS X, Linux, and Windows Applications] by Syd Logan. This book describes the essential best practices of a cross-platform development culture that was adopted companywide within Netscape, and embraced by Mozilla, which was critical in enabling Netscape and Mozilla to ship product with approximately the same level of quality across a wide spectrum of platforms to tens of millions of users.
Use typedefs for integer types whenever the integer must be of a definite size. For example, define and use the type INT32 for integers that must consist of exactly 32 bits on all target platforms. Or look for the C standard header file stdint.h. See also [http://www.parashift.com/c++-faq-lite/newbie.html#faq-29.5 this FAQ].
- C and C++ do not specify whether the char data type is signed or unsigned. This can be a problem when mixing char and int types in code — the classic example is reading characters from stdin in C into a unsigned char using the getchar() function, which returns an int and typically uses –1 to indicate end of file. If you mix char and int types, explicitly declare the char as signed or unsigned.
Use the C and C++ Standard Library (part of which is the STL) whenever possible.
- Standardize your project on the use of tabs and line endings so that the formatting the programmer intended is reproduced faithfully, regardless of the editor one uses. Configure the editor to expand tabs into spaces, with two to four spaces per tab. Ensure that lines of text end with newlines (LF). On Linux and Mac OS X, this is the default. Visual Studio also uses this as a default, but not all editors do. When installing Cygwin, make sure to choose the UNIX line endings option when prompted.
Use the -ansi and -pedantic-errors or comparable compiler options.
- Pay attention to compiler warnings; they tell you about uncertainty: that the code the compiler is generating on your behalf may lead to undefined or incorrect behavior at runtime. When other platforms are added to the mix, this uncertainty takes on another dimension because each platform may deal with the warned issue in different ways at runtime, which leads to a near certainty that the code causing the compiler warning will not be portable.
- With g++, the following flags create useful warnings: -Wall, - Werror, -std=c++98, -pedantic-errors.
- With Microsoft Visual C++, the following flags create useful warnings (roughly analogous to those listed above): /Wn, /Wall, /Wx, /Za
- Do not think that if you build your code using g++, along with the compiler flags listed above, then you do not need to enable the corresponding flags for Visual C++ since g++ already has issued all relevant warnings. It is even likely that code that would lead to the issuance of warnings by one compiler will be missed by another compiler.
Policy and management
- Make all of your platforms a priority from the very beginning on: To do cross-platform software well, parity, both in terms of functionality and quality, must be fully met on all the supported platforms, at all phases of the product's development and release. Perhaps the scariest situation one can face in cross-platform development is this question: "Now that we have it working on platform x, what about getting it to work on platform y?"
Code from a common codebase: When code cannot be shared, it should be hidden behind abstractions that provide a unified application programming interface (API). Applications using the abstract API should do so ignorant that the API is abstracting platform-specific functionality. The [http://en.wikipedia.org/wiki/Abstract_factory_pattern Abstract Factory Pattern] may be very useful here.
- Organize the project in your repository. Example layout:
lib lib1 mac include debug release win include debug release linux include debug release lib2 [...] src component1 src mac win linux inc mac win linux component2 [...] main
- Require developers to compile their code with different compilers. This can be done on a single platform (building with Microsoft Visual C++ and Borland C++, for example), or by building using different compilers available on other platforms.
- It helps you to avoid the use of compiler-specific features, flags, and macros.
- It minimizes the impact of various interpretations of C/C++ standards, and helps you avoid using unproven language features.
- Each compiler will produce different sets of warnings and errors, which makes development easier, and strengthens the code that you write.
- Different compilers generate different code, which can illuminate problems you might otherwise not detect.
- Require developers to build and smoke test their code on all platforms before they check it into the repository. (A smoke test is the first test made after changes to provide some assurance that the system under test will not catastrophically fail.)
- Test builds on each supported platform in regular intervals: Close the repository for normal check-ins, do the smoke tests on each platform. If a problem is detected and fixed, make a special check-in to the repository to resolve the issue. If all problems are resolved, reopen the repository for normal check-ins.
Build system and toolchain
- Use whatever compiler makes the most sense for a platform. On Windows, this typically means using Visual C++, and GNU g++ on Mac OS X and Linux. In order to produce cross-platform code, you need not standardize all your platforms upon a single toolchain.
- Use native IDEs for editing and debugging, but avoid using an IDE in place of GNU make (or some other command-line based build system) when it comes to architecting a solution to your build system.
Install and use [http://www.cygwin.com Cygwin] on Windows:
- There are literally dozens of command-line tools that UNIX developers may find useful in solving engineering problems, and the lack of access to these tools on a Windows system will be frustrating to most UNIX developers.
- The job of the build-system architect is to come up with a portable cross-platform build solution. If you are supporting Linux, Mac OS X, and Windows, UNIX-based command-line tools are available to help you with your job natively on both Linux and Mac OS X; and with Cygwin, they can be available on all three. BTW, the Mozilla project's build system is based on the following UNIX command-line tools, all available in Cygwin: bash (sh), make, perl, grep, awk, cvs, autoconf.
- Use a cross-platform make system. There are several tools freely available:
[wiki:completesearch/GNUBuildSystem autoconf/automake] work natively on Unix, on Windows via Cygwin. This tool is standard on Unix, but its use is [http://freshmeat.net/articles/view/889/ highly debated].
[wiki:completesearch/CMakeBuildSystem CMake] works on both Unix and Windows, without Cygwin. The KDE project has recently switched from the autotools to CMake (see [http://lwn.net/Articles/188693/ here]).
[http://www.snake.net/software/imake-stuff/imake-faq.html Imake], the make system of the X Window System, is present on Linux distributions natively, but needs to be installed on Mac OS X and Windows. Before installing on Windows, you need to decide whether you are going to be using GCC as your compiler (as part of Cygwin) or using Microsoft Visual C++ (without Cygwin). If you are using GCC, X11R6 for Windows can be downloaded and installed using Cygwin's Setup.exe program. If you are using Visual C++, you can download the X11R6 sources from X.org and build the X11R6 binaries from it, which contain the Imake binaries. A good description of Imake can be found in the book [http://proquest.safaribooksonline.com/9780321246424 Cross-Platform Development in C++: Building Mac OS X, Linux, and Windows Applications]; the author once worked on the Imake project.
- Avoid the serialization of binary data. Data serialization (a fancy term for writing application data to persistent storage) requires extra consideration in cross-platform applications, primarily because of the requirement that data written by an application running on one platform (for example, Windows) should be readable by instances of the application running on all other platforms. But penalties are involved with reading and writing binary data in a cross-platform context: endian issues, struct layout, size of intrinsic data types and enums, differences in type definitions. So agree on the number of bytes used to store values and agree on a byte-ordering convention (that is, little endian versus big endian), and ensure that writers convert to that convention before writing, and readers convert, too, before using the result as a value.