Pull to refresh

GObject: основы

Reading time 7 min
Views 20K
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

Весь цикл о 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!
Tags:
Hubs:
+29
Comments 16
Comments Comments 16

Articles