Writing Software for Portability, Part 1 By Gilbert Detillieux, Info West Inc. As the computer industry continues to evolve at an ever increasing pace, old systems and their software are becoming obsolete more quickly. The move to open systems technology and decreasing hardware costs make it feasible to throw out old hardware and entirely replace it every few years. But, what about our investment in software? The cost to produce and maintain it is increasing, and the complexity of the software is also increasing. To avoid having our software become obsolete too quickly, we have to design it so it will survive the transition to new systems. We must make it portable. What does software portability mean? First, software must be machine independent, so it can easily move to different hardware architectures and systems of different scales, from micros to mainframes. It also should be operating system independent, as much as possible, since this also will change with time and with hardware changes. Software also should be designed to be independent of the environment in which it runs -- user interfaces and interfaces to other programs it interacts with can change. It should also be independent of language and country specific features, if it will be used internationally. Finally, it should have version independence -- it should survive changes in hardware, operating system, and programming language version changes, as well as changes in its own code. The selection of the right programming language is important. A high level language is essential to assure machine independence, but it's important that this language follows industry standards, such as those set by ANSI and the ISO. Also, pick a language that will fit into corporate standards and coding practise, and that will be available on all your target systems. Avoid non-portable features, such as a particular compiler's language extensions, or ill-defined or implementation dependent features in a language. The last thing you want is to rely on a side-effect of an operator or other non-standard feature that will break when compiled on another system. Also avoid standards or language features that are too new, since these aren't yet widely supported or may change in future standards. A good choice for UNIX systems is ANSI C, but there might be other choices that are reasonable for your situation. Regardless of language, structure and modularity are important. Use a top-down design consisting of small independent code modules. If your language supports them, use header files for public declarations of module interfaces. Use these headers in modules that will need these interfaces, as well as in the module that defines them -- this ensures consistency in the declarations. Header files should not result in storage allocation or code generation -- i.e. they should only declare variable and functions, not define them. If the system has standard headers for declaring library routines and system calls, use them -don't make assumptions about these by declaring them yourself. With a well structured program, only a few low-level modules will be machine dependent or system dependent -- the higher level code should all be portable. These low-level modules, on the other hand, should be general enough to be reusable in any application. There should be a clear line between the high-level, application specific but portable code, and the low-level, application independent but machine specific code. System calls, and other non-portable constructs, should only occur in the low-level modules. You might have to create stubs to handle missing features on a particular system -- these should either fake the missing feature or return an error to the higher level modules to say this feature isn't supported. You also might need optional modules, that will be linked in only if certain system-specific features will be required. This might be the case for interfaces to other programs, such as mailers or spoolers, for example. You can design plug-in replacements that will be selected based on the system configuration (such as a mailer interface to either SMTP or UUCP, or a spooler interface to either LP or LPR). If your language supports conditional compilation constructs, you can use these to further isolate machine dependencies within modules. That way, one source module can support several systems, by simply selecting the right parts to compile. (If your language doesn't support conditional compilation, but you're developing on UNIX, consider using the C preprocessor or M4 on your source files to get that capability.) As much as possible, use pre-defined constants and macros as switches to control compilation of system specific features. (The C preprocessor usually defines a few symbols for the target operating system and architecture, such as "unix" and "sparc".) If required, you can define your own constants and macros, either as compilation switches, or to define system specific quantities. Make all such definitions easy to find and change -- don't bury them all over the place, but rather put them in a configuration header file, so they are localized and editing for a new configuration is kept to a minimum. (This not only saves time, but reduces the chances of introducing errors in the code.) Finally, use the "make" command and a Makefile, if your systems support it. This makes it easy to control the compilation, and eases configuration and installation. Configuration options, constants, and macros can all be defined in the Makefile -- this isolates them from the code, and allows flexibility in customizing the code without making any changes to the modules themselves. If you want to get fancy, you could even have a program or script that builds a Makefile tailored to a particular configuration, based on the system and user selected option. (Most X11 Window software now includes an Imakefile, a system independent Makefile, which can be run through a program that will produce a custom Makefile for your site.) So far, we've only looked at program structure and modularity. Even more important to assure portability is the way we use data structures and data types, in memory and in data files. We'll look at that next time.