Notes on C-code portability v0.63 (Jun 27th 2003) - Remember the byte order. If you really need the speedups from fiddling directly with the bytes of an int, make two versions of the code and select between them with an #ifdef. For example MIPS, 68k and PPC processors use a different byte order than PC (ABCD in the memory instead of DCBA). Setting the bytes of 16-bit and larger entities with shifting is a more portable choice. - Don't rely on the data types too much. For example int is guaranteed to be at least 16 bits, not 32. Long int is at least 32 bits. On 64-bit systems int probably cannot hold the size of a pointer -- do your pointer math some other way. A good way to solve this problem is to use globally defined data types where it counts. This way you only have to change them once for each platform. - The members of structures (especially chars) might or might not be aligned to system specific addresses. Reference structure members only by their name and use sizeof to get the real size of the struct instead of counting the sizes of the members by hand. An array of structs does not necessarily have the same size as the sum of the individual sizes. - Referencing unaligned 16-bit or larger entities causes a severe performance hit on most modern processors. In the worst case this might even cause a processor exception (for example on the 68000). - Take the byteorder and data types in account when designing own fileformats too. It may be easy to read a big lump of data with fread directly into memory, but it may mean a lot of converting on other platforms. ASCII-based files are usually larger but tend to cause less problems. Note however, that an ASCII representation may lose some precision from your floats. IEEE floats and the basic integer data types are portable as well as long as you remember the byte order. - Never use plain char to denote signed or unsigned bytes. Char is okay for 7-bit ASCII, but not whole byte values. Some compilers default to signed char and some to unsigned. One unwanted side-effect of this is that signed shifts to right produce different results than unsigned. - Use Makefiles to hide the system-dependent details such as compiler/linker flags and header/library paths. Compiler-specific workspaces are of no use on most platforms. Use real tab characters in your Makefiles instead of 8 spaces. - All the system-dependent code should be in low-level libraries that hide the details. It's way easier to write the library for a new platform instead of digging through the whole code. - Don't assume that uninitialized local variables are zero. Initialize your variables in the beginning of each function. The initial values might be zero on your system, but on some other not. The same goes for memory obtained with malloc. - Unix-like systems can traditionally handle large arrays on the stack, but this is not the case for all systems. If you insist on not using malloc for your arrays, declare local arrays as static so that they are stored on the heap instead -- heap size isn't usually as limited as the stack size. Using alloca is generally a bad idea because of the limited stack size. - Standardize. Libraries such as SDL, GLUT, PTC and OpenGL are portable by nature and make it way faster to port stuff, since you don't have to deal with the system API's (which is quite error-prone for beginners too). - Use #pragma's and compiler-specific features with care. You can live quite happily without them. For example GCC keeps your feet on the ground with -Wall -pedantic -ansi. - Assembler code isn't portable by nature. To make the porting process easier, encapsulate the assembly into modules instead of writing some ugly #pragma or __asm statements. Providing C alternatives of the same functions is both polite and also useful for yourself when changing things. NASM is a good tool for writing x86 assembly that is portable across the OSes and compilers on the PC. - Upper/lowercase letters do matter on many systems. CRAP.H, crap.h and Crap.h are different files on these systems. Be consistent with the case. - Plain printf() for debugging output does not work on all systems. fprintf(stderr,...) is better. You might also want to flush the output with fflush because on some systems the streams are buffered and don't get printed at once without flushing. - #include the headers for all the functions that you use. On your system the parameters might be correct and the compiler finds a correct function but don't take it for granted. For example math.h should always be included when using floating point numbers. - If you intend to alter the contents of a string after its declaration, it must be declared char[] instead of char*. Char* strings are often stored in read-only memory regions and altering them causes a sure crash. So avoid this: char *text="Zap"; text[1]='o'; - A clean return from the main function with value EXIT_SUCCESS prevents unwanted behavior on certain systems (for example Mac OS X). Zero is in practice OK too if you're very lazy. - CPU timing loops are a thing of the past. They work poorly on both slower and faster machines than yours. For example the timer and counting the played samples are more usable methods for timing. Don't count on high-resolution alarm timers: they may slow down because of the load and different systems have a different scheduling frequency. - Minor annoyances concerning source files: 8-bit native ASCII in for example comments might render the source code hard-to-read or next to unusable on some editors. Most editors consider tab to be 8 characters wide, so if you use a different length it's polite to save the tabs as spaces to keep the code readable. The issue of different linefeeds cannot be solved completely, but the single-character '\n' at least compiles on most platforms.