QEMU 设备模型简析(三):QOM 设计实现
前言
在上一篇文章中我们聚焦 QEMU 中面向对象的设备管理机制,阐述了引入对象模型的必要性,介绍了 QOM 基本功能和顶层设计,本文将在此基础上进一步深入,详细分析 QOM 对象系统的设计思路以及实现方式。
关键结构
通过之前的分析,我们已经知道 QOM 有如下几大关键数据结构:Object
、ObjectClass
、TypeInfo
和 TypeImpl
,他们的关系如下图所示:
通过上图不难看出,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 设备模型从使用到实现的全过程逻辑链条。