2023-01-30 GObject 与语言绑定

原计划正月初四开始学习,一直玩乐一直开心。初四初五被流浪地球和三体拦住了。沉迷于追剧,无法自拔。初六勉强学习了一天,将程序跑通。初七初八上午勉强学习一两个小时,下午都跑出门爬山溜达了,游玩让人心旷神怡啊。

参考链接:
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()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容