glib是一个跨平台实现的c语言基础库,实现了众多的基础功能。比如:
- 基本的数据结构:动态数组
GArray
,单/双向链表GSList/GList
,字符串数组GString
,哈希字典GHashTable
,队列GQueue/GAsyncQueue
,平衡二叉树GTree
,任意类型结构GVariant
等等。 - 基本工具类型:数据校验
GChecksum
,时间GDate, GDateTime, GTimer
,通用错误类型GError
,随机数GRand
,正则表达式GPatternSpec, g_regex_*()
,资源地址GUri
等等。 - 多线程相关:主线程执行
GMainLoop, GMainContext
, 线程GThread, GThreadPool
, 同步器GCond
, 单次执行工具GOnce
,递归锁GRecMutex
, 单个线程能锁多次而不死锁,读写锁GRWLock
,线程锁GMutex
- 还有一些其它功能。
很多著名的开源项目都基于此完成的。比如 gtk+,gnome
等等。而在WebRTC中的janus-gateway, kurento
也都是基于glib
实现的。
现在我们来了解一下glib
中的核心结构gobject
的实现细节。
首先查看手册,看看其中的基本介绍,和使用示例等。
摘抄总结如下:
重要的方面
1. 一个通用的类型系统,能够注册任意单层或多层单继承的结构体,或则接口。它能够管理相关对象创建,初始化,内存管理,维护父子关系,处理接口继承的动态实现。也就是说对象的指定实现是能够动态替换的。
2. 基础类型的实现。
3. GObject作为所有类型的基类。
4. 一个信号通知系统,能够灵活的让用户自定义重写对象的方法。
5. 一个可扩展的参数、值系统,支持对象的属性或者参数化类型。
主要提供两方面的功能:
1. 面向对象的基于C的API。
2. 自动且透明的API绑定到其它语言。
GObject主要实现了一下三个方面:
- 类型的自动构造和析构。
- 继承动态实现,采用运行时类型系统完成。
- 属性功能。
- 信号的发送与接受。
1. 类型的自动构造和析构
C语言本身不提供任何面向对象的特性,因此需要手动来写。我们知道C++实现此功能是在Class内部加入了一张虚函数表(vtable)来实现的。因此C语言实现此功能也差不多是这样。
GObject把功能一分为二,定义了两个结构体GObject
和GObjectClass
,且为每一个对象分配了一个唯一的ID
GObject
struct _GObject
{
GTypeInstance g_type_instance;
/*< private >*/
guint ref_count; /* (atomic) */
GData *qdata;
};
ref_count
主要是处理生命周期
·qdata`主要是处理属性及信号的发送、接受
GObjectClass
struct _GObjectClass
{
GTypeClass g_type_class;
/*< private >*/
GSList *construct_properties;
/*< public >*/
/* seldom overridden */
GObject* (*constructor) (GType type,
guint n_construct_properties,
GObjectConstructParam *construct_properties);
/* overridable methods */
void (*set_property) (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
void (*get_property) (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec);
void (*dispose) (GObject *object);
void (*finalize) (GObject *object);
/* seldom overridden */
void (*dispatch_properties_changed) (GObject *object,
guint n_pspecs,
GParamSpec **pspecs);
/* signals */
void (*notify) (GObject *object,
GParamSpec *pspec);
/* called when done constructing */
void (*constructed) (GObject *object);
/*< private >*/
gsize flags;
/* padding */
gpointer pdummy[6];
};
对象的方法
了解完这些后,我们看如何用类型ID及全局函数关联上这些方法。
首先查看全局对象的管理,在_g_object_type_init
中调用g_type_register_fundamental
注册上了GObject类型
GTypeInfo info = {
sizeof (GObjectClass),
(GBaseInitFunc) g_object_base_class_init,
(GBaseFinalizeFunc) g_object_base_class_finalize,
(GClassInitFunc) g_object_do_class_init,
NULL /* class_destroy */,
NULL /* class_data */,
sizeof (GObject),
0 /* n_preallocs */,
(GInstanceInitFunc) g_object_init,
NULL, /* value_table */
};
static const GTypeValueTable value_table = {
g_value_object_init, /* value_init */
g_value_object_free_value, /* value_free */
g_value_object_copy_value, /* value_copy */
g_value_object_peek_pointer, /* value_peek_pointer */
"p", /* collect_format */
g_value_object_collect_value, /* collect_value */
"p", /* lcopy_format */
g_value_object_lcopy_value, /* lcopy_value */
};
info.value_table = &value_table;
g_type_register_fundamental (G_TYPE_OBJECT, g_intern_static_string ("GObject"), &info, &finfo, 0);
g_value_register_transform_func (G_TYPE_OBJECT, G_TYPE_OBJECT, g_value_object_transform_value);
这里会把构造及析构相关函数挂载上,最终插入到一个全局HashTable上static_type_nodes_ht
,并且分配一个特定类型ID(20)给GObject,由于这个调用是在glib初始化中完成,所以GObject的类型ID固定为20。如果要把此库移植到特定平台上,要裁剪一些基础类型,可能会导致ID变化。实际运行中,对ID做了一个位移的处理,我也没看到处理的理由。
我们分析g_object_new
的逻辑看看,以下仅提取部分代码,并且把调用堆栈去掉了(inline)
gpointer
g_object_new (GType object_type,
const gchar *first_property_name,
...)
{
// 获取类型对应的GObjectClass对象
// 这里采用了懒加载的方式,即需要使用的时候才去初始化class对象。默认是未初始化的。
// 初始化会先分配内存,然后用上面注册的方法去初始化。
class = g_type_class_peek_static (object_type);
// 分配对象
// 中间主要是计算需要分配的内存大小,及父类的初始化,由于只支持单继承,故初始化顺序很好确定。
object = (GObject *) g_type_create_instance (class->g_type_class.g_type);
}
这里省略了TypeNode
的数据结构介绍。我们来理清几个关键的逻辑
- 继承关系的确定
- 重写的实现方式
- 属性的实现方式
- 信号的实现方式
1. 继承关系的确定
struct _TypeNode
{
...
guint n_children; /* writable with lock */
guint n_supers : 8;
GType *children; /* writable with lock */
GType supers[1]; /* flexible array */
};
摘取一部分定义,可以看到TypeNode
中把所有的父类及自身放到了supers
中,把子类放到了children
中。这样就可以明确的知道了父类和子类的关系了。而且supers
和children
中存储的就是对应的TypeNode
指针。
这里需要说明的是,基础类型和自定义类型的区别,由于基础类型没有父类,所以做了独立的处理。
2. 重写的实现
1. 接口的实现
首先添加实现接口
// derived_object_type 子类型
// TEST_TYPE_IFACE 接口类型
// iface_info 实现接口相关参数,初始化,析构,特定数据。
// 这里会根据iface_info 回调对应的初始化函数,然后手动挂载对应的实现到虚函数表
g_type_add_interface_static (derived_object_type, TEST_TYPE_IFACE, &iface_info);
而虚函数表保存在这里
struct _TypeNode
{
...
GData *global_gdata;
union {
GAtomicArray iface_entries; /* for !iface types */
GAtomicArray offsets;
} _prot;
...
};
iface_entries 保持的是IFaceEntries指针,IFaceEntry保存了虚函数表,这个vtable
定义和使用并不一致,定义为GTypeInterface,在实际使用中保持了实际接口的结构体对象的指针。
普通的虚函数实现
struct _GCustomObjectClass
{
GObjectClass parent_class;
...
}
用g_object_set_property
举例:
在class的构造函数中,直接把自己的g_custom_object_set_property
给替换g_object_set_property
就行了。
这里由于是单继承,且父类属性放到前面,所以任何类型的指针都可以直接转成父类指针使用。
3. 属性的实现方式
属性的两个函数
GLIB_AVAILABLE_IN_ALL
void g_object_set_property (GObject *object,
const gchar *property_name,
const GValue *value);
GLIB_AVAILABLE_IN_ALL
void g_object_get_property (GObject *object,
const gchar *property_name,
GValue *value);
而GObject的两个虚方法定义
struct _GObjectClass
{
...
/* overridable methods */
void (*set_property) (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
void (*get_property) (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec);
...
};
首先有个全局属性表pspec_pool
来保存属性对应关系,及类型和属性名字,或者属性id对应的实现类型。然后根据继承关系,确定set_property/get_property的实现函数,调取相应的函数来实现功能。
注意GObject并没有实现属性的存取。需要自己实现测功能,否则仅会输出警告。