原计划正月初四开始学习,一直玩乐一直开心。初四初五被流浪地球和三体拦住了。沉迷于追剧,无法自拔。初六勉强学习了一天,将程序跑通。初七初八上午勉强学习一两个小时,下午都跑出门爬山溜达了,游玩让人心旷神怡啊。
参考链接:
https://developer-old.gnome.org/gobject/stable/howto-gobject-code.html
计划任务分解:
1、编写一个 gobject 对象,可以设置文件名,可以写文件。
属性:属性名为我们要写的文件
信号:当写文件的动作发生时,发出 file_changed 信号。触发回调,包括对象自身的回调和用户绑定的回调。
2、编写 gtk-doc 规范的注释
3、实现语言绑定,编写 python 脚本来测试接口
创建对象
桌面环境开发大量用到了 gboject 的概念,这是用 C 代码模拟面向对象开发的一种机制。 gnome 还有另外一种方式,用更简单的 vala 来生成 C 代码,用它来定义自己的 gobject 对象更简单了,虽然我们没有必要自己写一个 gobject 对象,但是了解 gobject 机制,有助于我们理解 gnome 系列代码,可以更快地定位、调试问题。
1、gobject 模板代码
gobject 对象的定义格式是固定的,基本上就是按照下面的套路来写:
#define VIEWER_TYPE_FILE (viewer_file_get_type ())
#define VIEWER_FILE(object) (G_TYPE_CHECK_INSTANCE_CAST ( (object),VIEWER_TYPE_FILE, ViewerFile))
#define VIEWER_FILE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST( (klass), VIEWER_TYPE_FILE, ViewerFile))
#define VIEWER_IS_FILE(object) (G_TYPE_CHECK_INSTANCE_TYPE ( (object),VIEWER_TYPE_FILE))
#define VIEWER_IS_FILE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ( (klass), VIEWER_TYPE_FILE))
#define VIEWER_FILE_GET_CLASS(object) (G_TYPE_INSTANCE_GET_CLASS ( (object),VIEWER_TYPE_FILE, ViewerFileClass))
使用 GType 提供的宏 G_DEFINE_TYPE 来注册这个对象。
G_DEFINE_TYPE ( ViewerFile, viewer_file, G_TYPE_OBJECT);
这个宏主要做两项工作,一是实现函数 viewer_file_get_type,这个函数只有定义没有实现,很多人一开始并不知道这函数是哪儿来的,其实就是这个宏自动生成的,实现 gobject 对象必备。另一项工作是定义静态变量 viewer_file_parent_class 指向父类。我们在代码中经常可以看到 xxx_parent_class 就是这个宏生成的。
PS: 如果使用 G_DEFINE_TYPE_WITH_PRIVATE 的话,还会有其他一些设置私有数据的任务。
struct _ViewerFile
{
GObject parent_instance;
/* instance members */
ViewerFilePrivate *priv;
};
struct _ViewerFileClass
{
GObjectClass parent_class; //父
/* class members */
void (*change) (ViewerFile *self);
};
特别需要注意的是: parent_instance 和 parent_class 都在结构的最前面定义。这是由于结构体中定义的第一个成员一定在内存的最前面,也就是说,如果我们将基类声明为我们自定义类的第一个成员,那么我们就可以将指向某对象的指针类型强制转化为它基类的指针,这对实现继承至关重要。
2、gobject 对象的私有成员
我们经常可以在代码中看到self->priv 这样的成员,表示对象的私有数据。 有两种初始化方式:
1、G_DEFINE_TYPE_WITH_PRIVATE
这样定义后,就可以在对象初始化时获取私有结构供后续使用。
self->priv = viewer_file_get_instance_private (self);
2、G_DEFINE_TYPE
在 C 文件中定义宏:
#define VIEWER_FILE_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), VIEWER_TYPE_FILE, ViewerFilePrivate))
然后在类初始化的时候 class_init 调用 g_type_class_add_private (klass, sizeof (ViewerFilePrivate)); 对象初始化时调用 VIEWER_FILE_GET_PRIVATE 来获取私有结构。
推荐使用第一种,简单一点,没有警告。
3、gobject属性
属性的定义与安装就是在 _class_init 中处理的。很多时候我们在写代码时需要监听一个对象的属性变化,随之做相应的处理,属性变化的信号是 notify。需要对应去实现 ***_set_property 和 ***_get_property 函数。
比如 upower 中电池电量等都是作为属性,我们在监听电量变化时,监听这个属性的 notify 信号。
最开始没实现语言绑定时,我用的是 g_object_class_install_properties (object_class, N_PROPERTIES, object_properties);
来安装属性。后来为了实现语言绑定,参考 https://wiki.gnome.org/Projects/GObjectIntrospection/Annotations 里面的注释规则,挨个属性安装。
/**
* ViewerFile:file-name
*
* This property is filename.
**/
g_object_class_install_property (object_class, PROP_FILENAME,
g_param_spec_string ("file-name","file name","aaaa",NULL,G_PARAM_READWRITE));
有属性存在的话,就可以直接在新建对象时作为参数传入,比如
g_object_new (self, "file-name", "/home/xunli/test.txt", NULL);
属性值的设置与获取,通过 GValue 来传递,在 set_property 时从 GValue 中 get,在 get_property 时 set 到 GValue 中去。
4、gobject 信号
在示例程序中,新建了一个 file-changed 信号,每当写入文件后,就发出信号。
信号的回调函数分为两种,一种是我们新建信号时指定:
file_signals [CHANGED] =
g_signal_new ("file-changed", //signal_name
VIEWER_TYPE_FILE, //GType
//G_SIGNAL_RUN_LAST,
//G_SIGNAL_RUN_FIRST, // 信号发射的几个阶段
G_SIGNAL_RUN_CLEANUP,
G_STRUCT_OFFSET (ViewerFileClass, change),
NULL,
NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE,
0
);
change 函数的定义在 ViewerFileClass 结构中:
struct _ViewerFileClass
{
GObjectClass parent_class; //父
void (*change) (ViewerFile *self);
};
信号发射与回调函数被调用的五个阶段:
A signal emission consists of five stages, unless prematurely stopped:
Invocation of the object method handler for G_SIGNAL_RUN_FIRST signals
Invocation of normal user-provided signal handlers (where the after flag is not set)
Invocation of the object method handler for G_SIGNAL_RUN_LAST signals
Invocation of user provided signal handlers (where the after flag is set)
Invocation of the object method handler for G_SIGNAL_RUN_CLEANUP signals
可以在新建信号时,分别试试 G_SIGNAL_RUN_FIRST等,配合g_signal_connect_after 和 g_signal_connect 分别看看 connect 的回调函数与默认回调的顺序。
约定创建信号的回调为 offset,connect 关联的回调为 callback,在这样的组合下,他们的调用顺序为:
调用顺序 | G_SIGNAL_RUN_FIRST | G_SIGNAL_RUN_LAST | G_SIGNAL_RUN_CLEANUP |
---|---|---|---|
g_signal_connect | offset早于callback | callback早于offset | callback早于offset |
g_signal_connect_after | offset早于callback | offset早于callback | callback早于offset |
5、class_init
用C模拟面向对象的关键,一个是结构体定义时,parent_class 需要在结构体最开始的部分,可以实现类型转换;另一个就是 class_init 时,实现函数重载。
一般来说,class_init 可能需要重载 GObject 的 _init、_class_init、_set_property、_dispose、**_finalize 等。很多情况下我们还需要实现其他父类的接口函数,比如 caja 代码中的 fm-icon-container.c ,需要实现 CajaIconContainer。
CajaIconContainerClass *ic_class;
ic_class = &klass->parent_class;
ic_class->get_icon_text = fm_icon_container_get_icon_text;
ic_class->get_icon_images = fm_icon_container_get_icon_images;
ic_class->get_icon_description = fm_icon_container_get_icon_description;
ic_class->start_monitor_top_left = fm_icon_container_start_monitor_top_left;
ic_class->stop_monitor_top_left = fm_icon_container_stop_monitor_top_left;
ic_class->prioritize_thumbnailing = fm_icon_container_prioritize_thumbnailing;
如果我们的对象允许派生,还需要给虚函数加上默认实现的接口。
6、gobject 对象创建与销毁
调用顺序:
viewer-file.c viewer_file_class_init
viewer-file.c viewer_file_init
viewer-file.c viewer_file_set_property(125)---filename:/home/xunli/test/gobject/wt.txt
viewer_file_write(35)---buffer:hello_world
main.c main_changed
viewer-file.c viewer_file_change(49)
viewer-file.c viewer_file_dispose(186) dispose: io channel
viewer-file.c viewer_file_finalize 170: 31
g_object_new 这个函数的工作是:
- 第一件事: 它会将我们传递给它的参数排序,分出 construct 属性和非construct 属性,分别存入 list 中。
- 第二件事: 它会调用 constructor() 函数,这个函数的参数为上面分离出的 construct 属性。我们可以重写这个函数,但是,涉及到方方面面,想做这项工作 is difficult and ugly,默认情况下啥都不管也就可以了。
- 第三件事: g_object_construct() 会调用 g_type_create_instance() ,从而使得基类到子类的所有 xx_init() 函数都会被调用。而且我们的 construct 属性也会被设置,如果我们打印调试信息的话,可以看到 xx_set_property() 函数被调用。
- 第四件事: 当 g_object_construct() 返回后,我们的非 construct 属性会被设置,这时候也会走到 xx_set_property().
- 最后, g_object_new() 返回。
GObject的内存管理基于引用计数机制而实现:
- 调用 g_object_new 函数进行对象实例化的时候,引用计数为 1;
- 每次使用 g_object_ref函数引用对象时,对象的引用计数加 1;
- 每次使用 g_object_unref 函数为对象解除引用时,对象的引用计数减 1;
- 在 g_object_unref 函数中,如果发现对象的引用计数为 0,那么则调用对象的析构函数释放对象所占用的资源。
对于我们自己新建的 gobject 对象,需要定义 dispose 函数和 finalize 函数来释放资源。根据约定,dispose 函数用于释放我们这个对象它引用的成员对象的资源,比如 priv->file 是我们new出来的对象,就在 dispose 中对它解引用,在 finalize函数中做这个对象的其他资源清除工作。在 g_object_unref 时,它会先执行 _dispose,再执行 _finalize。
语言绑定
为了生成对应的 C API 元信息,我们在写函数接口注释的时候,需要遵循 gtk-doc 的规范。
生成动态链接库后,使用 g-ir-scanner 产生 libviewer-file.so 库的 API 元信息,生成的 Viewer-0.1.gir 是XML 格式的文件。在实际使用中,加载并解析 gir 这样的 XML 文件,效率较低,因此 GObject Introspection 定义了一种二进制格式,即 Typelib 格式,并提供 g-ir-compiler 工具将 GIR 文件转化为二进制格式。
参考链接:
https://gi.readthedocs.io/en/latest/buildsystems/meson.html
我写的是 meson.build 文件内容:
project('viewer-file', 'c', default_options : ['c_std=c17'])
gnome = import('gnome')
deps = [
dependency('gobject-2.0'),
dependency('gobject-introspection-1.0')
]
headers = [
'viewer-file.h'
]
sources = [
'viewer-file.c'
]
viewer_file_lib = library('viewer-file',
sources,
dependencies: deps,
install: true,
)
gnome.generate_gir(
viewer_file_lib,
namespace: 'Viewer',
nsversion: '0.1',
sources: headers + sources,
dependencies: deps,
install: true,
fatal_warnings: true,
includes: 'GObject-2.0',
)
一开始namespace定义为“ViewerFile”,在执行时候会报错,提示 VIEWER_IS_FILE_CLASS 无法通过,我没研究啥原因,改为 Viewer 通过了,也可以用。
编译生成的两个文件,分别拷贝到对应目录:
$ sudo cp Viewer-0.1.gir /usr/share/gir-1.0/
$ sudo cp Viewer-0.1.typelib /usr/lib/girepository-1.0/
将 libviewer-file.so 的目录加到 ld_conf 并更新库搜索路径 sudo ldconfig
然后就可以在 python 脚本中调用我们的对外接口了:
#/usr/bin/python3
import gi
gi.require_version('Viewer', '0.1')
from gi.repository import GLib,Viewer
if __name__ == '__main__':
# loop = GLib.MainLoop()
foo = Viewer.File()
foo.set_filename('/home/xunli/tmp/viwer')
#foo.set_property("file-name", "/home/xunli/tmp/viwer.txt");
foo.set_property("zoom-level", 3)
print(foo.get_property("zoom-level"))
print("filename is: ", foo.get_filename())
foo.run()
# loop.run()