Part Three of Writing Cross-Platform Software: Getting Started
Michael D. Crawford
hotcoder@gmail.com
Copyright © 2002 by Michael D. Crawford
Do you need a Cross-Platform Software Engineer?
Another issue in writing portable software is structure alignment and padding. It is common on single-platform software to write data to a file by writing a structure as a whole, like this:
Suppose we've already byte-swapped this to big-endian, but then write it out directly. What data makes it to disk?
The answer is that we don't know, it depends on the platform, and in fact the actual values written to the file will include garbage that depends on whatever was lying around in the memory where foo was allocated at.
(Note that this is a security hole - you may not consider your program security-critical but you don't know what data your users might consider sensitive during their use of your application, so always zero the buffers you will use to write data to disk. Also take care to fill disk blocks completely when writing to a file so that old data from previous documents does not end up in new ones. Read The Forum on Risks to the Public in Computers and Related Systems for dirty buffer horror stories.)
One might think we would have:
but we might also have:
or even:
What's going on here? sizeof( foo )
is compiler and
processor dependent for reasons of both architectural limitations and
efficiency.
Some processors are unable to read or write multibyte values from certain locations. Almost always they cannot read them from odd addresses. Frequently they cannot read them unless the address is a multiple of the size of the value - so a four-byte long can only be read if the address is a multiple of four, and a two-byte short can only be read if the address is a multiple of two. Failing to observe alignment rules will make the processor throw an exception and terminate the program, and even lock up the machine on some systems (or if it happens in kernel code).
Sometimes misaligned data can be read at efficiency's expense. For example the PowerPC can read longs at any even address but extra bus cycles are required if the address is not divisible by four. To read a long at an address divisible by two but not four, it reads four bytes including the upper two of the value, throwing two away, then reads another four bytes including the lower two of the value, throwing another two away. This requires extra accesses to the cache compared to reading an aligned value.
The compiler will insert unnamed pad bytes into the structure to maintain alignment. This is done in cooperation with the OS or library memory allocator, which always allocates blocks at the largest multiple conceivably needed for the architecture, and sometimes a greater value such as the multiples of 16 used on the PowerPC MacOS, to discourage blocks from taking up more than one cache line.
Because the PowerPC chip was developed to replace the 680x0 family in the Apple Macintosh, the Metrowerks CodeWarrior compiler for MacOS needs to handle both 68000 and PowerPC alignment. The PowerPC version of the MacOS will run emulated 68000 code and has emulated code in part of the system software - it even handles interrupts in the emulator. Also the API's for the operating system use structures defined according to 68000 alignment conventions.
A vast number of MacOS file formats - especially ad-hoc ones created without a lot of design effort by the original programmers - write structures directly to disk.
Macintosh compilers support a #pragma
that allows one
to switch alignments of structures as they are compiled, and you can set the
alignment of a whole application codebase to one or the other and then make
exceptions with the #pragma. The
#pragma's are only necessary in the headers where
structures are defined; they are transparent to user code.
I have even seen structure alignment change between different versions of the same compiler, as happened after an upgrade to the Think C compiler for the MacOS from Symantec. I discovered this in QuickLetter from Working Software while I was working there. It saved a structure in a Mac resource file [1] that was something like this:
The size of alignTest in early compiler versions was 258, I suppose because anArray had an even size and simple compiler logic aligned all even-size data at even addresses. So there was a garbage byte after aChar that had been written to millions of documents created by QuickLetter users.
In writing a more optimized upgrade to the compiler, Symantec's engineer must have released that even though a character array was even in size, it was declared as containing single-byte values which could be addressed at any location. The size of alignTest became 257 with no pad byte - so after rebuilding the program with our spiffy new compiler upgrade, the new build could read files it had created itself, but could not read files created by the old version, and the old version could not read files written by the new.
Symantec support told me there was no compiler option in this version to change the alignment so I changed the structure declaration to:
(This "improvement" caused so many complaints that Symantec later provided an option to turn it off.)
One should not design file formats to be written by direct binary copies of structures. The objects in file structures should be packed, and the data should either be written a field at a time, or for greater efficiency a number of fields should be copied from structures and packed into a character buffer that is then spit out to disk or network.
A number of libraries are available to handle byte swapping and writing structures to files. XDR - the External Data Representation (see RFC 1832 and RFC 1014) originally developed by Sun is a good example. I'll be surveying them and other cross-platform tools in future columns.
These are just two of the most basic issues that need to be dealt with in writing cross-platform code. Such low-level details may seem too trivial to be worth worrying about, but they are of critical importance. Things will get more fun later on when I cover cross-platform frameworks - it is possible to do simultaneous application development in C++ on XWindows, Windows, BeOS and MacOS, and it is possible to do this with software written under the GNU General Public License as well as other open-source licenses that allow proprietary development.
One aspect of cross-platform architectural design is layering.
We are all familiar with layered architectures - the TCP/IP protocol stack has reliable sequenced application-to-application transmission (TCP) layered on top of host to host unreliable packet transmission (IP) which is itself built on top of single-cable transmission (ethernet or PPP). In operating systems there is the kernel, the system call interface, the standard library, the XLib, and the XServer forming a set of layers with standard interfaces that allows raw iron to display desktop themes and run spreadsheets.
Layering is a good way to approach cross-platform development. It is not the only way, and it is possible to port code by just hammering on it until it works. But it really is best to approach the problem in layers, and it is the very best to work from a common codebase and to keep your code building on all platforms at the same time.
Andy Green has four machines on his desk and can alternate builds on several platforms with Zoolib every few minutes - he strongly recommends that people do this during development, especially if they are actually writing a cross-platform framework.
For several months I wrote a Mac and Windows application using Zoolib, writing for a week at a time on a Windows NT laptop, [2] (it runs BeOS and SlackWare Linux too) then zipping the source at the end of the week and building on a Macintosh before delivering both platforms to my client for testing.
I am to have a dedicated Linux server with wireless networking, so I can develop Windows or BeOS code on my laptop, build and test Linux software via an X server running on my laptop, and do PowerPC MacOS and Linux builds on my Mac 8500 - also 68000 MacOS builds on the Mac. Using a server to keep your code in one place definitely is better than FTPing a zipfile from machine to machine.
Give cross-platform development a try. It is surprisingly interesting work.
[1] Classic Mac OS files have two data streams, or "forks". The data fork is just a stream of bytes like a regular Unix file. The resource fork is a structured file in which variable-sized binary records are retrieved by four-byte types (usually expressed in ASCII, like 'TEXT' or 'ICON') and two-byte id's. Resource files can be read and written during run-time so they are used both for storing user interface items like dialog layouts, as well as an easy to use - but highly nonportable - file format.
[2] I chose my laptop specifically for cross-platform compatibility. Installing all these OSes on my Compaq 1800T was a fascinating challenge. I provide full details at http://www.goingware.com/laptop
![]() |
Voting for GoingWare at The Programming Pages will encourage more people to read these articles. |