What is G-object?
—
很多人被灌输了这样一种概念:要写面向对象程序,那么就需要学习一种面向对象编程语言,例如
C++
、
Java
、
C#
等等,而
C
语言是用来编写结构化程序的。
—
事实上,面向对象只是一种编程思想,不是一种编程语言。换句话说,面向对象是一种游戏规则,它不是游戏。
—
Gobject
,
亦称
Glib
对象系统,是一个程序库,它可以帮助我们使用
C
语言编写面向对象程序;它提供了一个通用的动态类型系统(
GType
)、一个基本类型的实现集(如整型、枚举等)、一个基本对象类型
-
Gobject
、一个信号系统以及一个可扩展的参数
/
变量体系。
Why Bother to use Gobject?
—
GObject
告诉我们,使用
C
语言编写程序时,可以运用面向对象这种编程思想。
—
Gobject
系统提供了一个灵活的、可扩展的、并且容易映射到其他语言的面向对象的
C
语言框架。
—
GObject
的动态类型系统允许程序在运行时进行类型注册,它的最主要目的有两个:
1
)使用面向对象的设计方法来编程。
GObject
仅依赖于
GLib
和
libc
,
通过它可使用纯
C
语言设计一整套面向对象的软件模块。
2
)多语言交互。在为已经使用
GObject
框架写好的函数库建立多语言连结时,可以很容易对应到许多语言,包括
C++
、
Java
、
Ruby
、
Python
和
.NET/Mono
等。
GObject
被设计为可以直接使用在
C
程序中,也
封装
至其他语言。
透明的跨语言互通性
—
Gobject
如何解决静态语言与动态语言的沟通问题?
—
在
python
语言中调用一个
C
的
API
:
C
的
API
是常常是一些从二进制文件中导出的函数集和全局变量。
C
的函数可以有任意数量的参数和一个返回值。每个函数有唯一的由函数名确定的标识符,并且由
C
类型来描述参数和返回值。类似的,由
API
导出的全局变量也是由它们的名字和类型所标识。一个
C
的
API
可能仅仅定义了一些类型集的关联。例如:
static void function_foo(int foo)
{
}
int main(int argc, char *argv[])
{
function_foo(10)
return 0;
}
如果你了解函数调用和
C
类型至你所在平台的机器类型的映射关系,你可
以在内存中解析到每个函数的名字从而找
到这些代码所关联的函数的位置,并且构
造出一个用在这个函数上的参数列表。
最后,你可以用这个参数列表来调用这
个目标
C
函数。第一个指令在堆栈上建立了
十六进制的值
0xa
(十进制为
10
)作为一个
32
位的整型,并调用了
function_foo
函数。就如你看到的,
C
函数的调用由
gcc
实现成了
本地机器码的调用(这是实现起来最快的
方法)。
push $0xa
call 0x80482f4 <function_foo>
有了
gcc
这个第三方,我们的代码与
机器的沟通更顺畅了!
记住:
GType
/
GObject
库不仅仅是为了设计向
C
开发者提供面向对象的特性,也是为了
透明的
跨语言互通性。
做一个受欢迎的协调者
—
为了实现调用
C
函数,
Python
解释器需要做:
(1)找到函数所处的位置:这个意味着在C编译器编译成的二进制文件中寻找这个函数。
(2)在可执行的内存中,载入有关这个函数的相关代码。
(3)在调用这个函数前,将Python的参数转换为C兼容的参数。
(4)用正确的方式调用这个函数。
(5)将C函数的返回值转换成Python兼容的变量并将其返回至Python代码中。
—
方案一:手动编写一些“粘合代码”,当每个函数被导入或导出时,使用这些代码将
Python
的参数转换为
C
兼容的参数,并将
C
的返回值转换为
Python
兼容的返回值。这个粘合代码将被连接到解释器上,从而解释器在解释
Python
程序时,可以完成程序中的调用
C
函数的工作。方案二:自动产生粘合代码,当每个函数被导入或导出时,使用一个特殊的编译器来读取原始的函数签名。
—
GLib
用的解决办法是,
使用
GType
库来保存
在当前运行环境中的
所有由开发者描述的
对象的描述。这些“
动态类型”库将被特
殊的“通用粘合代码”
来自动转换函数参数和进行函数调用在不同的运行环境之间。
GOBJECT模拟封装
在 GObject世界里,类是两个结构体的组合,一个是实例结构体,另一个是类结构体。有点绕。类、对象、实例有什么区别?可以这么理解,类-对象-实例,无非就是类型,该类型所声明的变量,变量所存储的内容。后面可以知道,类结构体初始化函数一般被调用一次,而实例结构体的初始化函数的调用次数等于对象实例化的次数。所有实例共享的数据,可保存在类结构体中,而所有对象私有的数据,则保存在实例结构体中。
下面我们摘取一段示例代码(包含示例结构体和类结构体)来帮助更好的理解上述概念:
GOBJCT如何模拟私有属性
—
一种最简单的办法,是在类的定义时,只需要向结构体中添加一条注释,用于标明哪些成员是私有的,哪些是可以被直接访问的。
C
语言认为,程序员应当知道自己正在干什么,而且保证自己的所作所为是正确的
。
—
第二种办法,也就是最常用的办法,是把需要设为私有属性的数据再次封装,并且将该封装实例的定义放到实现
.c
文件中。在上页的例子中,
GUPnPContextPrivate
的定义就被定义为私有,其定义放在
gupnp-context.c
文件中
C语言实现CLASS域GOBJECT支持
如何实现gobject面向对象支持呢?
很简单,我们只需要建立自己的头文件,并添加一些宏定义G_DEFINE_TYPE即可。
这样,GUPnPContext就成为了Gobject库认可的一类合法公民了,即成功的把GUpnPContextClass类所代表的type(类型)注册到了glib类型系统中,并且将成功获取到一个类型ID。
也就是说,当你设计新类时,GUPnPContext可以被考虑加进你的继承体系,同时GUPnPContext也可以被用于组合成其他的类。
进一步理解GType类型系统
—
Gtype
类型系统是
Glib
运行时类型认证和管理系统。
—
Gtype
API
是
Gobject
系统的
基础
,它提供注册和管理所有基本数据、用户定义对象和接口类型的技术实现。如:
G_DEFINE_TYPE
宏、
G_DEFINE_INTERFACE
宏、
g_type_register_static
函数等都在
GType
实现。
—前面提到
的
G_DEFINE_TYPE
宏,展开后主要用于
实现用户定义类型
,包括:声明类初始化函数、声明实例初始化函数、声明父类的一些信息、以及用于获取分配类型
ID
的
xx_xx_get_type
()
函数;如下图所示:
GOBEJCT如何实现继承
—
前面我们已经介绍,在
GObject
世界里,
类
是两个结构体的组合,一个是
实例结构体
,另一个是
类结构体
。
—
很容易理解,
GOBJECT
的继承需要实现实例结构体的继承和类结构体的继承。
—
在前面的例子,我们通过在
gupnpcontext
实例中显示声明
GSSDPClient
parent
来告知
gobject
系统
GSSDPClient
是
gupnpcontext
实例的双亲;同时,通过
GUPnPContextClass
定义中声明
GSSDPClientClass
parent_class
。通过实例结构体和类结构体的共同声明,
—
GOBJECT
知道
gupnpcontext
是
gssdpclient
的子类。
GOBJECT构造函数
—
Gobject
对象的初始化可分为
2
部分:类结构体初始化和实例结构体初始化。
类结构体初始化函数只被调用一次,而实例结构体的初始化函数的调用次数等于对象实例化的次数。这意味着,
所有对象共享的数据,可保存在类结构体中,而所有对象私有的数据,则保存在实例结构体中
。
多态的概念
—
多态指同一个实体同时具有多种形式。它是面向对象程序设计的一个重要特征。
—
把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。
—
赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。也就是说,父亲的行为像儿子,而不是儿子的行为像父亲。
—
我们这里讨论的多态,主要指运行时多态,其具体引用的对象在运行时才能确定。
为什么要在GOBJECT引入多态?
—
用
C
的
struct
可以实现对象。普通的结构体成员可以实现为成员数据,而对象的成员函数则可以由函数指针成员来实现。很多开源的软件也正是这么做的。
—
这样的实现有一些严重的缺陷:别扭的语法、类型安全问题、缺少封装,更实际的问题是
空间浪费严重
。每一个实例化的对象需要
4
字节的指针来指向其每一个成员方法,而这些方法对于类的每个实例(对象)应该都是相同的,所以是完全冗余的。假设一个类有
4
个方法,
1000
个实例,那么我们将浪费接近
16KB
的空间。
—
很明显,我们不需要为每个实例保存这些指针,我们只需要保存一张包含这些指针的表。
Gobject如何实现多态?
(1)Gobject为每个子类在内存中保存了一份包含成员函数指针的表. 这个表,就是我们在C++经常说到的虚方法表(vtable)。当你想调用一个虚方法时,你必须先向系统请求查找这个对象所对应的虚方法表。这张表包含了一个由函数指针组成的结构体。在调用这些函数时,需要在运行时查找合适的函数指针,这样就能允许子类覆盖这个方法,我们称之为“虚函数”。
(2) Gobject系统要求我们向它注册新声明的类型,系统同时要求我们去向它注册(对象的和类的)结构体构造和析构函数(以及其他的重要信息),这样系统就能正确的实例化我们的对象。
(3)Gobject系统通过枚举化所有的向它注册的类型来记录新的对象类型,并且要求所有实例化对象的第一个成员是一个指向它自己类的虚函数表的指针,每个虚函数表的第一个成员是它在系统中保存的枚举类型的数字表示。
由常用的g_object_new()想到的
—
g_object_new
能够为我们进行对象的实例化
.
所以它必然要知道对象对应的
类的数据结构
.
—
如上图示例,除第一个参数外,
很容易猜想后面的参数都是
“
属性名
-
属性值
”的配对。
—
第一个参数其实是一个宏:
具体细节可以不去管它,可以知道它是去获取数据类型xx_xx_get_type函数的作用就是告诉它有关PMDList类的具体结构。在*.c文件实现中,G_DEFINE_TYPE宏可以为我们生成xx_xx_get_type函数的实现代码。 它可以帮助我们最终实现类类型的定义。 。 当g_object_new从xx_xx_get_type函数那里获取类类型标识码之后,便可以进行对象实例的内存分配及属性的初始化。初始化函数在前面已有介绍。
GOBJECT多态:将丑陋封锁在内部
—
要想实现前面讲述的让
g_object_new
函数中通过“属性名
-
属性值”结构为
Gobject
子类对象的属性进行初始化,我们需要完成以下工作:
(1)实现xx_xx_set_property与xx_xx_get_property函数,完成g_object_new函数“属性名-属性值”结构向Gobject子类属性的映射;
(2)在Gobject子类的类结构体初始化函数中,让Gobject基类的两个函数指针set_property与get_property分别指向xx_xx_set_property与xx_xx_get_property。
(3)在Gobject子类的类结构体初始化函数中,为Gobject子类安装具体对象的私有属性。
可以看出,set_property是Gobject的虚函数实现,是运行时的多态。
GOBJECT多态:将优雅展示于外界
—
set_property
是
2
个函数指针,位于
Gobject
基类的类结构体中。这说明,它们可以被
Gobject
类及其子类的所有对象共享,并且各个对象都可以让这
2
个函数指针指向它所期望的函数。
—
类似的机制,在
C++
中被称为虚函数,主要用于实现多态。
—
由于有了这种机制,我们可以使用
g_object_new
函数在对象实例化时便进行对象的初始化。
—
当我们要获取或设置类的实例属性时,可直接使用统一的接口:
g_object_get_propertyg_object_set_property
GOBJECT属性实现:泛型与多态
假设我们需要一种数据类型,可以实现一个可以容纳多类型元素的链表,我想为这个链表编写一些接口,可以不依赖于任何特定的类型,并且不需要我为每种数据类型声明一个多余的函数。这种接口必然能涵盖多种类型,我们称它为GValue(Generic Value,泛型)。
要编写一个泛型的属性设置机制,我们需要一个将其参数化的方法,以及与实例结构体中的成员变量名查重的机制。从外部上看,我们希望使用C字符串来区分属性和公有API,但是内部上来说,这样做会严重的影响效率。因此我们枚举化了属性,使用索引来标识它们。
属性规格,在Glib中被称作!GParamSpec,它保存了对象的gtype,对象的属性名称,属性枚举ID,属性默认值,边界值等,类型系统用!GParamSpec来将属性的字符串
名转换为枚举的属性ID,GParamSpec也是一个能把所有东西都粘在一起的大胶水。
gobject属性设置
—
当我们需要设置或者获取一个属性的值时,传入属性的名字,并且带上
GValue
用来保存我们要设置的值,调用
g_object_set
/
get_property
。
g_object_set_property
函数将在
GParamSpec
中查找我们要设置的属性名称,查找我们对象的类,并且调用对象的
set_property
方法。这意味着如果我们要增加一个新的属性,就必须要覆盖默认的
set/
get_property
方法。而且基类包含的属性将被它自己的方法所正常处理,因为
GParamSpec
就是从基类传递下来的。最后,应该记住,我们必须事先通过对象的
class_init
方法来传入
GParamSpec
参数,用于安装上属性!
Gobject消息系统:闭包
一个Closure是一个抽象的、通用表示的回调(callback)。它是一个包含三个对象的简单结构:
(1)一个函数指针(回调本身) ,原型类似于:
return_type function_callback (... , gpointeruser_data);
(2) user_data指针用来在调用Closure时传递到callback。
(3)一个函数指针,代表Closure的销毁:当Closure的引用数达到0时,这个函数将被调用来释放Closure的结构。
一个GClosure提供以下简单的服务:
调用(g_closure_invoke):这就是Closure创建的目的: 它们隐藏了回调者的回调细节。
通知:相关事件的Closure通知监听者如Closure调用,Closure无效和Clsoure终结。监听者可以用注册g_closure_add_finalize_notifier(终结通知),g_closure_add_invalidate_notifier(无效通知)和g_closure_add_marshal_guards(调用通知)。
对于终结和无效事件来说,这是对等的函数(g_closure_remove_finalize_notifier和g_closure_remove_invalidate_notifier,但调用过程不是。
“一眼望穿”闭包
—
GClosureMarshal
是一个函数指针
,
但是要注意它是
用来定义回调函数类
型的而不是直接调用。
GObject
中真正的回调
是
marshal_data
,
这个是一个
void *
指针。
这个在可通过查看
C
语言
Marshaller
的实现来得到
证明。
Gobject
为什么搞这么复
杂?用于其它语言
间的绑定
.
闭包给多语言绑定带来了方便
我们从分析g_signal_new函数的使用来说明这个问题。第7个参数为GSignalMarshaller类型,它与前面体面提到的GClosureMarshal是一个东西,都是一个函数指针。
GSignalCMarshaller c_marshaller:该参数是一个GSignalCMarshall类型的函数指针,其值反映了回调函数的返回值类型和额外参数类型(所谓“额外参数”,即指除回调函数中instance和user_data以外的参数)。
例如,g_closure_marshal_VOID_VOID说明该signal的回调函数为以下的callback类型:
typedef void (*callback) (gpointer instance, gpointer user_data);
而g_closure_marshal_VOID_POINTER则说明该signal的回调函数为以下的callback类型:
typedef void (*callback) (gpointer instance,gpointer arg1,gpointer user_data);
GType return_type:该参数的值应为回调函数的返回值在GType类型系统中的ID。
guintn_params:该参数的值应为回调函数的额外参数的个数。
...: 这一系列的参数的值应为回调函数的额外参数在GType类型系统中的ID,且这一系列参数中第一个参数的值为回调函数的第一个额外参数在GType类型系统中的ID,依次类推。
可以认为,信号就是包含对可以连接到信号的闭包的描述和对连接到信号的闭包的调用顺序的规定的集合体。
事实上,它是用来翻译闭包的参数和返回值类型的,它将翻译的结果传递给闭包。之所以不直接调用callback或闭包,而在外面加了一层marshal的封装,主要是方便gobjec库与其他语言的绑定。例如,我们可以写一个pyg_closure_marshal_void_string函数,其中可以调用python语言编写的“闭包”并将其计算结果传递给Gvalue容器,然后再从Gvalue容器中提取计算结果。
Gobject消息系统:Signal机制
—
在
gobject
系统中,信号是一种定制对象行为的手段,也是一种多种用途的通知机制。
—
每一个信号都是和能发出信号的类型一起注册到系统中的。
—
该类型的使用者,需要实现信号与闭包的连接,在给定的信号和给定的
closure
间指定对应关系,这样在信号被发射时,闭包会被调用
。信号是
closure
被调用的主要机制
;
—
使用
GObject
信号机制,一般有三个步骤:
(1)信号注册,主要解决信号与数据类型的关联问题
(2)信号连接,主要处理信号与闭包的连接问题;
(3)信号发射, 调用callback进行处理。