Contents
- Introduction
- What is an API?
- About this Article
- API Design
- Functions
- Generalizing Function Parameters
- Generalizing Functions
- Callback - What's That?
- General Sort Function
- Enumerating Sort Functions
- API Implementation in C
- API Header
- API Source
Introduction
Most of the times we write code to solve our problems. Sometimes, however, we need to write code that solves problems for other programmers or write code that forms a basic foundation for the higher level stuff in our own applications. In both cases, we are primarily concerned with developing a well designed interface for the outside world. This article tries to explain some techniques used in designing such an interface, which we call API or Application Programming Interface. It talks about various basic elements of any API set and consistencies among those elements for creating a high level, easy to use programming interface or library.
Using the Code
The source code zip contains a demo VC++ 6.0 project. There is one header file "sort.h" and two source files "sort.c" and "test.c". You can build the project using VC++ 6.0 compiler or any other C compiler if you don't have one.
What is an API?
API or Application Programming Interface is a common programming interface with various elements like functions, defined types (structures, unions, enums, and typedefs) and constants, which any application can access to do a particular set of tasks. You might have used Win32 API, which are various functions in several system DLLs that any Windows application call. APIs are usually categorized according to the type of function it relates to. For example, there are User interface APIS, Graphic Device interface APIs, etc. Any OS provides a set of APIs for applications that run under it. Not restricted to OS, you can create your own custom API. Say you're developing a generic game engine or library, then you might require developing an API for the same.
About This Article
This article focuses on design principles rather than algorithms. I have chosen one of the common problems: sorting. Suppose that you are asked to write a sort library; a library of routines to implement various sorting algorithms. How would you do that? Probably, you would think about coding various functions for each type of sort, create a header file with the required prototypes and build a DLL out of it. Well, that's straight forward thinking. This article will focus on design of API for this sort library. Reading this article, however, you may feel that the design principles discussed in this article are unnecessarily making a simple task over complicated. Please note that this article does not apply to sorting but rather in general to API design. Sorting is just chosen as an example.
This article discusses API design in C language only, so I assume you are well versed with it. In particular, it provides the concept of generalized parameters, generalized functions, generic implementation, callbacks, enumerations, etc. Finally, it shows how to create a header file as well as a source file for the API.
API Design
As I mentioned earlier, API consists of a set of functions, defined types like structures, enums, typedefs and several constants. But do we really need them? Can't we simply create a set of functions each in its own form that does the sorting? Yes, of course, but we will follow a different approach. We will first analyze each function and its form and then try to generalize them if possible.
Functions
The major part of any API is a set of functions. So let us start with them. Each function is self contained and performs some specific task. For our sort library, let us consider just a few sort types:
- Bubble sort
- Quick sort
- Merge sort
I assume that you have knowledge about these sort algorithms to feel comfortable. If not then there is not much really to worry: we are concerned with the design rather than the algorithm. A quick review of the above functions tells us that they have a similar task to perform, i.e. Sort the given data. Can they have the same form too? By form, I mean to say the prototype. The following table shows that any one implementing them would usually create them in different forms.
Parameters for different sort functions:
- Bubble sort (
array[], size)
- Quick sort (
array[], lower, upper)
- Merge sort (
array1[], array2[], array3[], size1, size2, size3)
Bubble sort requires two parameters; array of elements and size: number of elements in the array.
Quick sort requires three parameters; array of elements, lower: lower bound of the array and upper: upper bound of the array. These are necessary for the recursive partition that is done on the array.
Merge sort requires 6 parameters; array1 is the first array of elements which is already sorted, array2 is the second array of sorted elements, and array3 is the destination array where two arrays are merged as a single sorted array. Arguments size1, size2, size3 are the size of the arrays array1, array2, and array3 respectively.
We would generally create the following prototypes for the above sort functions:
void bubble_sort (int array[], int size);
void quick_sort (int array[], int lower, int upper);
void merge_sort (int array1[], int array2[], int array3[],
int size1, int size2, int size3);
Although this is a simple straight forward approach to API design, we lack consistency in solving a set of problems that do a similar task: sorting in this case. Consistency in API design has many advantages:
- They are easy to learn for the higher level applications
- Consistent APIs are easy to use
- They allow flexibility in calling API functions: e.g., consistent APIs can be accessed via pointers of the same type.
Generalizing Function Parameters
The first step in generalizing functions is to generalize the function parameters. How do we represent data for the above sort functions? A typical sort algorithm requires the following parameters:
array[]: array to be sorted
size: number of elements in the array
You can notice that Bubble Sort requires parameters array and its size, Quick Sort requires array and its lower and upper bound instead. This is because of the partition algorithm involved in case of Quick sort. (Partitioning of array is done recursively on sub arrays which causes the lower and the upper bound to change. In case of bubble sort, lower bound can always be treated as 0 and upper bound as size-1.) We will now try to find a solution for consistency in representing sort parameters, which is very simple. We can employ a general way of representing the size of the array using lower bound and upper bound. This is a necessity for the quick sort but can be made compatible with bubble sort easily. So let us now modify our prototypes by replacing the size parameter in bubble_sort() with lower and upper:
void bubble_sort (int array[], int lower, int upper);
void quick_sort (int array[], int lower, int upper);
By the time I was discussing all about representing function parameters consistently, you might have asked what about the merge_sort ()? I'm coming to it! Have a look at the prototype below:
void merge_sort (int array1[], int array2[], int array3[],
int size1, int size2, int size3);
For your quick revision, array1 is the first sorted array and size1 is the number of elements in it, array2 is the second sorted array and size2 is the number of elements in it and array3 is the array where array1 and array2 are merged as a single sorted array and size3 is the number of elements it can hold. Converting size parameter for each set of array to general form changes the function prototype as below:
void merge_sort (int array1, int array2, int array3,
int lower1, int upper1, int lower2,
int upper2, int lower3, int upper3);
Meaning of each parameter is self explanatory. So, we have generalized the way to represent the sort parameters, but to some extent. We have not generalized the function signatures themselves, therefore the above transformations would serve no useful purpose.
Generalizing Functions
Let us now discuss about generalizing the functions that solve a similar task. Generalized functions have consistent signatures and can be called with pointers of the same type. If you have a pointer to one sort function, say bubble_sort(), you must be able to call the quick_sort() function with the same pointer by pointing it to that function. Obviously, this means having consistent return types, calling conventions and parameters. For all of the above functions, we have the same return type void and same calling convention __cdecl (by default) but not the parameters. So how can we do that? Once again the solution is simple. If we could wrap the parameters in some structure and pass the structure to the function then the problem is solved! Note that, however, the structure must be generic so as to hold the parameters for all kinds of sort: the problem we have already solved using generalized parameters. Let us now try to construct such a structure. The structure definition looks like the one below:
typedef struct _SORT_PARAMS {
int *array;
int lower;
int upper;
}SORT_PARAMS;
The structure SORT_PARAMS represents parameters for any sort function in a single unit. Note that array parameter is now in the form of pointer. Adapting ourselves to the changes happening in our API design, we once again change our function prototypes.
void bubble_sort (SORT_PARAMS *sort_params);
void quick_sort (SORT_PARAMS *sort_params);
void merge_sort (SORT_PARAMS *sort_params1,
SORT_PARAMS *sort_params2,
SORT_PARAMS *sort_params3);
The sort_params can now hold the entire information for a sort function. The three pointers to SORT_PARAMS in merge_sort() correspond to the three set of arrays and their bounds. Now, it is quite straight forward to note that different sort functions require different number of SORT_PARAMS. Since all the parameters in a merge_sort are of the same type, we can replace them by a single parameter. But can a single parameter contain three arrays? It can contain because it is of pointer type. Now it becomes the responsibility of the caller to pass a pointer to array of three SORT_PARAMS structure in case of merge_sort(). API documentation should clearly state this semantic.
Here are the final function prototypes that are part of our sort API.
void bubble_sort (SORT_PARAMS *params);
void quick_sort (SORT_PARAMS *params);
void merge_sort (SORT_PARAMS *params);
Callback - What's That?
Usually, an API library written in C/C++ provides a feature called callback. A callback is a function call made by a system to your function at a particular event. This lets you execute some code when something happens (usually in the form of events), e.g., open the file when download completes. Use of such a feature requires registering your function with the system first of all. Of course, to do this you need to obtain the address of your function and store it in some variable called function pointer. An example of a callback registration function in an API header is shown below:
void register_callback_function ( void (* pFn)(int) );
Suppose you have a function called my_callback_function() which you would like to register as a callback function using the above API function, it is done as shown below:
void my_callback_function (int data)
{
}
register_callback_function (&my_callback_function);
Note that signature of my_callback_function() must match with the type of function pointer parameter pFn. The reason to discuss all these matters related to callback mechanism is the need that usually arrives in our API design. Say, you are writing a piece of code where you need to sort a given set of data and you don't want to apply a specific sort for some reasons. Then, you want the caller to provide you with the sort function of his/her choice. In general, what I meant to say is that such mechanisms are usually employed in APIs. To implement such designs, we must have consistent functions that perform the required task. You might now be clearer as to why it is advantageous to generalize our sort functions. If not, then the following section will definitely help you:
void DoSomeTask (int data, void (* pFnSort) (SORT_PARAMS *));
Take an example of the above function prototype. It requires an integer data and a pointer to some sort function, which we can pass any of the ones that we have defined above based on the type of sort required.
Examples:
DoSomeTask (val, &bubble_sort);
DoSomeTask (val, &quick_sort);
Note that API documentation must clearly document the type of sort function DoSomeTask() expects. This is not the name of the sort like bubble sort and quick sort but the nature of the sort it uses internally. For e.g., it can state that sort function must operate on one array.
General Sort Function
We now define a general sort function. A general sort function is a proxy sort function, i.e., it behaves to the caller as the actual implementation that can perform all kinds of sorting but it internally makes use of some specific sort function. We need to pass the same SORT_PARAMS pointer that is passed to the specific sort function and one extra argument that determines the type of sort function to be used. So, let us design a prototype for a general sort function. Here's it:
void sort (SORT_PARAMS *sort_params, void (* pFnSort) (SORT_PARAMS *));
Here, parameter pFnSort is a pointer to a function that returns void and accepts a pointer to SORT_PARAMS type. This means we can pass the address of any generalized form of sort functions.
The function pointer parameter is complex in its form, which can irritate you if you need to type it several times in the header file. So, let us define a new type that represents the function pointer type.
typedef void (* SORT_FUNC) (SORT_PARAMS *);
SORT_FUNC is now a defined as a function pointer type that can be used to declare function pointers, which can point to our sort functions. Accordingly, we will now simplify the prototype of our general sort function as below:
void sort (SORT_PARAMS *sort_params, SORT_FUNC pFnSort);
Note that indirection (*) is not to be applied for pFnSort since SORT_FUNC is already defined as a pointer type. The example shown below illustrates how a general sort function sort() is called to sort an array with different sort types.
int arr1 [5] = { 5, 4, 3, 2, 1 };
SORT_PARAMS array1 =
{
arr1,
0,
4
};
sort (&array1, &bubble_sort);
sort (&array1, &quick_sort);
Enumerating Sort Functions
Enumeration is another thing frequently used in API design. We can enumerate the sort functions by assigning a constant value to each function as shown below:
#define BUBBLE_SORT 0x0
#define QUICK_SORT 0x1
#define MERGE_SORT 0x2
We now include a new general sort function that is based on enumerated values. It looks like this:
void sort_array (SORT_PARAMS *params, int sort_type);
Parameter sort_type must be between 0 and 2 for the above case. Implementation of enumerated sorting can be best done using a table of function pointers to various sort functions. Make sure, however, you have no gaps in constants assigned for each sort function. Another important thing is that entry position of each sort function in the table must match its enumeration constant.
static SORT_FUNC sort_funcs [] = {
&bubble_sort, &quick_sort, &merge_sort
};
void sort_array (SORT_PARAMS *params, int sort_type)
{
if (sort_type < 0 || sort_type >= sizeof(sort_funcs)/sizeof(SORT_FUNC))
{
return;
}
sort_funcs [sort_type] (params);
}
Examples of the calling above function:
sort_array (&array1, BUBBLE_SORT);
sort_aray (&array1, QUICK_SORT);
API Implementation in C
We discussed about designing and implementation of some basic API elements like constants, structures and functions for the sort library. Now, let us put them in a file such that applications can access them. We'll usually need to create two basic files:
- header file - sort.h
- source file - sort.c
The header contains constants, structures, types and function prototypes. The source file contains code for implementing the API functions and initialization of internal static data required.
API Header
An API header file for C programs usually begins with a conditional compilation preprocessor directive that allows only one time inclusion of the header file for any compilation process. This prevents multiple definitions of the prototypes and structures and is done using #ifndef directive.
A framework of the header file "sort.h" is shown below. We need to populate it with the header stuff.
#ifndef __SORT_H
#define __SORT_H
#endif
Constants section will include definitions of various enumerated sort functions, for example:
#define BUBBLE_SORT 0x0
Structures section is a placeholder for structure declarations. We have only one i.e. SORT_PARAMS.
In typedefs section, we will put any defined types. We have SORT_FUNC as a function pointer type definition.
Finally, prototypes section contains declaration of generalized sort functions and two general sort functions sort() and sort_data().
The complete source code for the API header is shown below:
#ifndef __SORT_H
#define __SORT_H
#define BUBBLE_SORT 0
#define QUICK_SORT 1
#define MERGE_SORT 2
typedef struct _SORT_PARAMS {
int *array;
int lower;
int upper;
} SORT_PARAMS;
typedef void (* SORT_FUNC) (SORT_PARAMS *);
void bubble_sort (SORT_PARAMS *params);
void quick_sort (SORT_PARAMS *params);
void merge_sort (SORT_PARAMS *params);
void sort (SORT_PARAMS *params, SORT_FUNC pFnSort);
void sort_array (SORT_PARAMS *params, int sort_type);
#endif __SORT_H
API Source
API source “sort.c” contains the code for all functions. The source code provided with this article contains both header and source file. An example file “test.c” is also included.
History
- 11th December, 2008: Initial post