2

从双向链表的设计开始』似乎将问题搞的有些复杂了。可能因为它是 4 年前写的,那时我喜欢将简单的问题复杂化……这篇文章尝试将 GObject 与 C++ 代码做一次『映射』,借助 C++ 来理解 GObject 的基本编程框架,然后借助代码生成器保护我们的手指。

先观察以下 C++ 代码:

#include <iostream>

class MyObject {
public:
        MyObject() {std::cout << "对象初始化" << std::endl;}
};

int main() {
        MyObject my_obj;
        return 0;
}

只要具备一点 C++ 类的知识,上述 C++ 代码应该不难理解。下面用 GObject 对其进行逐步模拟。

『类』的类型

以下 C++ 代码

class MyObject;

用 GObject 可表述为:

#include <glib-object.h>

typedef struct _MyObjectClass {
        GObjectClass parent_class;
} MyObjectClass;

也就是说,GObject 中的『类结构体』的作用是面向 GObject 类型系统声明一个『类』的类型。

需要注意一点,C++ 中的类可以不需要从其他类派生,而 GObject 的『类结构体』通常需要从一个叫做 GObjectClass 的『类结构体』派生而成。

类的定义

以下 C++ 代码

class MyObject {
};

用 GObject 可表述为:

#include <glib-object.h>

typedef struct _MyObject{
        GObject parent_instance;
} MyObject;
 
typedef struct _MyObjectClass {
        GObjectClass parent_class;
} MyObjectClass;

G_DEFINE_TYPE(MyObject, my_object, G_TYPE_OBJECT);

这就是说,GObject 中的『实例结构体』模拟的是 C++ 中的『类』类型的定义。

需要注意一点,C++ 中的类可以不需要从其他类派生,而 GObject 中的类,其『实例结构体』通常需要从一个叫做 GObject 的『实例结构体』派生而成。

动态类型系统

C++ 中的数据类型是静态的,在代码编译阶段由被编译器确定。尽管 C++ 提供了 RTTI(运行时类型识别),但是 RTTI 依然是 C++ 编译器实现的。

Python、Ruby、Lua 之类的动态语言,它们的数据类型是由解释器确定的。

GObject 中的数据类型是动态的,在程序运行阶段由 GObject 类型系统确定。如果对上一节的 GObject 代码仔细推敲,不难发现其数据类型是动态的,即『类』的类型是在程序运行时在 GObject 类型系统中注册的。

看下面的代码:

G_DEFINE_TYPE(MyObject, my_object, G_TYPE_OBJECT);

如果使用

$ gcc $(pkg-config --cflags gobject-2.0) -E your-source.c

那么便可将 G_DEFINE_TYPE 展开为下面的 C 代码:

static void my_object_init(MyObject * self);
static void my_object_class_init(MyObjectClass * klass);
static gpointer my_object_parent_class = ((void *) 0);
static gint MyObject_private_offset;
static void
my_object_class_intern_init(gpointer klass)
{
    my_object_parent_class = g_type_class_peek_parent(klass);
    if (MyObject_private_offset != 0)
        g_type_class_adjust_private_offset(klass, &MyObject_private_offset);
    my_object_class_init((MyObjectClass *) klass);
}

__attribute__ ((__unused__))
static inline gpointer
my_object_get_instance_private(const MyObject * self)
{
    return (((gpointer) ((guint8 *) (self) + (glong) (MyObject_private_offset))));
}

GType
my_object_get_type(void)
{
    static volatile gsize g_define_type_id__volatile = 0;
    if (g_once_init_enter(&g_define_type_id__volatile)) {
                GType g_define_type_id = g_type_register_static_simple(((GType) ((20) << (2))),
                                                                       g_intern_static_string("MyObject"),
                                                                       sizeof(MyObjectClass),
                                                                       (GClassInitFunc) my_object_class_intern_init,
                                                                       sizeof(MyObject),
                                                                       (GInstanceInitFunc) my_object_init,
                                                                       (GTypeFlags) 0);
        }
        return g_define_type_id__volatile;
};

上述代码中的 my_object_get_type 函数的定义被我简化了一下,只保留其大意,主要是因为实际的代码难以卒读。

GObject 类型系统之所以能够接受 MyObject 这个『类』的类型,完全拜 my_object_get_type 函数所赐。因为 my_object_get_type 函数调用了 g_type_register_static_simple 函数,后者由 GObject 类型系统提供,其主要职责就是为 GObject 类型系统扩充人马。my_object_get_typeg_type_register_static_simple 汇报:『我手里有个 MyObject 类,它由 GObject 类派生,它的姓名、三围、籍贯、民族分别为 ……@#$%^&*(……blab……blab……』,然后 g_type_register_static_simple 就为 MyObject 登记造册,从此 GObject 类型系统中就有了 MyObject 这号人物了。

my_object_get_type 函数的定义利用了 static 变量实现了以下功能:

  1. my_object_get_type 第一次被调用时,会向 GObject 类型系统注册 MyObject 类型,然后对 MyOject 类进行实例化,产生对象,最后返回对象的数据类型的 ID;

  2. my_object_get_type 第二次被调用开始,它就只进行 MyOject 类的实例化,不会再重复向 GObject 类型系统注册类型。

那么 my_object_get_type 会被谁调用?它会被 g_object_new 调用,所有 GObject 类派生的类型,皆可由 g_object_new 函数进行实例化,例如:

MyObject *my_obj = g_object_new(my_object_get_type(), NULL);

死啦死啦说,不拉屎会憋死我们,不吃饭活七八天,不喝水活五六天,不睡觉活四五天,琐事养我们也要我们的命……所以 G_DEFINE_TYPE 宏就出现了,它悄悄的执行着这些琐事……所以,C++ 们就出现了,class 们悄悄的执行着这些琐事……

构造函数

除了向 GObject 类型注册新类型的相关信息,G_DEFINE_TYPE 宏还为我们声明了『类』的类型与『类』的实例的初始化函数:

static void my_object_init(MyObject * self);
static void my_object_class_init(MyObjectClass * klass);

my_object_class_init 是『类』的类型初始化函数,它的作用是使得用户能够在『类』的类型初始化阶段插入一些自己需要的功能。这个函数是任何一种支持面向对象的编程语言都不需要的,因为这些语言的编译器(解释器)认为用户没有必要在『类』这种类型的初始化阶段执行自己的一些任务。但是 GObject 的类型管理系统需要这个东西,因为在这个阶段,用户可以『类』的类结构体(例如 MyObjectClas)中的数据进行一些符合自己需求的修改。请记住这一点,因为很快就可以看到我们有必要去修改『类』的类结构体。

my_object_init 是『类』的实例的初始化函数,可以将它理解为 C++ 对象的构造函数。

以下 C+ 代码,

#include <iostream>

class MyObject {
public:
        MyObject() {std::cout << "对象初始化" << std::endl;}
};

用 GObject 可描述为:

#include <stdio.h>
#include <glib-object.h>

typedef struct _MyObject{
        GObject parent_instance;
} MyObject;
 
typedef struct _MyObjectClass {
        GObjectClass parent_class;
} MyObjectClass;

G_DEFINE_TYPE(MyObject, my_object, G_TYPE_OBJECT);

static void my_object_class_init(MyObjectClass * klass) {
}

static void my_object_init(MyObject * self) {
        printf("对象初始化\n");
}

C++ 的对象除了有构造函数,还有析构函数。GObject 也有析构函数,但是这篇文章先放它一马,因为这是一个非常有趣的主题,最好是为它单独写一篇文章。

运行时类型识别

我们的程序已经在 GObject 数据类型系统中创造出来 MyObject 类型,那么用这种类型就可以产生无数个实例,它们就是类型为 MyObject 的对象。一个程序在运行时创造了一个 MyObject 类型的对象,那么这个程序知道这个对象的类型是 MyObject 类型,知道它所属的 MyObject 类型是派生自 GObject 类型么?这就是所谓的『RTTI(运行时类型识别)』,有的语言则将其称为『自省』。

事实上现在并没有任何一个程序能够具备自省能力,我们人类自身也无法回答『我是谁』这些的哲学问题。『我是谁』此类问题的答案,只有创造『我』的家伙才知道。

GObject 类型系统创造了『类』的类型,然后由『类』的类型创造出对象。因此,它知道任何一个它由所创造的对象的类型。例如,对于任何由 GObject 类派生出来的类,它的对象能够使得以下条件成立:

MyObject *my_obj = g_object_new(MY_TYPE_OBJECT, NULL);

if (G_IS_OBJECT(my_obj)) {
        printf("My type is GObject!\n");
}

也可以检查 my_obj 的类型是否为 MyObject 类:

if (G_TYPE_CHECK_INSTANCE_TYPE(my_obj, my_object_get_type())) {
        printf("My type is MyObject!\n");
}

为了方便,可以为 MyObject 类定义一个与 G_IS_OBJECT 相似的宏:

#define MY_IS_OBJECT(obj) (G_TYPE_CHECK_INSTANCE_TYPE(my_obj, my_object_get_type()))

然后,可以像下面这样检查 my_obj 的类型:

if (MY_IS_OBJECT(my_obj)) {
        printf("My type is MyObject!\n");
}

代码生成器

虽然 GObject 的代码看上去较为繁琐,但是这些代码具有一定的模式。为了节省字符输入,可以考虑为它写一个代码生成器。

我在 Emacs 里写了一个简单的生成器:

(defun make-gobject-h (project class parent)
  "Insert some macroes about gobject"
  (interactive "sWhat's the project name? \nsWhat's the class name? \nsWho's the parent of this class? ")
  (let ((P (upcase project)) 
    (O (upcase class))
    (p (downcase project))
    (o (downcase class))
    (pp (capitalize project))
    (oo (capitalize class)))
    (insert (format "#ifndef %s_%s_HEAD\n" P O))
    (insert (format "#define %s_%s_HEAD\n\n" P O))
    (insert (format "#include <glib-object.h>\n\n"))
    (insert (format "#define %s_TYPE_%s (%s_%s_get_type ())\n" P O p o))
    (insert (format "#define %s_%s(object) (G_TYPE_CHECK_INSTANCE_CAST((object), %s_TYPE_%s, %s%s))\n" P O P O pp oo))
    (insert (format "#define %s_%s_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), %s_TYPE_%s, %s%sClass))\n" P O P O pp oo))
    (insert (format "#define %s_IS_%s(object) (G_TYPE_CHECK_INSTANCE_TYPE((object), %s_TYPE_%s))\n" P O P O))
    (insert (format "#define %s_IS_%s_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), %s_TYPE_%s))\n" P O P O))
    (insert (format "#define %s_%s_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS((object), %s_TYPE_%s, %s%sClass))\n\n" P O P O pp oo))
    (insert (format "typedef struct _%s%s %s%s;\n" pp oo pp oo))
    (insert (format "typedef struct _%s%s {\n" pp oo))
    (insert (format "\t%s parent_instance;\n" parent))
    (insert (format "};\n"))
    (insert (format "typedef struct _%s%sClass %s%sClass;\n" pp oo pp oo))
    (insert (format "typedef struct _%s%sClass {\n" pp oo))
    (insert (format "\t%sClass parent_class;\n" parent))
    (insert (format "};\n\n"))
    (insert (format "GType %s_%s_get_type(void);\n\n" p o ))
    (insert (format "#endif\n"))))

这个生成器,在 Emacs 里的用法是执行 M-x make-gobject-h 命令,然后像下面这样回答三个问题:

Q:What's the project name?
A:My

Q:What's the class name?
A:Object

Q:Who's the parent of this class?
A:GObject

然后即可在 Emacs 当前缓冲区中生成以下代码:

#ifndef MY_OBJECT_HEAD
#define MY_OBJECT_HEAD

#include <glib-object.h>

#define MY_TYPE_OBJECT (my_object_get_type ())
#define MY_OBJECT(object) (G_TYPE_CHECK_INSTANCE_CAST((object), MY_TYPE_OBJECT, MyObject))
#define MY_OBJECT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), MY_TYPE_OBJECT, MyObjectClass))
#define MY_IS_OBJECT(object) (G_TYPE_CHECK_INSTANCE_TYPE((object), MY_TYPE_OBJECT))
#define MY_IS_OBJECT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), MY_TYPE_OBJECT))
#define MY_OBJECT_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS((object), MY_TYPE_OBJECT, MyObjectClass))

typedef struct _MyObject MyObject;
typedef struct _MyObject {
    GObject parent_instance;
};
typedef struct _MyObjectClass MyObjectClass;
typedef struct _MyObjectClass {
    GObjectClass parent_class;
};

GType my_object_get_type(void);

#endif

上述代码,可以保存为 my-object.h 文件,然后再新建一份 my-object.c 文件,在其中放置以下内容:

#include "my-object.h"
 
G_DEFINE_TYPE(MyObject, my_object, G_TYPE_OBJECT);
 
static void
my_object_init(MyObject *self)
{
        printf("对象初始化\n");
}
 
static void
my_object_class_init(MyObjectClass *klass)
{
}

这样就完成了一个完整的 GObject 版本 MyObject 类的定义。

my-object.c 中的代码,也可以写一个代码生成器来生成:

(defun make-gobject-c (project class parent-type)
  "Insert some macroes about gobject"
  (interactive "sWhat's the project name? \nsWhat's the class name? \nsWhat's the parent type of this class? ")
  (let ((p (downcase project))
    (o (downcase class))
    (pp (capitalize project))
    (oo (capitalize class)))
    (insert (format "#include \"%s-%s.h\"\n\n" p o))
    (insert (format "G_DEFINE_TYPE(%s%s, %s_%s, %s)\n\n" pp oo p o parent-type))
    (insert (format "static void\n"))
    (insert (format "%s_%s_init(%s%s *self)\n{\n\n}\n\n" p o pp oo))
    (insert (format "static void\n"))
    (insert (format "%s_%s_class_init(%s%sClass *klass)\n{\n\n}\n\n" p o pp oo))))

在 Emacs 里执行 M-x make-gobject-c 命令,然后像下面这样回答三个问题:

Q:What's the project name?
A:My

Q:What's the class name?
A:Object

Q:What's the parent type of this class?
A:G_TYPE_OBJECT

然后即可生成上述的 my-object.c 文件中的主要内容。

总结

GObject 的数据类型系统最大的特点是数据类型的动态性,虽然是不得已而为之,但是却别有一番情趣。基于这种数据类型系统,我们能够在程序的运行过程中创造新的数据类型,能够由一种数据类型派生出新的数据类型,还能够识别对象的类型。

虽然 GObject 的这一切看上去非常繁琐,但是通过代码生成器就可以明白,如果遵守一些约定,繁琐的代码可以基于 4 个参数生成。

GObject 的类型系统既然是动态的,它支持程序在运行中生成数据类型,也支持程序在运行中销毁数据类型。有关数据类型的销毁,这个功能比较偏僻,一般情况下用不着,若对这个主题感兴趣,可参考:http://www.gonwan.com/?p=50


garfileo
6k 声望1.9k 粉丝

这里可能不会再更新了。


引用和评论

0 条评论