前言

上一篇文章中我们聚焦 QEMU 中面向对象的设备管理机制,阐述了引入对象模型的必要性,介绍了 QOM 基本功能和顶层设计,本文将在此基础上进一步深入,详细分析 QOM 对象系统的设计思路以及实现方式。

关键结构

通过之前的分析,我们已经知道 QOM 有如下几大关键数据结构:ObjectObjectClassTypeInfoTypeImpl,他们的关系如下图所示:

通过上图不难看出,QOM 对象模型的核心结构是 Object,这个结构体十分简洁,只保存了有关类型和父类的信息。ObjectClass 结构体就比较复杂,保存指向类型信息 TypeImpl 结构体的指针,保证了能够获取有关类型的信息。TypeImpl 存储着一个类型的信息,包括类型名称、类型大小、是否抽象类、父类名称、父类类型指针、ObjectClass 等。TypeInfo 结构体没有直接与其他任何数据结构产生直接关联,通过前面的分析我们已经知道,这是面向开发者的一个工具结构,主要用于在注册类型的时候提供类型的基本信息,在类型注册伊始,QEMU 会自动生成对应的 TypeImpl 结构体,保存类型的全部信息。

面向对象特性

首先,我们需要回顾一下面向对象的基本特征:

  • 封装
  • 接口
  • 继承
  • 析构
  • 静态成员
  • 多态
  • 动态类型装换

下面我们将详细分析上述特性在 QOM 对象系统中的具体实现。

封装

在分析 QOM 如何实现封装之前,我们需要再次回顾 Object 结构体的定义:

/* include/qom/object.h: 153 */

struct Object
{
    /* private: */
    ObjectClass *class;
    ObjectFree *free;
    GHashTable *properties;
    uint32_t ref;
    Object *parent;
};

这里需要关注的是第一行的注释,它表示结构体中的所有属性都是私有的,只能被类的内部成员访问和修改。但是,仅靠 C 语言中的结构体是无法实现对私有变量的访问控制的,因此 QEMU 在 Object 中引入了属性表,即 properties 指针,它指向一张哈希表,该表含了 Object 中的所有可以访问、修改的数据和函数其中每一个键值对表示 property 的名称以及指向相应 ObjectProperty 结构体的指针。下面给出 ObjectProperty 结构体的实现代码:

/* include/qom/object.h: 88 */

struct ObjectProperty
{
    char *name;
    char *type;
    char *description;
    ObjectPropertyAccessor *get;
    ObjectPropertyAccessor *set;
    ObjectPropertyResolve *resolve;
    ObjectPropertyRelease *release;
    ObjectPropertyInit *init;
    void *opaque;
    QObject *defval;
};

可以看到,ObjectProperty 结构体包含了这个属性的名称、类型、描述、读写方法以及解析和释放函数,还包括这个 property 特有的属性,使用 opaque 指针来表示。QOM 通过 ObjectProperty 结构体将对象的每个数据都保存在这样一个单元之中,再利用一个哈希表实现对对象所有数据的统一管理,进而实现了数据封装。当用户需要向 Object 中增加属性时,需要调用 object_property_add 函数:

/* qom/object.c: 1257 */

ObjectProperty *
object_property_add(Object *obj, const char *name, const char *type,
                    ObjectPropertyAccessor *get,
                    ObjectPropertyAccessor *set,
                    ObjectPropertyRelease *release,
                    void *opaque)
{
    return object_property_try_add(obj, name, type, get, set, release,
                                   opaque, &error_abort);
}

该函数通过进一步调用 object_property_try_add 函数向 properties 所指向的哈希表中插入了一个新的属性:

/* qom/object.c: 1206 */

ObjectProperty *
object_property_try_add(Object *obj, const char *name, const char *type,
                        ObjectPropertyAccessor *get,
                        ObjectPropertyAccessor *set,
                        ObjectPropertyRelease *release,
                        void *opaque, Error **errp)
{
    ObjectProperty *prop;
    size_t name_len = strlen(name);

    if (name_len >= 3 && !memcmp(name + name_len - 3, "[*]", 4)) {
        int i;
        ObjectProperty *ret = NULL;
        char *name_no_array = g_strdup(name);

        name_no_array[name_len - 3] = '\0';
        for (i = 0; i < INT16_MAX; ++i) {
            char *full_name = g_strdup_printf("%s[%d]", name_no_array, i);

            ret = object_property_try_add(obj, full_name, type, get, set,
                                          release, opaque, NULL);
            g_free(full_name);
            if (ret) {
                break;
            }
        }
        g_free(name_no_array);
        assert(ret);
        return ret;
    }

    if (object_property_find(obj, name) != NULL) {
        error_setg(errp, "attempt to add duplicate property '%s' to object (type '%s')",
                   name, object_get_typename(obj));
        return NULL;
    }

    prop = g_malloc0(sizeof(*prop));

    prop->name = g_strdup(name);
    prop->type = g_strdup(type);

    prop->get = get;
    prop->set = set;
    prop->release = release;
    prop->opaque = opaque;

    g_hash_table_insert(obj->properties, prop->name, prop);
    return prop;
}

继承

在面向对象编程中主要包括三种继承形式:

  • 可视继承: QEMU 中可视继承主要用于处理图形界面的相关问题,这里不做深入讨论
  • 实现继承: 子类能够直接使用基类的属性和方法而无需重新编写代码
  • 接口继承: 子类仅使用基类的属性和方法名称,属性和代码的具体内容需要重新编写代码实现

对于实现继承,QOM 通过结构体的包含关系完成。在 QEMU 中我们创建一个新类时,会实现两个数据结构:类的数据结构 ObjectClass 和对象的数据结构 Object,由于这两个结构体中的第一个成员变量就是父类(对象),那么只要通过 “指针 + 偏移量” 就可以直接使用父类的属性和方法,完成了实现继承的功能。

对于接口继承,QEMU 中定义了专门的接口结构:

/* include/qom/object.h: 514 */

struct InterfaceClass
{
    ObjectClass parent_class;
    /* private: */
    ObjectClass *concrete_class;
    Type interface_type;
};

在 QOM 中一个类可以实现多个接口,也就是接口继承。ObjectClass 结构体中与接口继承相关的属性是 interfaces,它指向一条链表,链表中的每个元素都是一个指向 InterfaceClass 的指针,再通过其中的 interface_type 指针指向一个 TypeImpl 结构体,我们可以通过给该指针指向的 TypeImpl 结构体中的函数指针赋值,从而达到实现对应接口的目的。

多态

多态是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。为了实现多态,QOM 实现了一个非常重要的功能,即动态强制类型转换(dynamic cast)。我们可以使用相关的函数,将一个 Object 的指针在运行时 cast 为子类对象的指针,可以将一个 ObjectClass 的指针在运行时 cast 为子类的指针。这样就可以调用子类中定义的函数指针来完成相应的功能。动态 cast 功能主要由 object_class_dynamic_cast 函数实现:

/* qom/object.c: 908 */

ObjectClass *object_class_dynamic_cast(ObjectClass *class,
                                       const char *typename)
{
    ObjectClass *ret = NULL;
    TypeImpl *target_type;
    TypeImpl *type;

    if (!class) {
        return NULL;
    }

    /* A simple fast path that can trigger a lot for leaf classes. */
    type = class->type;
    if (type->name == typename) {
        return class;
    }

    target_type = type_get_by_name(typename);
    if (!target_type) {
        /* target class type unknown, so fail the cast */
        return NULL;
    }

    if (type->class->interfaces &&
            type_is_ancestor(target_type, type_interface)) {
        int found = 0;
        GSList *i;

        for (i = class->interfaces; i; i = i->next) {
            ObjectClass *target_class = i->data;

            if (type_is_ancestor(target_class->type, target_type)) {
                ret = target_class;
                found++;
            }
         }

        /* The match was ambiguous, don't allow a cast */
        if (found > 1) {
            ret = NULL;
        }
    } else if (type_is_ancestor(type, target_type)) {
        ret = class;
    }

    return ret;
}

析构

QOM 的通过 “引用计数” 来判断何时调用析构函数删除对象,Object 的结构体中有一个专门用于对 Object 引用的计数变量 ref,如果 ref 的值减少为 0,就意味着系统不会继续使用这个对象了,那么就可以对相应的内存空间等进行回收操作:

/* qom/object.c: 1192 */

void object_unref(void *objptr)
{
    Object *obj = OBJECT(objptr);
    if (!obj) {
        return;
    }
    g_assert(obj->ref > 0);

    /* parent always holds a reference to its children */
    if (qatomic_fetch_dec(&obj->ref) == 1) {
        object_finalize(obj);
    }
}

在注册类型时,可以通过定义 TypeInfo 结构体中的 instance_finalize 实现自定义析构函数,对于引用计数为 0 的 Object 进行垃圾回收操作。QOM 提供了默认析构函数:

/* qom/object.c: 688 */

static void object_finalize(void *data)
{
    Object *obj = data;
    TypeImpl *ti = obj->class->type;

    object_property_del_all(obj);
    object_deinit(obj, ti);

    g_assert(obj->ref == 0);
    g_assert(obj->parent == NULL);
    if (obj->free) {
        obj->free(obj);
    }
}

Object 数据结构中有一个 ObjectFree * 类型的函数指针 free,当 Object 的引用计数为 0 时,就会调用这个函数进行垃圾回收:

/* qom/object.c: 677 */

static void object_deinit(Object *obj, TypeImpl *type)
{
    if (type->instance_finalize) {
        type->instance_finalize(obj);
    }

    if (type_has_parent(type)) {
        object_deinit(obj, type_get_parent(type));
    }
}

该函数会调用 TypeImpl 中的实例析构函数。如果存在父类,则会递归继续调用父类的实例析构函数。这里之所以需要调用父类实例析构函数是因为一个 Object 结构体的第一个成员变量就是父类对象的实例,因此当我们需要对对象析构时,不仅要调用当前类的析构方法,也需要调用父类的析构方法将结构体中的第一个成员进行析构。

总结

QOM 实现了一套较为完备的对象管理系统,包括自动化的对象注册、完善的对象管理以及巧妙的动态类型转换,本文梳理了 QOM 实现中几大关键数据结构之间的联系,详细分析了封装、多态、继承以及析构等面向对象特性在 QOM 中的集体实现,与之前的文章相呼应,打通了 QEMU 设备模型从使用到实现的全过程逻辑链条。

参考资料