Pull to refresh

GObject: инкапсуляция, инстанциация, интроспекция

*nixGTK+CООPDevelopment for Linux
… а также другие страшные слова! (с)

Прежде чем мы познакомимся с некоторыми продвинутыми возможностями объектной системы типов GLib, необходимо поговорить о ряде моментов, которые мы не затронули в предыдущих двух статьях. В этот раз мы познакомимся ближе с базовым типом GObject, поговорим о том, что любой наследник базового GObject представляет собой двуединство (а зачастую триединство) отдельных объектов-структур, во что раскрываются загадочные макросы в начале заголовочных файлов и файлов с исходным кодом, при помощи какого инструментария работает суровый местный RTTI, почему у GObject и его потомков два деструктора (и три конструктора), равно как и о ряде других интересных мелочей.

image

Весь цикл о GObject:


GObject: основы
GObject: наследование и интерфейсы
GObject: инкапсуляция, инстанциация, интроспекция

Структуры. Много структур.


Как мы знаем, потомки GObject могут быть наследуемыми — derivable и ненаследуемыми — final. В общем случае derivable GObject состоит из совокупности трёх объектов: структуры-класса, структуры-инстанции и структуры с приватными данными.

Со структурой-классом всё более-менее просто — она описывается в заголовочном файле и содержит экземпляр классовой структуры родителя и указатели на функции — «виртуальные методы». Хорошим тоном считается добавить последним полем структуры небольшой массив void-указателей для обеспечения ABI-совместимости. Экземпляр такой структуры создаётся в одном экземпляре при создании первой инстанции данного типа.

/* animalcat.h */

/* С вашего позволения, продолжу разбирать тему на котопримерах :) */

typedef struct _AnimalCat AnimalCat;
typedef struct _AnimalCatClass AnimalCatClass;
typedef struct _AnimalCatPrivate AnimalCatPrivate;

struct _AnimalCatClass
{
	GObjectClass parent_class; /* родительская классовая структура */
	void (*say_meow) (AnimalCat*); /* виртуальный метод */
	gpointer padding[10]; /* массив указателей; gpointer - переопределение void* */
};

Для final-типов определять структуру-класс нет необходимости.

Структура с приватными данными нужна для derivable-объектов. Она определяется в файле с исходным кодом, а доступ к ней можно получить через автоматически генерируемую функцию вида animal_cat_get_instance_private(). В таком случае макрос в начале.с-файла должен иметь вид G_DEFINE_TYPE_WITH_PRIVATE (NamespaceObject, namespace_object, PARENT_TYPE). Можно использовать и макрос G_DEFINE_TYPE_WITH_CODE (с включенным в него макросом G_ADD_PRIVATE).

/* animalcat.c */
#include "animalcat.h"
G_DEFINE_TYPE_WITH_PRIVATE(AnimalCat, animal_cat, G_TYPE_OBJECT)
/* G_DEFINE_TYPE_WITH_CODE(AnimalCat, animal_cat, G_TYPE_OBJECT, G_ADD_PRIVATE (AnimalCat)) */

struct _AnimalCatPrivate
{
	char* name;
	double weight;
	int age;
};

static void animal_cat_init(AnimalCat* self)
{
	AnimalCatPrivate* priv = animal_cat_get_instance_private(self);
	priv->age = 0;
	priv->name = "Barsik";
	/* и так далее */
}

Предполагается, что все данные инкапсулированы. Для доступа к ним можно использовать обычные обёртки — геттеры и сеттеры, но, как мы увидим впоследствии, GObject предоставляет для этого куда как более мощное средство — свойства (properties).

Структура-инстанция, как и структура с приватными данными, создаётся для каждого экземпляра объекта. Это, собственно, сам объект, с которым в основном будет работать конечный пользователь. Структура автоматически генерируется для derivable-типов посредством макроса из заголовочного файла, так что программисту нет нужды делать это самому. Для final-типов же её необходимо описывать вручную в файле с исходным кодом. Поскольку в данном случае структура не является частью публичного интерфейса объекта, она может содержать приватные данные. Очевидно, в таком случае нет необходимости в создании отдельной private-структуры.

/* animaltiger.c */
struct _AnimalTiger
{
	AnimalCat parent; /* обязательно первым полем должен идти экземпляр родительского объекта */
	int speed; /* приватные данные */
};

Что касается интерфейсов, для их реализации необходимо определить лишь интерфейсную структуру, очень похожей на обычную классовую. Структура самого объекта вида _AnimalPredator будет сгенерирована автоматически.

/* animalpredator.h */
typedef struct _AnimalPredatorInterface AnimalPredatorInterface;
struct _AnimalPredatorInterface
{
	GTypeInterface parent; /* все интерфейсы унаследованы от GTypeInterface */
	void (*hunt) (AnimalPredator* self); /* виртуальный метод */
};


Наглядная таблица-шпаргалка:
image

Динамическое определение типа на практике


В заголовочных файлах мы начинали описание нового типа с использования двух макросов, которые, в свою очередь, преобразуются в целый набор макроопределений. В старых версиях GLib было необходимо вручную описывать весь этот инструментарий. Посмотрим, чем из этого мы можем воспользоваться.

ANIMAL_TYPE_CAT: возвращает целочисленный идентификатор типа GType. Этот макрос тесно связан с системой типов GType, лежащей в основе GObject. С ним вы обязательно встретитесь, я упомянул его лишь для того, чтобы было понятно, откуда он берётся. Функции вида animal_cat_get_type(), которые использует данное макроопределение, генерируются автоматически в файле с исходным кодом при раскрытии макросов семейства G_DEFINE_TYPE.

ANIMAL_CAT (obj): приведение к указателю на данный тип. Обеспечивает безопасный каст, а также производит проверки времени исполнения. Как вы могли заметить, система наследования в GObject в общем случае построена на том, что структуры содержат первым полем экземпляр родительской структуры, а значит, по соглашениям вызовов Си, указатель на объект совпадает с указателем на всех предков, от от которых он унаследован. Несмотря на это, желательно пользоваться предоставленным макросом, а не обычным Си-кастом. Кроме того, в ряде случаев (например, при приведении к типу реализованного интерфейса), приведение в стиле Си вообще не будет работать.

ANIMAL_CAT_CLASS (klass): аналогичный макрос для классовых структур. Соглашение предписывает не использовать слово class для совместимости с компиляторами С++.

ANIMAL_IS_CAT (obj): как следует из названия, это макроопределение производит проверку, является ли obj указателем на данный тип (и не является ли NULL-указателем). Хорошим тоном считается начинать методы объекта с такой проверки.

void animal_cat_run (AnimalCat *self)
{
	assert(ANIMAL_IS_CAT (self));
	g_return_if_fail (ANIMAL_IS_CAT (self)); /* или средствами GLib */
	/* остальной код */
}

ANIMAL_IS_CAT_CLASS (klass): то же самое для классовых структур.

ANIMAL_CAT_GET_CLASS (obj): возвращает указатель на соответствующую классовую структуру.

Похожий набор макроопределений генерируется и для интерфейсов.

ANIMAL_PREDATOR (obj): каст к типу интерфейса.
ANIMAL_IS_PREDATOR (obj): проверка на соответствие типа.
ANIMAL_PREDATOR_GET_IFACE (obj): получение структуры интерфейса.

Имя объекта можно получить, используя макрос G_OBJECT_TYPE_NAME (obj), возвращающий си-строку с именем типа.

Макросы в начале файла с исходным кодом G_DEFINE_TYPE и его расширенные версии генерируют указатель вида animal_cat_parent_class, возвращающий указатель на классовую структуру родительского объекта, а также функцию вида animal_cat_get_instance_private(), если мы использовали соответствующий макрос.

Деструкторы и другие виртуальные функции


Как мы помним, при создании любого наследника GObject запускаются функции вида animal_cat_init(). Они выполняют ту же роль, что и конструкторы C++ и Java. С деструкторами ситуация сложнее.

Управление памятью в GObject реализовано с использованием подсчёта ссылок. При вызове функции g_object_new(), количество ссылок устанавливается равным единице. В дальнейшем мы можем увеличивать их число при помощи g_object_ref() и уменьшать при помощи g_object_unref(). Когда число ссылок станет равным нулю, будет запущена процедура уничтожения объекта, состоящая из двух фаз. Сначала вызывается функция dispose(), которая может вызываться многократно. Основная её задача — разрешить в случае необходимости циклические ссылки. После этого единожды вызывается функция finalize(), в которой выполняется всё то, для чего обычно используются деструкторы — освобождается память, закрываются открытые файловые дискрипторы и т. д.

Такая сложная система была спроектирована для облегчения создания биндингов к высокоуровневым языкам, в том числе с автоматическим управлением памятью. На практике, в Си-коде используется обычно только finalize(), если объект предполагает наличие деструктора.

Функции dispose() и finalize(), а также ряд других, о которых мы будем говорить дальше, виртуальные и определены в GObjectClass.

static void animal_cat_finalize(GObject* obj)
{
	g_print("Buy!\n"); /* вариант printf() из GLib */

	/* освобождаем память и т. д. */

	G_OBJECT_CLASS (animal_cat_parent_class)->finalize(obj); /* в конце обязательно вызывайте аналогичный деструктор родительского класса */
}

static void animal_cat_class_init(AnimalCatClass* klass)
{
	GObjectClass* obj_class = G_OBJECT_CLASS (klass);
	obj_class->finalize = animal_cat_finalize; /* переопределяем деструктор */
}

Последняя строчка функции animal_cat_finalize() может показаться требующей дополнительных пояснений. Указатель animal_cat_parent_class на родительский класс создаётся при раскрытии макроса G_DEFINE_TYPE и его расширенных версий. Мы вызываем соответствующую функцию из родительского класса, который в данном случае непосредственно является структурой GObjectClass, а она, в свою очередь, вызывает finalize() предыдущего класса в цепочке. Нет необходимости беспокоиться о том, что родительский класс может не содержать переопределения finalize(), об этом позаботится система GObject.

Осталось только напомнить о том, что деструктор вызывается лишь тогда, когда обнуляется счётчик ссылок:

int main(int argc, char** argv)
{
	AnimalCat* cat = animal_cat_new();
	g_object_unref(cat); /* без этой строчки не сработает */
}

Кроме двух деструкторов, GObjectClass содержит два дополнительных виртуальных конструктора. constructor() вызывается до уже известного нам animal_cat_init() и непосредственно создаёт экземпляр данного типа, constructed() — после. Нелегко придумать ситуацию, в которой вам понадобится переопределять эти функции, если вы, конечно, не решили пропатчить сам GLib. В документации разработчики приводят пример с реализацией синглтона, но в реальном коде я ни разу подобные случаи не встречал. Однако для достижения максимальной гибкости на всех этапах жизненного цикла инстанции объекта разработчики посчитали нужным сделать эти функции виртуальными.

Кроме того, GObjectClass содержит виртуальные функции get_property() и set_property(), которые необходимо переопределить для использования в своих собственных объектах такой мощной возможности базового типа GObject и его потомков, как свойства. Об этом мы поговорим в следующей статье.
Tags:gobjectglibgtkgtk+gnomelinuxunixfossgnu
Hubs: *nix GTK+ C ООP Development for Linux
Total votes 13: ↑12 and ↓1 +11
Views4.6K

Popular right now

Senior system developer/ С++
from 170,000 ₽GETMOBITМосква
Системный администратор Linux
from 150,000 to 250,000 ₽Action techМоскваRemote job
Middle C++ Developer (ARM/Linux)
from 120,000 ₽AXOYARemote job
Devops / Linux администратор
from 160,000 to 250,000 ₽Софт ПроектМоскваRemote job
Системный администратор Linux DevOps
from 100,000 to 150,000 ₽X-KeeperКрасногорскRemote job