GObject — часть библиотеки GLib, реализующая объекто-ориентированнные расширения для чистого Си. Подобная концепция, помимо самой GLib, используется в таких проектах, как GStreamer, GSettings, ATK, Pango и весь проект GNOME в целом, а также в большом количестве прикладных приложений: GIMP, Inkscape, Geany, Gedit и многих других. Большое количество языков программирования, начиная от таких мейнстримовых, как Python и Java, и заканчивая изысками вроде Haskell или D, имеют привязки к GLib/GTK+, а для значительного количества языков биндинги к GTK+ вообще является единственным способом построения GUI.
В отличие от других схожих проектов, GObject отличают архитектурные особенности, целью которых является лёгкая и прозрачная реализация привязок библиотек, написанных с применением чистого Си и GObject, к другим языкам программирования, в том числе с динамической типизацией и управлением памятью при помощи сборщика мусора. Именно этим объясняется некоторое ощущение переусложнённости, которое может возникнуть у программиста, приступившего к знакомству с GObject API. Тем не менее, эта система очень продуманная и логичная, так что проблем с пониманием всего изложенного ниже у программиста, знакомого с C++ или Java, возникнуть не должно.
Данная статья иллюстрирует самые основы работы с объектной системой типов GLib.
![image](http://illustrators.ru/uploads/illustration/image/816802/main_816802_original.jpg)
GObject: основы
GObject: наследование и интерфейсы
GObject: инкапсуляция, инстанциация, интроспекция
На самом базовом уровне системы типов GObject лежит система GType, реализующая доступное в run-time описание всех возможных типов, которыми оперирует программист. Эта система выступает своеобразным «клеем», связывающим код на Си с кодом на других языках, имеющих привязки к библиотекам, построенным на использовании динамической системы типов GLib.
Система типов GObject поддерживает одиночное наследование (а с учётом концепции Java-подобных интерфейсов, и более сложные концептуальные решения), инкапсуляцию, виртуальные функции, так называемые свойства, схожие с полями С++, но обладающие при этом большим количеством дополнительных возможностей, а также систему сигналов, позволяющих создавать эффективные и развитые конструкции в рамках событийно-ориентированной парадигмы.
Система типов GObject состоит из трёх основных сущностей — фундаментальных неистанцируемых типов вроде gchar (аналога char из чистого Си), gpointer (аналога указателя типа void), gboolean и т. п.; основная цель переопределения этих типов — унификация и переносимость; инстанцируемых классифицируемых типов, в общих чертах подобных классам C++; и неинстанцируемых классифицируемых типов — интерфейсов, подобных интерфейсам Java или чисто абстрактным классам C++.
Попробуем написать простой пример, демонстрирующий работу библиотеки GObject. Для начала познакомимся с основными соглашениями, принятыми в среде разработчиков, использующих GLib. Название любого объекта имеет родовое название — эдакий namespace, и видовое. Например, все объекты, присутствующие в библиотеке GLib, имеют префикс «G», объекты GTK+ — префикс «Gtk». Названия объектов пишутся в «верблюжей» нотации, а названия функций-методов, относящихся к этим объектам, в «змеинной». Макросы традиционно пишутся в uppercase, слова разделяются подчёркиваниями. Например, AnimalCat — это объект Cat, относящийся к «пространству имён» Animal, а его «методы» будут иметь вид вроде animal_cat_say_meow().
Создадим два файла, описывающих наш новый котообъект — animalcat.c и animalcat.h. Добавим в заголовочный файл защиту от повторного включения и подключим библиотеку GObject:
Добавим два традиционных макроса, которые используются в GLib для совместимости с компиляторами C++ и закроем защиту от повторного включения:
Добавим два самых главных определения, которые понадобятся в нашем заголовочном файле. Первый макрос имеет вид:
то есть в нашем конкретном случае:
Эта функция возвращает структуру GType, содержащую всю основную информацию о типе — название, объём памяти, требуемый объектом, ссылки на функции инициализации и финализации класса и объекта (аналоги конструкторов и деструкторов C++), и т. д.
Второй макрос в общем виде выглядит так:
а в нашем случае:
Это макроопредение раскладывается в целый набор важных макросов, производящих преобразование типа, проверку на принадлежность к конкретному типу и т. п. В последнем аргументе родительским классом объявлен GObject, от которого наследуются все объекты системы типов GObject.
По сути дела, любой GObject или объект, унаследованный от него, состоит из двух структур: _NamespaceObject и _NamespaceObjectClass, в нашем случае:
Условимся называть их собственно объектом и классом. В актуальных версиях GObject в общем случае нет необходимости реализовывать структуру объекта в явном виде, зачастую она генерируется автоматически в результате раскрытия макроса. Класс необходимо реализовывать явно, если нашей целью является построение иерархии наследования с переопределением виртуальных функций.
GObject бывают двух типов, отличающихся возможностью наследования — derivable и final. Во втором случае последний макрос выглядел бы как G_DECLARE_FINAL_TYPE, а в структуру _NamespaceObject пришлось бы объявлять явным образом в .c-файле. В случае derivable-объекта объявлять эту структуру не надо, она сгенерируется автоматически при раскрытии макроса.
Опишем структуру _AnimalCatClass. Эта структура существует в единственном экземпляре, создаётся она при создании первой инстанции нашего объекта, а уничтожается после уничтожения последней инстанции. В качестве первого поля она должна содержать аналогичную структуру родительского класса, в нашем случае GObject, так как мы наследуемся напрямую от него. После этого идут другие поля, главным образом указатели на функции, реализующие функционал виртуальных методов, а также поля, подобные static-полям классов C++.
Для примера, класс _AnimalCatClass может выглядеть так:
Дополним заголовочный файл объявлениями конкретных методов. Объекты, унаследованные от GObject, создаются кодом, который в самом простом виде выглядит следующим образом:
Подобный код принято оборачивать в функции вида:
Завершим заголовочный файл объявлением конкретного метода:
Как мы можем видеть, в отличие от языков вроде C++, в данном случае нам необходимо явно передавать указатель на конкретную инстанцию.
Итак, заголовочный файл теперь выглядит следующим образом:
Перейдём к файлу с собственно исходным кодом. Подключим наш заголовочный файл, а также stdio.h, необходимый для мяуканья нашего образца.
Настало время для ещё одного важного макроса, который на этот раз имеет следующий вид:
Последний аргумент, который мы передаём в макрос, подобен тому, который мы определяли в начале нашего заголовочного файла, актуальный для родительского объекта. Для нашего котообъекта он должен выглядеть как ANIMAL_TYPE_CAT, но в данном случае мы используем подобный макрос нашего родительского объекта: G_TYPE_OBJECT. Применительно к нашей ситуации, эта строчка должна выглядеть так:
Помните, чуть ранее мы говорили о разнице между derivable и final-gobject? Если бы мы определяли final-объект, не подразумевающий возможность наследования от него, дальше мы должны были бы определить структуру
которая должна содержать по крайней мере одно поле (и непременно первое) — аналогичную структуру родительского объекта. Такого рода обязательные требования связаны с тем, что адрес указателя на структуру родительского объекта должен быть идентичен адресам указателей на структуры всех унаследованных от него объекта.
В нашем случае структура выглядела бы как минимум так:
Ещё раз напомню — определять явно данную структуру необходимо только в случае использования GObject типа final, а мы в нашем примере это определение опускаем.
Приступим непосредственно к исполняемому коду.
Определим функцию animal_cat_say_meow(), чтобы продемонстрировать, как в GObject работает механизм виртуальных функций (чуть далее вы увидите, зачем это нужно).
Также определим тот реально вызываемый метод, который был объявлен в заголовочном файле:
Следующие две важные функции вызываются автоматически — первая при создании первой инстанции данного объекта, вторая — при создании любой конкретной инстанции, таким образом, являясь в определённом смысле аналогом конструктора C++.
Как вы видите, мы переопределили «виртуальную» функцию, определённую в структуре-классе нашего объекта.
Что происходит в коде, описанном выше? В функции animal_cat_class_init(), при инициализации классовой структуры нашего объекта, ссылке на функцию say_meow присваивается адрес функции animal_cat_real_say_meow(), после чего, при внешнем вызове функции animal_cat_say_meow() при помощи макроса ANIMAL_CAT_GET_CLASS мы получаем указатель на классовую структуру нашего объекта и вызываем функцию, адрес которой в данный момент присвоен полю say_meow в структуре AnimalCatClass. В объектах-наследниках мы можем переопределить это поведение в соответствующей функции, чьё название заканчивается на class_init.
Приступим к «конструктору»:
Сделаем функцию-обёртку, которая будет возвращать указатель на новую инстанцию объекта AnimalCat:
Пишем простую функцию main() для проверки работоспособности нашего кода:
и Makefile:
Собираем и запускаем:
В отличие от других схожих проектов, GObject отличают архитектурные особенности, целью которых является лёгкая и прозрачная реализация привязок библиотек, написанных с применением чистого Си и GObject, к другим языкам программирования, в том числе с динамической типизацией и управлением памятью при помощи сборщика мусора. Именно этим объясняется некоторое ощущение переусложнённости, которое может возникнуть у программиста, приступившего к знакомству с GObject API. Тем не менее, эта система очень продуманная и логичная, так что проблем с пониманием всего изложенного ниже у программиста, знакомого с C++ или Java, возникнуть не должно.
Данная статья иллюстрирует самые основы работы с объектной системой типов GLib.
![image](http://illustrators.ru/uploads/illustration/image/816802/main_816802_original.jpg)
Весь цикл о GObject:
GObject: основы
GObject: наследование и интерфейсы
GObject: инкапсуляция, инстанциация, интроспекция
На самом базовом уровне системы типов GObject лежит система GType, реализующая доступное в run-time описание всех возможных типов, которыми оперирует программист. Эта система выступает своеобразным «клеем», связывающим код на Си с кодом на других языках, имеющих привязки к библиотекам, построенным на использовании динамической системы типов GLib.
Система типов GObject поддерживает одиночное наследование (а с учётом концепции Java-подобных интерфейсов, и более сложные концептуальные решения), инкапсуляцию, виртуальные функции, так называемые свойства, схожие с полями С++, но обладающие при этом большим количеством дополнительных возможностей, а также систему сигналов, позволяющих создавать эффективные и развитые конструкции в рамках событийно-ориентированной парадигмы.
Система типов GObject состоит из трёх основных сущностей — фундаментальных неистанцируемых типов вроде gchar (аналога char из чистого Си), gpointer (аналога указателя типа void), gboolean и т. п.; основная цель переопределения этих типов — унификация и переносимость; инстанцируемых классифицируемых типов, в общих чертах подобных классам C++; и неинстанцируемых классифицируемых типов — интерфейсов, подобных интерфейсам Java или чисто абстрактным классам C++.
Попробуем написать простой пример, демонстрирующий работу библиотеки GObject. Для начала познакомимся с основными соглашениями, принятыми в среде разработчиков, использующих GLib. Название любого объекта имеет родовое название — эдакий namespace, и видовое. Например, все объекты, присутствующие в библиотеке GLib, имеют префикс «G», объекты GTK+ — префикс «Gtk». Названия объектов пишутся в «верблюжей» нотации, а названия функций-методов, относящихся к этим объектам, в «змеинной». Макросы традиционно пишутся в uppercase, слова разделяются подчёркиваниями. Например, AnimalCat — это объект Cat, относящийся к «пространству имён» Animal, а его «методы» будут иметь вид вроде animal_cat_say_meow().
Создадим два файла, описывающих наш новый котообъект — animalcat.c и animalcat.h. Добавим в заголовочный файл защиту от повторного включения и подключим библиотеку GObject:
#ifndef _ANIMAL_CAT_H_
#define _ANIMAL_CAT_H_
#include <glib-object.h>
Добавим два традиционных макроса, которые используются в GLib для совместимости с компиляторами C++ и закроем защиту от повторного включения:
G_BEGIN_DECLS
/* все наши дальнейшие определения будут тут */
G_END_DECLS
#endif /* _ANIMAL_CAT_H_ */
Добавим два самых главных определения, которые понадобятся в нашем заголовочном файле. Первый макрос имеет вид:
#define OURNAMESPACE_TYPE_OUROBJECT ournamespace_ourobject_get_type()
то есть в нашем конкретном случае:
#define ANIMAL_TYPE_CAT animal_cat_get_type()
Эта функция возвращает структуру GType, содержащую всю основную информацию о типе — название, объём памяти, требуемый объектом, ссылки на функции инициализации и финализации класса и объекта (аналоги конструкторов и деструкторов C++), и т. д.
Второй макрос в общем виде выглядит так:
G_DECARE_DERIVABLE_TYPE (NamespaceObject, namespace_object, NAMESPACE, OBJECT, ParentClass)
а в нашем случае:
G_DECLARE_DERIVABLE_TYPE (AnimalCat, animal_cat, ANIMAL, CAT, GObject)
Это макроопредение раскладывается в целый набор важных макросов, производящих преобразование типа, проверку на принадлежность к конкретному типу и т. п. В последнем аргументе родительским классом объявлен GObject, от которого наследуются все объекты системы типов GObject.
По сути дела, любой GObject или объект, унаследованный от него, состоит из двух структур: _NamespaceObject и _NamespaceObjectClass, в нашем случае:
struct _AnimalCat
struct _AnimalCatClass
Условимся называть их собственно объектом и классом. В актуальных версиях GObject в общем случае нет необходимости реализовывать структуру объекта в явном виде, зачастую она генерируется автоматически в результате раскрытия макроса. Класс необходимо реализовывать явно, если нашей целью является построение иерархии наследования с переопределением виртуальных функций.
GObject бывают двух типов, отличающихся возможностью наследования — derivable и final. Во втором случае последний макрос выглядел бы как G_DECLARE_FINAL_TYPE, а в структуру _NamespaceObject пришлось бы объявлять явным образом в .c-файле. В случае derivable-объекта объявлять эту структуру не надо, она сгенерируется автоматически при раскрытии макроса.
Опишем структуру _AnimalCatClass. Эта структура существует в единственном экземпляре, создаётся она при создании первой инстанции нашего объекта, а уничтожается после уничтожения последней инстанции. В качестве первого поля она должна содержать аналогичную структуру родительского класса, в нашем случае GObject, так как мы наследуемся напрямую от него. После этого идут другие поля, главным образом указатели на функции, реализующие функционал виртуальных методов, а также поля, подобные static-полям классов C++.
Для примера, класс _AnimalCatClass может выглядеть так:
struct _AnimalCatClass
{
GObjectClass parent_class;
void (*say_meow)(AnimalCat*);
};
Дополним заголовочный файл объявлениями конкретных методов. Объекты, унаследованные от GObject, создаются кодом, который в самом простом виде выглядит следующим образом:
g_object_new(NAMESPACE_TYPE_OBJECT, NULL);
Подобный код принято оборачивать в функции вида:
AnimalCat*
animal_cat_new();
Завершим заголовочный файл объявлением конкретного метода:
void
animal_cat_say_meow(AnimalCat* self);
Как мы можем видеть, в отличие от языков вроде C++, в данном случае нам необходимо явно передавать указатель на конкретную инстанцию.
Итак, заголовочный файл теперь выглядит следующим образом:
#ifndef _ANIMAL_CAT_H_
#define _ANIMAL_CAT_H_
#include <glib-object.h>
G_BEGIN_DECLS
#define ANIMAL_TYPE_CAT animal_cat_get_type()
G_DECLARE_DERIVABLE_TYPE (AnimalCat, animal_cat, ANIMAL, CAT, GObject)
struct _AnimalCatClass
{
GObjectClass parent_class;
void (*say_meow) (AnimalCat*);
};
AnimalCat*
animal_cat_new();
void
animal_cat_say_meow(AnimalCat* self);
G_END_DECLS
#endif /* _ANIMAL_CAT_H_ */
Перейдём к файлу с собственно исходным кодом. Подключим наш заголовочный файл, а также stdio.h, необходимый для мяуканья нашего образца.
#include <stdio.h>
#include "animalcat.h"
Настало время для ещё одного важного макроса, который на этот раз имеет следующий вид:
G_DEFINE_TYPE (NamespaceObject, namespace_object, G_TYPE_OBJECT)
Последний аргумент, который мы передаём в макрос, подобен тому, который мы определяли в начале нашего заголовочного файла, актуальный для родительского объекта. Для нашего котообъекта он должен выглядеть как ANIMAL_TYPE_CAT, но в данном случае мы используем подобный макрос нашего родительского объекта: G_TYPE_OBJECT. Применительно к нашей ситуации, эта строчка должна выглядеть так:
G_DEFINE_TYPE (AnimalCat, animal_cat, G_TYPE_OBJECT)
Помните, чуть ранее мы говорили о разнице между derivable и final-gobject? Если бы мы определяли final-объект, не подразумевающий возможность наследования от него, дальше мы должны были бы определить структуру
struct _NamespaceObject
{
ParentObject parent;
};
которая должна содержать по крайней мере одно поле (и непременно первое) — аналогичную структуру родительского объекта. Такого рода обязательные требования связаны с тем, что адрес указателя на структуру родительского объекта должен быть идентичен адресам указателей на структуры всех унаследованных от него объекта.
В нашем случае структура выглядела бы как минимум так:
struct _AnimalCat
{
GObject parent;
};
Ещё раз напомню — определять явно данную структуру необходимо только в случае использования GObject типа final, а мы в нашем примере это определение опускаем.
Приступим непосредственно к исполняемому коду.
Определим функцию animal_cat_say_meow(), чтобы продемонстрировать, как в GObject работает механизм виртуальных функций (чуть далее вы увидите, зачем это нужно).
static void
animal_cat_real_say_meow(AnimalCat* self)
{
printf("Cat say: MEOW!\n");
}
Также определим тот реально вызываемый метод, который был объявлен в заголовочном файле:
void
animal_cat_say_meow(AnimalCat* self)
{
AnimalCatClass* klass = ANIMAL_CAT_GET_CLASS (self);
klass->say_meow(self);
}
Следующие две важные функции вызываются автоматически — первая при создании первой инстанции данного объекта, вторая — при создании любой конкретной инстанции, таким образом, являясь в определённом смысле аналогом конструктора C++.
static void
animal_cat_class_init(AnimalCatClass* self)
{
printf("First instance of AnimalCat was created\n");
self->say_meow = animal_cat_real_say_meow;
}
Как вы видите, мы переопределили «виртуальную» функцию, определённую в структуре-классе нашего объекта.
Что происходит в коде, описанном выше? В функции animal_cat_class_init(), при инициализации классовой структуры нашего объекта, ссылке на функцию say_meow присваивается адрес функции animal_cat_real_say_meow(), после чего, при внешнем вызове функции animal_cat_say_meow() при помощи макроса ANIMAL_CAT_GET_CLASS мы получаем указатель на классовую структуру нашего объекта и вызываем функцию, адрес которой в данный момент присвоен полю say_meow в структуре AnimalCatClass. В объектах-наследниках мы можем переопределить это поведение в соответствующей функции, чьё название заканчивается на class_init.
Приступим к «конструктору»:
static void
animal_cat_init(AnimalCat* self)
{
printf("Kitty was born.\n");
}
Сделаем функцию-обёртку, которая будет возвращать указатель на новую инстанцию объекта AnimalCat:
AnimalCat*
animal_cat_new()
{
return g_object_new(ANIMAL_TYPE_CAT, NULL);
}
Пишем простую функцию main() для проверки работоспособности нашего кода:
#include «animalcat.h"
int
main(int argc, char** argv)
{
AnimalCat* cat_a = animal_cat_new();
AnimalCat* cat_b = animal_cat_new();
animal_cat_say_meow(cat_a);
return 0;
}
и Makefile:
CFLAGS = -Wall -g `pkg-config --cflags glib-2.0 gobject-2.0`
LDFLAGS = `pkg-config --libs glib-2.0 gobject-2.0`
EXEC = kitty
SRC = main.c animalcat.c animalcat.c
OBJ = main.o animalcat.o animalcat.o
$(EXEC): $(OBJ)
$(CC) -o $@ $^ $(CFLAGS) $(LDFLAGS)
%.o: %.c
$(CC) -c -o $@ $< $(CFLAGS)
.PHONY: clean
clean:
rm -f $(OBJ) $(EXEC)
Собираем и запускаем:
make
./kitty
First instance of AnimalCat was created.
Kitty was born.
Kitty was born.
Cat say: MEOW!