类型系统概念

类型系统概念

简介

大多数现代编程语言都自带其本机对象系统和额外的基本算法语言结构。正如 GLib 是这种基本类型和算法(链表、哈希表等)的实现,GLib 对象系统为 C 提供了灵活、可扩展、有意简化(到其他语言)的对象导向框架所需的实现。提供的主要元素可以概括为:

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

背景

GObject 及其较低级别的类型系统 GType 被用于 GTK 和大多数 GNOME 库以提供

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

许多程序员习惯于使用仅编译或仅动态解释的语言,并且不明白跨语言互操作性相关联的挑战。本介绍试图使人们对这些挑战有一个深刻的了解,并简要描述 GLib 选择的解决方案。

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

数据类型和编程

可以说,一种编程语言仅仅是创建数据类型并操纵它们的方式。大多数语言提供了一些语言原生类型和一些原语,这些原语可以基于这些原语创建更复杂的数据类型。

在C语言中,提供了如char、long、pointer等类型。在C代码的编译过程中,编译器将这些语言类型映射到目标架构的机器类型。如果你使用C解释器(假设存在),解释器(负责解释源代码并执行它的程序)在程序执行时(或者如果它使用即时编译器引擎,则在执行之前)将语言类型映射到目标机器的机器类型。

Perl和Python是解释型语言,它们并没有提供类似于C的语言类型定义。Perl和Python程序员操作变量,变量的类型仅在其第一次赋值或第一次使用并被赋予类型时决定。解释器通常还会提供许多从一种类型到另一种类型的自动转换。例如,在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函数和由Linux计算机上的GCC生成的关联的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位整数压入堆栈,然后调用function_foo。正如你所看到的,C函数调用是由GCC作为本地函数调用实现的(这可能是可能的实现中速度最快的一种)。

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

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

上面描述的过程相当复杂,有非常多方法可以让这一过程对C和Python程序员完全自动且透明。

  • 第一种解决方案是手动编写大量的粘合代码,每个导出或导入的函数都需要编写一次。这些粘合代码执行Python到C的参数转换以及C到Python的返回值转换。然后,这些粘合代码与解释器链接,使得Python程序能够调用Python函数,并将工作委派给C函数。
  • 另一种更好的解决方案是使用一种特殊的编译器自动生成粘合代码,每种导出或导入的函数只需生成一次,该编译器读取原始函数签名。

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

使用GType实现的最大优点是位于运行域边界处的粘合代码只需编写一次:下图对此进行了更清晰的说明。

目前,存在多种通用粘合代码,这使得可以直接在众多语言中使用用GType编写的C对象,而无需大量工作:既不需要自动生成,也不需要手动生成大量粘合代码。

尽管这一目标可能是值得赞扬的,但其追求对整个GType/GObject库产生了重大影响。如果程序员忘记了GType/GObject库不仅旨在为C程序员提供类(Object)特性,还有跨语言的透明互操作性,那么他们可能会对下文章节中暴露的复杂特性感到困惑。

GLib 动态类型系统

在GLib类型系统中,类型比通常理解的对象类型更加通用。最好通过查看类型系统中注册新类型的结构和函数来解释。

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 中实现的C函数,您应该使用这些函数在程序的类型系统中注册新的GType。您可能永远不需要使用 g_type_register_fundamental(),但如果你想,最后一章将解释如何创建新的基本类型。

基本类型是顶级类型,不继承自其他任何类型,而其他非基本类型则来自其他类型。在初始化时,类型系统不仅初始化其内部数据结构,还注册了多个核心类型:其中一些是基本类型。其他类型则来自这些基本类型。

基本和非基本类型的定义如下

  • 类大小:GTypeInfoclass_size 字段。
  • 类初始化函数(C++构造函数):GTypeInfobase_initclass_init 字段。
  • 类析构函数(C++析构函数):GTypeInfobase_finalizeclass_finalize 字段。
  • 实例大小(C++中new的参数):GTypeInfoinstance_size 字段。
  • 实例化策略(C++ new运算符的类型):GTypeInfon_preallocs 字段。
  • 复制函数(C++复制运算符):GTypeInfovalue_table 字段。
  • 类型特征标志:GTypeFlags

基本类型也通过一组存储在 GTypeFundamentalInfo 中的 GTypeFundamentalFlags 定义。非基本类型此外还通过其父类型定义,父类型通过 g_type_register_static()g_type_register_dynamic() 中的 parent_type 参数传递。

复制函数

所有 GLib 类型(基本和非基本、分类和非分类、可实例化和不可实例化)之间最显著的共同点是通过单个 API 来复制/分配它们。

GValue 结构用作所有这些类型的抽象容器。其简单的 API(在 gobject/gvalue.h 中定义)可以用来调用类型注册期间注册的 value_table 函数:例如 g_value_copy() 将一个 GValue 的内容复制到另一个 GValue。这类似于 C++ 赋值,它调用 C++ 复制运算符来修改 C++/C 结构/类的默认位(bit)-by-bit 复制语义。

以下代码显示了如何复制 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));
}

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

用于指定这些赋值函数的值表在 GTypeValueTable 中进行了文档化。

有趣的是,由于这些 value_tables 对于非基本类型是从父类型继承的,因此在类型注册期间指定 value_table 的可能性非常小。

约定

当在头文件中创建新类型并将其导出时,用户应遵守一些约定。

  • 类型名称(包括对象名称)必须至少有三个字符长,以字母 a-z 或 A-Z 或 ‘_’ 开始。
  • 使用 object_method 模式命名函数:要在对象类型 file 的实例上调用名为 save 的方法,请调用 file_save
  • 使用前缀以避免与其他项目命名空间的冲突。如果您的库(或应用程序)的名称是 Viewer,请使用前缀 viewer_ 为所有您的函数名称。例如:viewer_file_save
  • 前缀应为一个词,即不应在第一个字母之后包含任何大写字母。例如,使用 Exampleprefix 而不是 ExamplePrefix
  • 这允许从骆驼式名称自动且无歧义地生成名称的划线-小写版本。请参阅 名称歧义
  • 如果传递给类型和内省工具的参数更多,可以支持多词前缀,但最好避免需要这样做。
  • 对象/类名称(如本例中的 File)可以包含多个词。例如,LocalFile
  • 创建一个名为 PREFIX_TYPE_OBJECT 的宏,该宏始终返回关联对象类型的 GType。对于 Viewer 命名空间中 File 类型的对象,使用:例如,VIEWER_TYPE_FILE。该宏通过名为 prefix_object_get_type 的函数实现;例如,viewer_file_get_type
  • 使用 G_DECLARE_FINAL_TYPEG_DECLARE_DERIVABLE_TYPE 定义您的对象的其他常规宏。
  • PREFIX_OBJECT (obj),它返回类型为 PrefixObject 的指针。此宏用于通过在所需位置强制显式转换来执行显式类型安全性。它还通过在运行时进行检查来强制动态类型安全性。可以在生产构建中禁用动态类型检查(请参阅 Building GLib)。例如,我们创建 VIEWER_FILE (obj) 以保持先前的示例。
  • PREFIX_OBJECT_CLASS (klass),与之前的铸造宏严格等价:它使用动态类型检查对类结构进行静态铸造。它预期返回一个指向类型为PrefixObjectClass的类结构的指针。例如:VIEWER_FILE_CLASS
  • PREFIX_IS_OBJECT (obj),返回一个布尔值,表示输入对象实例指针是否非NULL且类型为OBJECT。例如,VIEWER_IS_FILE
  • PREFIX_IS_OBJECT_CLASS (klass),如果输入类指针是指向类型为OBJECT的类,则返回布尔值。例如,VIEWER_IS_FILE_CLASS
  • PREFIX_OBJECT_GET_CLASS (obj),返回与给定类型实例关联的类指针。这个宏用于静态和动态类型安全目的(就像之前的铸造宏一样)。例如,VIEWER_FILE_GET_CLASS

这些宏的实现相当简单:在gtype.h中提供了许多简单易用的宏。对于上面的示例,我们会写出以下简单的代码来声明这些宏

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

除非你的代码有特殊要求,你可以使用G_DEFINE_TYPE宏来定义一个类

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;
}

名称混编

GObject工具,特别是反射,依赖于明确地在类型名称(例如camel-case,如GNetworkMonitorMyViewerFile)和函数前缀(小写带下划线,如g_network_monitormy_viewer_file)之间进行转换。然后可以使用这些前缀来前缀方法,如g_network_monitor_can_reach()my_viewer_file_get_type()

从camel-case到小写带下划线的转换算法

  1. 输出是输入的小写版本,包含零个或多个“split”。
  2. 在任何输入“split”的地方,在输出中插入一个下划线。
  3. 在以下位置分割输入
    • 在非大写字母的字符之后的每个字符(索引为非零)是大写字母
    • 如果第二个字符(索引一)是大写字母,且第一个字符(索引零)也是大写字母
    • 在非大写字母的字符之后的每个字符(索引大于二)是大写字母,且前两个字符也是大写字母

不可实例化的无类的基本类型

许多类型无法由类型系统实例化,并且没有类。其中大部分是基本的简单类型,如gchar,且已由GLib注册。

在罕见的情况下,需要将此类类型注册到类型系统时,由于这些类型也是大多数情况下基本类型,因此可以使用零填充GTypeInfo结构。

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方便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 结构作为第一个成员。这些 C 类型(来自 gtype.h)的声明如下

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 字段的首字段,它是指向 B 的类结构的指针。

благодаря этим простым условиям, можно detect the type of every object instance by doing

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

或者更简洁地

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

初始化和销毁

这些类型的实例化可以使用 g_type_create_instance() 来执行,该函数将查找与请求的类型关联的类型信息结构。然后,使用用户声明的实例大小和实例化策略(如果 n_preallocs 字段设置为非零值,类型系统将按块分配对象的实例结构,而不是为每个实例 malloc)来获取一个用于存储对象实例结构的缓冲区。

如果这是创建的对象的第一个实例,类型系统必须创建一个类结构。它分配一个缓冲区来存储对象的类结构并将其初始化。类结构的第一个部分(即:内嵌父类结构)通过从父类的类结构复制内容进行初始化。类结构的其余部分初始化为零。如果没有父类,整个类结构将初始化为零。然后,类型系统从最顶层的根本对象到最底层的导出对象调用 base_init 函数(GBaseInitFunc)。随后调用对象的 class_init 函数(GClassInitFunc)以完成类结构的初始化。最后,初始化对象的接口(我们将在稍后更详细地讨论接口初始化)。

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

对象实例的销毁通过 g_type_free_instance() 来完成非常简单:如果有一个实例池,实例结构将返回实例池;如果这是对象的最后一个存活实例,则将销毁类。

类销毁(在 GType 中,销毁的概念有时部分地被称为终结)是与初始化相对应的过程:首先销毁接口。然后,调用最派生的 class_finalize 函数(GClassFinalizeFunc)。最后,从最派生的类型到底层的根本类型调用 base_finalize 函数(GBaseFinalizeFunc),并释放类结构。

基本初始化/终止过程与C++的构造函数/析构函数模式非常相似。但实际细节有所不同,因此不要被表面的相似之处所迷惑。GTypes没有实例销毁机制。用户负责在现有的GType代码基础上实现正确的销毁语义。(这就是GObject所做的事情)此外,通常不需要与GType的base_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,多个类都将遵守该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()将给定的ViewerFile类型实现也记录在类型系统中(viewer_editable_get_type()返回了ViewerEditable的类型)。GInterfaceInfo结构包含了接口实现的信息

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

接口初始化

当创建了一个实现接口的类(无论是直接实现还是通过父类继承实现)的第一个实例时,其类结构将遵循“可实例化的类类型:对象”部分中所述的过程进行初始化。之后,与该类型关联的接口实现将被初始化。

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

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

因此,建议使用 default_init 函数来初始化接口。无论有多少实现,这个函数只会被调用一次。《code>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_finalizebase_finalize 都会在销毁接口的每个实现时精确调用一次。因此,如果您使用这些函数之一,您需要一个静态整型变量来保存接口实现实例的数量,以便接口类仅在被销毁一次时(当整型变量达到零)。

上述过程可以总结如下:

调用时间 调用的函数 函数的参数
为实现接口的类型调用 g_type_free_instance() 的最后调用 接口的 interface_finalize 函数 在接口的虚表中
接口的 base_finalize 函数 在接口的虚表中

GObject 基类

前一章讨论了GLib动态类型系统的细节。GObject库还包含一个名为GObject的基基本类型的实现。

GObject是一个可实例化的基本类。它实现了

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

所有使用GLib类型系统(如GTK和GStreamer)的GNOME库都继承自GObject,因此了解它是如何工作的细节很重要。

对象实例化

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

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

GObject明确保证所有类和实例成员(除了指向父类的字段)都将设置为零。

一旦完成所有构建操作并设置了构造器属性,就会调用构建类方法。

GObject继承的对象可以重写这个构建类方法。下面的例子演示了如何ViewerFile重写父类的构建过程

#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_class_init预计将重写对象的方法,并设置类自己的方法。在上面的例子中,构建方法是唯一重写的方法:它设置为viewer_file_constructed

一旦g_object_new()获取到初始化的类结构,它会通过其构造器方法创建新的对象实例,如果该构造器已在viewer_file_class_init中重写。重写的构造器必须回溯到父类的构造器。为了找到父类并回溯到父类构造器,我们可以使用由宏G_DEFINE_TYPE为我们设置的viewer_file_parent_class指针。

最后,在链中的最后一个构造器调用g_objectconstructor的时候,它会通过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() 对目标类型的每次调用 目标类型的类 constructor 方法: GObjectClass->constructor 在对象的实例上 如果您需要以自定义方式处理构建属性或实现单例类,则重写构造器方法并在进行自己的初始化前确保调用父类的初始化方法。如果有疑问,请不要重写构造器方法。
类型的 instance_init 函数 在从基本类型到目标类型的类继承树中。为每个类型提供 instance_init 在每个实例结构中只调用一次。 在设置构建属性之前,提供一个用于初始化对象的 instance_init 函数。这是初始化 GObject 实例的首选方法。此函数相当于 C++ 构造函数。
目标类型的类 constructed 方法: GObjectClass->constructed 在对象的实例上 如果您需要在工作完毕后执行对象初始化步骤。这是对象初始化过程中的最后一步,并且只有当 constructor 方法返回新的对象实例(而不是,例如,现有的单例)时才调用。

读者应该在函数调用顺序中的一个微小转折感到担忧:虽然技术上类的构造器方法是在调用 GTypeinstance_init 函数之前被调用(因为调用 instance_initg_type_create_instance() 是由 g_object_constructor 调用的,它是最顶级的类构造器方法,用户需要链式调用到它),但用户提供的代码将始终在 GType 的 instance_init 函数之后运行,因为用户提供的构造器(警告过您)必须在做任何有用的事情之前调用链式方法。

对象内存管理

GObject 的内存管理 API 比较复杂,但背后的理念很简单:目标是提供一个基于引用计数的灵活模型,该方法可以集成到使用或需要不同内存管理模型的应用程序中(例如垃圾回收)。以下描述了用于操作此引用计数的函数。

引用计数

g_object_ref()g_object_unref() 函数分别增加和减少引用计数。这些函数是线程安全的。g_clear_object() 是一个便利的前面做得包装 g_object_unref(),它还将指针清除。

引用计数在 g_object_new() 时初始化为 1,这意味着调用者是新创建的引用的唯一所有者。(如果对象是从 GInitiallyUnowned 派生的,则此引用是“浮动的”,必须“下潜”,即转换为一个真实引用。)当引用计数达到零时,即当最后一个所有者调用 g_object_unref() 的对象的引用时,将调用 dispose()finalize() 类方法。

最后,当调用完 finalize() 方法后,会调用 g_type_free_instance() 方法释放对象实例。根据在注册类型时确定的内存分配策略(通过某个 g_type_register_* 函数),对象的实例内存将释放或返回给该类型的对象池。一旦对象被释放,如果它是该类型的最后一个实例,则如“可实例化的类类型:对象”和“不可实例化的类类型:接口”章节中所述,类型的类将被销毁。

下表总结了 GObject 的销毁过程

调用时间 调用的函数 函数的参数 注意事项
对于目标类型的最后一个实例的最后调用 g_object_unref() 目标类型的析构类函数 GObject 实例 当析构结束时,对象不应保留对任何其他成员对象的引用。对象还应该能够在执行 finalize 之前响应用户的方法调用(可能带有错误代码但无内存违规)。析构可以多次执行。析构应在返回给调用者之前链到其父实现。
目标类型的 finalize 类函数 GObject 实例 finalize 预期会完成由 dispose 启动的销毁过程。它应完成对象的销毁。finalize 只执行一次。finalize 应在返回给调用者之前链到其父实现。有关更多信息,请参阅“引用计数和循环”章节。
对于目标类型的最后一个实例的最后调用 g_object_unref() 接口的 interface_finalize 函数 在接口的虚表中 在实际中从不使用。不太可能需要它。
接口的 base_finalize 函数 在接口的虚表中 在实际中从不使用。不太可能需要它。
目标类型的class_finalize函数 在目标类型的类结构上 在实际中从不使用。不太可能需要它。
类型的base_finalize函数 在从基本类型到目标类型的类继承树上。每个类结构都会调用一次base_init 在实际中从不使用。不太可能需要它。

弱引用

弱引用用于监视对象销毁:g_object_weak_ref() 添加了一个监视回调,它不持有对象引用,但在对象调用其析构方法时会被调用。在实例析构时,对象上的弱引用会自动释放,因此没有必要从 GWeakNotify 回调中调用 g_object_weak_unref()。请记住,对象实例不会传递给 GWeakNotify 回调,因为对象已经析构。相反,回调接收到对象之前位置处的指针。

弱引用也用于实现 g_object_add_weak_pointer()g_object_remove_weak_pointer()。这些函数在应用到的对象上添加一个弱引用,确保在对象最终化时将用户提供的指针置为空。

类似地,GWeakRef 可以用于在需要线程安全时实现弱引用。

引用计数和循环

GObject 的内存管理模型被设计为容易与现有代码集成,使用垃圾回收。这就是为什么销毁过程分为两个阶段:第一个阶段,在 dispose() 处理器中执行,旨在释放对其他成员对象的所有引用。第二个阶段,由 finalize() 处理器执行,旨在完成对象的销毁过程。在两阶段之间,对象方法应该能够在程序错误之间正常运行。

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

这解释了之前关于 dispose() 处理器的规则之一:dispose() 处理器可以被多次调用。假设我们有一个引用计数循环:对象 A 引用了 B,而 B 又引用了对象 A。假设我们已经检测到循环,我们想要销毁这两个对象。一种方法是向其中一个对象调用 g_object_run_dispose()

如果对象A释放了它对所有对象的引用,这意味着它也释放了对对象B的引用。如果对象B没有被其他人拥有,这是它的最后一条引用计数,这意味着最后的unref会运行B的dispose处理器,进而释放B对对象A的引用。如果这是A的最后一条引用计数,最后一次unref会运行A的dispose处理器,在调用A的finalize处理器之前,这将是第二次运行!

上述示例可能看起来有点牵强,但如果GObjects被语言绑定处理,这确实可能发生,因此应严格遵循对象销毁规则。

对象属性

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()首先确保使用这个名字在一个文件的class_init处理器中注册了属性。如果确实如此,它会遍历类层次结构,从最底层的最派生类型到最顶层的最基本类型,找到注册了该属性的类。然后它会尝试将用户提供的GValue转换为关联属性的GValue类型的GValue。

如果用户提供了如图所示的GValue signed char,如果对象的属性被注册为unsigned int类型,g_value_transform()会尝试将输入的signed char转换为unsigned int。当然,转换的成功取决于所需转换函数的存在。在实际情况下,几乎总会有一个匹配的转换,并且如果需要,会执行转换。

转换后,由g_param_value_validate()验证GValue,确保用户存储在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中检索出param_id,该id是由g_object_class_install_property()存储的。

一旦对象通过其set_property类方法设置了属性,执行会返回到g_object_set_property(),它会确保在对象的实例上以更改的属性为参数发出“notify”信号,除非由于g_object_freeze_notify()而冻结了通知。

可以使用g_object_thaw_notify()重新启用通过“notify”信号通知属性修改。重要的是要记住,即使通知冻结时属性被改变,通知一旦解除冻结,就会为每个更改的属性发出一次“notify”信号:不会因为冻结通知机制而丢失任何属性更改,尽管对单个属性的多个通知会被压缩。信号只能通过通知冻结机制来延迟。

每次修改属性时都要设置GValues似乎是一项乏味的工作。在实际操作中,很少有人这样做。函数g_object_set_property()g_object_get_property()是为语言绑定设计的。对于应用程序,有一个更简单的方法,将在下文中描述。

同时访问多个属性

值得注意的是,函数g_object_set()g_object_set_valist()(可变参数版本)可以一次设置多个属性。上面的客户端代码可以重写为

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

这使我们免去了使用g_object_set_property()时需要处理的GValues。上面的代码将针对每个修改的属性触发一个通知信号。

也提供等效的_get版本:g_object_get()g_object_get_valist()(可变参数版本)可以一次获取多个属性。

这些高级函数有一个缺点——它们不提供返回值。在使用时,应关注参数类型和范围。一个明显的错误来源是传递与属性期望类型不同的数据;例如,当属性期望浮点数时传递整型,从而导致后续参数偏移一定数量的字节。忘记终止的NULL也会导致不确定的行为。

这解释了g_object_new()g_object_newv()g_object_new_valist()是如何工作的:它们解析用户提供的可变数量参数,在对象成功构造后,只对参数调用g_object_set()。对于每个设置的属性,都会发射“通知”信号。

The GObject messaging system

Closures

Closures是异步信号发送概念的核心,这个概念被广泛用于GTK和GNOME应用程序中。闭包是一种抽象,是回调的一般表示。它是一个包含三个对象的小结构:

  • 一个函数指针(回调本身),其原型如下:
  • 传递给回调的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_notifier()g_closure_remove_invalidate_notifier()),但没有用于调用过程的事件注销函数

CClosures

如果您使用C或C++将回调连接到特定事件,您将使用简单的GCClosures,它们具有相当简单的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闭包编组器将表示目标函数参数的GValues数组转换为C风格的函数参数列表,使用此新参数列表调用用户提供的C函数,获取函数的返回值,将其转换为GValue并将其返回给编组器调用者。

使用的通用C闭包编组器是g_cclosure_marshal_generic(),该编组器使用libffi实现所有函数类型。除了性能关键代码之外,通常不需要为不同类型定制编组器,因为在libffi-based编组器可能太慢的情况下。

以下是一个自定义编组器的示例,展示了如何将GValues转换为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_valist()g_signal_new()中的任何一个函数

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() 系列函数来注册一个闭包。这是一个实例特定的操作:闭包仅在使用某个实例的消息发射时调用

也可以在某个信号上连接不同类型的回调:发射挂钩在信号发射时被调用,无论发射是在哪个实例上。发射挂钩通过 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);
  • instance_and_params 数组包含信号的输入参数清单。数组的第一个元素是在其上调用信号的实例指针。数组中的后续元素包含信号的参数清单。
  • signal_id 识别要调用的信号
  • detail 标识要调用的信号的特定细节。细节是一种魔法令牌/参数,它在一开始发射信号时被传递,供连接到该信号的作用域使用,用于过滤掉不需要的信号发射。在大多数情况下,您可以安全地将此值设置为0。有关此参数的更多信息,请参阅“细节参数”部分。
  • 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_FIRST状态重新开始。

累加函数在所有状态下都会被调用,在调用每个闭包之后(不包括在RUN_EMISSION_HOOKRUN_CLEANUP状态下)。它将闭包的返回值累加到信号返回值中,并返回TRUEFALSE。如果在任何时刻它不返回TRUE,则发射跳转到RUN_CLEANUP状态。

如果没有提供累加函数,则g_signal_emit()会返回最后一个处理器运行的值。

细节参数

与信号发射或信号连接相关的所有函数都有一个名为细节的参数。有时,这个参数可能被API隐藏,但它始终以某种形式存在。

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

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

在四个主要的信号发射函数中,一个将它在信号名称参数中隐藏起来:g_signal_emit_by_name()。另外三个函数有一个显式 detail 参数,再次是 GQuark:g_signal_emit()g_signal_emitv()g_signal_emit_valist()

如果用户向发射函数提供了 detail,则它会在发射期间使用,以匹配同样提供 detail 的闭包。如果闭包的 detail 与用户提供的 detail 不匹配,它将不会调用(即使它连接到正在发射的信号)。

这个完全可选的过滤机制主要用于优化经常因多种原因而发射的信号:客户可以在闭包的打包代码运行之前过滤掉他们感兴趣的哪些事件。例如,这被 GObject 的 notify 信号大量使用:每当修改 GObject 的属性时,GObject 不仅发射.notify 信号,还关联属性名称作为 detail 到这个信号发射。这允许希望只通知单一属性变更的客户在接收到事件之前过滤掉大多数事件。

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