To a programmer, reading data from the keyboard or the CDROM is essentially the same thing. All the programmer cares about is that the bits of data come from somewhere. It is one of the fundamental roles of the operating system to provide this abstraction for the programmer.
Abstraction is implemented by an Application Programming Interface. The programmer designs a set of functions and variables as the API which other programmers will use. A common method used in the Linux Kernel is function pointers.
Example 1-1. Abstraction with function pointers
#include <stdio.h> /* The API to implement */ struct greet_api 5 { int (*say_hello)(char *name); int (*say_goodbye)(void); }; 10 /* Our implementation of the hello function */ int say_hello_fn(char *name) { printf("Hello %s\n", name); return 0; 15 } /* Our implementation of the goodbye function */ int say_goodbye_fn(void) { 20 printf("Goodbye\n"); return 0; } /* A struct implementing the API */ 25 struct greet_api greet_api = { .say_hello = say_hello_fn, .say_goodbye = say_goodbye_fn }; 30 /* main() doesn't need to know anything about how the * say_hello/goodbye works, it just knows that it does */ int main(int argc, char *argv[]) { 35 greet_api.say_hello(argv[1]); greet_api.say_goodbye(); printf("%p, %p, %p\n", greet_api.say_hello, say_hello_fn, &say_hello_fn); 40 exit(0); };
Code such as the above is the simplest example of constructs used repeatedly through the Linux Kernel (and indeed many other projects). Lets have a look at some specific elements.
We start out with a structure that defines the API. The functions whose names are encased in parenthesis with a pointer marker describe a function pointer[1]. The function pointer describes the prototype of function it must point to; pointing it at a function without the correct return type or parameters will generate a compiler warning, and will probably lead to incorrect operation or crashes.
We then have our implementation of the API. Often for more complex functionality you will see an idiom where API implementation functions will only be a wrapper around another function that is conventionally prepended with or or two underscores (i.e. say_hello_fn() would call another function _say_hello_function()). This avoids "namespace pollution" and often enables significant changes in the internal workings whilst leaving the API constant. Our implementation is very simple however, and doesn't even need it's own support functions.
Second to last, we fill out the function pointers in struct greet_api greet_api. The name of the function is a pointer, therefore there is no need to take the address of the function (i.e. &say_hello_fn).
Finally we can call the API functions through the structure in main.
Libraries have two roles which illustrate abstraction.
Allow programmers to reuse commonly accessed code.
Act as a black box implementing functionality for the programmer.
For example, a library implementing access to the raw data in JPEG files has both the advantage that the many programs who wish to access image files can all use the same library and the programmers building these programs do not need to worry about the exact details of the JPEG file format, but can concentrate their efforts on what their program wants to do with the image.
The essential points to note are
We are free to change the internal implementation at any time; say to write to a file rather than the screen.
We can setup function pointers as handy ways to implement an API
Compile the above program with gcc and run it.
Using the %p specifier of printf find the address of the say_hello_fn function. Compare this to the values of the same function. Are they the same?
Extend the API in some creative way by specifying a new function in the struct greet_api and implementing it.
[1] | Often you will see that the names of the parameters are omitted, and only the type of the parameter is specified. This allows the implementer to specify their own parameter names avoiding warnings from the compiler. |