tcx4c70
虚拟化软件工程师
Adam Tao's Blog

QOM(QEMU Object Module)浅析(一)

本文基于QEMU 7.0。

QEMU中同一类别的设备可能有很多种,对于同类不同种的设备,需要调用不同的函数进行相应的处理;并且,同一总线下可能挂在了很多不同种,甚至不同类别的设备,也需要调用不同的函数进行相应的处理。为了能方便的处理这些情况,面向对象的思想是必不可少的。为此,QEMU中实现了一套面向对象的模型——QOM(QEMU Object Module)。

QOM实现了类似JAVA的单根继承的结构,可实现多个接口,并可对对象的属性进行动态的增加、移除、修改。其中比较重要的几个结构如下:

  • Object: 所有可实例化的对象的基类;
  • ObjectClass: 所有类对象的基类;
  • Interface: 所有接口的基类;
  • ObjectProperty: 对象可动态增加、移除、修改的属性;
  • TypeInfo: 保存对应类型的信息(包括继承关系、实现的接口、是否是抽象类、类对象的初始化函数、实例的初始化函数(构造函数?)等);
  • TypeImpl: TypeInfo的进一步抽象,以此来初始化对应的类对象及对应的示例。该结构对用户是透明的,用户只需定义TypeInfo。

每个结构的具体信息及其字段的具体含义可在qemu/include/qom/object.h中查看。它们之间的关系如以下的UML所示:

PlantUML Syntax:
abstract class Object
class ObjectClass << (S, #FF7700) Singleton >>
class InterfaceClass << (S, #FF7700) Singleton >>
class UserCreatableClass << (S, #FF7700) Singleton >>
interface Interface
interface UserCreatable
Object -> ObjectClass : class
Object o--> Object : parent
ObjectClass o-> TypeImpl : type
Object *--> ObjectProperty : properties
ObjectClass *--> ObjectProperty : properties
ObjectClass *--> InterfaceClass : interfaces
ObjectClass <|-- InterfaceClass
ObjectClass <--o InterfaceClass : concrete_class

TypeInfo *--> InterfaceInfo : interfaces
TypeImpl <--o InterfaceClass: interface_type
TypeImpl o--> TypeImpl : parent_type
TypeImpl o-> ObjectClass : class
TypeImpl *--> InterfaceImpl : interfaces
TypeImpl .> TypeInfo

InterfaceClass <|-- UserCreatableClass
Interface <|-- UserCreatable
Interface .> InterfaceClass
UserCreatable .> UserCreatableClass

若想要新增一种类,则需要实现以下几个结构体:

  • 新增一个Object:若为接口,则只需typedef一下即可,如typedef struct MyInterface MyInterface;;否则的话,该结构体中放置对象的各种各成员变量,其中第一个成员必须是Object或其子类;
  • 新增一个ObjectClass:该结构体放置对象的各种成员函数和类成员变量,其中第一个成员必须是ObjectClass或其子类(若为接口,则第一个成员必须是Interface
  • TypeInfo:该结构体主要用于注册该类的typename、继承关系、构造析构函数等。

QOM中面向对象的特性


利用QOM,QEMU实现了在C语言中编写面向对象风格的代码。虽然使用起来,与C++、JAVA这种原生支持面向对象的语言略复杂了些,但至少封装、继承、多态三大面向对象的特性都可以实现。

封装

封装自不用说,C语言中的结构体就能实现封装;当然, 像publicprotectedprivate这种访问属性是没法实现了,虽说将对象中部分数据的类型定义放到.c文件中或将数据定义为void *opaque也能实现类似private的能力,但是这样代码的可阅读性和可维护性就大大降低了,因此对于数据访问属性的控制更多还是靠代码中的约定。如DeviceState定义中的parent_objprivate的,你不应该直接访问它。

struct DeviceState {
    /*< private >*/
    Object parent_obj;
    /*< public >*/

    const char *id;
    char *canonical_path;
    bool realized;
    bool pending_deleted_event;
    QemuOpts *opts;
    int hotplugged;
    BusState *parent_bus;
    QLIST_HEAD(, NamedGPIOList) gpios;
    QLIST_HEAD(, BusState) child_bus;
    int num_child_bus;
    int instance_id_alias;
    int alias_required_for_version;
};

继承

继承分为对类的泛化和对接口的实现,这两者在QOM中主要是通过定义结构体TypeInfo中的成员实现的。在TypeInfo中通过指定nameparent分别指定该类及其父类的名字(应该说是type name或者type id,需保证全局唯一),在interfaces中指定该类实现的接口的名字。同时,为了向上类型转换和向下类型转换方便,还需要分别将该类父类对应的ObjectObjectClass结构体作为该类对应的ObjectObjectClass结构体中的首位成员。

interface Interface0 {
}

interface Interface1 {
}

abstract class Base extends Object {
}

class Derived0 extends Base implements Interface0 {
}

class Derived1 extends Base implements Interface0, Interface1 {
}

例如,上述Java代码“等价”下面用QOM实现的代码:

typedef struct Interface0 Interface0;
typedef struct Interface0Class {
    InterfaceClass parent_class;
};
static const TypeInfo interface0_info {
    .name   = "Interface0",
    .parent = TYPE_INTERFACE,
};

typedef struct Interface1 Interface1;
typedef struct Interface1Class {
    InterfaceClass parent_class;
};
static const TypeInfo interface1_info {
    .name   = "Interface1",
    .parent = TYPE_INTERFACE,
};

typedef struct Base {
    Object parent_obj;
};
typedef struct BaseClass {
    ObjectClass parent_class;
};
static const TypeInfo base_info {
    .name     = "Base",
    .parent   = TYPE_OBJECT,
    .abstract = true,
};

typedef struct Drived0 {
    Base parent_obj;
};
typedef struct Drived0Class {
    BaseClass parent_class;
};
static const TypeInfo drived0_info {
    .name          = "Drived0",
    .parent        = "Base",
    .instance_size = sizeof(Drived0),
    .interfaces    = (InterfaceInfo[]) {
        { "Interface0" },
        { },
    },
};

typedef struct Drived1 {
    Base parent_obj;
};
typedef struct Drived1Class {
    BaseClass parent_class;
};
static const TypeInfo drived1_info {
    .name          = "Drived1",
    .parent        = "Base",
    .instance_size = sizeof(Drived1),
    .interfaces    = (InterfaceInfo[]) {
        { "Interface0" },
        { "Interface1" },
        { },
    },
};

说到继承就不得不说一下类型转换,尤其是安全的向下类型转换。由于QEMU本身使用C语言实现的,而C语言本身不支持OO,因此不能像C++或JAVA那样使用.->访问父类的成员,只能先将对象向上转换为父类对象,然后再访问父类的成员。此外,C语言也无法做到给成员函数自动传递this指针(或其他类似的概念),只能再成员函数的参数中增加一个指向该对象的指针,调用时手动传递该对象的指针,而这个参数类型只能为指向父类的指针。若子类中的override这个成员函数,常常需要访问子类的成员,因此需要将该参数向下转换为指向子类的指针。由于以上两点原因,QEMU中涉及到QOM的代码中类型转换的次数非常多。QOM中为了能更方便、安全地进行类型转换,提供了下面的helper 函数:

/**
 * object_dynamic_cast:
 * @obj: The object to cast.
 * @typename: The @typename to cast to.
 *
 * This function will determine if @obj is-a @typename.  @obj can refer to an
 * object or an interface associated with an object.
 *
 * Returns: This function returns @obj on success or #NULL on failure.
 */
Object *object_dynamic_cast(Object *obj, const char *typename);

/**
 * object_dynamic_cast_assert:
 *
 * See object_dynamic_cast() for a description of the parameters of this
 * function.  The only difference in behavior is that this function asserts
 * instead of returning #NULL on failure if QOM cast debugging is enabled.
 * This function is not meant to be called directly, but only through
 * the wrapper macro OBJECT_CHECK.
 */
Object *object_dynamic_cast_assert(Object *obj, const char *typename,
                                   const char *file, int line, const char *func);

这两个函数都可以进行安全的动态类型转换,但主要有以下2点不同:

  1. obj不是指向typename类型的指针时,object_dynamic_cast会返回NULL;而object_dynamic_cast_assert则会调用assert()使得当前程序退出;
  2. object_dynamic_cast只是做了类型检查,而object_dynamic_cast_assert除此之外还将(成功的)类型转换的结果缓存了下来以供下次类型转换使用。因此,当某种类型频繁地转换为另一种类型时,使用object_dynamic_cast_assert会更快。

由于多数情况下,当obj不是我们期望的类型时,说明代码逻辑出了问题,我们无法进行进一步的处理,只能退出程序,因此object_dynamic_cast_assert更常用一些。而QOM也用宏封装了这个函数,方便我们调用:

/**
 * OBJECT_CHECK:
 * @type: The C type to use for the return value.
 * @obj: A derivative of @type to cast.
 * @name: The QOM typename of @type
 *
 * A type safe version of @object_dynamic_cast_assert.  Typically each class
 * will define a macro based on this type to perform type safe dynamic_casts to
 * this object type.
 *
 * If an invalid object is passed to this function, a run time assert will be
 * generated.
 */
#define OBJECT_CHECK(type, obj, name) \
    ((type *)object_dynamic_cast_assert(OBJECT(obj), (name), \
                                        __FILE__, __LINE__, __func__))

一般地,每一种QOM对象都会对OBJECT_CHECK再次封装,如#define ARM_CPU(obj) OBJECT_CHECK(ARMCPU, (obj), TYPE_ARM_CPU)

多态

每个类对应的ObjectClass结构体类似C++中的虚函数表和类成员变量,因此QOM中多态的实现关键就是如何正确地初始化ObjectClass中的函数指针。这一步是在TypeInfo中的class_init回调函数中实现的,在每个类的第一个实例初始化时,会调用对应类的class_init回调函数先初始化ObjectClass。由于父类和子类Object中的class会指向不同的ObjectClass,因此若子类想要重写(override)父类中的函数,只需要在class_init回调函数中将ObjectClass中相应的成员函数赋值为子类自己实现的函数即可。

构造


在QOM中,对象的构造(初始化)分为以下4步:

  1. 注册TypeInfo
  2. 根据TypeInfo注册TypeImpl
  3. 根据TypeImpl构造类对象;
  4. 根据TypeImpl构造对象。

其中,对于同一种对象的多个实例来说,1~3步只会执行一次。比如,在第一次构造TYPE_ARM_HOST_CPU对象时,1~4步都会执行;而之后再次构造TYPE_ARM_HOST_CPU对象时,则只会执行第4步。

注册TypeInfo

由于C语言本身不支持OO,无法像在JAVA中用类似class Derived0 extends Base implements Interface0的方式声明一个类并指定继承关系、实现关系等,因此若要在C语言中实现OO则必须要有数据结构来保存这些信息。在QOM中,这部分信息需要利用TypeInfo这个结构体来提供。TypeInfo具体的字段及其含义如下:

  • name:该类的typename,必须;
  • parent:父类的typename,必须:
  • abstract:是否为抽象类,若为抽象类则不能实例化(默认不是抽象类);
  • class_size:类对象的大小,若为0则与父类类对象的大小相同(默认与父类类对象大小相同);
  • class_init:类对象构造函数,虚函数表的初始化需在此完成;QOM中调用顺序为先调用父类类对象的构造函数,再调用子类类对象的构造函数;
  • class_base_init:在构造子类类对象时,先构造父类类对象,然后将父类类对象拷贝到子类类对象中,而这种内存拷贝可能会造成副作用,此时class_base_init可用来抵消这种副作用;QOM中会在调用完所有父类类对象构造函数之后、调用子类类对象构造函数之前调用class_base_init,调用顺序为按照从子类到父类的顺序调用所有父类类对象的class_base_init(但不会调用本身类对象的class_base_init);
  • class_data:传递给类对象构造函数class_initclass_base_init的入参;
  • interfaces:该类实现的接口列表,列表最后一个元素需为空;
  • instance_size:实例的大小,若为0则与父类实例的大小相同;
  • instance_init:实例构造函数,与OO语言相同,除非与父类成员变量初始化的值不同,否则只需初始化子类的成员变量即可;QOM中调用顺序为先调用父类实例的构造函数,再调用子类实例的构造函数;
  • instance_post_init:在调用完所有的构造函数后,会调用子类及父类的instance_post_init来结束初始化的流程;QOM中会在调用完成所有构造函数后调用instance_post_init,顺序为先调用子类的instance_post_init,再调用父类的instance_post_init
  • instance_finalize:实例析构函数,与OO语言相同,只需释放子类的成员变量,不需释放父类实例的成员变量,也不需释放子类实例(否则会造成double free);QOM中调用顺序为先调用子类实例的析构函数,再调用父类实例的析构函数。

用户还需要通过type_register_statictype_init来注册TypeInfo。比如上文继承中示例代码中的类可以通过以下方式来注册对应的TypeInfo

static void register_types(void)
{
    type_resgister_static(&interface0_info);
    type_resgister_static(&interface1_info);
    type_resgister_static(&base_info);
    type_resgister_static(&drived0_info);
    type_resgister_static(&drived1_info);
}
type_init(register_types)

根据TypeInfo注册TypeImpl

上一节中type_register_static会根据TypeInfo生成对应的TypeImpl并将其加入到全局的哈希表中,便于后续根据类型名(即TypeInfo中的name)查找到对应的TypeImpl,而TypeImpl才是真正保存类型信息的结构,TypeInfo只是为了方便开发者。而type_init则是注册对应的register函数,但没有调用这个函数(type_init是通过宏生成一个__attribute__((constructor))的函数,在该函数中将参数传入的函数加入到一个链表中,__attribute__((constructor))属性可以保证生成的函数会在main之前被调用)。在main函数中通过module_call_init(MODULE_INIT_QOM)调用通过type_init注册的函数,也就是实现了根据TypeInfo注册TypeImpl

根据TypeImpl构造类对象

个人理解,类对象就是一个存放该类的实例共用的信息的结构,比如callback(这个功能类似C++中的虚函数表)、属性。因此一个类只需要一个类对象,该类的所有实例共用这一个类对象。在该类实例化第一个实例对象时,会首先通过type_initialize实例化类对象,并将TypeImpl中的class指向该类对象。

static void type_initialize(TypeImpl *ti)
{
    TypeImpl *parent;

    // 如果已经该类的类对象已经初始化了,则直接返回
    if (ti->class) {
        return;
    }

    ti->class_size = type_class_get_size(ti);
    ti->instance_size = type_object_get_size(ti);
    /* Any type with zero instance_size is implicitly abstract.
     * This means interface types are all abstract.
     */
    if (ti->instance_size == 0) {
        ti->abstract = true;
    }
    if (type_is_ancestor(ti, type_interface)) {
        ...
    }
    ti->class = g_malloc0(ti->class_size);

    parent = type_get_parent(ti);
    if (parent) {
        // 如果该类存在父类,则首先初始化父类的类对象,再将父类的类对象拷贝过来
        type_initialize(parent);
        ...
        memcpy(ti->class, parent->class, parent->class_size);
        ...
    } else {
        ti->class->properties = g_hash_table_new_full(
            g_str_hash, g_str_equal, g_free, object_property_free);
    }

    ti->class->type = ti;
    // 上述拷贝父类类对象时如果存在副作用,则通过class_base_init消除
    while (parent) {
        if (parent->class_base_init) {
            parent->class_base_init(ti->class, ti->class_data);
        }
        parent = type_get_parent(parent);
    }
    // 调用class_init初始化类对象;由于上述已经实例化父类的类对象了,因此class_init的调用顺序是先调用父类的class_init,再调用子类的class_init
    if (ti->class_init) {
        ti->class_init(ti->class, ti->class_data);
    }
}

根据TypeImpl构造对象

当构造对象时,首先根据类型名查找到对应的TypeImpl,分配一段对应大小(大小为type->instance_size)的内存后调用object_initialize_with_type进行对象的初始化。object_initialize_with_type首先会利用obj->calss = type->class将对象与其对应的类对象关联起来,然后调用object_init_with_type来依次调用构造函数(instance_init)来完成初始化。

static void object_initialize_with_type(void *data, size_t size, TypeImpl *type)
{
    Object *obj = data;

    ...
    memset(obj, 0, type->instance_size);
    obj->class = type->class;
    object_ref(obj);
    ...
    object_init_with_type(obj, type);
    object_post_init_with_type(obj, type);
}

static void object_init_with_type(Object *obj, TypeImpl *ti)
{
    // 先调用父类的构造函数
    if (type_has_parent(ti)) {
        object_init_with_type(obj, type_get_parent(ti));
    }

    // 再调用子类的构造函数
    if (ti->instance_init) {
        ti->instance_init(obj);
    }
}

析构


QOM的对象在构造出来之后需要依赖引用计数来进行内存的管理,这需要开发者手动调用object_ref/object_unref,当引用计数达到0时,则会调用object_finalize进行对象的析构,析构时首先会调用子类的析构函数,再调用父类的析构函数,最后调用free释放该对象的内存。

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

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);
    if (obj->free) {
        obj->free(obj);
    }
}

参考资料


  1. QEMU学习笔记——QOM(Qemu Object Model)
  2. QEMU设备的对象模型QOM

Adam Tao

文章作者

发表回复

textsms
account_circle
email

Adam Tao's Blog

QOM(QEMU Object Module)浅析(一)
本文基于QEMU 7.0。 QEMU中同一类别的设备可能有很多种,对于同类不同种的设备,需要调用不同的函数进行相应的处理;并且,同一总线下可能挂在了很多不同种,甚至不同类别的设备…
扫描二维码继续阅读
2020-03-22