home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
C/C++ Users Journal 1990 - 1995
/
CUJ.iso
/
unix
/
1993.txt
< prev
next >
Wrap
Text File
|
1996-02-07
|
3MB
|
101,098 lines
Designing an Extensible API in C
Charles Mirho
Charles Mirho is a consultant in New Jersey, specializing in Multimedia. He
holds a Masters Degree in Computer Engineering from Rutgers University. He can
be reached on CompuServe at: 70563,2671.
Definition of an Extensible API
An application program interface is the set of function calls that a software
library exports to applications. These functions, along with their parameters,
are usually prototyped in a header, or include file. In addition to
prototypes, the header file also contains definitions for structures used by
the functions, and #defines for flags and return values. All these components
of the header file make up the complete definition of the API.
An extensible API can accomodate growth in the library without requiring
changes to existing applications (beyond a possible recompile). Listing 1
contains a simple, extensible API.
The function in Listing 1 is useful in GUI APIs. It defines a region of the
display where a mouse click can be trapped. A typical call to the function
might look something like
#define ID_HELP_BUTTON 10
int vertices[] = {10,20, 50,20, 50,40, 10,40};
REGION HelpButton = {sizeof(vertices)/sizeof(int)/2,
&vertices[0]};
.
.
.
DefineHotSpot (ID_HELP_BUTTON, &HelpButton);
The value of ID_HELP_BUTTON varies depending on the application.
ID_HELP_BUTTON also provides a unique ID for the region. The second parameter,
&HelpButton, defines the boundary of the region as a set of vertices. Notice
that the REGION structure is defined in API.H, the header file for the API.
REGION contains a field for the number of vertices in the region, and a
pointer to a list of vertices (coordinate pairs). Early versions of the
library might only support rectangular hot spots (four vertices), but the API
is extensible because more complex shapes can be used in the future without
altering the prototype for DefineHotSpot. Compare Listing 1 to a
non-extensible version of the same API in Listing 2.
Since the region is always rectangular, only two vertices are required,
specifying the upper-left and lower-right corners of the rectangle. While this
function may seem cleaner and more intuitive at first glance, it is extremely
confining. If future versions of the library must support regions with more
than four vertices, then you must choose one of two undesirable alternatives:
You can create an extensible version of DefineHotSpot. Now developers must
learn two functions, since the old version of DefineHotSpot must be retained
for compatibility. The result is a cluttered API.
You must modify existing applications that use DefineHotSpot to support the
new function API.
Structured Parameters
The extensible version of DefineHotSpot (Listing 1) uses a structured
parameter, while the non-extensible version (Listing 2) passes all the
parameters through the function prototype. Using structured parameters is one
of the best ways to design an API that is more extensible. As the capabilities
of the library expand, fields can be added to the structures without changing
the function prototype. A good application of this technique is in functions
that return information, such as
int GetSystemInfo (SYSTEM_INFO
* sinfo);
This function is used to get information about devices in the computer system.
The SYSTEM_INFO structure:
typedef struct tagSYSTEM_INFO
{
int num_displays; /* number of attached displays */
int num_printers; /* number of attached printers */
int num_drives; /* number of attached disk drives */
} SYSTEM_INFO;
has fields to hold the important properties of the system. (I kept it short
for clarity.)
Later versions of the API can expand the structure to accomodate new additions
to the system, such as tape drives, as in:
typedef struct tagSYSTEM_INFO
{
int_num displays; /* number of attached displays */
int_num_printers; /* number of attached printers */
int_num_drives; /* number of attached disk drives */
int_num_tapes; /* number of attached tape drives */
} SYSTEM_INFO;
Because the features of the system are passed through the API in the form of a
structure, rather than as separate parameters, it is easy to add a field for
tape drives.
The Size Field
You can add even more flexibility to structured parameters with the size
field. The size field holds the size, in bytes, of the structure containing
it. When using a size field, you must make it the first field in the
structure, as in
typedef struct tagSYSTEM_INFO
{
int size; /* size of this structure */
int num_displays; /* number of attached displays */
int num_printers; /* number of attached printers */
int num_drives; /* number of attached disk drives */
int num_tapes; /* number of attached tape drives */
} SYSTEM_INFO;
The size field makes it possible for existing applications to use newer
versions of the library without performing a recompile. This is especially
useful on platforms that use dynamic linking, because dynamic link libraries
are often packaged separately and sold directly to customers. Application
developers often have no control over which version of the library customers
are using.
To see how the size field can save a recompile, look at the SYSTEM_INFO
structure again. When the num_tapes field is added, the size of the structure
changes. It would normally be necessary to recompile applications that use the
structure so that static and dynamic allocations reserve the correct amount of
storage. Otherwise, the newer library would write too much data into the
structure parameter, corrupting memory. However, if the first field of the
structure contains the structure's size, and you are careful to add fields
only to the end of the structure, then the structure can be extended without
the need to recompile existing applications. The library simply examines the
size field to determine which version of the structure the application is
passing. If the application is passing the older structure, the size will be
smaller, and the library knows not to fill the extended fields. Listing 3
contains an example.
In Listing 3, the library keeps the declaration of the old SYSTEM_INFO
structure as oSYSTEM_INFO. The oSYSTEM_INFO structure does not appear in the
header file that applications use.
Interpretation Flag
Suppose the GetSystemInfo function is extended in the future to report details
about particular devices in the system. You can use the same function to get
the number of displays in the system, and details about the displays the
system is using, as in:
typedef struct tagDISPLAY_INFO
{
int size;
/* size of this structure */
int displayno;
/* display to get info on */
int xpixels;
/* display width in pixels */
int ypixels;
/* display height in pixels */
int bits_per_pixel;
/* bits per pixel */
int planes;
/* video planes */
} DISPLAY_INFO;
You can insure that the GetDisplayInfo function will support this and any
other device-specific structures that come along by changing the original
prototype to
int GetSystemInfo
(int flag, unsigned char *info);
GetDisplayInfo now accepts a byte-aligned pointer instead of a pointer to a
specific structure. The function interpretes the pointer differently,
depending of the value of the flag parameter. You call the function for
general system information with
/* API.H */
#define GET_SYSTEM_INFO 1
#define GET_DISPLAY_INFO 2
.
.
/* application */
SYSTEM_INFO sinfo = { sizeof(SYSTEM_INFO), 0, 0, 0 };
.
.
GetSystemInfo (GET_SYSTEM_INFO, (unsigned char *)
sinfo);
For details on display devices, you call the function with
/* application */
DISPLAY_INFO dinfo = { sizeof (DISPLAY