Tim Berens is president of Back Office Applications, Inc., a contract software house founded in 1986. He specializes in C development for MS-DOS and UNIX, and his projects have ranged from SNA communications to Girl Scout Cookie accounting software. He can be reached at 6691 Centerville Business Parkway, Dayton, OH 45459, (513) 434-5444.
This article describes a C-programming technique for storing procedure addresses in a table. This technique allows the developer to execute the procedures using methods similar to those for accessing data, such as numbers, that are stored in a table. To demonstrate the use of procedure tables, I will describe a problem that was solved using a two-dimensional procedure table.
The Problem
We needed to develop a method to handle interfaces with multiple database-management systems that would not change existing code in any way. This task arose after developing a medium-size DOS-based business application for one of our clients. After the system reached about 200,000 lines of code, the client asked us to port the application to run under XENIX. At this point, an odd combination of technical and business requirements forced us to use the Informix database-management system when running on XENIX, but continue to use the c-tree File Handler as the database-management system when on DOS.To further complicate matters, some of the files in the XENIX version had to be read using a pipe to a background SQL server, instead of using direct function calls as on the DOS version. This meant that we needed to read data from three different file formats, with three very different interfaces. In addition, because of our need to continue to support both XENIX and DOS, we wanted identical versions of source code on both operating systems. We also wanted to preserve the existing code.
The Solution
Fortunately, we anticipated a situation similar to this very early during the design process, long before writing any code. To minimize the impact of interfacing with multiple database-management systems later, we developed a generic database-interface layer and routed all calls to the database through this layer. It consisted of a suite of functions such as:read_equal_rec Reads a record whose index matches the key values passed.
save_rec Saves the record passed.
read_next_rec Reads the next record.
This interface layer in turn made the calls to the low-level database functions that actually read from or wrote to the file system.
The additional layer slowed the system down, but only very slightly. And the savings in development time we realized in the long run was worth the slight drop in speed.
To handle the multiple interfaces, we considered several methods. All these methods involved replacing the database-interface layer with another layer that determined the file type being requested, and then called the appropriate function in the next layer to perform the operation. The process of routing the calls to the correct routines differed among the various methods. The method we finally chose used a two-dimensional procedure table to handle the routing.
The Implementation
Each of the calls to the database-interface routines receives a variety of arguments including a pointer to the file structure associated with the file being read. (This should not be confused with the standard library's FILE structures. It is a structure of our own creation.) This structure's members store such information as the size of the records, a buffer for holding the records, etc.To facilitate the routing, we added two members to the file structure. One, called filetype, is an integer in the range of 0 to 2. The value stored in this member corresponds with the type of database file (c-tree, Informix, or SQL) that this structure represents. The other is an integer in the range of 0 to 22 to represent each of the functions in the database-interface layer.
Each of these two numbers represents an offset into one of the dimensions of the two-dimensional file_operations procedure table. When the application calls a database-interface function, it uses the filetype and the assigned function number to calculate a position in the two-dimensional procedure table. The application then executes the function whose address is stored at this position. This procedure table is defined in Listing 1.
The application maps each call to a database-interface function to a function pointer in the procedure table through the use of a macro. We wrote a macro for each function in the database library and placed these in a file called filetype.h. Then we added this file to a standard header, which is included in every file in our system. The somewhat frightening line of code shown in Listing 2 does nothing more than replace the call to read_equal_rec with the address of the proper function to read a record depending on the filetype of the file being read. For example, a call to read_equal_rec for a file of filetype 2 would be mapped into file_operations[0][2]. This would cause the function SQL_read_equal_rec to be executed.
The Conclusion
The two-dimensional procedure table was only conceptually difficult. Once the system was designed, it required only a couple hundred lines of code to implement. The most time-consuming aspect of our port to XEN1X was writing the database interface routines. But after the database interface routines were written for each of the file types (c-tree, Informix, and SQL), it required only a recompile of the code for the system to fire to life.