Glib中Gobject的创建:阅读Vte源码的基础知识

发布于:2022-11-29 ⋅ 阅读:(312) ⋅ 点赞:(0)

目录

多余的话:

 正题:Gobject简介

Namespace

GObject – 2.0

以下是翻译官网文档:

类型系统概念

简介

背景

数据类型和编程

导出C API

 GLib 动态类型系统

拷贝函数

约定惯例

不可实例化、没有类结构的基本类型

可实例化的类类型:对象

初始化和析构

非实例化的类类型:接口

接口初始化

接口析构

GObject基类

对象实例化

对象内存管理

引用计数

弱引用

引用计数和引用循环

对象属性

一次访问多个属性

GObject消息传递系统

闭 包

C 闭包

非C闭包(对于无所畏惧的人)

信号

信号注册

信号连接

信号发射

详细参数



多余的话:

        最近想把弱电常用的工具集成在一起,这样在施工中会减少很多麻烦。在施工的电脑中安装各种调试工具非常凌乱。如果把各个厂商的功能都集中在一个软件里,无论是更换电脑还是维护工具都变得非常简单。首要的就是串口工具,我使用QT实现。QT实现了串口通信的封装QSerialPort类,一切都变得非常简单。但是,QT并没有很好的终端控制台支持。样例中也只是简单的使用QPlainTextEdit类实现显示交互。但对于标准的终端控制台少了太多功能。搜遍网络也没有很好的实现思路。无奈只能去读开源的VTE源代码。

        VTE是一个库(libvte),用于实现GTK+的终端仿真器小部件,以及使用它的最小示例应用程序(vte)。Vte主要用于gnome终端,但也可用于在游戏中嵌入控制台/终端,编辑器、IDE等。

        VTE库的实现依附于GTK+系统,使用GTK+提供的Gobject系统以支持泛型编程对类编程的支持。通过Gobject可以很好的支持其他编程语言的接口调用。这样,VTE库也就具备了对C,C++,python等编程语言的支持功能。

        这是我通过QPlainTextEdit类实现的显示交互终端,虽然他已经能够工作。但,还的确非常简陋。这已经是我在爬了很多博客后,能够实现的最好结果。所以,无奈老老实实去读大神们的源代码吧。VTE使用GTK+库实现,所以先恶补一下GTK+系统的基本知识。


 正题:Gobject简介

Namespace

GObject – 2.0

The base type system and object class

Version 2.72
Authors GTK Development Team
License LGPL-2.1-or-later
Website https://www.gtk.org
Source GNOME / GLib · GitLab

        Gobject , 亦称 Glib 对象系统,是一个程序库,它可以帮助我们使用 C 语言编写面向对象程序;它提供了一个通用的动态类型系统( GType )、一个基本类型的实现集(如整型、枚举等)、一个基本对象类型 - Gobject 、一个信号系统以及一个可扩展的参数 / 变量体系。

        GObject 告诉我们,使用 C 语言编写程序时,可以运用面向对象这种编程思想。
— Gobject 系统提供了一个灵活的、可扩展的、并且容易映射到其他语言的面向对象的 C 语言框架。
— GObject 的动态类型系统允许程序在运行时进行类型注册,它的最主要目的有两个:
1 )使用面向对象的设计方法来编程。 GObject 仅依赖于 GLib 和 libc , 通过它可使用纯 C 语言设计一整套面向对象的软件模块。
2 )多语言交互。在为已经使用 GObject 框架写好的函数库建立多语言连结时,可以很容易对应到许多语言,包括 C++ 、 Java 、 Ruby 、 Python 和 .NET/Mono 等。 GObject 被设计为可以直接使用在 C 程序中,也 封装 至其他语言。
————————————————
版权声明:本文为CSDN博主「Smith先生」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/acs713/article/details/7778051


以下是翻译官网文档:

类型系统概念

简介

        大多数现代编程语言都有自己的本地对象系统和附加的基本算法语言结构。正如GLib是这些基本类型和算法(链表、哈希表等)的实现一样,GLib对象系统也为C提供了灵活、可扩展且易于映射(到其他语言中)的面向对象框架的必要实现。所提供的主要元素可概括为: 

  • 一个泛型类型系统,用于注册任意单个继承的平面和深层派生类型以及结构化类型的接口。它负责各种对象和类结构的创建、初始化和内存管理,维护父/子关系,并处理此类类型的动态实现。也就是说,它们特定于类型的实现在运行时是可重定位/可卸载的。
  • 基本类型实现的集合,例如整数、双精度数、枚举和结构化类型等。
  • 以GObject基本类型为基础的对象层次结构的基本类型实现示例。
  • 一种信号系统,允许用户非常灵活地定制虚拟/可覆盖的对象方法,并且可以用作强大的通知机制。
  • 一个可扩展的参数/值系统,支持所有提供的基本类型,这些类型一般可用于处理对象属性或以其他方式参数化类型。

背景

        GTK和大多数GNOME库使用GObject和低级类型系统GType来提供:

  • 基于C的API面向对象实现。
  • 到其他编译或解释语言的自动透明API绑定。

        许多程序员习惯于使用只编译或只动态解释的语言,不了解与跨语言互操作性相关的挑战。本简介试图深入了解这些挑战,并简要介绍GLib选择的解决方案。

        以下章节将更详细地介绍GType和GObject是如何工作的,以及作为C程序员如何使用它们。请记住,允许从其他解释语言访问C对象是主要的设计目标之一:这通常可以解释这个库中有时相当复杂的API和特性。

数据类型和编程

        可以说,编程语言只是创建数据类型并对其进行操作的一种方法。大多数语言都提供了许多语言本机类型和一些基元类型,以基于这些基元类型创建更复杂的类型。

        在C语言中,该语言提供了char、long、pointer等类型。在C代码编译期间,编译器将这些语言类型映射到编译器的目标体系结构机器类型。如果您使用的是C解释器(假设存在),解释器在运行时、程序执行期间(如果使用实时编译器引擎,则在执行之前)将语言类型映射到目标机器的机器类型。

        Perl和Python是解释语言,它们实际上并不提供类似于C语言的类型定义。解释器还常常提供许多从一种类型到另一种类型的自动转换。例如,在Perl中,如果给定所需的上下文,保存整数的变量可以自动转换为字符串:

my $tmp = 10;
print "this is an integer converted to a string:" . $tmp . "\n";

        当然,当语言提供的默认转换不直观时,通常也可以显式指定转换。

导出C API

        C API由一组函数和全局变量定义,这些函数和变量通常从二进制文件导出。C函数有任意数量的参数和一个返回值。因此,每个函数都由函数名和描述函数参数和返回值的C类型集唯一标识。API导出的全局变量同样通过名称和类型进行标识。

        因此,一个C API仅仅是由一组与一组类型相关联的名称定义的。如果您知道函数调用约定以及C类型到您所在平台使用的机器类型的映射,您可以解析每个函数的名称,以查找与此函数关联的代码在内存中的位置,然后为该函数构造一个有效的参数列表。最后,您只需使用参数列表触发对目标C函数的调用。

        为了便于讨论,下面是一个示例C函数和GCC在Linux计算机上生成的相关32位x86汇编代码:

static void
function_foo (int foo)
{
}

int
main (int   argc,
      char *argv[])
{
    function_foo (10);

    return 0;
}
push   $0xa
call   0x80482f4 <function_foo>

        上面显示的汇编代码非常简单:第一条指令将十六进制值0xa(十进制值10)作为32位整数推送到堆栈上并进行调用。如您所见,C函数调用由GCC实现为本地函数调用(这可能是最快的实现)。对于调用function_foo函数。

        现在,假设我们想从Python程序调用C函数。为此,Python解释器需要:function_foo

  • 查找函数所在的位置。这可能意味着要找到导出此函数的C编译器生成的二进制文件。
  • 将函数的代码加载到可执行内存中。
  • 在调用函数之前,将Python参数转换为C兼容参数。
  • 使用正确的调用约定调用函数。
  • 将C函数的返回值转换为与Python兼容的变量,以将它们返回到Pythone代码。

        上面描述的过程非常复杂,有很多方法可以使它对C和Python程序员完全自动化和透明:

  • 第一种解决方案是手工编写大量粘合代码,为导出或导入的每个函数编写一次,这样可以进行Python到C的参数转换和C到Pythone的返回值转换。然后,这个粘合代码与解释器链接,解释器允许Python程序调用Python函数,这些函数将工作委托给C函数。
  • 另一个更好的解决方案是,使用一个读取原始函数签名的特殊编译器,为导出或导入的每个函数自动生成一次粘合代码。

        GLib使用的解决方案是使用GType库,它在运行时保存程序员操作的所有对象的描述。然后,这种所谓的动态类型库被特殊的通用粘合代码用来在不同的运行时域之间自动转换函数参数和函数调用约定

GType实现的解决方案的最大优点是,位于运行时域边界的粘合代码只需编写一次:下图更清楚地说明了这一点。

 GLib 动态类型系统

        由 GLib 类型系统操作的类型比通常理解为 Object 类型的类型更通用。通过查看用于在类型系统中注册新类型的结构和函数,可以最好地解释它。

typedef struct _GTypeInfo               GTypeInfo;
struct _GTypeInfo
{
  /* interface types, classed types, instantiated types */
  guint16                class_size;

  GBaseInitFunc          base_init;
  GBaseFinalizeFunc      base_finalize;

  /* classed types, instantiated types */
  GClassInitFunc         class_init;
  GClassFinalizeFunc     class_finalize;
  gconstpointer          class_data;

  /* instantiated types */
  guint16                instance_size;
  guint16                n_preallocs;
  GInstanceInitFunc      instance_init;

  /* value handling */
  const GTypeValueTable *value_table;
};

GType
g_type_register_static (GType            parent_type,
                        const gchar     *type_name,
                        const GTypeInfo *info,
                        GTypeFlags       flags);

GType
g_type_register_fundamental (GType                       type_id,
                             const gchar                *type_name,
                             const GTypeInfo            *info,
                             const GTypeFundamentalInfo *finfo,
                             GTypeFlags                  flags);

   g_type_register_static,  g_type_register_dynamic 和  g_type_register_fundamental是声明在gtype.h,定义在gtype.c中用来在类型系统中注册一个新的GType的C语言函数。g_type_register_fundamental很少用到,最后的章节将解释怎样创建一个基本的类型。

        基本类型是顶级类型,它不派生自任何其他类型,而其他非基本类型派生自基本类型。初始化后,类型系统不仅初始化其内部数据结构,而且还注册许多核心类型:其中一些是基本类型。其他类型是从这些基本类型派生的类型。

基本型和非基本型由以下操作定义:

  • 类大小: GTypeInfo中的class_size域;
  • 类初始化函数(C++构造函数):GTypeInfo中的base_init域和class_init域;
  • 类析构函数(C++析构函数):GTypeInfo中的base_finalize域和class_finalize域;
  • 实例大小 (C++参数更改为 new): GTypeInfo中的instance_size域;
  • 实例化策略(类似C++中的new运算符):GTypeInfo中的n_preallocs域;
  • 复制函数(C++复制运算符):GTypeInfo中的value_table域;
  • 类型特征标志:GTypeFlags;

      基本类型还有在GTypeFundamentalInfo中的一系列GTypeFundamentalFlags定义。非基本类型的定义还包括在 g_type_register_static 和  g_type_register_dynamic函数中作为parant_type参数传递的父类型。

拷贝函数

        所有 GLib 类型(基本型和非基本型、类化和非类化、实例化和非实例化)的共同点是他们都可以通过一个API来进行复制和赋值。

        该结构用作所有这些类型的抽象容器。其简单的 API(gobject/gvalue.h中定义可以用来调用在类型注册时注册的value_table函数:例如g_value_copy函数把一个GValue的内容复制到另一个GValue。这跟C++中通过赋值操作符重定义来修改默认的结构/类的按位复制语义很相似。

以下代码向你展示了如何复制一个64位的整数和一个GObject实例指针:

static void test_int (void)
{
  GValue a_value = G_VALUE_INIT;
  GValue b_value = G_VALUE_INIT;
  guint64 a, b;

  a = 0xdeadbeef;

  g_value_init (&a_value, G_TYPE_UINT64);
  g_value_set_uint64 (&a_value, a);

  g_value_init (&b_value, G_TYPE_UINT64);
  g_value_copy (&a_value, &b_value);

  b = g_value_get_uint64 (&b_value);

  if (a == b) {
    g_print ("Yay !! 10 lines of code to copy around a uint64.\n");
  } else {
    g_print ("Are you sure this is not a Z80 ?\n");
  }
}

static void test_object (void)
{
  GObject *obj;
  GValue obj_vala = G_VALUE_INIT;
  GValue obj_valb = G_VALUE_INIT;
  obj = g_object_new (VIEWER_TYPE_FILE, NULL);

  g_value_init (&obj_vala, VIEWER_TYPE_FILE);
  g_value_set_object (&obj_vala, obj);

  g_value_init (&obj_valb, G_TYPE_OBJECT);

  /* g_value_copy's semantics for G_TYPE_OBJECT types is to copy the reference.
   * This function thus calls g_object_ref.
   * It is interesting to note that the assignment works here because
   * VIEWER_TYPE_FILE is a G_TYPE_OBJECT.
   */
  g_value_copy (&obj_vala, &obj_valb);

  g_object_unref (G_OBJECT (obj));
  g_object_unref (G_OBJECT (obj));
}

        关于上述代码的重要一点是,复制调用的确切语义是未定义的,因为它们依赖于复制函数的实现。某些复制函数可能决定分配新的内存块,然后将数据从源复制到目标。其他人可能只想增加实例的引用计数,并将引用复制到新的 GValue

        用于指定这些赋值函数的值表记录在 GTypeValueTable中。

        有趣的是,您也不太可能在类型注册期间指定value_table ,因为这些类型是从非基本类型的父类型继承的。

约定惯例

用户在创建要导出到头文件中的新类型时应遵循一些约定:

  • 类型名称(包括对象名称)的长度必须至少为三个字符,并以“a–z”、“A–Z”或“_”开头。
  • 使用函数名称的约定:函数名应该使用object_method格式。例如,一个file类型的save方法应该被命名为file_save
  • 使用前缀可避免与其他项目发生命名空间冲突。如果库(或应用程序)名为 Viewer,则所有函数名称的前缀为 viewer_,例如:viewer_object_method
  • 创建一个名为PREFIX_TYPE_OBJECT的宏,该宏始终返回关联对象类型的 GType。例如,对Viewer命名空间中的File类型,使用VIEWER_TYPE_FILE返回FILE类型的GType,该宏应该由函数prefix_object_get_type来实现,例如viewer_file_get_type
  • 使用 G_DECLARE_FINAL_TYPEG_DECLARE_DERIVABLE_TYPE为对象定义各种其他常规宏。
  • PREFIX_OBJECT(obj),它返回类型为PrefixObject 的指针。这个宏用于进行显式类型转换时确保静态类型安全,它同时通过运行时检查来确保动态类型安全。在生产环境中可以禁用动态类型检查(请参考GLib的编译)。例如,还是Viewer中的File类型,该宏就是VIEWER_FILE(obj)
  • PREFIX_OBJECT_CLASS (klass),这严格等同于前面的强制转换宏:它通过对类结构进行动态类型检查来执行静态强制转换。它应返回指向类型PrefixObjectClassVIEWER_FILE_CLASS的类结构的指针。
  • PREFIX_IS_OBJECT (obj),返回一个布尔值来确定输入的对象实例是否为非NULL并且类型时Object类型。例如VIEWER_IS_FILE (obj),判断obj是否为FILE类型的对象。
  • PREFIX_IS_OBJECT_CLASS (klass),返回一个布尔值来确定所输入的类结构指针是否是Object类型的类结构类型。例如VIEWER_IS_OBJECT_CLASS (klass)
  • PREFIX_OBJECT_GET_CLASS(obj),它返回与给定类型的实例关联的类指针。此宏用于静态和动态类型安全目的(就像以前的转换宏一样)。例如:VIEWER_FILE_GET_CLASS

        这些宏的实现非常简单:gtype.h中提供了许多简单易用的宏。对于我们上面使用的示例,我们将编写以下简单的代码来声明宏:

#define VIEWER_TYPE_FILE viewer_file_get_type()

/*#define G_DECLARE_FINAL_TYPE (
  ModuleObjName,
  module_obj_name,
  MODULE,
  OBJ_NAME,
  ParentName
  )
  一个方便的宏,用于在头文件中为(目前)不打算子类化的类型发出通常的声明。
*/
G_DECLARE_FINAL_TYPE (ViewerFile, viewer_file, VIEWER, FILE, GObject)

        那么可以使用G_DEFINE_TYPE宏来定义一个新类型:

/*
 #define G_DEFINE_TYPE (
  TN,
  t_n,
  T_P
 )

 类型实现的便利宏,它声明类初始化函数、实例初始化函数和名为指向父类的静态变量。此外,它定义了一个函 
 数。

TN
新类型的名称,采用驼峰大小写命名。

t_n
新类型的名称,小写,单词由"_"分隔。

T_P
父类型。GType
*/
G_DEFINE_TYPE (ViewerFile, viewer_file, G_TYPE_OBJECT)

        否则,必须手动实现该功能:viewer_file_get_type

GType viewer_file_get_type (void)
{
  static GType type = 0;
  if (type == 0) {
    const GTypeInfo info = {
      /* You fill this structure. */
    };
    type = g_type_register_static (G_TYPE_OBJECT,
                                   "ViewerFile",
                                   &info, 0);
  }
  return type;
}

不可实例化、没有类结构的基本类型

        有很多类型是无法被类型系统实例化,并且没有一个类结构的基本类型。大多数基本类型是那些如gchar一样的普通类型,它们已经被GLib类型系统所注册。

        在极少数情况下,需要在类型系统中注册这样的类型,用零填充GTypeInfo结构,因为这些类型在大多数情况下也是基本的GType类型。

GTypeInfo info = {
  .class_size = 0,

  .base_init = NULL,
  .base_finalize = NULL,

  .class_init = NULL,
  .class_finalize = NULL,
  .class_data = NULL,

  .instance_size = 0,
  .n_preallocs = 0,
  .instance_init = NULL,

  .value_table = NULL,
};

static const GTypeValueTable value_table = {
  .value_init = value_init_long0,
  .value_free = NULL,
  .value_copy = value_copy_long0,
  .value_peek_pointer = NULL,

  .collect_format = "i",
  .collect_value = value_collect_int,
  .lcopy_format = "p",
  .lcopy_value = value_lcopy_char,
};

info.value_table = &value_table;

type = g_type_register_fundamental (G_TYPE_CHAR, "gchar", &info, &finfo, 0);

        具有不可实例化的类型可能看起来有点无用:如果无法实例化该类型的实例,那么类型有什么用呢?这些类型中的大多数都与 GValue结合使用:GValue使用整数或字符串初始化,并使用注册类型的 value_table传递。而GValue以及基本类型的扩展在与对象的属性和信号一起工作的时候会很有用。

可实例化的类类型:对象

        在类中注册并声明为可实例化的类型与对象最相似。虽然GObject(在GObject基类中详细介绍)是最著名的可实例化类类型,但作为继承层次结构基础的其他类型的类似对象已经在外部开发,它们都是基于下面描述的基本特性构建的。

        例如,下面的代码显示了如何在类型系统中注册此类基本对象类型(不使用任何 GObject 便利 API):

typedef struct {
  GObject parent_instance;

  /* instance members */
  char *filename;
} ViewerFile;

typedef struct {
  GObjectClass parent_class;

  /* class members */

  /* the first is public, pure and virtual 第一种是公共的、纯粹的、虚拟的*/
  void (*open)  (ViewerFile  *self,
                 GError     **error);

  /* the second is public and virtual */
  void (*close) (ViewerFile  *self,
                 GError     **error);
} ViewerFileClass;

#define VIEWER_TYPE_FILE (viewer_file_get_type ())

GType
viewer_file_get_type (void)
{
  static GType type = 0;
  if (type == 0) {
    const GTypeInfo info = {
      .class_size = sizeof (ViewerFileClass),
      .base_init = NULL,
      .base_finalize = NULL,
      .class_init = (GClassInitFunc) viewer_file_class_init,
      .class_finalize = NULL,
      .class_data = NULL,
      .instance_size = sizeof (ViewerFile),
      .n_preallocs = 0,
      .instance_init = (GInstanceInitFunc) viewer_file_init,
    };
    type = g_type_register_static (G_TYPE_OBJECT,
                                   "ViewerFile",
                                   &info, 0);
  }
  return type;
}

         在第一次调用viewer_file_get_type时,名为ViewerFile的类型将在类型系统中注册为继承自类型G_TYPE_OBJECT的类型。

        每个对象必须定义两个结构:类结构和实例结构。所有的类结构必须包含一个GTypeClass结构作为第一个成员。所有实例结构必须包含一个GTypeInstance结构作为第一个成员。来自gtype.h的这些C类型的声明如下所示:

struct _GTypeClass
{
  GType g_type;
};

struct _GTypeInstance
{
  GTypeClass *g_class;
};

        这些约束允许类型系统确保每个对象实例(由指向对象实例结构的指针标识)在其第一个字节中包含一个指向对象类结构的指针。

这种关系最好通过一个例子来解释:让我们以从对象A继承的对象B为例:

/* A definitions */
typedef struct {
  GTypeInstance parent;
  int field_a;
  int field_b;
} A;

typedef struct {
  GTypeClass parent_class;
  void (*method_a) (void);
  void (*method_b) (void);
} AClass;

/* B definitions. */
typedef struct {
  A parent;
  int field_c;
  int field_d;
} B;

typedef struct {
  AClass parent_class;
  void (*method_c) (void);
  void (*method_d) (void);
} BClass;

        C标准要求,C结构体的第一个字段必须从用于在内存中保存该结构体字段的缓冲区的第一个字节开始存储。这意味着对象B的实例的第一个字段是A的第一个字段,而A的第一个字段又是GTypeInstance的第一个字段,而GTypeInstance的第一个字段又是g_class,一个指向B的类结构的指针。

由于这些简单的条件,可以通过执行以下操作来检测每个对象实例的类型:

B *b;
b->parent.parent.g_class->g_type

或者,更紧凑地说:

B *b;
((GTypeInstance *) b)->g_class->g_type

初始化和析构

        这些类型的实例化可以用g_type_create_instance完成,它将查找与所请求的类型相关的类型信息结构。然后,使用用户声明的实例大小和实例化策略(如果n_preallocs字段设置为非零值,类型系统将以块为单位分配对象的实例结构,而不是为每个实例分配内存)获得一个缓冲区来保存对象的实例结构。

        如果这是对象的第一个实例,类型系统必须创建一个类结构。它分配一个缓冲区来保存对象的类结构,并对其进行初始化。类结构的第一部分(即:嵌入的父类结构)通过复制父类结构的内容来初始化。类结构的其余部分初始化为0。如果没有父类,则整个类结构初始化为0。然后,类型系统从最顶层的基本对象到最底层的派生对象调用base_class_initialization函数(GBaseInitFunc)。然后调用对象的class_init (GClassInitFunc)函数,完成类结构的初始化。最后,初始化对象的接口(稍后将详细讨论接口初始化)。

        一旦类型系统有了一个指向已初始化的类结构的指针,它就会将对象的实例类指针设置为对象的类结构,并调用对象的instance_init (GInstanceInitFunc)函数,从最基本的类型到最基本的派生类型。

        通过g_type_free_instance销毁对象实例非常简单:如果存在实例结构,则将实例结构返回到实例池,如果这是该对象的最后一个活着的实例,则销毁该类。

        类的析构(析构的概念在GType中有时被部分称为终结)是初始化的对称过程:接口首先被析构。然后,调用最派生的class_finalize (GClassFinalizeFunc)函数。最后,base_class_finalize (GBaseFinalizeFunc)函数从最底层的大多数派生类型调用到最顶层的最基本类型,并释放类结构。

        基类的初始化/终结过程非常类似于c++的构造函数/析构函数范式。尽管实际细节不同,但重要的是不要被表面上的相似之处迷惑。gtype没有实例销毁机制。在现有的GType代码上实现正确的销毁语义是用户的责任。(这就是GObject所做的:参见GObject基类。)此外,通常不需要与GTypebase_initclass_init回调函数等价的c++代码,因为c++不能在运行时真正创建对象类型。

构造和析构流程可以总结如下:

调用时间 调用的函数 函数的参数
目标类型的首次调用g_type_create_instance() 类型的函数base_init 在从基本类型到目标类型的类的继承树上。 为每个类结构调用一次。base_init
目标类型的函数class_init 在目标类型的类结构上
接口初始化,请参阅“接口初始化”一节
对目标类型的每次调用g_type_create_instance() 目标类型的函数instance_init 在对象的实例上
目标类型的最后一次调用g_type_free_instance() 界面破坏,请参阅“界面销毁”部分
目标类型的函数class_finalize 在目标类型的类结构上
类型的函数base_finalize 在从基本类型到目标类型的类的继承树上。 为每个类结构调用一次。base_finalize

非实例化的类类型:接口

        GType的接口与Java的接口非常相似。它们允许描述几个类将遵循的通用API。想象一下高保真设备上的播放、暂停和停止按钮,这些按钮可以看作是一个播放界面。一旦你知道它们的作用,你就可以控制你的CD播放器,MP3播放器或任何使用这些符号的东西。

        要声明一个接口,你要注册一个从GTypeInterface继承而来的不可实例化的类型,如下所示:

#define VIEWER_TYPE_EDITABLE viewer_editable_get_type ()
G_DECLARE_INTERFACE (ViewerEditable, viewer_editable, VIEWER, EDITABLE, GObject)

struct _ViewerEditableInterface {
  GTypeInterface parent;

  void (*save) (ViewerEditable  *self,
                GError         **error);
};

void viewer_editable_save (ViewerEditable  *self,
                           GError         **error);

        接口的函数 viewer_editable_save 可以简单地实现为: 

void
viewer_editable_save (ViewerEditable  *self,
                      GError         **error)
{
  ViewerEditableinterface *iface;

  g_return_if_fail (VIEWER_IS_EDITABLE (self));
  g_return_if_fail (error == NULL || *error == NULL);

  iface = VIEWER_EDITABLE_GET_IFACE (self);
  g_return_if_fail (iface->save != NULL);
  iface->save (self);
}

        viewer_editable_get_type注册了一个名为ViewerEditable的类型,它继承自G_TYPE_INTERFACE。在继承树中,所有接口都必须是G_TYPE_INTERFACE的子接口。

        接口只能由一个结构定义,该结构的第一个成员必须是GTypeInterface结构。接口结构应该包含接口方法的函数指针。为每个简单地直接调用接口方法的接口方法定义辅助函数是一种很好的风格:viewer_editable_save是其中之一。

        如果没有特殊需求,你可以使用G_IMPLEMENT_INTERFACE宏来实现一个接口:

static void
viewer_file_save (ViewerEditable *self)
{
  g_print ("File implementation of editable interface save method.\n");
}

static void
viewer_file_editable_interface_init (ViewerEditableInterface *iface)
{
  iface->save = viewer_file_save;
}

G_DEFINE_TYPE_WITH_CODE (ViewerFile, viewer_file, VIEWER_TYPE_FILE,
                         G_IMPLEMENT_INTERFACE (VIEWER_TYPE_EDITABLE,
                                                viewer_file_editable_interface_init))

        如果您的代码确实有特殊要求,则必须编写自定义get_type函数来注册您的GType,该GType继承自某些GObject,并实现ViewerEditable接口。例如,这段代码注册了一个新的ViewerFile类,它实现了ViewerEditable:

static void
viewer_file_save (ViewerEditable *editable)
{
  g_print ("File implementation of editable interface save method.\n");
}

static void
viewer_file_editable_interface_init (gpointer g_iface,
                                     gpointer iface_data)
{
  ViewerEditableInterface *iface = g_iface;

  iface->save = viewer_file_save;
}

GType
viewer_file_get_type (void)
{
  static GType type = 0;
  if (type == 0) {
    const GTypeInfo info = {
      .class_size = sizeof (ViewerFileClass),
      .base_init = NULL,
      .base_finalize = NULL,
      .class_init = (GClassInitFunc) viewer_file_class_init,
      .class_finalize = NULL,
      .class_data = NULL,
      .instance_size = sizeof (ViewerFile),
      .n_preallocs = 0,
      .instance_init = (GInstanceInitFunc) viewer_file_init
    };

    const GInterfaceInfo editable_info = {
      .interface_init = (GInterfaceInitFunc) viewer_file_editable_interface_init,
      .interface_finalize = NULL,
      .interface_data = NULL,
    };

    type = g_type_register_static (VIEWER_TYPE_FILE,
                                   "ViewerFile",
                                   &info, 0);

    g_type_add_interface_static (type,
                                 VIEWER_TYPE_EDITABLE,
                                 &editable_info);
  }
  return type;
}

        g_type_add_interface_static记录给定类型同时实现FooInterface的类型系统(foo_interface_get_type返回FooInterface的类型)。GInterfaceInfo结构体保存了接口实现的信息:

struct _GInterfaceInfo
{
  GInterfaceInitFunc     interface_init;
  GInterfaceFinalizeFunc interface_finalize;
  gpointer               interface_data;
};

接口初始化

        当第一次创建实现接口(直接或从超类继承实现)的可实例化类类型时,它的类结构将按照“可实例化类类型:对象”一节中描述的过程进行初始化。之后,初始化与该类型关联的接口实现。

        首先分配一个内存缓冲区来保存接口结构。然后将父接口结构复制到新的接口结构中(此时父接口已经初始化)。如果没有父接口,则用0初始化接口结构。然后初始化g_type和g_instance_type字段:g_type设置为派生最多的接口的类型,g_instance_type设置为实现该接口的派生最多的类型的类型。

        调用接口的base_init函数,然后调用接口的default_init函数。最后,如果该类型已经注册了接口的实现,则调用该实现的interface_init函数。如果一个接口有多个实现,base_initinterface_init函数将对每个初始化的实现调用一次。

        因此,建议使用default_init函数来初始化接口。这个函数对于接口只调用一次,不管有多少个实现。default_init函数由G_DEFINE_INTERFACE声明,它可以用来定义接口:

G_DEFINE_INTERFACE (ViewerEditable, viewer_editable, G_TYPE_OBJECT)

static void
viewer_editable_default_init (ViewerEditableInterface *iface)
{
  /* add properties and signals here, will only be called once */
}

        或者你可以在GType函数中手动定义接口:

GType
viewer_editable_get_type (void)
{
  static gsize type_id = 0;
  if (g_once_init_enter (&type_id)) {
    const GTypeInfo info = {
      sizeof (ViewerEditableInterface),
      NULL,   /* base_init */
      NULL,   /* base_finalize */
      viewer_editable_default_init, /* class_init */
      NULL,   /* class_finalize */
      NULL,   /* class_data */
      0,      /* instance_size */
      0,      /* n_preallocs */
      NULL    /* instance_init */
    };
    GType type = g_type_register_static (G_TYPE_INTERFACE,
                                         "ViewerEditable",
                                         &info, 0);
    g_once_init_leave (&type_id, type);
  }
  return type_id;
}

static void
viewer_editable_default_init (ViewerEditableInterface *iface)
{
  /* add properties and signals here, will only called once */
}

        总之,接口初始化使用以下函数:

调用时间 调用的函数 函数的参数 备注
首次调用任何类型实现接口g_type_create_instance() 界面功能base_init 在接口的可变性上 很少需要使用它。根据实现接口的实例化类类型调用一次。
对每个类型实现接口的第一个调用g_type_create_instance() 界面功能default_init 在接口的可变性上 在此处注册接口的信号、属性等。将被调用一次。
首次调用任何类型实现接口g_type_create_instance() 实现的功能interface_init 在接口的可变性上 初始化接口实现。为实现接口的每个类调用。初始化接口结构中指向实现类实现的接口方法指针。

接口析构

        当注册了接口实现的可实例化类型的最后一个实例被销毁时,与该类型关联的接口实现也会被销毁。

        为要销毁一个接口实现,GType首先调用实现的interface_finalize函数,然后调用接口派生最多的base_finalize函数。

interface_finalize
base_finalize

        同样,理解interface_finalizebase_finalize在销毁接口的每个实现时都只调用一次是很重要的,正如在“接口初始化”一节中所述。因此,如果要使用这些函数之一,就需要使用一个静态整数变量,该变量保存接口实现的实例数量,这样接口的类只销毁一次(当整数变量为零时)。

以上过程可归纳如下:

调用时间 调用的函数 函数的参数
对类型实现接口的最后一次调用g_type_free_instance() 界面功能interface_finalize 在接口的可变性上
界面功能base_finalize 在接口的可变性上

GObject基类

        前一章讨论了GLib的动态类型系统的细节。GObject库还包含名为GObject的基本类型的实现。GObject是一种基本的类可实例化类型。它实现了:

  • 具有引用计数功能的内存管理
  • 实例的构建/销毁
  • 具有set/get函数对的通用对象属性
  • 易于使用信号

        所有使用GLib类型系统的GNOME库(如GTKGStreamer)都继承自GLib类型系统,这就是为什么理解GObject它如何工作的细节很重要。

对象实例化

        函数族可以用来实例化从GObject基类型继承而来的任何GType。所有这些函数都确保GLib的类型系统正确初始化了类和实例结构,然后在某个点调用构造函数类方法g_object_new(),该方法用于:

  • 通过g_type_create_instance()分配和清除内存
  • 使用构造属性初始化对象的实例。

GObject显式地保证所有类和实例成员(指向父类的字段除外)都被设置为零。

完成所有构造操作并设置构造函数属性后,将调用构造的类方法。

继承的对象允许重写此构造的类方法。下面的例子展示了:GObjectViewerFile如何覆盖父进程的构造过程:

#define VIEWER_TYPE_FILE viewer_file_get_type ()
G_DECLARE_FINAL_TYPE (ViewerFile, viewer_file, VIEWER, FILE, GObject)

struct _ViewerFile
{
  GObject parent_instance;

  /* instance members */
  char *filename;
  guint zoom_level;
};

/* will create viewer_file_get_type and set viewer_file_parent_class */
G_DEFINE_TYPE (ViewerFile, viewer_file, G_TYPE_OBJECT)

static void
viewer_file_constructed (GObject *obj)
{
  /* update the object state depending on constructor properties */

  /* Always chain up to the parent constructed function to complete object
   * initialisation. */
  G_OBJECT_CLASS (viewer_file_parent_class)->constructed (obj);
}

static void
viewer_file_finalize (GObject *obj)
{
  ViewerFile *self = VIEWER_FILE (obj);

  g_free (self->filename);

  /* Always chain up to the parent finalize function to complete object
   * destruction. */
  G_OBJECT_CLASS (viewer_file_parent_class)->finalize (obj);
}

static void
viewer_file_class_init (ViewerFileClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = viewer_file_constructed;
  object_class->finalize = viewer_file_finalize;
}

static void
viewer_file_init (ViewerFile *self)
{
  /* initialize the object */
}

如果用户用:ViewerFile实例化一个对象

ViewerFile *file = g_object_new (VIEWER_TYPE_FILE, NULL);

        如果这是此类对象的第一个实例化,那么viewer_file_class_init 函数将在任何viewer_file_base_class_init函数之后被调用。这将确保这个新对象的类结构被正确初始化。在这里,期望viewer_file_base_class_init覆盖对象的类方法并设置类自己的方法。在上面的例子中,构造函数方法是唯一被覆盖的方法:它被设置为viewer_file_construct

        一旦g_object_new获得对已初始化类结构的引用,它将调用其构造函数方法来创建新对象的实例。由于它刚刚被viewer_file_class_init重写到viewer_file_construct​​​​​​​,因此调用了后者,并且由于它的实现是正确的,因此它链接到其父构造函数。这里的问题是如何找到父构造函数。一种方法(在GTK+源代码中使用)是将原始构造函数保存在viewer_file_class_init的静态变量中,然后从viewer_file_construct重用它。首选的方法是使用g_type_class_peekg_type_class_peek_parent函数。【翻译不确定:viewer_file_parent_class为我们设置的指针G_DEFINE_TYPE。】

        最后,在某个时刻,g_object_constructor将由链中的最后一个构造函数调用。该函数通过g_type_create_instance分配对象实例的缓冲区,这意味着如果已经注册了instance_init函数,则在此时调用该函数。在instance_init返回之后,对象被完全初始化,并且应该准备好响应任何用户请求。当g_type_create_instance返回时,g_object_constructor设置构造属性(即给g_object_new的属性)并返回给用户的构造函数,然后允许该构造函数进行有用的实例初始化…

        这个所描述的处理过程看起来有一点难懂(在我看来确实难懂),但是由下面的表格可以清楚的概况起来,当g_object_new调用时,相关函数的调用顺序。

调用时间 调用的函数 函数的参数 备注
目标类型的首次调用g_object_new() 目标类型的函数base_init 在从基本类型到目标类型的类的继承树上。 为每个类结构调用一次。base_init 从未在实践中使用过。您不太可能需要它。
目标类型的函数class_init 在目标类型的类结构上 在这里,您应该确保初始化或重写类方法(即,将函数指针分配给每个类的方法),并创建与对象关联的信号和属性。
界面功能base_init 在接口的可变性上
界面功能interface_init 在接口的可变性上
对目标类型的每次调用g_object_new() 目标类型的类方法:constructorGObjectClass->constructor 在对象的实例上 如果需要以自定义方式处理构造属性,或实现单例类,请重写构造函数方法,并确保在执行自己的初始化之前链接到对象的父类。如有疑问,请不要重写构造函数方法。
类型的函数instance_init 在从基本类型到目标类型的类的继承树上。为每个类型提供的 为每个实例结构调用一次。instance_init 提供一个函数,用于在设置对象的构造属性之前对其进行初始化。这是初始化 G 对象实例的首选方法。此函数等效于C++构造函数。instance_init
目标类型的类方法:constructedGObjectClass->constructed 在对象的实例上 如果需要在设置完所有构造属性后执行对象初始化步骤。这是对象初始化过程中的最后一步,仅当方法返回新的对象实例(而不是现有的单例)时才调用。constructor

        读者应该关注函数调用顺序中的一个小变化:虽然从技术上讲,类的构造函数方法在GTypeinstance_init函数之前被调用(因为调用instance_initg_type_create_instance是由g_object_constructor调用的,g_object_constructor是顶级类的构造函数方法,用户被期望链接到它),但在用户提供的构造函数中运行的用户代码总是在GType的instance_init函数之后运行,因为用户提供的构造函数必须(你已经被警告过)在做任何有用的事情之前链接起来。​​​​​​​

对象内存管理

        用于gobject的内存管理API有点复杂,但其背后的思想相当简单:目标是提供基于引用计数的灵活模型,该模型可以集成到使用或需要不同内存管理模型(如垃圾收集)的应用程序中。下面描述了用于操作此引用计数的方法。

引用计数

        函数g_object_ref/g_object_unref分别增加和减少引用计数。从GLib 2.8开始,这些函数都是线程安全的。引用计数由g_object_new初始化为1,这并不奇怪,这意味着调用者目前是新创建引用的唯一所有者。当引用计数达到零时,也就是说,当持有对象引用的最后一个客户机调用g_object_unref时,将调用disposefinalize类方法。

        引用计数被g_object_new初始化为1,这意味着调用者当前是该新建实例的唯一拥有者。当引用计数被减到0,也就是当拥有该引用的最后一个用户程序调用g_object_unref的时候,disposefinalize类方法被调用。

        最后,在finalize被调用之后,将调用g_type_free_instance来释放对象实例。根据注册类型时决定的内存分配策略(通过g_type_register_* 函数之一),对象的实例内存将被释放或返回到该类型的对象池。一旦对象被释放,如果它是该类型的最后一个实例,该类型的类将被销毁,正如“可实例化类类型:对象”和“不可实例化类类型:接口”小节中描述的那样。

下表总结了一个GObject的销毁过程:

调用时间 调用的函数 函数的参数 备注
对目标类型实例的最后一次调用g_object_unref() 目标类型的释放类函数 对象实例 当释放结束时,对象不应包含对任何其他成员对象的任何引用。该对象还应该能够应答客户端方法调用(可能带有错误代码,但没有内存冲突),直到执行 finalize。释放可以执行多次。释放应链接到其父实现,紧接在返回到调用方之前。
目标类型的终结类函数 对象实例 预计定稿将完成处置所启动的销毁过程。它应该完成物体的破坏。完成将仅执行一次。finalize 应该在返回调用方之前链接到其父实现。有关详细信息,请参阅“引用计数和周期”部分。
对目标类型的最后一个实例的最后一个调用g_object_unref() 界面功能interface_finalize 在接口的可变性上 从未在实践中使用过。您不太可能需要它。
界面功能base_finalize 在接口的可变性上 从未在实践中使用过。您不太可能需要它。
目标类型的函数class_finalize 在目标类型的类结构上 从未在实践中使用过。您不太可能需要它。
类型的函数base_finalize 在从基本类型到目标类型的类的继承树上。 为每个类结构调用一次。base_init 从未在实践中使用过。您不太可能需要它。

弱引用

        若引用用于监控对象的析构:g_object_weak_ref函数添加一个监控回调,该函数并不持有该实例的引用,但是回调会在实例运行它的dispose方法时调用。因此,每一个弱引用回调在实例析构时可能会被调用几次(因为在对象析构时dispose函数会被调用多次)。

        使用g_object_weak_unref来从一个对象中删除弱引用回调。

        弱引用也可以通过g_object_add_weak_pointerg_object_remove_weak_pointer函数来实现。这些函数为给定的对象实例添加一个弱引用指针,当对象实例被析构时该弱引用会被设为0。

        线程安全的弱引用可以通过GWeakRef来实现。

引用计数和引用循环

        GObject的内存管理模型被设计成可以使用垃圾收集轻松地集成到现有代码中。这就是为什么销毁过程被分为两个阶段:在dispose处理程序中执行的第一个阶段应该释放对其他成员对象的所有引用。第二个阶段由finalize处理程序执行,应该完成对象的销毁过程。对象方法应该能够在这两个阶段之间运行而不会出现程序错误。

        这个两步销毁过程对于打破引用计数循环非常有用。虽然循环的检测取决于外部代码,但一旦检测到循环,外部代码就可以调用g_object_run_dispose,这实际上将打破任何现有的循环,因为它将运行与对象关联的dispose处理程序,从而释放对其他对象的所有引用。

        这解释了前面提到的关于dispose处理程序的一条规则:可以多次调用dispose处理程序。假设我们有一个引用计数循环:对象a引用B, B本身引用对象a。假设我们已经检测到这个循环,我们想销毁这两个对象。做到这一点的一种方法是在其中一个对象上调用g_object_run_dispose

        如果对象A释放了它对所有对象的所有引用,这意味着它释放了对对象B的引用。如果对象B不属于任何人,这是它的最后一个引用计数,这意味着最后一个unref运行Bdispose处理程序,而Bdispose处理程序又反过来释放B对对象A的引用。如果这是A的最后一个引用计数,这最后一个unref运行Adispose处理程序,该处理程序在Afinalize处理程序被调用之前第二次运行!

        上面的例子似乎有点做作,但如果语言绑定处理gobject,就可能发生这种情况——因此应该严格遵守对象销毁的规则。

对象属性

        GObject的一个很好的特性是对象属性的通用get/set机制。当对象被实例化时,应该使用对象的class_init处理程序向g_object_class_install_properties注册对象的属性。

        了解对象属性如何工作的最佳方法是查看如何使用它的真实示例:

// Implementation

typedef enum
{
  PROP_FILENAME = 1,
  PROP_ZOOM_LEVEL,
  N_PROPERTIES
} ViewerFileProperty;

static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };

static void
viewer_file_set_property (GObject      *object,
                          guint         property_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  ViewerFile *self = VIEWER_FILE (object);

  switch ((ViewerFileProperty) property_id)
    {
    case PROP_FILENAME:
      g_free (self->filename);
      self->filename = g_value_dup_string (value);
      g_print ("filename: %s\n", self->filename);
      break;

    case PROP_ZOOM_LEVEL:
      self->zoom_level = g_value_get_uint (value);
      g_print ("zoom level: %u\n", self->zoom_level);
      break;

    default:
      /* We don't have any other property... */
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
viewer_file_get_property (GObject    *object,
                          guint       property_id,
                          GValue     *value,
                          GParamSpec *pspec)
{
  ViewerFile *self = VIEWER_FILE (object);

  switch ((ViewerFileProperty) property_id)
    {
    case PROP_FILENAME:
      g_value_set_string (value, self->filename);
      break;

    case PROP_ZOOM_LEVEL:
      g_value_set_uint (value, self->zoom_level);
      break;

    default:
      /* We don't have any other property... */
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
viewer_file_class_init (ViewerFileClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->set_property = viewer_file_set_property;
  object_class->get_property = viewer_file_get_property;

  obj_properties[PROP_FILENAME] =
    g_param_spec_string ("filename",
                         "Filename",
                         "Name of the file to load and display from.",
                         NULL  /* default value */,
                         G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);

  obj_properties[PROP_ZOOM_LEVEL] =
    g_param_spec_uint ("zoom-level",
                       "Zoom level",
                       "Zoom level to view the file at.",
                       0  /* minimum value */,
                       10 /* maximum value */,
                       2  /* default value */,
                       G_PARAM_READWRITE);

  g_object_class_install_properties (object_class,
                                     N_PROPERTIES,
                                     obj_properties);
}

// Use

ViewerFile *file;
GValue val = G_VALUE_INIT;

file = g_object_new (VIEWER_TYPE_FILE, NULL);

g_value_init (&val, G_TYPE_UINT);
g_value_set_char (&val, 11);

g_object_set_property (G_OBJECT (file), "zoom-level", &val);

g_value_unset (&val);

        上面的客户端代码看起来很简单,但很多事情都发生在引擎盖下:

  g_object_set_property 首先要确保一个属性连同它的名字在类型fileclass_init函数中被注册,对象系统将从最底派生类型到最顶基类型遍历继承树,寻找注册了该属性的类型,然后将用户提供的GValue转换为属性相关的GValue类型。

        如果用户提供了一个有符号char类型,而对象属性被注册为无符号int类型,g_value_transform函数会试着将输入的有符号char类型转换为无符号int类型。当然,能否转换成功取决于所需的转换函数是否存在。在实践中,几乎总是需要一个转换并且当需要的时候转换总是会发生。

        转换后,GValueg_param_value_validate验证,它确保存储在GValue中的用户数据与属性的GParamSpec指定的特征相匹配。在这里,我们在class_init中提供的GParamSpec有一个验证函数,它确保GValue包含一个尊重GParamSpec的最小和最大边界的值。在上面的例子中,客户端的GValue不考虑这些约束(它被设置为11,而最大值是10)。因此,g_object_set_property函数将返回一个错误。

        如果用户的GValue被设置为一个有效值,g_object_set_property将继续调用对象的set_property类方法。在这里,由于我们的ViewerFile的实现覆盖了这个方法,执行将跳转到viewer_file_set_property,在从GParamSpec中检索到由g_object_class_install_property存储的param_id[3]之后。

        一旦由对象的set_property类方法设置了属性,执行就返回到g_object_set_property,这将确保在对象的实例上发出“通知”信号,并将更改的属性作为参数,除非通知被g_object_freeze_notify冻结。

        G_object_thaw_notify可用于通过“notify”信号重新启用属性修改通知。重要的是要记住,即使在属性更改通知被冻结时更改了属性,只要属性更改通知被解冻,“通知”信号将对每个更改的属性发出一次:“通知”信号不会丢失属性更改,尽管单个属性的多个通知被压缩。信号只能通过通知冻结机制来延迟。

        每次想要修改属性时设置gvalue听起来像是一项乏味的任务。实际上很少有人这样做。函数g_object_set_propertyg_object_get_property用于语言绑定。对于应用来说,有一种更简单的方法,下面介绍。

一次访问多个属性

        有趣的是,可以使用g_object_setg_object_set_valist(可变变量版本)函数一次设置多个属性。上面显示的客户端代码可以重写为:

ViewerFile *file;
file = /* */;
g_object_set (G_OBJECT (file),
              "zoom-level", 6, 
              "filename", "~/some-file.txt", 
              NULL);

        这使我们不必管理在使用g_object_set_property时需要处理的gvalue。上面的代码将为每个修改的属性触发一个通知信号发射。

        等效的_get版本也可用:g_object_getg_object_get_valist(可变的版本)可用于一次性获取多个属性。

        这些高级函数有一个缺点——它们不提供返回值。在使用参数类型和范围时,应该注意它们。一个已知的错误来源是传递与属性期望的类型不同的类型;例如,当属性需要浮点值时传递一个整数,从而将所有后续参数移动若干字节。此外,忘记结束符NULL将导致未定义的行为。

        这解释了g_object_new, g_object_newvg_object_new_valist是如何工作的:它们解析用户提供的变量数量的参数,并且只有在对象成功构造之后才在参数上调用g_object_set。“通知”信号将为每个属性集发出。

GObject消息传递系统

闭 包

        闭包是异步信号传输概念的核心,该概念在整个 GTK 和 GNOME 应用程序中被广泛使用。闭包是一种抽象,是回调的泛型表示形式。它是一个小结构,包含三个对象:

  • 一个函数指针(回调本身),其原型如下所示:return_type function_callback (... , gpointer user_data);
  • 在调用闭包时传递给回调的指针user_data
  • 一个函数指针,它表示闭包的析构函数:每当闭包的引用计数达到零时,将在释放闭包结构之前调用此函数

        GClosure结构代表了所有闭包实现的通用功能:对于每个想要使用GObject类型系统的独立运行时,都存在不同的闭包实现。GObject库提供了一个简单的GCClosure类型,这是一个与C/ c++回调一起使用的闭包的特定实现。

GClosure提供简单的服务:

  • 调用(g_closure_invoke):这是创建闭包的目的:它们向回调调用方隐藏回调调用的细节。
  • 通知:闭包通知监听器某些事件,如闭包调用、闭包失效和闭包终结。监听器可以用g_closure_add_finalize_notifier(终结通知)、g_closure_add_invalidate_notifier(失效通知)和g_closure_add_marshal_guards(调用通知)注册。存在用于终结和失效事件的对称注销函数(g_closure_remove_finalize_notifierg_closure_remove_invalidate_notifier),但不存在用于调用过程的对称注销函数。[5]

C 闭包

        如果您正在使用Cc++将回调连接到给定的事件,您可以使用简单的gc闭包,它具有非常少的API,或者更简单的g_signal_connect函数(稍后将介绍)。

        G_cclosure_new将创建一个新的闭包,该闭包可以调用用户提供的callback_func,并使用用户提供的user_data作为最后一个参数。当闭包完成时(销毁过程的第二阶段),如果用户提供了destroy_data函数,它将调用它。

        G_cclosure_new_swap将创建一个新的闭包,该闭包可以调用用户提供的callback_func,并使用用户提供的user_data作为第一个参数(而不是像g_cclosure_new那样作为最后一个参数)。当闭包完成时(销毁过程的第二阶段),如果用户提供了destroy_data函数,它将调用它。

非C闭包(对于无所畏惧的人)

        如上所述,闭包隐藏了回调调用的详细信息。在C中,回调调用就像函数调用一样:它是为被调用函数创建正确的堆栈帧并执行调用程序集指令的问题。

        C 闭包编组器将表示目标函数参数的 GValue 数组转换为 C 样式函数参数列表,使用此新参数列表调用用户提供的 C 函数,获取函数的返回值,将其转换为 GValue 并将此 GValue 返回给编组器调用方。

        通用的C闭包编组器可以作为g_cclosure_marshal_generic使用libffi实现对所有函数类型的编组。除了基于libffi的编组器可能太慢的性能关键代码外,不需要针对不同类型的自定义编组器。

        下面给出一个自定义编组器的示例,说明如何将gvalue转换为C函数调用。编组器适用于以整数作为第一个参数并返回void的C函数。

g_cclosure_marshal_VOID__INT (GClosure     *closure,
                              GValue       *return_value,
                              guint         n_param_values,
                              const GValue *param_values,
                              gpointer      invocation_hint,
                              gpointer      marshal_data)
{
  typedef void (*GMarshalFunc_VOID__INT) (gpointer     data1,
                                          gint         arg_1,
                                          gpointer     data2);
  register GMarshalFunc_VOID__INT callback;
  register GCClosure *cc = (GCClosure*) closure;
  register gpointer data1, data2;

  g_return_if_fail (n_param_values == 2);

  data1 = g_value_peek_pointer (param_values + 0);
  data2 = closure->data;

  callback = (GMarshalFunc_VOID__INT) (marshal_data ? marshal_data : cc->callback);

  callback (data1,
            g_marshal_value_peek_int (param_values + 1),
            data2);
}

        还有其他类型的编组器,例如有一个通用的Python编组器,它被所有的Python闭包使用(Python闭包用于调用用Python编写的回调)。这个Python编组器将表示函数参数的输入GValue列表转换为Python元组,这是Python中的等效结构。

信号

        GObject的信号与标准的UNIX信号无关:它们将任意特定于应用程序的事件与任意数量的侦听器连接起来。例如,在GTK中,从窗口系统接收每个用户事件(击键或鼠标移动),并在小部件对象实例上以信号发射的形式生成一个GTK事件。

        每个信号都在类型系统中与它可以发出的类型一起注册:当该类型的用户注册要在信号发出时调用的闭包时,他们被认为连接到给定类型实例上的信号。用户还可以自己发射信号,或从连接到信号的闭包中的一个内部停止发射信号。

        当在给定类型实例上发出信号时,将调用该类型实例上连接到该信号的所有闭包。所有连接到这样一个信号的闭包表示其签名如下所示的回调函数:

return_type
function_callback (gpointer instance,
                   ...,
                   gpointer user_data);

信号注册

        要在现有类型上注册一个新信号,我们可以使用g_signal_newv, g_signal_new_valistg_signal_newfunction中的任意一个:

guint
g_signal_newv (const gchar        *signal_name,
               GType               itype,
               GSignalFlags        signal_flags,
               GClosure           *class_closure,
               GSignalAccumulator  accumulator,
               gpointer            accu_data,
               GSignalCMarshaller  c_marshaller,
               GType               return_type,
               guint               n_params,
               GType              *param_types);

这些函数的参数数量有点吓人,但它们相对简单:

  • signal_name: 可以用来唯一识别给定信号的字符串
  • itype: 是发出信号的实例类型
  • signal_flags: 定义连接到这个信号的闭包的调用顺序
  • class_closure: 这是信号的默认闭包:如果在信号发出时它不是NULL,它将在信号发出时被调用。与连接到该信号的其他闭包相比,调用这个闭包的时间部分取决于signal_flags。
  • accumulator:这是一个函数指针,在每个闭包被调用后调用它。如果返回FALSE,则停止信号发射。如果它返回TRUE,信号发射正常进行。它还用于基于所有调用闭包的返回值计算信号的返回值。例如,累加器可以忽略闭包返回的NULL;或者它可以构建闭包返回值的列表。
  • accu_data:此指针将向下传递到发射期间对累加器的每次调用
  • c_marshaller:连接到这个函数的所有闭包的默认C调用器
  • return_type:这是信号返回值的类型
  • n_params:信号的参数数量
  • param_types:这是一个 GType 数组,指示信号的每个参数的类型。此数组的长度由n_params 指示。

         从上面的定义中可以看出,信号基本上是对可以连接到此信号的闭包的描述,以及对连接到该信号的闭包将被调用的顺序的描述。

信号连接

如果要连接到具有闭包的信号,则有三种可能性:

  • 您可以在信号注册时注册类闭包:这是系统范围的操作。即:在支持给定信号的任何类型的实例上,每次发射给定信号时都会调用该类闭包
  • 可以使用g_signal_override_class_closure,它覆盖给定类型的类闭包。可以只在注册信号的类型的派生类型上调用此函数。此函数仅用于语言绑定。
  • 可以用g_signal_connect函数族注册闭包。这是一个特定于实例的操作:闭包仅在给定实例发出给定信号期间被调用。

         也可以在给定的信号上连接不同类型的回调:无论在哪个实例上发出给定的信号,只要发出该信号,就会调用发射钩子。例如,发射挂钩用于在应用程序中获取所有mouse_clicked发射,以便能够发出小的鼠标单击声音。发射钩子用g_signal_add_emission_hook连接,用g_signal_remove_emission_hook移除。

信号发射

信号发射是通过使用g_signal_emit函数族来完成的。

void
g_signal_emitv (const GValue  instance_and_params[],
                guint         signal_id,
                GQuark        detail,
                GValue       *return_value);
  • GValuesinstance_and_params数组包含信号的输入参数列表。数组的第一个元素是调用信号的实例指针。数组的下列元素包含信号的参数列表。
  • Signal_id标识要调用的信号。
  • Detail标识要调用的信号的特定细节。detail是一种神奇的令牌/参数,在信号发射期间传递,连接到信号的闭包使用它来过滤不需要的信号发射。在大多数情况下,您可以安全地将该值设置为零。有关该参数的更多详细信息,请参见“细节参数”一节。

        如果没有指定累加器,则Return_value保存发射期间调用的最后一个闭包的返回值。如果在信号创建期间指定了一个累加器,则使用该累加器作为发射期间调用的所有闭包的返回值的函数来计算返回值。如果在发射过程中没有调用闭包,return_value仍然被初始化为0 /null。​​​​​​​

信号发射可以分解为6个步骤:

  1. RUN_FIRST:如果在信号注册过程中使用了G_SIGNAL_RUN_FIRST标志,并且存在针对该信号的类闭包,则调用类闭包。
  2. EMISSION_HOOK:如果将任何发射钩添加到信号中,则会从第一个到最后添加调用它们。累积返回值。
  3. HANDLER_RUN_FIRST:如果任何闭包与g_signal_connect函数族连接,如果它们没有被阻塞(使用g_signal_handler_block函数族),它们将在这里运行,从第一个连接到最后一个连接。
  4. RUN_LAST:如果在注册期间设置了G_SIGNAL_RUN_LAST标志,并且设置了类闭包,则在这里调用它。
  5. HANDLER_RUN_LAST:如果任何闭包连接到g_signal_connect_after函数家族,如果它们在HANDLER_RUN_FIRST期间没有被调用,如果它们没有被阻塞,它们将在这里运行,从第一个连接到最后一个连接。
  6. RUN_CLEANUP:如果在注册期间设置了G_SIGNAL_RUN_CLEANUP标志,并且设置了类闭包,则在这里调用它。信号发射在这里完成。

        如果在发射期间的任何时刻(RUN_CLEANUPEMISSION_HOOK状态除外),其中一个闭包使用g_signal_stop_emission停止信号发射,则发射将跳到RUN_CLEANUP状态。

        在调用每个闭包之后(在RUN_EMISSION_HOOKRUN_CLEANUP中除外),累加器函数在所有状态下都被调用。它将闭包返回值累加到信号返回值中,并返回TRUEFALSE。如果在任何时候,它不返回TRUE,则排放将跳转到RUN_CLEANUP状态。

        如果没有提供累加器函数,则最后一次运行的处理程序返回的值将由g_signal_emit返回。

详细参数

        所有与信号发射或信号连接相关的函数都有一个名为detail的参数。有时,这个参数被API隐藏,但它总是以某种形式存在。​​​​​​​

        在三个主要连接函数中,只有一个具有显式的详细参数作为GQuark:g_signal_connect_closure_by_id。[6]

        另外两个函数,g_signal_connect_closureg_signal_connect_data,隐藏了信号名称标识中的细节参数。其detailed_signal参数是一个字符串,标识了要连接的信号的名称。这个字符串的格式应该匹配signal_name::detail_name。例如,连接到名为notify::cursor_position的信号实际上会连接到带有cursor_position详细信息的名为notify的信号。在内部,如果存在detail字符串,则将其转换为GQuark。

        在四个主要的信号发射函数中,有一个函数将信号隐藏在其信号名称参数g_signal_connect中。其他三个函数也有一个显式的g夸克参数:g_signal_emitg_signal_emitvg_signal_emit_valist

        如果用户向发射功能提供了详细信息,则在发射过程中会将其用于与也提供详细信息的闭包进行匹配。如果闭包的详细信息与用户提供的详细信息不匹配,则不会调用它(即使它连接到正在发出的信号)。

        这种完全可选的过滤机制主要用于优化信号,这些信号通常由于许多不同的原因而发出:客户端可以在闭包的编组代码运行之前过滤掉它们感兴趣的事件。例如,GObject 的通知信号广泛使用:每当在 G 对象上修改属性时,GObject 会将修改的属性名称作为详细信息关联到该信号,而不仅仅是发出通知信号。这允许希望仅收到对一个属性的更改的通知的客户端在接收大多数事件之前对其进行筛选。

        作为一个简单的规则,用户可以并且应该将filt参数设置为零:这将完全禁用该信号的可选过滤。


注6 GQuark是一个唯一代表一个字符串的整数。可以用函数g_quark_from_string 和 g_quark_to_string在该整数和字符串之间切换。

本文含有隐藏内容,请 开通VIP 后查看