今天还是记录一个小点。最近在看 QOM 的原理,发现如果一开始就能把 QOM 中类和对象的关系搞明白,那么理解它的原理就会简单一些。下面谈谈我个人的认识。
首先,QOM 的全称是 QEMU Object Model,是 QEMU 中模拟面向对象的一种机制,也可以称为一种规范。我们需要理解这种规范,然后才能借助 QOM 机制实现面向对象的设备模拟。由于 QEMU 需要模拟很多设备,这些设备之间既存在共性又存在差异,因此为了描述它们之间的关系,使用面向对象的方法是比较合适的,比如网卡、串口、显卡等都是通过对象来抽象的。在 QEMU 中,我们可以将网卡看成是一个类,它的父类是一个 PCI 设备类,这个 PCI 设备类的父类是设备类,这就是用继承的方式来描述它们之间的关系。
看过 QEMU 代码的同学都知道,在 QOM 中,类和对象都需要通过结构体来定义。什么?对象不是 new 出来的吗?(请原谅我学识浅薄。)为啥对象也要通过结构体来定义?类通过结构体来定义,我还能理解,毕竟在 C++ 里,结构体也是一种特殊的类嘛。
既然类和对象需要分别定义,那它们之间如何关联呢?这个好回答,通过 TypeImpl 结构体进行关联,不过 QOM 提供给开发者的是 TypeInfo 结构体,这两个结构体几乎相同。TypeInfo 结构体里面包括:
- name,指的是类型的名字,
- parent,指的是父类型的名字,
- instance_size 指的是对象的大小,
- instance_init 指的是对象的初始化函数,
- class_size 指的是类的大小,
- class_init 指的是类的初始化函数。
下面是 hw/net/e1000.c 代码中通过 TypeInfo 结构体指定的类和对象的关系:
static const TypeInfo e1000_base_info = {
.name = TYPE_E1000_BASE,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(E1000State),
.instance_init = e1000_instance_init,
.class_size = sizeof(E1000BaseClass),
.abstract = true,
.interfaces = (InterfaceInfo[]) {
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ },
},
};
这里其实有点隐晦,不太容易看明白,总之结论是,上面这个 e1000_base_info 结构体实例将类(E1000BaseClass)和对象(E1000State)关联起来了,这里的逻辑是,TypeInfo 结构体保存了两组变量分别用来初始化类和对象:
1)变量 instance_size 和 instance_init, 用于给对象分配指定大小(sizeof(E1000State))的内存,并使用指定的函数(e1000_instance_init)来初始化这块内存(E1000State 对象)
2)变量 class_size 和 class_init,用于给类分配指定大小(sizeof(E1000BaseClass))的内存,并使用指定的函数(e1000_class_init,上面没写,需要看更多的代码)来初始化这块内存(E1000BaseClass 类)。
那么,类和对象有什么区别和联系呢?我的理解是,在 QOM 里面,一个 TypeInfo 指定的类,主要保存一些函数指针和该类所共有的成员变量,这些函数指针和共有成员变量的值是相对比较固定的,没必要每个对象都保存一份,可以将其由类结构体来保存,TypeInfo 指定的对象,主要保存了一些差异化的数据。
同时,更为重要的一点是,在运行时,对象如何能访问到类里面的函数指针和变量?QOM 为类和对象设计的基类(ObjectClass)、基本对象(Object)保证了在运行时对象能够快速找到所属类的基本信息,这样就能使用类的函数指针和公共信息实现特定的任务。
综述,为了复用,类和对象是分开表示的。为了复用,类和对象在定义时需要指明继承关系。类继承的原因是想要复用父类的方法和成员变量。对象继承的原因是为了实现多态。现在回过头来想想,在 C++ 里面类和对象也有类似的关系,即类定义的成员函数不占用对象的内存。
类和对象继承的实现是:在定义类(结构体)和对象(结构体)时,将父类或父对象放置在结构体的第一个元素上,这样子类结构体实例和父类结构体实例之间可以进行快速转换,因为它们的首地址相同。下面是类 E1000BaseClass 和对象 E1000State 定义的继承关系: