一点五编程
如何支持对象实现“多接口”
何为“接口”?
本文所述的“接口”,是指实现类似Go
语言里面的interface
,或者Rust
语言里的trait object
这类动态派发的技术。
“一点五编程”支持多个接口吗?
一点五编程
的“术”部分,即(*p)->f(p)
,不支持多个接口。因为这个技术依赖于:
接口结构体指针,必须是接口实现者的第一个字段。
换言之,一点五编程的“点五”,即(*p)->f(p)
在带来极大的便利性的同时,也带来了一层限制。
那我们把条件放宽,能否实现一个兼容“一点五编程”的多接口方案呢?答案是可以,本文将介绍一种简单可用的实现方案。
单接口示例(用于对比)
先定义要实现的两个接口:bird_i
包含了fly
,dog_i
包含了run
和bark
:
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;
};
先看传统的一点五编程,如何实现这两个接口。
定义一只普通的狗(哈士奇),来实现dog_i
接口:
struct husky {
struct dog_i *interface;
const char *name;
};
定义一个全局接口husky_interface
对象,这个接口对象,将被所有struct husky
实例共享。
struct dog_i husky_interface = {
.run = (dog_run_fn_t)husky_run,
.bark = (dog_bark_fn_t)husky_bark,
};
然后是husky
对dog_i
的实现部分:
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;
}
接下来,定义一只普通的鸟(乌鸦),来实现bird_i
接口:
struct crow {
struct bird_i *interface;
const char *name;
};
定义一个全局接口对象crow_interface
,这个接口对象,将被所有struct crow
实例共享。
struct bird_i crow_interface = {
.fly = (bird_fly_fn_t)crow_fly,
};
然后是crow
对bird_i
的实现部分:
int crow_fly(struct crow *self, int distance) {
my_printf("Crow %s is flying, target distance: %d\n", self->name, distance);
return 0;
}
实现完毕。使用示例如下:
struct husky hus;
dog_run_simple((struct dog_i **)&hus, 10);
struct crow black;
bird_fly_simple((struct bird_i **)&black, 1000);
这其中,dog_run_simple
和bird_fly_simple
这两个函数,只是对(*p)->f(p)
的简单包装:
int dog_run_simple(struct dog_i **dog, int distance) {
return (*dog)->run(dog, distance);
}
int bird_fly_simple(struct bird_i **bird, int distance) {
return (*bird)->fly(bird, distance);
}
兼容“一点五编程”的多接口实现
简而言之,这个方案使用了一个单向链表,将所有的接口对象(或者说虚函数表)串起来, 每个链表节点,都带有一个标志其接口类型的TAG。巧妙之处在于,结构体会进行一些“位移”,以兼容“一点五编程”。
+--------+ +--------+
| ---+--. | ---+--.
+--------+ | +--------+ |
VARIABLE OBJECT | DOG | | | BIRD | |
+--------+ .---> +--------+ .---> +--------+ `---> +--------+ `--->
| ---+--' | ---+--' | run | | fly |
+--------+ +--------+ +--------+ +--------+
| | | bark |
| | +--------+
| |
| |
+--------+
现在我们来实现它。先要定义一些辅助的结构体和宏:
struct complex_i_header {
void *next;
void *tag;
};
#define H_ADDR_TO_I_ADDR(h) (void *)((uintptr_t)h + sizeof(struct complex_i_header))
#define I_ADDR_TO_H_ADDR(i) (struct complex_i_header *)((uintptr_t)i - sizeof(struct complex_i_header))
#define COMPLEX_INTERFACE(TYPE) \
struct { \
struct complex_i_header h; \
TYPE i; \
}
然后定义一个遍历这个链表,并调整OFFSET(以兼容“一点五编程”)的辅助函数:
static int find_i_from_chain(void *interface_chain, void *tag, void **result) {
struct complex_i_header *h;
h = I_ADDR_TO_H_ADDR(interface_chain);
if (h->tag == tag) {
*result = interface_chain;
return 0;
}
if (h->next == NULL)
return 1;
return find_i_from_chain(h->next, tag, result);
}
接下来定义一只“会飞的狗”(天使狗),同时实现bird_i
和dog_i
接口:
struct angel_dog {
void *interface;
int wing_span;
const char *name;
};
利用上面定义的宏,我们可以定义两个全局接口对象angel_dog_interface_1
和angel_dog_interface_2
。
这两个接口对象,将被所有struct angel_dog
实例共享。
初始化的过程中,将这两个接口直接串起来。(有多少个接口串多少个)
COMPLEX_INTERFACE(struct bird_i)
angel_dog_interface_1 = {
.h.next = NULL,
.h.tag = &bird_i_anchor,
.i.fly = (bird_fly_fn_t)angel_dog_fly,
};
COMPLEX_INTERFACE(struct dog_i)
angel_dog_interface_2 = {
.h.next = H_ADDR_TO_I_ADDR(&angel_dog_interface_1),
.h.tag = &dog_i_anchor,
.i.run = (dog_run_fn_t)angel_dog_run,
.i.bark = (dog_bark_fn_t)angel_dog_bark,
};
然后是struct angel_dog
对bird_i
和dog_i
的实现部分:
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;
}
接着我们定义两个函数Demo,来使用实现了多接口的对象:
int bird_fly_complex(void *bird, int distance) {
struct bird_i *i;
if (find_i_from_chain(*(void **)bird, &bird_i_anchor, (void **)&i))
return 1;
return i->fly(bird, distance);
}
int dog_run_complex(void *dog, int distance) {
struct dog_i *i;
if (find_i_from_chain(*(void **)dog, &dog_i_anchor, (void **)&i))
return 1;
return i->run(dog, distance);
}
使用实例:
struct angel_dog air;
//...
dog_run_complex(&air, 100);
bird_fly_complex(&air, 2000);
前面提到的兼容性,体现在这里:
因为链表里的第一个接口是struct dog_i
类型的,所以也可以用传统的“一点五编程”方式使用它。
dog_run_simple((struct dog_i **)&air, 10);
后记
因为篇幅原因,代码经过了轻度精简,部分与核心算法无关的代码被省去。
希望有志学习“一点五编程”的学习者,将文中所属内容亲自实践一番,补上缺失部分将其运行起来, 体味这套方案的运作方式,思考它的优劣,以及其是否有存在的必要。