简单的来说,GObject是一个程序库,它可以帮助我们使用C语言编写面向对象的程序。
很多人被灌输了这样一种概念:要写面向对象程序,那么就需要学习一种面向对象编程语言,例如C++、Java、C# 等等,而 C 语言是用来编写结构化程序的。事实上,面向对象只是一种编程思想,不是一种编程语言。换句话说,面向对象是一种游戏规则,它不是游戏。GObject 告诉我们,使用 C 语言编写程序时,可以运用面向对象这种编程思想。
(一)GObject中模拟类的数据封装
C++是典型的使用面向对象编程思想的语言,我们在这里将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对其进行逐步模拟。
1.1 [类结构体]
以下 C++ 代码:
class MyObject;
用 GObject 可表述为:
#include <glib-object.h>
typedef struct _MyObjectClass {
GObjectClass parent_class;
} MyObjectClass;
1.2 [实例结构体]
以下 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 世界里,类是两个结构体的组合,一个是实例结构体,另一个是类结构体。例如, MyObject 是实例结构体,MyObjectClass 是类结构体,它们合起来便可以称为 MyObject类。
为什么要这么做呢?因为这是用C来模拟面向对象的思想。
在GObject中一个对象的产生遵循如下原则:
如果产生的是该类的第一个实例,那么先分配Class结构体,再分配针对该实例的结构体。否则直接分配针对该实例的结构。也就是说在Class结构体中所有的内容,是通过该类生成的实例所公有的。而实例化每个对象时,为其单独分配专门的实例用结构体。
也许你会注意到,MyObject类的实例结构体的第一个成员是 GObject 结构体,MyObject类的类结构体的第一个成员是 GObjectClass 结构体。其实,GObject 结构体与 GObjectClass 结构体分别是 GObject类的实例结构体与类结构体,当它们分别作为 MyObject类的实例结构体与类结构体的第一个成员时,这意味着 MyObject类继承自GObject类。
总结:
每个类必须定义为两个结构体:它的类结构体和它的实例结构体。所有的类结构体的第一个成员必须是一个GTypeClass结构,所有的实例结构体的第一个成员必须是GTypeInstance结构。
(二)继承 GObject 类的好处
为什么MyObject类一定要将 GObject 类作为父类?主要是因为 GObject 类具有以下功能:
• 基于引用计数的内存管理
• 对象的构造函数与析构函数
• 可设置对象属性的 set/get 函数
• 易于使用的信号机制
在后面再具体讨论这些优点。
(三)既然已经有了类的类结构体和实例结构体了,怎么让GObject系统知道我们这个类呢?就需要想GObject系统中注册这个类,GObject中向我们提供了一个简单的宏来完成这个任务:G_DEFINE_TYPE。
G_DEFINE_TYPE (MyObject, my_object,G_TYPE_OBJECT);
G_DEFINE_TYPE 可以让 GObject 库的数据类型系统能够识别我们所定义的 MyObject类类型,它接受三个参数,第一个参数是类名,即 MyObject;第二个参数则是类的成员函数(面向对象术语称之为”方法“或”行为“)名称的前缀,例如 my_object_get_type 函数即为 MyObject类的一个成员函数,"my_object"是它的前缀;第三个参数则指明MyObject类类型的父类型为G_TYPE_OBJECT。嗯,这个 G_TYPE_OBJECT也是一个宏。这个宏在下一小节讲述。
那么,这个G_DEFINE_TYPE宏是怎么完成向GObject系统中注册这个类呢?
#define G_DEFINE_TYPE(TN, t_n, T_P) G_DEFINE_TYPE_EXTENDED (TN, t_n, T_P, 0, {})
#define G_DEFINE_TYPE_EXTENDED(TN, t_n, T_P, _f_, _C_) _G_DEFINE_TYPE_EXTENDED_BEGIN (TN, t_n, T_P, _f_) {_C_;} _G_DEFINE_TYPE_EXTENDED_END()
#define _G_DEFINE_TYPE_EXTENDED_BEGIN(TypeName, type_name, TYPE_PARENT, flags) \
\
static void type_name##_init (TypeName *self); \
static void type_name##_class_init (TypeName##Class *klass); \
static gpointer type_name##_parent_class = NULL; \
static void type_name##_class_intern_init (gpointer klass) \
{ \
type_name##_parent_class = g_type_class_peek_parent (klass); \
type_name##_class_init ((TypeName##Class*) klass); \
} \
\
gulong\
type_name##_get_type (void) \
{ \
static volatile gsize g_define_type_id__volatile = 0; \
if (g_once_init_enter (&g_define_type_id__volatile)) \
{ \
gulongg_define_type_id = \
g_type_register_static_simple (TYPE_PARENT, \
g_intern_static_string (#TypeName), \
sizeof (TypeName##Class), \
(GClassInitFunc) type_name##_class_intern_init, \
sizeof (TypeName), \
(GInstanceInitFunc) type_name##_init, \
(GTypeFlags) flags); \
{ /* custom code follows */
#define _G_DEFINE_TYPE_EXTENDED_END() \
/* following custom code */ \
} \
g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); \
} \
return g_define_type_id__volatile; \
} /* closes type_name##_get_type() */
下面还是以MyObject为例,看下面的代码:
G_DEFINE_TYPE(MyObject, my_object, G_TYPE_OBJECT);
那么便可将 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 void my_object_class_intern_init(gpointer klass)
{
my_object_parent_class = g_type_class_peek_parent(klass);
my_object_class_init((MyObjectClass *) klass);
}
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 = <span style="color:#FF0000;">g_type_register_static_simple</span>(((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_type向 g_type_register_static_simple汇报:我手里有个MyObject 类,它由GObject类派生,它的姓名、三围、籍贯、民族分别为 ……@#$%^&*(……balaba……balaba……),然后 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们悄悄的执行着这些琐事……
那么,G_DEFINE_TYPE宏是怎么执行完这些琐事的呢?从上面的代码中可以看出来,如果在.c文件中声明一个G_DEFINE_TYPE宏的话,就会自动生成一个my_object_get_type (void)函数, 而这也就是需要在.h头文件中声明的原因。从上面的代码我们还可以看出, G_DEFINE_TYPE 声明了两个函数, 但是并没有实现, 需要定义对象的用户自己去实现, 这两个函数是:
static void my_object_init(MyObject * self);
static void my_object_class_init(MyObjectClass * klass);
这两个函数是对象的初始化函数, 相当于C++中的构造函数, 第一个函数在每个对象创建的时候都会被调用, 第二个函数只有在第一次创建对象的时候才会被调用。确切的说,是调用g_type_class_ref 时, 如果 class 没有初始化, 就会调用 my_object_class_init. 关于这两个函数,在(五)中讲解。
(四)GObject的一些规范
当用户在头文件中创建新类型时,有一些规范用户需要注意:
• 使用object_method的形式来定义函数名称:例如在一个bar类中定义一个名为foo的函数,则用bar_foo。
• 使用前缀来避免与其他工程的命名空间冲突。如果你的库(或应用程序)名为Marman,那么所有的函数名称前缀为maman_。举例:maman_object_method。
• 创建一个宏命为PREFIX_OBJECT_TYPE用来返回GType关联的对象类型。比如,Bar这个类在一个以maman前缀的库中,则使用MANMAN_BAR_TYPE。另有一个不成文的规定是,定义一个使用全局静态变或一个名为prefix_object_get_type的函数来实现这个宏。
• 创建一个宏命名为PREFIX_OBJECT(obj)来返回一个指向PrefixObject类型的指针。这个宏用于必要时安全地强制转换一个静态类型。运行环境检查时,同样也是安全地执行动态类型。在处理过程中禁用动态类型检查是可行的。例如,我们可以创建MAMAN_BAR(obj)来保持先前的例子。
• 如果类型是类化的,那么创建一个命令为PREFIX_OBJECT_CLASS(klass)的宏。这个宏与前面那个是非常相似的:它以类结构的动态类型检查来进行静态转换,并返回一个指向PrefixObjectClass这个类型的类结构的指针。同样,例子为:MAMAN_BAR_CLASS。
• 创建一个宏命名为PREFIX_IS_BAR(obj):这个宏用于判断输入的对象实例是否是BAR类型的。
• 如果类型是类化的,创建一个名为PREFIX_IS_OBJECT_CLASS(klass)的宏,与上面的类似,返回输入的类型指针是否是OBJECT类型。
• 如果类型是类化的,创建一个名为PREFIX_OBJECT_GET_CLASS,返回一个实例所属的类的类型指针。这个宏因为安全的原因,被静态和动态类型所使用,就像上面的转换宏一样。
至于这些宏的实现是非常直观的:一些数量的简单使用的宏由gtype.h提供。
继续按照上面我们的例子,我们写了下面的代码来声明这些宏(其中GST表明是在GStreamer):
#define GST_MY_OBJECT_TYPE (my_object_get_type ())
#define GST_MY_OBJECT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_MY_OBJECT_TYPE, MyObject))
#define GST_MY_OBJECT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_MY_OBJECT_TYPE, MyObjectClass))
#define GST_IS_MY_OBJECT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_MY_OBJECT_TYPE))
#define GST_IS_MY_OBJECT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_MY_OBJECT_TYPE))
总结
也许我讲的很明白,也许我一点都没有讲明白。但是使用 GObject 库模拟基于类的数据封装,或者用专业术语来说,即 GObject 类类型的子类化,念起来比较拗口,便干脆简称GObject 子类化,其过程只需要以下五步:
1. 在 .h 文件中包含 glib-object.h;
2. 在 .h 文件中构建实例结构体与类结构体,并分别将 GObject 类的实例结构体与类结构体置于成员之首;
3. 在 .h 文件中定义 PREFIX_OBJECT_TYPE宏,并声明prefix_object_get_type函数;
4. 在 .h 文件中继续完成上面那5个宏定义;
5. 在 .c 文件中调用 G_DEFINE_TYPE 宏产生类型注册代码。
除了向 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");
}
(六)GObject属性设置
当我们需要设置或者获取一个属性的值时,传入属性的名字,并且带上GValue用来保存我们要设置的值,调用g_object_set/get_property。
g_object_set_property函数将在GParamSpec中查找我们要设置的属性名称,查找我们对象的类,并且调用对象的set_property方法。这意味着如果我们要增加一个新的属性,就必须要覆盖默认的set/get_property方法。而且基类包含的属性将被它自己的方法所正常处理,因为GParamSpec就是从基类传递下来的。最后,应该记住,我们必须事先通过对象的class_init方法来传入GParamSpec参数,用于安装上属性!
在类的类结构体初始化函数,首先要覆盖 GObject 类的两个函数指针:
static void
my_object_class_init (PMDListClass *klass)
{
GObjectClass *base_class = G_OBJECT_CLASS (klass);
base_class->set_property = my_object_set_property;
base_class->get_property = my_object_get_property;
set_property 和 get_property 是两个函数指针,它们位于 GObject 类的类结构体中。注意,想要设置属性,先需要在init函数中将这些属性安装上去,是通过g_object_class_install_property函数来安装的。
首先要明确的是,在插件中,能够设置的属性是什么?而这些属性就是对应的MyObject结构体中的某些成员。所以,采用一个枚举来列举所有可以设置的属性你。
一定要注意,g_object_class_install_property 函数的第二个参数值不能为 0。在使用枚举类型来定义 ID 时,为了避免 0 的使用,一个比较笨的技巧就是像下面这样设计一个枚举类型:
enum {
PROP_0,
PROP_DEVICE,
PROP_USE_V4L2SRC_MEMORY,
PROP_FRAME_PLUS,
};
其中的 PROP_0,只是占位符,它不被使用。
其实,这两个函数的实现都是很简单的,多看几个这样的代码就能理解。关于安装属性的代码,重点是g_object_class_install_property函数和GValue 类型/GParamSpec 类型变量容器。这些知识点上网搜索即可。同时,在后面的文件分析中,也有这些函数等的详细介绍。
(七)析构函数
在GObject中,生成一个类是由父到子,析构的时候自然与之相对是由子到父。
GObject的内存管理并没有采用垃圾回收的方式【JAVA就采用此方式】,而是采用了引用计数机制。GObject的析构其实分为两步,一步是dispose【曝光】,另一步是finalize【终结】。分别用来unref和free对象。(但是在IMX的析构函数中,不存在dispose,有关这两步的讨论可以查看http://garfileo.is-programmer.com/2011/2/27/the-analog-of-classed-type-based-gobject.24798.html里面的GObject 子类对象的析构过程这一节中循环引用的争论。)
所谓基于引用计数的内存管理,可大致描述为:
• 使用 g_object_new 函数进行对象实例化的时候,对象的引用计数为 1;
• 每次使用 g_object_ref函数引用对象时,对象的引用计数便会增 1;
• 每次使用g_object_unref 函数为对象解除引用时,对象的引用计数便会减 1;
• 在 g_object_unref 函数中,如果发现对象的引用计数为 0,那么则调用对象的析构函数释放对象所占用的资源。
所以,在finalize析构函数中,需要做的就是释放在本文件中申请的内存,然后向上回溯析构。所以,在finalize 函数的最后,都有相似的代码:
G_OBJECT_CLASS (parent_class)->finalize (gobject);
这一行代码就是请求父类对象进行析构。因为 C 语言不是内建支持面向对象,所以继承需要从上至下的进行结构体包含,那么析构就除了要释放自身资源还需要引发父类对象的析构过程,这样才可以彻底消除整条继承链所占用的资源。
但是G_OBJECT_CLASS (parent_class)是怎么找到对应的父类呢?这个parent_class变量或宏我们又是在哪里申请的?
这个还继续归结于G_DEFINE_TYPE,在gstimxv4l2src.c文件中,有下面的代码:
#define gst_imx_v4l2src_parent_class parent_class
G_DEFINE_TYPE (GstImxV4l2Src, gst_imx_v4l2src, GST_TYPE_PUSH_SRC);
其中G_DEFINE_TYPE这个宏用于向GObject系统中注册GstImxV4l2Src这个类,它会展开成下面的代码(上面已经列举过这段代码,但是没有分析有关父类的,于是继续粘贴出来):
#define G_DEFINE_TYPE(TN, t_n, T_P) G_DEFINE_TYPE_EXTENDED (TN, t_n, T_P, 0, {})
#define G_DEFINE_TYPE_EXTENDED(TN, t_n, T_P, _f_, _C_) _G_DEFINE_TYPE_EXTENDED_BEGIN (TN, t_n, T_P, _f_) {_C_;} _G_DEFINE_TYPE_EXTENDED_END()
#define _G_DEFINE_TYPE_EXTENDED_BEGIN(TypeName, type_name, TYPE_PARENT, flags) \
\
static void type_name##_init (TypeName *self); \
static void type_name##_class_init (TypeName##Class *klass); \
<span style="color:#FF0000;">static gpointer type_name##_parent_class = NULL; \</span>
static void type_name##_class_intern_init (gpointer klass) \
{ \
<span style="color:#FF0000;">type_name##_parent_class = g_type_class_peek_parent (klass);</span> \
type_name##_class_init ((TypeName##Class*) klass); \
} \
\
gulong\
type_name##_get_type (void) \
{ \
static volatile gsize g_define_type_id__volatile = 0; \
if (g_once_init_enter (&g_define_type_id__volatile)) \
{ \
gulongg_define_type_id = \
g_type_register_static_simple (TYPE_PARENT, \
g_intern_static_string (#TypeName), \
sizeof (TypeName##Class), \
(GClassInitFunc) type_name##_class_intern_init, \
sizeof (TypeName), \
(GInstanceInitFunc) type_name##_init, \
(GTypeFlags) flags); \
{ /* custom code follows */
#define _G_DEFINE_TYPE_EXTENDED_END() \
/* following custom code */ \
} \
g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); \
} \
return g_define_type_id__volatile; \
} /* closes type_name##_get_type() */
有关父类的代码我标红了,可以看出 type_name##_parent_class 是一个静态的全局指针,它在type_name##_class_intern_init 函数中指向 type_name 类的父类结构体。另外,还可以看出 type_name 类的类结构体初始化函数type_name##_class_init 是由 type_name##_class_intern_init函数调用的,而后者会被 g_object_new 函数调用。
在 type_name##_class_init 函数调用之前,将 type_name##类的父类结构体的地址保存为 type_name##_parent_class 指针是有用的。因为我们在type_name类的类结构体初始化函数 type_name##_class_init 中覆盖了type_name 类所继承的父类结构体的 dispose 与 finalize 方法,而在 type_name对象的 dispose 与 finalize 函数中,我们需要将对象的析构向上回溯到其父类,这时如果直接从type_name 类的类结构体中提取父类结构体,那么就会出现 type_name对象的 dispose 与 finalize 函数的递归调用。由于预先保存了 type_name 类的父类结构体地址,那么就可以保证回溯析构的顺利进行。
所以,对应到gstimxv4l2src.c文件中,就是gst_imx_v4l2src_parent_class这个变量保存的GstImxV4l2Src结构体的父类结构体的地址。然后就可以通过
G_OBJECT_CLASS (parent_class)->finalize ((GObject *) (v4l2src));
等等方法来向上回溯析构了。
参考文献:
简单,再简单一些:https://segmentfault.com/a/1190000003861212
GObject 学习笔记汇总: http://garfileo.is-programmer.com/2011/2/27/the-analog-of-classed-type-based-gobject.24798.html
使用C语言进行面向对象的开发--GObject入门:http://www.cnblogs.com/pingf/archive/2009/11/20/1606742.html
I'm TualatriX: https://imtx.me/?page=76