Portability


Portability Across MS-DOS C Compilers

Scott Robert Ladd


Scott Robert Ladd is a full-time free-lance writer with over 15 years computer programming experience. Scott works mostly in C and C++ when he's not hiking and shooting photos with his wife and daughter. He can be contacted at 3652 County Road 730 Gunnison, CO 81230. (303) 641-4219.

How Portable Is Portable?

Programs written in C are often touted as being more portable than those written in other languages. To be very portable, a program should use only the C language as defined by ANSI. After all, the ANSI standard exists to promote portability.

Most MS-DOS programs, though, use low-level facilities that are not part of the ANSI standard. Still, MS-DOS C compilers are remarkably similar. Language extensions, such as the near and far keywords, are universally accepted with only minor differences in meaning. It's the non-ANSI extensions such as directory searching and hardware port I/O that are different from compiler to compiler. Experience has taught me that these differences range from the minor to the major.

Is portability among MS-DOS C compilers a mirage, then? No. When I publish C programs, they need to be as generic as possible. Presenting a program that will, say, compile only with Microsoft's QuickC will generate a torrent of letters from Borland and Zortech enthusiasts who want a version for their compiler. Necessity is the mother of invention. In this case, my invention was a header file that allows me to create programs that automatically adjust to the specifics of the C compiler being used. Using portable.h, I can write system-level programs that will compile using C compilers from Microsoft, Borland, Zortech, and WATCOM.

The Header portable.h

Listing 1 shows portable.h, a header file containing macros that ease porting C programs among MS-DOS compilers. The file consists almost exclusively of macro definitions that alias or replace compiler dependencies. There are four sections in portable.h, each concerned with a different aspect of portability.

Every MS-DOS C compiler predefines a macro identifying itself. Microsoft C defines the _MSC_VER macros, beginning with v6.0. Microsoft QuickC v2.5 and later create the _QC macro. WATCOM C defines the __WATCOMC__ macro. Borland's Turbo C and C++ define the __TURBOC__ macro. Zortech C and C++ use the __ZTC__ macros. You can use these macros in conditional preprocessor statements to select compiler-specific characteristics.

The first macro definition conditionally creates a proper version of the MK_FP macro. You use this macro to create a far (32-bit) pointer from 16-bit segment and offset values. MK_FP is found in most compiler libraries, but Microsoft's C compilers do not define it. If portable.h does not find a MK_FP macro, it creates one.

Searching Directories

The next section of the include file defines macros and types used in searching MS-DOS file directories. Working with directory search functions under MS-DOS can be frustrating. Each compiler vendor uses unique function and structure identifiers. Macros to the rescue! The MS-DOS file data structure has the same format for all of the compilers, even if the names of the structure and its members are different. I therefore created my own data structure, DOSFileData, that can be used in place of the structures peculiar to each compiler. The function macros that follow cast a pointer to a DOSFileData structure to the specific structure type supported by a specific compiler.

How does a program work with the compiler-specific function names and parameter lists? The solution is to define macros that translate between compilers. The FIND_FIRST macro hides a compiler's implementation of the MS-DOS 0x4E (find first file) function. FIND_NEXT is an alias for the function call to the MS-DOS 0x4F (find next file) function.

Accessing I/O Ports

Aliases also help you perform I/O to ports. Bytes and words can be written to and read from the I/O ports in your PC. Access to I/O ports is required for video, serial, and other hardware-level programming tasks. The names for port I/O functions are consistent across most compilers — only Borland uses unique names. So I created the macros IN_PORT, IN_PORTW, OUT_PORT, and OUT_PORTW as aliases for the actual function names.

Borland's Turbo C and C++ support pseudoregister variables and inline interrupt calls. The assignment _AX = 1 directly assigns 1 to the AX processor register. The geninterrupt function compiles to an inline INT instruction. Obviously, Borland's ability to directly load registers and call system BIOS and MS-DOS services is a performance booster. Other MS-DOS C compilers don't support these language extensions.

That would normally preclude the use of pseudoregisters in "portable" programs. But again, macros can be used to solve the problem. In the last section of portable.h, a macro is defined for each pseudoregister variable when a non-Borland compiler is begin used. The pseudoregister is replaced by an assignment to a member of a struct REGS named CPURegs. Calls to geninterrupt are replaced with calls to int86. I've successfully used these macros in several Turbo C-specific programs to allow the code to migrate to other C compilers.

Conclusion

I've found portable.h to be useful both in presenting articles and doing consulting work. I can develop a generic program using one compiler and be able to use that program with another C compiler. I'm sure you'll find extensions that can cover other compiler inconsistencies. Have fun!