C语言实现“多接口”的另一种方式

前面有一篇文章介绍了如何使用兼容一点五编程的方式去支持“多接口”。 作为对比,本文提供另一种实现“多接口”的方式,来帮助大家从另一个角度理解“多接口”实现的要点和面对的困难。

代码示例

为了方便参照对比,接口定义部分,同前面那篇文章一样:

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;
};

接下来是接口的实现者,同前面那篇文章类似:

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;
}

注意看,这里特地把接口字段bird_interface放到了“非首字段”,以和一点五编程方案形成更明显的视觉区分。

同样的方式,我们实现一下另一个接口:

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;
}

然后是一个实现了多个接口的对象:

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;
}

上面的代码,除了接口指针所在的字段位置不同,总体上还是跟一点五编程非常类似的。

这是必然的,通过虚函数表或类似结构来进行接口编程时,“实现者”这部分必定是相似的。 区别主要体用在“使用者”这一边。

接下来我们编写“使用侧”的代码。首先必须定义下面这个宏:

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

这个宏,是用来通过字段地址,反推出字段所属的结构体对象地址。 这个宏在Linux中经常使用,Linux中的链表等数据结构,都是基于CONTAINER_OF的原理实现的。

offsetof这个关键字可以用来实现CONTAINER_OF宏,但是本文还是使用更传统,编译器支持更广的计算方案。

接着我们准备好用来测试的数据:

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

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

直接使用的话,代码按上去比较复杂:

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);

上面的调用语句太长,我们提取关键信息简化一下得到:

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

上面的形式还不够清晰,让我们将它等价变换成另一种形式:

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

省略掉业务参数,只保留核心骨架,这个公式等同于:

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

变成这种形式,大家就能发现这种方案,跟一点五编程(*p)->f(p)的区别了。

同时,这个公式也出现在前面一篇文章《C++的虚函数,在机器层面的实现方式》中, 大家可以对比思考,融会贯通。

对于不同类型的obj,这个OFFSET_OF_obj_i的值不同,而OFFSET_OF_obj_i这个值只在编译期才有,运行期没有, 所以这种形式的调用,只好通过宏来实现简化和统一。

使用方式简化

通过引入这样一个宏:

#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__)

调用形式可以在视觉上得到简化:

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);

使用方式再度简化

如果我们对使用再加一个约束:要求字段必须用特定的名字。(这个限制是否过于苛刻另谈) 比如bird_i的接口字段必须叫bird_interfacedog_i的接口字段必须叫dog_interface

那么我们就可以对外提供更简洁的接口了:

#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__)

使用的时候,用户只需要这样调用接口:

DOG_BARK(&hus, 8);

BIRD_FLY(&black, 9);

DOG_BARK(&air, 10);

BIRD_FLY(&air, 11);

总结

此方案实现的“多接口”对比前面那篇文章的链表方案,优点在于:运行性能更高,实现原理也相对更简单。

但缺点也很明显:没有实现真正的动态化。调用者需要向实现者提供很多对象之外的额外信息,比如对象类型,接口字段名。

这个缺点无法有效避免,相关信息要么通过参数传进去,要么通过“约定”对实现者的代码做限制。 在C语言里,我们可以通过宏粗略地掩盖一下。