发布者: ouyang

GType类型系统(上)中我们简要说明了gtype的使用,不过却留下了很多疑问。

这一篇中,我们将从gtype的源码角度解释这些问题。

文章结构

首先,我们概要介绍下GType。

然后,分析其核心数据结构 TypeNode。并简要分析其创建类、对象、添加接口的流程。

最后,分析其如何实现面向对象三大特性:封装、继承、动态绑定 (多态)。

GType是什么?

在gtype.h中有如下的typedef 语句:

#if     GLIB_SIZEOF_SIZE_T != GLIB_SIZEOF_LONG || !defined __cplusplus
typedef gsize                           GType;
#else   /* for historic reasons, C++ links against gulong GTypes */
typedef gulong                          GType;
#endif

不必继续刨根究底,不难看出,它其实是一个数值类型。

这个类型到底代表着什么呢?是不是有一个全局数组(或者链表)记录着每一个类型对应的ID和其类结构和实例结构?

我们分别用16进制和10进制方式打印出OrangutanType(基本类型)和HumanType(子类)的地址值:

Orangutan Type 10 : 196, 16: 0xc4

Human Type      10 : 5277952, 16: 0x508900

看起来Orangutan似乎是符合的,因为GType要预先注册一些类型占了一部分。而Human Type却不像了,那么是否是做了什么Hash,而不是数组呢?如果是Hash的话,它又是怎么解决冲突的?

真正的答案却不是上述的任何一个,我们来看看GType类型查找函数:

static inline TypeNode*
lookup_type_node_I (register GType utype)
{
  if (utype > G_TYPE_FUNDAMENTAL_MAX)
    return (TypeNode*) (utype & ~TYPE_ID_MASK);
  else
    return static_fundamental_type_nodes[utype >> G_TYPE_FUNDAMENTAL_SHIFT];
}

可以看出,如果不是基本类型(utype > G_TYPE_FUNDAMENTAL_MAX),它会把传入的地址值与上一个掩码之后强转为TypeNode(TypeNode是GType中真正存储类型数据的地方,后面将重点介绍)。

对于基本类型,则存在一个全局数组static_fundamental_type_nodes中。

如果不是基本类型(utype > G_TYPE_FUNDAMENTAL_MAX),它会把传入的地址值与上一个掩码之后强转为TypeNode(TypeNode是GType中真正存储 类型数据的地方,后面将重点介绍)。GType这个数值型变量保存了它对应类型(TypeNode)的地址值。

static TypeNode *
static_fundamental_type_nodes[(G_TYPE_FUNDAMENTAL_MAX >> G_TYPE_FUNDAMENTAL_SHIFT) + 1] = { NULL, };
 
static GHashTable  *static_type_nodes_ht = NULL;

另外一个故意标识出来的全局变量GHashTable也是用来存储非基本类型的。具体作用下文再述。

我们先来看最重要的数据结构:TypeNode吧。

TypeNode数据结构

/* --- structures --- */
struct _TypeNode
{
  guint volatile ref_count;
  GTypePlugin *plugin;
  guint        n_children; /* writable with lock */
  guint        n_supers : 8;
  guint        n_prerequisites : 9;
  guint        is_classed : 1;
  guint        is_instantiatable : 1;
  guint        mutatable_check_cache : 1;   /* combines some common path checks */
  GType       *children; /* writable with lock */
  TypeData * volatile data;
  GQuark       qname;
  GData       *global_gdata;
  union {
    GAtomicArray iface_entries;     /* for !iface types */
    GAtomicArray offsets;
  } _prot;
  GType       *prerequisites;
  GType        supers[1]; /* flexible array */
};

作为一个类型,它需要有
1)类名
2)继承链信息(父类,子类)
3)标志位(能否被实例化?能否被继承?)
4)接口信息

  1. 显然GQuark qname;这个数据成员保存了类名信息。但是GQuark是个什么东西呢?它是一个非0整数,用来标识一个特定的字符串。显然,这样做的目的是节省内存空间。

  2. n_children, *children 记录了子类的个数和子类指针。而supers[]这个数组存储了其父类信息。为什么子类是指针,而父类是数组呢?前文有提到:gtype支持多层次单根继 承。并不支持多继承。它的supers这个结构中存储了所有的父类信息,如下: 自己 + 父类 + 父父类 + ... + 基本类型 + 0, 即第一个是自己的类型,其后跟着所有多层父类。已经最终父类基本类型。最后以0作为数组的结束。

  3. GTypePlugin是dynamic 类型使用的。

  4. n_prerequisites 和GType *prerequisites;看起来像是接口类,因为接口类的行为就是在初始化类之前需要初始化这些接口类并实现其纯虚函数。但其实不是,这些prerequisites只是interface的prerequisites。也就是说具有prerequisites的接口类,实现其接口类的同时,也必须实现这些prerequisites类型。
    它的行为跟prerequisites类似,那么gtype为什么还要画蛇添足的来这一笔?这是因为Interface在gtype里面是不可被继承的, 不能作为基类。我们可以调用g_type_add_interface来为一个类添加接口类,却不能在g_type_register_XXX这样的函数 中把它作为基类传入。而prerequisites就不同了,它可以是接口类,也可以是普通类型。

  5. 接口类: 接口是面向对象的关键。gtype要达到类似面向对象的目的。就必须支持它。那么interface 去哪儿了?从名字和注释来看_prot这个union很像。但它里面的iface_entries却是一个GAtomicArray, 它的类型是:

     typedef struct _GAtomicArray GAtomicArray;
     struct _GAtomicArray {
         volatile gpointer data;
     };
    

哦,原来是一个指针类型。然而在gtype中,最重要的TypeNode索引都是通过GType这个保存其地址数值类型,那么任何一个gpointer都可能隐含真正的类型。不过,在解释这个问题之前,我们还需要先来看几个数据结构:

struct _IFaceHolder
{
  GType           instance_type;
  GInterfaceInfo *info;
  GTypePlugin    *plugin;
  IFaceHolder    *next;
};
 
struct _IFaceEntry
{
  GType           iface_type;
  GTypeInterface *vtable;
  InitState       init_state;
};
 
struct _IFaceEntries {
  guint offset_index;
  IFaceEntry entry[1];
};
typedef struct _IFaceEntries    IFaceEntries;
typedef struct _IFaceEntry      IFaceEntry;
typedef struct _IFaceHolder IFaceHolder;

单从名字来看,我们已经可以看出它们是用来存储interface及其相关数据的结构。其中GTypeInterface的类型很简单:

struct _GTypeInterface
{
  /*< private >*/
  GType g_type;         /* iface type */
  GType g_instance_type;
};

除了类型g_type外,还有一个实现该接口类的类型索引g_instance_type。当然,interface也是类型,也可能需要init和final函数,它们包含在GInterfaceInfo中:

struct _GInterfaceInfo
{
  GInterfaceInitFunc     interface_init;
  GInterfaceFinalizeFunc interface_finalize;
  gpointer               interface_data;
};

但这两个信息却是分开存放的。添加接口的时候存放的位置也不相同(IFaceHolder在Interface中, entries则在对应的类中,详见后面分析)。

不过,到此为止,还没有跟TypeNode扯上关系。所以,它们是如何存放在TypeNode中的呢?让我们来看看它们的get和set函数(其实是一个宏定义):

#define CLASSED_NODE_IFACES_ENTRIES(node)   (&(node)->_prot.iface_entries)
#define iface_node_get_holders_L(node)      ((IFaceHolder*) type_get_qdata_L ((node), static_quark_iface_holder))
#define iface_node_set_holders_W(node, holders) (type_set_qdata_W ((node), static_quark_iface_holder, (holders)))

如我们所料,IFaceEntries这些包含interface的结构由_prot.iface_entries来索引。IFaceHolder则通过type_get_data_L和辅助全局变量static_quark_iface_holder来查找。

  1. static_quark_iface_holder是一个GQuark值。是一个静态全局变量。
  2. type_get_qdata_L 则是在TypeNode的global_data包含的指针QData* qdata中按GQuark值查找对应数据指针data:

     typedef struct _QData QData;
     struct _GData
     {
          guint  n_qdatas;
          QData *qdatas;
     };
     struct _QData
     {
           GQuark   quark;
           gpointer data;
     };
    

到这里,TypeNode需要分析的成员变量只剩下TypeData 这个难啃的骨头了,为便于理解,再次不嫌麻烦的罗列代码:

union _TypeData
{
  CommonData         common;
  BoxedData          boxed;
  IFaceData          iface;
  ClassData          class;
  InstanceData       instance;
};
 
truct _CommonData
{
  GTypeValueTable  *value_table;
};
 
struct _BoxedData
{
  CommonData         data;
  GBoxedCopyFunc     copy_func;
  GBoxedFreeFunc     free_func;
};
 
struct _IFaceData
{
  CommonData         common;
  guint16            vtable_size;
  GBaseInitFunc      vtable_init_base;
  GBaseFinalizeFunc  vtable_finalize_base;
  GClassInitFunc     dflt_init;
  GClassFinalizeFunc dflt_finalize;
  gconstpointer      dflt_data;
  gpointer           dflt_vtable;
};
 
struct _ClassData
{
  CommonData         common;
  guint16            class_size;
  ...
};
 
struct _InstanceData
{
  CommonData         common;
  guint16            class_size;
  ...
};

TypeData是一个union,根据其类型不同有四种不同的结构:
IFaceData, ClassData, InstanceData分别对应着接口、类和实例数据类型。而BoxedData是一个盒子,用来装多个数据(比如把几个简单数据封装到一起)。

到这里,我们对gtype类型系统有了更深的认识。但是这些变量是如何起作用的?各种类型注册,访问,修改等等操作是如何起作用的?还需要进一步的分析。

类型注册做了什么?

本文重点关注static type。以下分析都基于此。

那么,我们在g_type_register_static的时候,gtype究竟做了些什么事情呢?

GType
g_type_register_static (GType            parent_type,
            const gchar     *type_name,
            const GTypeInfo *info,
            GTypeFlags   flags);

它需要四个参数:父类类型,类型名,类型信息以及类型的标识位。

从前面的分析,我们可以猜测这个函数的作用就是给TypeNode各个域赋值。它主要调用type_node_new_W和type_data_make_W来新建TypeNode并为其各个域赋值:

  1. 它首先调用type_node_new_W来new一个新的node,并从其父类继承一些属性,包括父类列表,interfaces,是否是一个类,能否被实例化等属性。并在父类的children中加上自己。并插入到static_type_nodes_ht(文章开头提及)这个hash表中, 等等。为什么需要hash表?不是说GType的返回值就是它本身的地址么?没错,GType就是TypeNode的地址(后面将看到),但是如果不通过这个地址来访问呢?另外一种常见的情况应该是通过类名来访问吧。这个时候hash表就起作用了:

     GType
     g_type_from_name (const gchar *name)
     {
         GType type = 0;
         ...
         type = (GType) g_hash_table_lookup (static_type_nodes_ht, name);
         ...
         return type;
     }
    
  2. 然后调用type_add_flags_W,将用户传入的flags更新到TypeNode中。

  3. 这个时候剩下的只需要更新由GTypeInfo传入的参数了。它通过调用type_data_make_W 来实现。

     static void
     type_data_make_W (TypeNode *node, 
       const GTypeInfo  *info,
       const GTypeValueTable *value_table);
    

注意这个函数的参数类型,第一个前面有介绍。第二个参数在前一节的例子中都有详细的注释。
第三个GTypeValueTable是什么东西呢?

我们回过头去看BoxedData这个东西,它的第一个成员就是CommonData也就是一个GTypeValueTable。我们说过BoxedData是for Box type的,也就是包含有几个类型组成的类型盒子。
有时候,我们需要定义自己的数据类型,比如说:

struct _human
{
   int m_handleNO;
   void *handles;
}

GType并不知道如何初始化它,如何拷贝它。这个时候我们需要为它定义一个GTypeValueTable,这个结构体中定义了一系列函数指针,用来操作对应的GValue对象。这个GValue对象就是真正的box。 在大多数情况下,我们并不需要实现GTypeValueTable,gtype会为我们处理好。

如果非要关注它的细节,gtype关于GTypeValueTable的注释已经给出了很好的解释。gobject也实现了自己的GTypeValueTable。更加详细的分析将在后面介绍gobject的时候来介绍它。

从参数类型也可以看出,type_data_make_W 所做的工作就是注册该类和实例的“构造函数”,“析构函数”。根据类型不同(类,实例,接口)来初始化不同的类型数据(ClassData, InstanceData, IFaceData)。

g_type_register_static分析完毕了吗?不,还有一个最关键的问题:返回的GType是如何拿到的?还是回到代码:

static TypeNode*
type_node_any_new_W (..)
{
    ...
    TypeNode *node;
    ...
    type = (GType) node;
 
    g_assert ((type & TYPE_ID_MASK) == 0);
    ...
}

看到没?它就是把地址强转为GType。至于为什么要assert一下,则是安全性的考虑了。TYPE_ID_MAST = 1 << 2 - 1也就是011。

即是说,node的内存起始地址后两位一定为0。这不难理解,因为malloc需要考虑对齐的情况。同时这也解释了文章开头lookup_type_node_I 为什么要与掉低2位了。

GType内存里面的类型图如下:

g_type_create_instance背后

向GType成功的注册了我们的类之后,我们创建一个该类的实例。
回忆一下C++ 类初始化的过程:
... -\> 父父类 -\> 父类 -\> 本身

为了简化问题,我们只考虑单根继承。在这个过程中,我们可以看到,C++没有特殊考虑接口类。

然而gtype 不一样,gtype接口是有别于类的。接口不能被继承,类可以被继承。接口和类在TypeNode中的位置和低位也不一样。
因此,它肯定需要分别初始化父类列表和接口列表。

如我们所料,真正的g_type_create_instance流程是:

由于对象分为类对象和实例对象:

1) 首先它会递归调用g_type_class_ref来初始化其父类(父父类…)的类对象。值得注意的有:

       a)  从名字和调用流程都可以看出。它是通过引用计数的方式来保证类对象是全局唯一的。这也体现了类和实例分开的一个好处:节省内存空间。

       b)  初始化的顺序跟C++一只,先父类后子类。但是在最后对父类类对象调用了g_type_class_unref。因为此时类对象已经初始化完毕,可以“过河拆桥”了。

       c)  既然有unref, 当然有ref增加引用计数了。这段代码在上面的流程图中并没有体现出来。它藏在type_data_make_W中,根据类的data是否初始化用 g_atomic_int_set ((int *) &node->ref_count, 1); 和g_atomic_int_inc ((int *) &node->ref_count);来增加引用计数。

       d)  另一个值得注意的问题是接口类的初始化function的调用也在这里边完成。详细的代码在type_class_init_Wm

​2)   然后初始化实例对象。其顺序跟初始化类对象是一致的。

如何添加接口类

既然接口不能被继承,那么如何为一个类添加接口或者说实现某个接口呢?

接口根据其是否可以动态的加载和卸载,也分为静态和动态两种。跟前文一样,我们同样把static作为分析的重点。

为了添加一个接口,需要指定类的类型(GType)、接口的类型(GType)以及类如何实现接口的信息(GInterfaceInfo) 。

其工作流程是:

根据传入的类型,找到需要添加接口的类及接口类对应的TypeNode,然后调用type_add_interface_Wm将这三个参数传入。

(额外说明的一点是,或许细心的读者已经注意到,g_type内置的类型参数很多后面都加了后缀(比如_Wm),这些后缀都有它的含义,感兴趣的可以参考gtype.c文件。)

前文有提到,一个类“继承”的接口类信息包括接口类信息(GTypeInterface)和该接口的init, final等接口类信息(GInterfaceInfo),它们分别保存在IFaceEntries (_prot.entries)和IFaceHolder(gdata)中。所以为类添加一个接口,需要在这两个地方添加其信息。

  1. 由于IFaceHolder是一个链式结构,所以新添加的接口将作为新的链表头被添加到IFaceHolder中。这一部分代码直接在type_add_interface_Wm中:

     IFaceHolder *iholder = g_new0 (IFaceHolder, 1);
     ...
     iholder->next = iface_node_get_holders_L (iface);
     iface_node_set_holders_W (iface, iholder);
     iholder->instance_type = NODE_TYPE (node);
     iholder->info = info ? g_memdup (info, sizeof (*info)) : NULL;
    

    可以看到,IFaceHolder存放在iface中,也就是interface中。然后,其instance_type指向了实现它的类,也就是要添加该接口的类。因此,我们可以通过一个interface的类型,找到哪些类实现了它。同时,如果该类已经初始化,需要调用一次interface对应的init函数。

  2. 而IFaceEntry呢,则在type_add_interface_Wm调用的函数type_node_add_iface_entry_W中。
    与IFaceHolder不同,其实是一个IFaceEntry的数组,并用offset_index这个guint变量来提供一种快捷访问。IFaceEntry的代码在前面有列出,可以看到它里面存储了iface_type,就是指向对应的interface的指针地址。这样既可以通过interface找到它所有的实现类(父类到子类),也可以通过类找到其实现的interface(子类到父类)。 type_add_interface_Wm仅仅只需要更新本类的IFaceEntries就够了吗?其实不是,由于interface对应着面向对象的抽象类或者接口类的概念。它需要满足父类的接口类子类同样要实现。这一点在type_add_interface_Wm中有详细的注释说明,这里不再赘述。

现在我们添加了我们的接口类,实现了继承。需要考虑面向对象的另一个特性:封装。

怎样实现封装

封装有什么样的好处?

如果一个类所以数据外界是可以直接访问的,那么在代码中。肯定有需要class->data形式的代码。这样如果我们的数据模型发生更改,就有很多的工作要做。而如果数据封装的好。数据发生变化时,我们只需要维持接口一致性就够了。
在gtype里面,我们可以通过g_type_class_add_private来添加私有属性。

关于这一部分的代码,它本身就比较简单。gtype里面在其函数定义之前又做了详细的介绍并给出了使用例子。这里不再花篇幅来介绍它。

总结

这里介绍的几个接口,旨在通过这几个比较难啃的部分了解gtype内部工作机制。更多的接口可参考源码和glib官方文档。

让我们总结一下GType的类型特点:

  1. 多层次、单个继承的类型系统;

  2. 接口不能实例化,不能被继承。但可以作为类的一个属性被子类间接的继承。

  3. 类分为两个部分:类对象和实例对象。类中通常存放接口,数据通常放在实例对象中。



-EOF-
睿初科技软件开发技术博客,转载请注明出处

blog comments powered by Disqus