I have an exceptionally long history with software portability which
began in the early 1980s. My first C programming was on the legendary
VAX-11/780 running Berkeley UNIX, and I quickly learned the dangers of
that platform. The 32-bit VAX architecture was so forgiving and generous
of error that moving to other platforms quickly revealed the bad
assumptions made while programming on the VAX. These lessons have
stuck with me for years.
Even in 1983, I routinely moved my C code back and forth between the 32-bit VAX and my 8-bit CP/M Z80 system which ran the also legendary BDS C Compiler by Leor Zolman (I still have the v1.5 user manual on my shelf, the first piece of software I ever purchased). This was the start of a career in software portability, and it even included teaching a week-long class at AT&T Bell Laboratories in Holmdel NJ entitled "Portability, Efficiency and Maintainability".
Below are two specific projects I've worked on that had "portability" higher on their to-do list than most, followed by some more detailed thoughts on portability in general. This is by no means the exhaustive list of software that runs on multiple platforms, but it's a flavor of them.
James Gosling, now best known for his involvement in the creation of Java, was well known in the early 1980s as the author of Gosling Emacs. This very powerful editor predated GNU Emacs, and at the time was marketed commercially by UniPress Software. We had a tape of Gosling Emacs at school, and I undertook to learn it inside and out.
In addition to using lint extensively on the code to clean up bugs and nonportable issues, I undertook to port Emacs to the Onyx C8002, which was based on the Zilog Z8000 chip. This was a quasi-16-bit architecture which had a much smaller address space than provided by the VAX. It took a great deal of conditional compilation to eliminate the fluff and non-critical components: this Onyx system ran UNIX 7th Edition and had only the ed line editor - no vi.
I ultimately was successful in making this port, and the resulting code was dubbed minimacs and was used quite a bit on campus. It also somehow came to the attention of the folks at UniPress Software, and they licensed my changes from the university: the Math Department where I was based received royalty checks from Unipress for several years due to my work.
UniPress also hired me to visit their New Jersey headquarters and help in a porting effort: they had many UNIX machines around that needed Emacs builds, and I got familiar with many of them. Though much of the code was quite portable, areas such as signal handling and terminal I/O were troublesome and required changes to the code and the master build environment.
I don't believe UniPress still distributes Gosling Emacs - GNU seems
to have taken over - but this was my first large-scale introduction
to porting.
VSI-FAX
I was the principal author of the first commercial release of the VSI-FAX
UNIX facsimile system, and in that capacity was responsible for porting
our software to more than the 30 supported UNIX platforms. As with Emacs,
we found that some parts of the code ported very nicely, but others
(signals, terminal I/O, shared memory) did not.
I created the very extensive build environment and front-ends to the C compiler and linker that allowed us to craft the compiler environment (#include path, define macros, find libraries) that did not burden the individual subdirectory makefiles with huge long command lines. While working in one of these subdirectories, it was a simple matter to type "make" and it would use the correct compiler, compiler flags, and other associated tools. This approach proved to be extraordinarily successful over the years: the build system is largely as I created it more than 10 years ago.
Then in 1997 I led the effort to port our software to Windows NT. Originally another developer attempted this by using a commercial UNIX-emulation layer (NuTCracker), but we found it wanting. Though much of the code had long had its UNIX-dependent parts isolated, we found that some of the key parts simply did not lend themselves to abtractions: whole new versions were created to take proper advantage of the functions offered by the Win32 architecture.
Win32 provided a radically differnet approach to interprocess communication, plus the entire NT "service" model and installers had to be accomodated. But the fortuitous use of the MKS Toolkit - with the stable of traditional UNIX tools ported to NT - we were able to use the same build environment as the UNIX product line. The early NT developers were all UNIX folks, and we all preferred the Korn Shell and vi to the Microsoft IDE tools anyway.
Using correct word sizes can lead to more optimal data storage and processor effiency with minimal wasted space on data types that are "obviously" too large for their intended purpose.
Most modern processors will fault when accessing these values at odd addresses, but architectures like the VAX permitted them (though with a modest performance penalty). Software that packed data very tightly into memory buffers without alignment would work correctly on the VAX but would fail badly on machines with stricter alignment restrictions. The "fix" often required copying data from their unaligned forms into temporary variables for manipulation, then copying them back. This made for very ugly code. Proper attention to word alignment (typically by using C structures instead of maintaining "raw" buffers) would have alleviated this whole mess.
With the introduction of ANSI C in the late 1980s, this introduced an even stickier problem: should one use the outstanding features of ANSI C (function prototypes, the const and volatile type qualifiers, the <stdarg> facilities, etc.) or not? At the time, portable software could simply not rely on ANSI C being widely available - GNU C was not yet mature - and it was often an agonizing tradeoff on just how much of ANSI C could be use.
Ultimately, most software porters created portability macros that allowed the use of many of these features in code that could be straight K&R or ANSI. Found in header files would be:
This compiled in function prototypes when they were available, but omitted them when not so. Sadly, this only worked for the function declarations: the function definitions didn't lend themselves to convenient macro support such as this.#ifdef __STDC__ # define PROTO(args) args #else # define PROTO(args) (/*nothing*/) # define const /*nothing*/ # define volatile /*nothing*/ #endif ... extern char *strcpy PROTO((char *dst, const char *src)); extern int printf PROTO((const char *format, ...));
But no amount of clever macros could completley hide the differences: some facilities simply had to be conditionally compiled depending on the ANSI- or non-ANSI-ness of the compiler. Notable was support for variadic functions:
This kind of mechanism - though not terribly attractive - permits use of the best features of both worlds.#ifdef __STDC__ # include <stdarg.h> #else # include <varargs.h> #endif #ifdef __STDC__ void die(const char *format, ...) #else void die(va_alist) va_dcl #endif { va_list args; #ifndef __STDC__ char *format; #endif #ifdef __STDC__ va_start(args, format); #else va_start(args); format = va_arg(args, char *); #endif vfprintf(stderr, format, args); va_end(args); exit(1); }
In addition to the compiler, there is the linker, the set of third-party libraries required to build the software, the make tool, and the overall configuration scripts that set up the developer's environment. Software that uses TCP/IP sockets often requires more than one library, and the particular libraries vary widely.
Even the ability to use perl and shell scripts as "helpers" is a consideration: fantastic tools can be built with perl, but a requirement that a platform even have a recent perl interpreter might be an onerous one.
Some examples limited to just the UNIX platforms:
These are just the start of questions that arise when porting software. Some of the questions are of relatively minor impact, such as knowing how signals are handled. With a certain amount of conditional compilation, it's possible to support all the varied flavors of signal semantics and be relatively sure that they will work correctly, but if (for instance) the select() system call is not available on all platforms that are to be supported, it puts a very serious damper on how a program does I/O.
Virtually all modern systems support select(), but this wasn't the case ten years ago, and even those that did would not include serial devices in the class of supported file descriptors. "Working around" the lack of select() meant a complete re-architecting of the software that could have benefitted from it.
When considering a port to entirely different operating systems, such as Windows NT, these issues become much larger. Though the ANSI C library is highly portable across all supported platforms, most "real" software uses operating system functions that are far beyond the standard C library. UNIX and Win32 have fundamentally different approaches to things like process management and interprocess communication, and these cannot often be abstracted away. Win32's WaitForMultipleObjects() API call and I/O completion ports are often too useful to avoid using simply in the name of portability, so separate modules for UNIX and Win32 evolve.
Ultimately, the final test of software portability is in the porting itself: by moving the source to the new system and trying to build it, a whole throng of issues suggested above will veritably come out of the woodwork to frustrate the efforts. Only after solving these problems many times does one start to take pre-emptive action to engineer portability in from the very start.