To address the first point directly: function pointers are useful as a means to expose a standard set of functionality. Consider how a character device driver for the linux kernel operates, for instance. When you write a character device driver you are presented with a struct file_operations (fops).
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
int (*readdir) (struct file *, void *, filldir_t);
/* ... lots of other calls ... */
};
Why? Consider what you do with a character device; read from it, write to it, and so on. But since each device requires it's own implementation there needs to be a way to indicate this within the module code.
One solution to this is to have a set of skeleton functions declared in a header file. Require that all modules include this file and provide definitions for the functions. There are two obvious drawbacks to this approach:
- Module writers are forced to fill in an empty body for all functions that will not be used
- Any changes to the interface require reimplementation of the entire set of modules using the now outdated file
Another solution is to use a structure that contains function pointers and allow developers to use that when writing to the interface. This solves the two issues with the skeleton declaration approach: any function not defined is set to nil in the structure and adding new members to the structure wont break existing code. Ok... if there is some fancy offset calculation being done there is potential for breakage, but that is suspect behavior in the first place.
Obviously removing entries creates compatibility issues in either solution.
Here is how the function pointer approach works in the linux kernel:
/* linux-2.6.37/drivers/i2c/i2c-dev.c:513 */
static const struct file_operations i2cdev_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read = i2cdev_read,
.write = i2cdev_write,
.unlocked_ioctl = i2cdev_ioctl,
.open = i2cdev_open,
.release = i2cdev_release,
};
Where each of the i2cdev_* functions are defined locally to that file and any operations not defined in the struct are nil (aio_fsynch, for example).
That actually represents dynamic code loading in it's entirety. You can change the behavior of a system by loading modules at runtime. But that is in the kernel and not applicable in userland; what if we wanted to do that for a process we control? That is something that Erlang lets you do and - because I'm bent on reinventing the wheel for experience - I wanted to do it in C as well. Note: I'm sure that Erlang is not the only language that supports this; it just happens to be what I was reading recently.
Lets start with the interface that is exposed:
#ifndef JRB_DYNOPS__H__
#define JRB_DYNOPS__H__
/*
* Operations supported by our dynamic runtime:
* init: Initialize the new code
* speak: process strings in some way
* destroy: Cleanup code
*/
struct dynops {
void (*init)();
void (*speak)(const char *);
void (*destroy)();
};
/*
* Fill in the opeartions for a particular implementation
*/
void get_ops (struct dynops *);
#endif /*JRB_DYNOPS__H__*/
And two clients writing to that interface:
Client 1
#include <stdio.h>
#include <string.h>
#include "dynops.h"
static void lib1_speak (const char * text) {
fprintf (stderr, "[LIB1] %s\n", text);
}
/* only implements speak. Ignores init & destroy */
static struct dynops my_ops = {
.speak = lib1_speak,
};
void get_ops (struct dynops ** ops) {
memcpy(*ops,&my_ops,sizeof(my_ops));
}
Client 2
#include <stdio.h>
#include <string.h>
#include "dynops.h"
static void lib2_init () {
fprintf (stderr, "Initializing lib2\n");
}
static void lib2_destroy () {
fprintf (stderr, "Cleanup lib2\n");
}
static void lib2_speak (const char * text) {
fprintf (stderr, "[LIB2] %s\n", text);
}
static struct dynops my_ops = {
.init = lib2_init,
.speak = lib2_speak,
.destroy = lib2_destroy,
};
void get_ops (struct dynops ** ops) {
memcpy(*ops,&my_ops,sizeof(my_ops));
}
This driver is relatively crude. Notification of code change come by way of a signal and a well-known path indicates where the code resides. In addition, the mechanism for copying the structure of function pointers should reside in the driver itself and not in the clients. I think it is sufficient, however, to convey the idea.
int main () {
unsigned long count = 0;
char iteration[255] = {0};
signal (SIGUSR1, trigger_load);
while (1) {
snprintf (iteration, 255, "Iteration: %lu\n", ++count);
do_ops (iteration);
usleep (500000);
}
return 0;
}
And some implementation
void trigger_load (int sig) {
if (sig != SIGUSR1)
return;
FILE * new_lib = fopen("/tmp/dynlib", "r");
if (! new_lib)
return;
fscanf (new_lib, "%s", lib_name);
fclose (new_lib);
the_lib = lib_name;
reload = 1;
}
/* ... */
void do_ops (const char * str) {
if (reload) {
load_lib (the_lib);
reload = 0;
}
if (ops.speak)
ops.speak(str);
}
With that setup we can now control the behavior of that driver at runtime. The example here runs the driver in it's own terminal and in another terminal commands are executed to load new code. The table below shows the execution in sequence - dynamic code execution in C.
Terminal 1
$ ./a.out 'libops1.so' successfully loaded [LIB1] Iteration: 40 ... [LIB1] Iteration: 52 'libops2.so' successfully loaded Initializing lib2 [LIB2] Iteration: 53 ... [LIB2] Iteration: 62 Cleanup lib2 'libops1.so' successfully loaded [LIB1] Iteration: 63 ...
Terminal 2
$ pgrep a.out 5993 $ echo "libops1.so" > /tmp/dynlib $ kill -10 5993 $ echo "libops2.so" > /tmp/dynlib $ kill -10 5993 $ echo "libops1.so" > /tmp/dynlib $ kill -10 5993
No comments :
Post a Comment