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 implementCONTAINER_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)
inOPF 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 bebird_interface
, and field fordog_i
have to bedog_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.