Another Way For Multiple Interface In C Language

One previous article has introduced a way to implement multiple interfaces in a way that is compatible with OPF Programming.

For comparison, this article will introduce a new way to implement multiple interfaces. We hope this new way provides some help when you are trying to understand the key to implement multiple interfaces and the difficulties you are facing.

Code Sample

To make things easier, the interface definitions have been kept same as the previous article.

typedef int (*bird_fly_fn_t)(void *self, int distance);
typedef int (*dog_run_fn_t)(void *self, int distance);
typedef int (*dog_bark_fn_t)(void *self, int count);

struct bird_i {
    bird_fly_fn_t fly;
};

struct dog_i {
    dog_run_fn_t run;
    dog_bark_fn_t bark;
};

Object types who implement those interfaces are just like the previous article, too.

struct crow {
    const char *name;
    struct bird_i *bird_interface;
};

int crow_fly(struct crow *self, int distance);

struct bird_i crow_interface = {
    .fly = (bird_fly_fn_t)crow_fly,
};

int crow_init(struct crow *self, const char *name) {
    self->bird_interface = &crow_interface;
    self->name = name;
    return 0;
}

int crow_fly(struct crow *self, int distance) {
    my_printf("Crow %s is flying, target distance: %d\n", self->name, distance);
    return 0;
}

We put the bird_interface on a non-first position to highlight its difference from the OPF Programming compatible way.

Let's implement the other interface for another type:

struct husky {
    const char *name;
    struct dog_i *dog_interface;
};

int husky_run(struct husky *self, int distance);
int husky_bark(struct husky *self, int distance);

struct dog_i husky_interface = {
    .run = (dog_run_fn_t)husky_run,
    .bark = (dog_bark_fn_t)husky_bark,
};

int husky_init(struct husky *self, const char *name) {
    self->dog_interface = &husky_interface;
    self->name = name;
    return 0;
}

int husky_run(struct husky *self, int distance) {
    my_printf("Husky %s is running, target distance: %d\n", self->name, distance);
    return 0;
}

int husky_bark(struct husky *self, int distance) {
    my_printf("Husky %s is barking, target count: %d\n", self->name, distance);
    return 0;
}

Now, here is the object type who implements both interfaces:

struct angel_dog {
    const char *name;
    int wing_span;
    struct bird_i *bird_interface;
    struct dog_i *dog_interface;
};

int angel_dog_fly(struct angel_dog *self, int distance);
int angel_dog_run(struct angel_dog *self, int distance);
int angel_dog_bark(struct angel_dog *self, int distance);

struct bird_i angel_dog_bird_interface = {
    .fly = (bird_fly_fn_t)angel_dog_fly,
};

struct dog_i angel_dog_dog_interface = {
    .run = (dog_run_fn_t)angel_dog_run,
    .bark = (dog_bark_fn_t)angel_dog_bark,
};

int angel_dog_init(struct angel_dog *self, const char *name, int wing_span) {
    self->dog_interface = &angel_dog_dog_interface;
    self->bird_interface = &angel_dog_bird_interface;
    self->wing_span = wing_span;
    self->name = name;
    return 0;
}

int angel_dog_fly(struct angel_dog *self, int distance) {
    my_printf("Angel dog %s is flying, target distance: %d\n", self->name, distance);
    return 0;
}

int angel_dog_run(struct angel_dog *self, int distance) {
    my_printf("Angel dog %s is running, target distance: %d\n", self->name, distance);
    return 0;
}

int angel_dog_bark(struct angel_dog *self, int distance) {
    my_printf("Angel dog %s is barking, target count: %d\n", self->name, distance);
    return 0;
}

The code is pretty similar to the code in OPF Programming compatible way except that struct fields for interface pointers are in different offsets.

That similarity is not by accident. When we are using Function Table to do interface programming, the implementor part will be similar. The main difference is on the user part.

Let's wrtie the user part code. First, we need to define this macro:

#define CONTAINER_OF(P_OBJ, TYPE, FIELD) \
    ((TYPE *)((char *)P_OBJ - (uintptr_t)(&((TYPE *)0)->FIELD)))

With this macro, we can get the address of the object from the address of the interface pointer.

offsetof, who is supported by modern compilers, can be used to implement CONTAINER_OF, but we are using a more traditional way in this article.

Let's prepare some testing data:

struct crow black;
struct husky hus;
struct angel_dog air;

crow_init(&black, "Black");
husky_init(&hus, "Hus");
angel_dog_init(&air, "Air", 8);

If we use these data directly, code will be complicated:

hus.dog_interface->bark(CONTAINER_OF(&hus.dog_interface, struct husky, dog_interface), 1);

black.bird_interface->fly(CONTAINER_OF(&black.bird_interface, struct crow, bird_interface), 2);

air.dog_interface->bark(CONTAINER_OF(&air.dog_interface, struct angel_dog, dog_interface), 3);

air.bird_interface->fly(CONTAINER_OF(&air.bird_interface, struct angel_dog, bird_interface), 4);

The calling statements are too long. If we fetch out the key information and simplify it, we can get:

obj.i->f(&obj.i - OFFSET_OF_obj_i, 1);

It is still not clear enough. By doing some transformations, we can get an even simpler form:

(*p)->f(p - OFFSET_OF_obj_i, 1);

By removing unnecessary arguments, the expression can be represented as:

(*p)->f(p - N);

Now we can see the difference between this method and the (*p)->f(p) in OPF Programming.

And this expression also exists in previous article "C++ Virtual Function In The Machine Level". Comparing these 2 articles may help a lot.

For different types, the OFFSET_OF_obj_i is different. And the OFFSET_OF_obj_i only exists during compile time. You can't get it in runtime. This is why we have to use macros here.

Simplification On Usage

By introducing this macro:

#define INTERFACE_CALL(P_OBJ, I_FIELD, METHOD, ...) \
    (P_OBJ)->I_FIELD->METHOD(CONTAINER_OF(&(P_OBJ)->I_FIELD, typeof(*(P_OBJ)), I_FIELD), __VA_ARGS__)

We can simplify the calling code:

INTERFACE_CALL(&hus, dog_interface, bark, 5);

INTERFACE_CALL(&black, bird_interface, fly, 6);

INTERFACE_CALL(&air, dog_interface, bark, 6);

INTERFACE_CALL(&air, bird_interface, fly, 7);

More Simplification On Usage

If we add one more restriction: The fields for interfaces have to be specific names.

e.g. Field for bird_i interface have to be bird_interface, and field for dog_i have to be dog_interface, etc.

Then we can provide even simpler interfaces by implementing these macros:

#define BIRD_FLY(P_OBJ, ...) \
    INTERFACE_CALL(P_OBJ, bird_interface, fly, __VA_ARGS__)

#define DOG_BARK(P_OBJ, ...) \
    INTERFACE_CALL(P_OBJ, dog_interface, bark, __VA_ARGS__)

Examples of using the simplified interfaces:

DOG_BARK(&hus, 8);

BIRD_FLY(&black, 9);

DOG_BARK(&air, 10);

BIRD_FLY(&air, 11);

Conclusion

The multiple interfaces implementation in this article is simpler and more efficient than the OPF Programming compatible way.

But it also has some flaws: It's not dynamic enough. Users have to provide many information like data type, filed name, etc.

We can not really fix the flaw, we can only cover it with macros.