GObject Introspection(简称 GI)用于产生与解析 C 程序库 API 元信息,以便于动态语言(或托管语言)绑定基于 C + GObject 的程序库。
为了正确的生成 C 库的 API 元信息,GI 要求 C 库的实现者要么使用一种特定的代码注释规范,即 Gtk-Doc [1],要么使用带有类型自省功能的 GObject 库。
首先来看如何基于 Gtk-Doc 注释产生 API 元信息,见下面的示例:
#ifndef FOO_H #define FOO_H void foo_hello (void); #endif
#include <stdio.h> #include "foo.h" /** * foo_hello: * * This function just for test. */ void foo_hello (void) { printf ("hello foo!\n"); }
使用 gcc 编译上述 C 代码,生成一个共享库:
$ gcc -fPIC -shared foo.c -o libfoo.so
然后使用 g-ir-scanner 产生 libfoo.so 库的 API 元信息:
$ g-ir-scanner --namespace=Foo --nsversion=1.0 --library=foo foo.h foo.c -o Foo-1.0.gir
所生成的 Foo-1.0.gir 文件是 XML 格式,其中记录了 libfoo.so 库的 API 元信息,如下:
<?xml version="1.0"?> <!-- This file was automatically generated from C sources - DO NOT EDIT! To affect the contents of this file, edit the original C definitions, and/or use gtk-doc annotations. --> <repository version="1.2" xmlns="http://www.gtk.org/introspection/core/1.0" xmlns:c="http://www.gtk.org/introspection/c/1.0" xmlns:glib="http://www.gtk.org/introspection/glib/1.0"> <namespace name="Foo" version="1.0" shared-library="libfoo.so" c:identifier-prefixes="Foo" c:symbol-prefixes="foo"> <function name="hello" c:identifier="foo_hello"> <doc xml:whitespace="preserve">This function just for test.</doc> <return-value transfer-ownership="none"> <type name="none" c:type="void"/> </return-value> </function> </namespace> </repository>
认真观察 Foo-1.0.gir 文件内容,可以发现其中有一部分信息是我们在运行 g-ir-scanner 命令时提供的,还有一部分信息来源于 foo.c 源文件中的代码注释。
Foo-1.0.gir 文件的作用就是以一种固定的格式描述 libfoo.so 库的 API 元信息,我们可以将这种文件称为 GIR 文件。有了 GIR 文件,就可以不用再通过库的头文件来获取所需的函数符号了。
在实际使用中,加载并解析 Foo-1.0.gir 这样的 XML 文件,效率较低,因此 GObject Introspection 定义了一种二进制格式,即 Typelib 格式,并提供 g-ir-compiler 工具将 GIR 文件转化为二进制格式,例如:
$ g-ir-compiler Foo-1.0.gir -o Foo-1.0.typelib
一旦有了 Typelib 格式的文件,我们就可以通过它来调用它所关联的程序库的 API,例如:
#include <girepository.h> int main (void) { GIRepository *repository; GError *error = NULL; GIBaseInfo *base_info; GIArgument retval; g_type_init(); g_irepository_prepend_search_path ("./"); repository = g_irepository_get_default (); g_irepository_require (repository, "Foo", "1.0", 0, &error); if (error) { g_error ("ERROR: %s\n", error->message); return 1; } base_info = g_irepository_find_by_name (repository, "Foo", "hello"); if (!base_info) { g_error ("ERROR: %s\n", "Could not find Foo.hello"); return 1; } if (!g_function_info_invoke ((GIFunctionInfo *)base_info, NULL, 0, NULL, 0, &retval, &error)) { g_error ("ERROR: %s\n", error->message); return 1; } g_base_info_unref (base_info); return 0; }
上述代码所完成的主要工作如下:
g_irepository_prepend_search_path
函数将当前目录添加到 Typelib 文件搜索目录列表;g_irepository_get_default
函数获取默认的 GIRepository 实例;g_irepository_require
函数可载入指定的 Typelib 文件;g_irepository_find_by_name
函数可从给定的 GIRepository 实例中根据指定的命名空间与 API 名称获取 API 的元信息,将其存入一个 GIBaseInfo
实例并返回;g_function_info_invoke
函数可以执行 GIBaseInfo
实例中所记载的 API 元信息对应的 API,本例即 foo_hello
函数;g_base_info_unref
函数用于释放一个 GIBaseInfo
实例的引用。编译上述的 test.c 文件,可使用以下命令:
$ gcc `pkg-config --cflags --libs gobject-introspection-1.0` test.c -o test
运行所生成的程序,它便会正确的调用 libfoo.so 库中定义的 foo_hello
函数。
从上面的 libfoo.so 与 Typelib 文件的生成及其应用可以看出,GI 可以根据 C 库的实现者提供的代码注释产生 API 元信息,库的使用者则可以通过 GI 与 API 元信息去调用相应的 API。这样做有什么好处?
试想,假如许多 C 库都采用同一种代码注释规范,那么 g-ir-scanner 就可以生成它们的 API 元信息文件。如果我们使用动态语言(托管语言)对这些 C 库进行绑定时,就不需要对这些 C 库逐一实现绑定,只需对 GI 库进行绑定,通过它来解析 Typelib 文件,从而直接调用这些 C 库 API。也就是说,GI 是让 C 库开发者多做了一点工作,从而显著降低了 C 库绑定的工作量。事实上,GI 并没有让 C 库开发者多做什么工作,因为对 API 进行详细的注释是每个 C 库开发者都要去做的工作,GI 只是要求 C 库开发者使用 Gtk-Doc 格式的注释而已。
使用 Gtk-Doc 格式的注释可以帮助 g-ir-scanner 生成详细且正确的 API 元信息,这是 GI 提供的一种约定。但是如果程序库是基于 GObject 实现的,由于 GObject 类型系统具有自省功能,而 GI 可以利用这一功能在不借助 Gtk-Doc 注释的情况下自动生成『类』(即面向对象编程中的类的概念)的元信息。例如下面的 Bibtex 类 [2](命名空间为 Kb
):
#ifndef KB_BIBTEX_H #define KB_BIBTEX_H #include <glib-object.h> #define KB_TYPE_BIBTEX (kb_bibtex_get_type ()) #define KB_BIBTEX(object) \ G_TYPE_CHECK_INSTANCE_CAST ((object), KB_TYPE_BIBTEX, KbBibtex) #define KB_IS_BIBTEX(object) \ G_TYPE_CHECK_INSTANCE_TYPE ((object), KB_TYPE_BIBTEX)) #define KB_BIBTEX_CLASS(klass) \ (G_TYPE_CHECK_CLASS_CAST ((klass), KB_TYPE_BIBTEX, KbBibtexClass)) #define KB_IS_BIBTEX_CLASS(klass) \ (G_TYPE_CHECK_CLASS_TYPE ((klass), KB_TYPE_BIBTEX)) #define KB_BIBTEX_GET_CLASS(object) \ (G_TYPE_INSTANCE_GET_CLASS ((object), KB_TYPE_BIBTEX, KbBibtexClass)) typedef struct _KbBibtex KbBibtex; struct _KbBibtex { GObject parent; }; typedef struct _KbBibtexClass KbBibtexClass; struct _KbBibtexClass { GObjectClass parent_class; }; GType kb_bibtex_get_type (void); void kb_bibtex_printf (KbBibtex *self); #endif
#include "kb-bibtex.h" G_DEFINE_TYPE (KbBibtex, kb_bibtex, G_TYPE_OBJECT); #define KB_BIBTEX_GET_PRIVATE(object) (\ G_TYPE_INSTANCE_GET_PRIVATE ((object), KB_TYPE_BIBTEX, KbBibtexPrivate)) typedef struct _KbBibtexPrivate KbBibtexPrivate; struct _KbBibtexPrivate { GString *title; GString *author; GString *publisher; guint year; }; enum PROPERTY_BIBTEX { PROPERTY_0, PROPERTY_TITLE, PROPERTY_AUTHOR, PROPERTY_PUBLISHER, PROPERTY_YEAR, N_PROPERTIES }; static void kb_bibtex_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { KbBibtex *self = KB_BIBTEX (object); KbBibtexPrivate *priv = KB_BIBTEX_GET_PRIVATE (self); switch (property_id) { case PROPERTY_TITLE: if (priv->title) g_string_free (priv->title, TRUE); priv->title = g_string_new (g_value_get_string (value)); break; case PROPERTY_AUTHOR: if (priv->author) g_string_free (priv->author, TRUE); priv->author = g_string_new (g_value_get_string (value)); break; case PROPERTY_PUBLISHER: if (priv->publisher) g_string_free (priv->publisher, TRUE); priv->publisher = g_string_new (g_value_get_string (value)); break; case PROPERTY_YEAR: priv->year = g_value_get_uint (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void kb_bibtex_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { KbBibtex *self = KB_BIBTEX (object); KbBibtexPrivate *priv = KB_BIBTEX_GET_PRIVATE (self); GString *similar = NULL; switch (property_id) { case PROPERTY_TITLE: g_value_set_string (value, priv->title->str); break; case PROPERTY_AUTHOR: g_value_set_string (value, priv->author->str); break; case PROPERTY_PUBLISHER: g_value_set_string (value, priv->publisher->str); break; case PROPERTY_YEAR: g_value_set_uint (value, priv->year); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void kb_bibtex_init (KbBibtex *self) { } static void kb_bibtex_class_init (KbBibtexClass *klass) { g_type_class_add_private (klass, sizeof (KbBibtexPrivate)); GObjectClass *base_class = G_OBJECT_CLASS (klass); base_class->set_property = kb_bibtex_set_property; base_class->get_property = kb_bibtex_get_property; GParamSpec *properties[N_PROPERTIES] = {NULL,}; properties[PROPERTY_TITLE] = g_param_spec_string ("title", "Title", "Bibliography title", NULL, G_PARAM_READWRITE); properties[PROPERTY_AUTHOR] = g_param_spec_string ("author", "Author", "Bibliography author", NULL, G_PARAM_READWRITE); properties[PROPERTY_PUBLISHER] = g_param_spec_string ("publisher", "Publisher", "Bibliography Publisher", NULL, G_PARAM_READWRITE); properties[PROPERTY_YEAR] = g_param_spec_uint ("year", "Year", "Bibliography year", 0, G_MAXUINT, 0, G_PARAM_READWRITE); g_object_class_install_properties (base_class, N_PROPERTIES, properties); } void kb_bibtex_printf (KbBibtex *self) { gchar *title, *author, *publisher; guint year; g_object_get (G_OBJECT (self), "title", &title, "author", &author, "publisher", &publisher, "year", &year, NULL); g_printf (" Title: %s\n" " Author: %s\n" "Publisher: %s\n" " Year: %d\n", title, author, publisher, year); g_free (title); g_free (author); g_free (publisher); }
使用下面命令生成共享库 libKbBibtex.so:
$ gcc -fPIC -shared `pkg-config --cflags --libs gobject-2.0` \ KbBibtex.c -o libKb.so
所生成的 GIR 文件内容如下:
<?xml version="1.0"?> <!-- This file was automatically generated from C sources - DO NOT EDIT! To affect the contents of this file, edit the original C definitions, and/or use gtk-doc annotations. --> <repository version="1.2" xmlns="http://www.gtk.org/introspection/core/1.0" xmlns:c="http://www.gtk.org/introspection/c/1.0" xmlns:glib="http://www.gtk.org/introspection/glib/1.0"> <include name="GLib" version="2.0"/> <include name="GObject" version="2.0"/> <package name="gobject-2.0"/> <namespace name="Kb" version="1.0" shared-library="libKbBibtex.so" c:identifier-prefixes="Kb" c:symbol-prefixes="kb"> <class name="Bibtex" c:symbol-prefix="bibtex" c:type="KbBibtex" parent="GObject.Object" glib:type-name="KbBibtex" glib:get-type="kb_bibtex_get_type" glib:type-struct="BibtexClass"> <method name="printf" c:identifier="kb_bibtex_printf"> <return-value transfer-ownership="none"> <type name="none" c:type="void"/> </return-value> </method> <property name="author" writable="1" transfer-ownership="none"> <type name="utf8"/> </property> <property name="publisher" writable="1" transfer-ownership="none"> <type name="utf8"/> </property> <property name="title" writable="1" transfer-ownership="none"> <type name="utf8"/> </property> <property name="year" writable="1" transfer-ownership="none"> <type name="guint"/> </property> <field name="parent"> <type name="GObject.Object" c:type="GObject"/> </field> </class> <record name="BibtexClass" c:type="KbBibtexClass" glib:is-gtype-struct-for="Bibtex"> <field name="parent_class"> <type name="GObject.ObjectClass" c:type="GObjectClass"/> </field> </record> </namespace> </repository>
所生成的 GIR 文件中,详细的记录了 Bibtex 类的属性、方法、父类等元信息。对于支持面向对象的动态语言(托管语言)而言,可以根据类的元信息动态生成类,例如 Python 的元类编程 [3] 便支持这种方式。
目前 Python, Ruby, Lua, JavaScript 等动态语言均已实现了 GI 的绑定,可以调用所有支持 GI 的 C 库。
下面来看如何使用 JavaScript 使用上文 libKb.so 库中的类。
首先使用 g-ir-compiler 将 kb-1.0.gir 文件转化为 Typelib 格式,并将其复制到 /usr/lib/girepository-1.0/
目录:
$ g-ir-compiler Kb-1.0.gir -o Kb-1.0.typelib $ sudo cp Kb-1.0.typelib /usr/lib/girepository-1.0
为了方便测试,也可将 libKb.so 复制到 /usr/lib
目录,或者将下面的 JavaScript 文件置于 libKb.so 所在目录。
const Kb = imports.gi.Kb; function test () { let bibtex = new Kb.Bibtex ({title:"The {\\TeX}Book", author:"Knuth, D. E.", publisher:"Addison-Wesley Professional", year:1984}); bibtex.printf (); } test ();
当我们使用 gjs 运行 test.js 时,便会调用 libKb.so 中定义的 Bibtex
类的 printf
方法。
$ gjs test.js Title: The {\TeX}Book Author: Knuth, D. E. Publisher: Addison-Wesley Professional Year: 1984
从上面的示例可以看到,虽然我们没有为 libKb.so 进行 JavaScript 绑定,但是后者的确可以使用前者定义的类。如果你熟悉 Python, Ruby, Lua 等语言的话,也可以像 JavaScript 那样调用 libKb.so 的功能。
GI 就像是一座桥梁,沟通着 C 的世界与动态语言的世界。在 GNOME 项目的推动下,越来越多的 C + GObject 库支持 GI,这意味着动态语言的资源也越来越丰富。在 GI 的技术框架中,用 C + GOBject 实现项目的核心功能,用各种各样的动态语言来构建上层逻辑,是非常容易的事情。
静态语言没有『元类编程』的功能,所以它是不可能像动态语言那样根据 API 或类的元信息『在线』生成 API 或类的『绑定』。这样看上去,GI 对静态语言似乎意义不大。但是考虑到静态语言可以根据 GI 提供的 C 库的元信息自动生成“离线”的绑定代码,然后将这些代码编译为可用的模块。这样做虽然没有动态语言那般优雅,但是依然显著降低了 C 库绑定的工作量,因为这个过程是完全可以由一个程序自动完成。例如 Haskell 对 GI 的绑定项目——haskell-gi [4],便是利用 GI 产生的元信息自动生成 GLib, Gtk+ 等程序库的绑定代码。