qemu如何實現面向物件模型QOM(程式碼講解)
(說明:本文件只是對QOM中關鍵實現片段進行敘述,更加詳細的程式碼,請檢視本文涉及的程式碼檔案)
有兩個問題需要解答:
- QOM中如何將所有的類儲存起來的,並且完整地呈現給使用者的。
面向物件程式設計,有三個重要的特性——封裝、繼承和多型。封裝可以隱藏實現細節,使得程式碼模組化;繼承可以擴充套件已存在的程式碼模組(類);它們的目的都是為了程式碼重用。而多型則是為了實現另一個目的——介面重用。多型的作用,就是為了類在繼承和派生的時候,保證使用“家譜”中任一類的例項的某一屬性時的正確呼叫。本文件將對QOM中實現的封裝、繼承和多型三個特性分別展開敘述。
- 封裝
封裝就是將資料或函式等集合在一個個的單元中(我們稱之為類)。被封裝的物件通常被稱為抽象資料型別。 - 繼承
繼承是指這樣一種能力:它可以使用現有類的所有功能,並在無需重新編寫原來的類的情況下對這些功能進行擴充套件。
通過繼承建立的新類稱為“子類”或“派生類”。被繼承的類稱為“基類”、“父類”或“超類”。
繼承的過程,就是從一般到特殊的過程。 - 多型
多型是指同一操作作用於不同的物件,可以有不同的解釋,產生不同的執行結果。在執行時,可以通過指向基類的指標,來呼叫實現派生類中的方法。
- 封裝
整體把握幾個資料結構
下面這張圖可以簡要說明ObjectClass、Object、TypeImpl、InterfaceClass之間的關係。通過這些資料結構中的指標,我們可以很方便地找到其他對應的資料結構。
QOM如何將所有的類儲存起來的
我們將會看到,QOM中維持了一個靜態變數的hash表,這張hash表中儲存了所有註冊過的類的TypeImpl資料結構。
使用者如何通過QOM提供的資料結構表示一個類?
在我們之前對QOM的介紹篇中已經說明了,在qemu中如何建立一個新的類。使用者在建立類時要實現兩個資料結構:描述類的資料結構(第一個屬性必須是父類資料結構的例項)、描述類產生的例項的資料結構(第一個屬性必須是父類物件的資料結構的例項),並且需要建立一個數據結構TypeInfo的例項,用來描述這種類的型別。
通過建立TypeInfo,可以告訴qemu哪些資訊呢?
我們再次檢視一下TypeInfo資料結構,這個資料結構中包含了這個型別的父類的名字,insatnce的size資訊、class的size資訊,instance的初始化和垃圾回收函式、class的初始化和垃圾回收函式。有了這些資訊我們就可以:
* 為instance、class初始化第一個屬性(instance的第一個屬性必須是父類物件的資料結構的例項,class的一個屬性必須是父類的資料結構的例項)
* 為instance、class分配合適的記憶體空間,這樣我們就可以將一個Object的指標動態cast為我們需要的物件型別。
* 類的資料結構一般包含大量的函式指標,用於對物件進行操作。class的init函式可以將這些函式指標初始化。然後所有含有這個類資料結構指標的物件,都可以呼叫這些函式。
* 物件的資料結構一般包含了大量變數,是物件的一些屬性。instance的init函式可以把這些屬性初始化,相當於C++中的建構函式。
* 一個類可以實現多個介面,這些介面也是一個類,並且是抽象類,含有虛擬函式指標。
可以說,有了這個資料結構,就有了類的最基本的資訊。(程式碼在include/qom/object.h中)
struct TypeInfo
{
const char *name; // 這個型別的名字
const char *parent; //這個型別的父類的名字
size_t instance_size; //物件對應資料結構的size
// instance如何初始化和最後的垃圾回收
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);
bool abstract; //這個類是否是抽象的,也就是是否有虛擬函式
size_t class_size; //類對應資料結構的size
// 類如何初始化和最後的垃圾回收
void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data);
void (*class_finalize)(ObjectClass *klass, void *data);
void *class_data;
// 這個類所實現的介面
InterfaceInfo *interfaces;
};
一個類是如何註冊到QOM中的?
QOM提供了type_register和type_register_static方法,使用者可以呼叫這兩個方法註冊一個Type,需要傳進去的引數就是TypeInfo的指標。(程式碼在qom/object.c中)
TypeImpl *type_register(const TypeInfo *info)
{
assert(info->parent);
return type_register_internal(info);
}
TypeImpl *type_register_static(const TypeInfo *info)
{
return type_register(info);
}
可以看到它們都呼叫了type_register_internal(TypeInfo*)函式。(程式碼在qom/object.c中)
static TypeImpl *type_register_internal(const TypeInfo *info)
{
TypeImpl *ti;
ti = type_new(info);
type_table_add(ti);
return ti;
}
type_new(TypeInfo*)函式將TypeInfo資料結構中的相關資訊,賦值給TypeImpl資料結構中的屬性。而type_table_add(TypeImpl *)函式將呼叫type_new()獲得的TypeImpl指標儲存到靜態的hash表中。
我們回顧一下TypeImpl的資料結構:(程式碼在qom/object.c中)
struct TypeImpl
{
const char *name;
size_t class_size;
size_t instance_size;
void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data);
void (*class_finalize)(ObjectClass *klass, void *data);
void *class_data;
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);
bool abstract;
const char *parent;
TypeImpl *parent_type;
ObjectClass *class;
int num_interfaces;
InterfaceImpl interfaces[MAX_INTERFACES];
};
而type_new()函式就是針對TypeInfo中的各種資訊,給TypeImpl賦值:(程式碼在qom/object.c中)
static TypeImpl *type_new(const TypeInfo *info)
{
TypeImpl *ti = g_malloc0(sizeof(*ti));
int i;
g_assert(info->name != NULL);
if (type_table_lookup(info->name) != NULL) {
fprintf(stderr, "Registering `%s' which already exists\n", info->name);
abort();
}
ti->name = g_strdup(info->name);
ti->parent = g_strdup(info->parent);
ti->class_size = info->class_size;
ti->instance_size = info->instance_size;
ti->class_init = info->class_init;
ti->class_base_init = info->class_base_init;
ti->class_finalize = info->class_finalize;
ti->class_data = info->class_data;
ti->instance_init = info->instance_init;
ti->instance_post_init = info->instance_post_init;
ti->instance_finalize = info->instance_finalize;
ti->abstract = info->abstract;
for (i = 0; info->interfaces && info->interfaces[i].type; i++) {
ti->interfaces[i].typename = g_strdup(info->interfaces[i].type);
}
ti->num_interfaces = i;
return ti;
}
最後,通過type_table_add()儲存到靜態的hash表中(程式碼在qom/object.c中)
static void type_table_add(TypeImpl *ti)
{
assert(!enumerating_types);
g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
}
static GHashTable *type_table_get(void)
{
static GHashTable *type_table; //靜態的型別的hash表,儲存了全部被註冊的型別
if (type_table == NULL) {
type_table = g_hash_table_new(g_str_hash, g_str_equal);
}
return type_table;
}
面向物件特性的實現
封裝的實現
在考察QOM如何實現封裝時,我們需要再次審視Object這個資料結構包含了哪些屬性:(程式碼在/include/qom/object.h中)
struct Object
{
/*< private >*/
ObjectClass *class; //指向對應的類的資料結構的指標
ObjectFree *free; //當引用計數為0時呼叫
GHashTable *properties; //Object中的所有屬性的hash表
uint32_t ref; //物件的引用計數
Object *parent; //指向父物件的指標
};
值得注意的是,這個Object的資料結構中的一行註釋:“private”,它表示Object中的所有屬性都是私有的,只能被類的內部函式訪問、修改。這個資料結構中體現封裝特性的私有變數是properties,它是一張hash表,這個變數包含了Object中的所有可訪問和修改的資料、函式。這個Hash表中,每一個entry有對應的key和value,key是這個property的name,而value是一個ObjectProperty資料結構的指標,ObjectProperty的實現程式碼如下:(程式碼在include/qom/object.h中)
typedef struct ObjectProperty
{
gchar *name;
gchar *type;
gchar *description;
ObjectPropertyAccessor *get;
ObjectPropertyAccessor *set;
ObjectPropertyResolve *resolve;
ObjectPropertyRelease *release;
void *opaque;
} ObjectProperty;
可以看到,ObjectProperty包含了這個屬性的名字、型別、描述、get和set的方法,解析(resolve)和釋放(release)的方法,以及這個property特有的屬性,用void *型別的指標來表示。
QOM使用這樣一個數據結構,將物件的每個資料都儲存在這樣一個單元之中,從而讓實現了封裝。
當用戶需要向Object中增加屬性時,就可以直接呼叫object_property_add函式,這個函式向object的properties的hash表中插入了一個property。(程式碼在qom/object.c中)
ObjectProperty *
object_property_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;
char *name_no_array = g_strdup(name);
name_no_array[name_len - 3] = '\0';
for (i = 0; ; ++i) {
char *full_name = g_strdup_printf("%s[%d]", name_no_array, i);
ret = object_property_add(obj, full_name, type, get, set,
release, opaque, NULL);
g_free(full_name);
if (ret) {
break;
}
}
g_free(name_no_array);
return ret;
}
if (g_hash_table_lookup(obj->properties, 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;
}
繼承的實現
繼承包括:實現繼承、介面繼承和可視繼承三種。可視繼承與圖形介面相關,我們不考慮。 實現繼承是指使用基類的屬性和方法而無需額外編碼的能力; 介面繼承是指僅使用屬性和方法的名稱、但是子類必須提供實現的能力。
* 實現繼承
我們建立一個新類時,需要實現兩個資料結構:類的資料結構和物件的資料結構,由於類的資料結構中第一個屬性就是父類的資料結構的例項,而物件的資料結構中,第一個屬性就是父類對應的物件的例項。這樣的實現方式,使得QOM天然地支援顯現繼承。
* 介面繼承
介面在QOM中也有一個對應的類的資料結構:(程式碼在include/qom/object.h中)
struct InterfaceClass
{
ObjectClass parent_class; //它的父類就是ObjectClass
/*< private >*/
ObjectClass *concrete_class; //實現這個介面的類的指標
Type interface_type; //這個interface的型別(TypeImpl*指標),這個屬性指示要實現的介面型別。
};
在QOM中一個類可以實現多個介面,也就是實現介面繼承。在ObjectClass中與介面繼承相關的屬性就是interfaces,它在ObjectClass是一條連結串列,連結串列中的每個entry都是一個InterfaceClass的指標,最終對應到interface的TypeImpl資料結構的指標。我們可以通過給TypeImpl指標對應的類資料結構中的函式指標賦值,達到實現介面的目的。(程式碼在include/qom/include.h中)
struct ObjectClass
{
/*< private >*/
Type type;
GSList *interfaces;
const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];
ObjectUnparent *unparent;
};
多型的實現
多型是指同一操作作用於不同的物件,可以有不同的解釋,產生不同的執行結果。為了實現多型,QOM實現了一個非常重要的功能,就是動態cast。我們可以使用相關的函式,將一個Object的指標在執行時cast為子類物件的指標,可以將一個ObjectClass的指標在執行時cast為子類的指標。這樣就可以呼叫子類中定義的函式指標來完成相應的功能。
動態cast檢查的主要實現函式是:(程式碼在qom/object.c中)
ObjectClass *object_class_dynamic_cast(ObjectClass *class,
const char *typename)
{
ObjectClass *ret = NULL;
TypeImpl *target_type;
TypeImpl *type;
if (!class) {
return NULL;
}
type = class->type; //class的type指標總是指向這個類的TypeImpl,因此從這裡獲得這個類的型別。
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;
}
//檢查是否需要動態cast為介面類物件,然後檢查每個介面
//主要檢查目標型別是不是當前所指向的型別的祖先。
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;
}
垃圾回收
在Object的資料結構中有一個變數:ref,這個變數用於對Object引用的計數,如果ref的值變為0,就意味著系統不會繼續使用這個物件了,那麼就可以對這個變數的記憶體空間等進行回收操作。
* 在TypeInfo中,我們可以定義instance_finalize,對於引用計數為0的Object進行垃圾回收操作。
* Object資料結構中有一個ObjectFree *型別的函式指標free,當Object的引用計數為0時,就會呼叫這個函式進行垃圾回收。
* QOM自己實現了預設的垃圾回收操作:(程式碼在qom/object.c中)
//減少obj的引用計數,如果引用計數為0,將進行垃圾回收
void object_unref(Object *obj)
{
if (!obj) {
return;
}
g_assert_cmpint(obj->ref, >, 0);
/* parent always holds a reference to its children */
if (atomic_fetch_dec(&obj->ref) == 1) {
object_finalize(obj);
}
}
//obj的預設的解構函式
static void object_finalize(void *data)
{
Object *obj = data;
TypeImpl *ti = obj->class->type;
object_property_del_all(obj); //刪除obj中的所有屬性
object_deinit(obj, ti); //呼叫TypeImpl中finalize函式進行析構(請看後面)
g_assert_cmpint(obj->ref, ==, 0); //確定引用計數為0
if (obj->free) {
obj->free(obj); //如果obj有free函式指標,那麼就會呼叫該函式
}
}
// 呼叫TypeImpl中的例項解構函式。如果存在父類,需要繼續呼叫父類的例項解構函式
// 呼叫父類例項解構函式是因為一個物件資料結構中,第一個屬性就是父類物件的例項
// 當我們需要對物件析構時,不僅要呼叫當前類的析構方法,也需要呼叫父類的析構方法
// 將物件中的第一個屬性進行析構。
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));
}
}