一点五编程如何支持对象实现“多接口”

何为“接口”?

本文所述的“接口”,是指实现类似Go语言里面的interface,或者Rust语言里的trait object这类动态派发的技术。

“一点五编程”支持多个接口吗?

一点五编程的“”部分,即(*p)->f(p),不支持多个接口。因为这个技术依赖于:

接口结构体指针,必须是接口实现者的第一个字段。

换言之,一点五编程的“点五”,即(*p)->f(p)在带来极大的便利性的同时,也带来了一层限制。

那我们把条件放宽,能否实现一个兼容“一点五编程”的多接口方案呢?答案是可以,本文将介绍一种简单可用的实现方案。

单接口示例(用于对比)

先定义要实现的两个接口:bird_i包含了flydog_i包含了runbark

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

然后是huskydog_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,
};

然后是crowbird_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_simplebird_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_idog_i接口:

struct angel_dog {
    void *interface;
    int wing_span;
    const char *name;
};

利用上面定义的宏,我们可以定义两个全局接口对象angel_dog_interface_1angel_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_dogbird_idog_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);

后记

因为篇幅原因,代码经过了轻度精简,部分与核心算法无关的代码被省去。

希望有志学习“一点五编程”的学习者,将文中所属内容亲自实践一番,补上缺失部分将其运行起来, 体味这套方案的运作方式,思考它的优劣,以及其是否有存在的必要。