Pull to refresh

GObject: наследование и интерфейсы

Reading time8 min
Views6.2K
В комментариях к прошлой статье часто высказывалось мнение, что система GObject не нужна ввиду наличия C++ и других высокоуровневых языков. Помимо чисто технических моментов, о которых уже поговорили в комментариях, хотелось бы затронуть другой аспект. Вероятно, большинство комментаторов видит смысл существования объектной системы GLib в упорном нежелании сишников-ретроградов пользоваться благами цивилизации и смиряться с неумолимой поступью прогресса. Вероятно, так оно и было на заре развития Glib/GTK, зародившихся в мире UNIX-систем, GNU, open-source, идей Столлмана, и т. п. Большая часть того поколения хакеров действительно предпочитали Си, в то время как C++ был относительно молод и неразвит и преимущества его использования казались не настолько очевидными.

Сегодня, разумеется, для новых проектов большинство из нас предпочтёт использование более удобных, лаконичных и безопасных языков, даже если будет знаком со всеми нюансами использования GObject. Однако не стоит упускать из виду, что за 20 с лишним лет существования GLib/GTK с их использованием были созданы тысячи приложений и библиотек, многие из которых активно развиваются и поныне тысячами программистов со всего мира. В них добавляется новый функционал, вылавливаются баги, их адаптируют к современным технологиям вроде HiDPI-экранов, Wayland, Vulkan, и т. д. Для того, чтобы читать (дополнять, исправлять) код таких проектов, необходимо иметь базовые знания объектно-ориентированных расширений для Си, о котором мы с вами ведём речь.

Засим милости прошу под кат. Тренируемся, как обычно, на кошках :)



Весь цикл о GObject:


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

Наследование от потомков GObject


В общем случае GObject подразумевает только единичное наследование. Для множественного наследования предполагается использовать Java-подобные интерфейсы, о которых мы поговорим во второй части статьи.

Создадим тип Tiger, который будет наследоваться от Cat, описанный нами в прошлой статье. На этот раз сделаем его final-объектом и, таким образом, будем предполагать, что в дальнейшем от него наследование производиться не будет.

Создадим animaltiger.h:

#ifndef _ANIMAL_TIGER_H_
#define _ANIMAL_TIGER_H_
#include "animalcat.h"

G_BEGIN_DECLS

#define ANIMAL_TYPE_TIGER animal_tiger_get_type()
G_DECLARE_FINAL_TYPE (AnimalTiger, animal_tiger, ANIMAL, TIGER, AnimalCat)

Обратите внимание на то, что мы использовали макрос G_DECLARE_FINAL_TYPE вместо G_DECLARE_DERIVABLE_TYPE для описания объекта типа final, а также на то, что последним аргументом макроса мы указали «родителя» — AnimalCat.

Если бы мы предполагали дальнейшую цепочку наследования с derivable-объектом, нам необходимо был бы описать класс и первым полем в нём создать экземпляр родительского класса; в данном случае нам в этом нет необходимости.

/* 
struct _AnimalTigerClass
{
	AnimalCatClass parent_class;
}; 
*/

Объявим функцию, возвращающую новую инстанцию нашего тигра:

AnimalTiger* 
animal_tiger_new();

G_END_DECLS

#endif /* _ANIMAL_TIGER_H_ */

Тут можно было бы объявить другие методы, специфические для объекта-тигра, но для упрощения ограничимся тем, который достался ему от родителя — say_meow. Это виртуальная функция, которой мы дадим специфическую для нашего типа реализацию. После этого закрываем макрос G_END_DECLS. В принципе, нет необходимости обрамлять заголовочный файл макросами G_BEGIN_DECLS / G_END_DECLS, они раскладываются в банальное extern «C» {} и нужны для совместимости с компиляторами C++. Это лишь правила хорошего тона, принятые в среде разработчиков на GLib/GTK+.

Приступим к файлу с исходным кодом animaltiger.c:

#include <stdio.h>

#include "animaltiger.h"

struct _AnimalTiger
{
	AnimalCat parent;
};

Подключаем заголовочник и стандартный ввод-вывод и описываем структуру-основу нашего объекта. В данном случае, поскольку мы имеем дело с final-объектом, структуру-класс мы не создавали, а вот основную структуру, структуру-инстанцию, необходимо описать явным образом.

G_DEFINE_TYPE (AnimalTiger, animal_tiger, ANIMAL_TYPE_CAT)

В начальном макросе последним параметром идёт макрос, возвращающий информацию о типе родителя. Если помните, этот макрос мы с вами определили в заголовочном файле animalcat.h.

Создадим функцию animal_cat_real_say_meow():

static void
animal_tiger_real_say_meow(AnimalTiger* self)
{
	printf("Tiger say: ARRRRGH!!!\n");
}

И две главные функции:

static void
animal_tiger_class_init(AnimalTigerClass* self)
{
	printf("First instance of AnimalTiger was created.\n");
	AnimalCatClass* parent_class = ANIMAL_CAT_CLASS (self);
	parent_class->say_meow = animal_tiger_real_say_meow;
}

static void
animal_tiger_init(AnimalTiger* self)
{
	printf("Tiger cub was born.\n");
}

В функции animal_tiger_class_init, которая будет вызываться при создании первой инстанции нашего объекта, мы получаем указатель на родительский класс при помощи макроса ANIMAL_CAT_CLASS. Этот макрос, как и ряд других, создаётся при раскрытии макросов в заголовочных файлах, таких, как G_DECLARE_FINAL_TYPE. Дальше мы просто переопределяем функцию на созданную нами несколькими строчками выше. В нашем «конструкторе» animal_tiger_init() ничего особенного, просто посигналим о создании нашего объекта.

В функции animal_tiger_new() всё устроено аналогично родительскому классу:

AnimalTiger*
animal_tiger_new()
{
	return g_object_new(ANIMAL_TYPE_TIGER, NULL);
}

Проверим, как работает наш новый тип:

/* main.c */
#include "animaltiger.h"

int
main(int argc, char** argv)
{
	AnimalTiger* tiger = animal_tiger_new();
	animal_cat_say_meow(tiger);
	return 0;
}

Дополним наш Makefile из прошлой статьи новыми файлами, соберём и запустим:

First instance of AnimalCat was created.
First instance of AnimalTiger was created.
Little cat was born.
Tiger cub was born.
Tiger say: ARRRRGH!!!

Как мы видим, сначала отрабатывают функции вида _class_init, потом конструкторы отдельных инстанций _init. Когда мы вызываем метод родительского объекта и передаём в качестве параметра указатель на инстанцию потомка, отрабатывает переопределённая функция потомка.

Интерфейсы


Помимо прямого наследования, в GObject имеется концепция наследования интерфейсов. Интерфейсы — неинстанцируемые типы, схожие с чисто абстрактными классами C++ или интерфейсами Java.

Пусть наш тигр приобретёт свойства хищника — он будет реализовывать интерфейс AnimalPredator (разумеется, все кошачьи являются хищниками, но не будем вдаваться в зоологические тонкости). Создадим файл animalpredator.h:

#ifndef _ANIMAL_PREDATOR_H_
#define _ANIMAL_PREDATOR_H_

#include <glib-object.h>

G_BEGIN_DECLS

#define ANIMAL_TYPE_PREDATOR animal_predator_get_type()
G_DECLARE_INTERFACE (AnimalPredator, animal_predator, ANIMAL, PREDATOR, GObject)

struct _AnimalPredatorInterface
{
	GTypeInterface parent;
	void (*hunt)(AnimalPredator*);
	void (*eat_meat)(AnimalPredator*, int);
};

void 
animal_predator_hunt(AnimalPredator* self);

void 
animal_predator_eat_meat(AnimalPredator* self, int quantity);

G_END_DECLS

#endif /* _ANIMAL_PREDATOR_H_ */

Как видите, здесь всё выглядит похожим на заголовочник обычного derivable-наследника GObject. Обратим внимание на несколько моментов:

  • основным макросом тут является G_DECLARE_INTERFACE вместо G_DECLARE_DERIVABLE_TYPE (а последним «аргументом» в этом макросе всё так же является GObject);
  • классовая структура имеет название вида _AnimalPredatorInterface, а не _AnimalPredatorClass;
  • первое поле в ней отведено для типа GTypeInterface, а не для GObjectClass.

Также в структуре мы создаём два указателя для виртуальных функций, которые будут реализовываться конкретными инстанцируемыми типами. Напоследок объявим две функции-обёртки для этих указателей — animal_predator_hunt и animal_predator_eat_meat.

Теперь создадим вспомогательный файл animalpredator.c, который не следует путать с реализацией данного интерфейса — получившийся объектный модуль всего лишь запускает конкретные имплементации.


#include <stdio.h>
#include "animalpredator.h"

G_DEFINE_INTERFACE (AnimalPredator, animal_predator, G_TYPE_OBJECT)

static void
animal_predator_default_init(AnimalPredatorInterface* iface)
{
        printf("The first instance of the object that implements AnimalPredator interface was created\n");
}

void
animal_predator_hunt(AnimalPredator* self)
{
	AnimalPredatorInterface* iface = ANIMAL_PREDATOR_GET_IFACE (self);
	iface->hunt(self);
}

void 
animal_predator_eat_meat(AnimalPredator* self, int quantity)
{
	AnimalPredatorInterface* iface = ANIMAL_PREDATOR_GET_IFACE (self);
	iface->eat_meat(self, quantity);
}

Тут всё просто: начальный макрос G_DEFINE_INTERFACE, подобный G_DEFINE_TYPE, который мы использовали в начале animalcat.c и animaltiger.c, функция-конструктор animal_predator_default_init, в которую можно поместить произвольный код, выполняющийся при создании первой инстанции объекта, реализующего наш интерфейс, и функции-обёртки, запускающие конкретные реализации виртуальных функций интерфейса. Макрос ANIMAL_PREDATOR_GET_IFACE возвращает классовую структуру интерфейса, точно так же, как макроc ANIMAL_CAT_CLASS возвращал структуру родительского класса.

Приступим к имплементации нашего интерфейса. Дополним наш animaltiger.c:

#include "animalpredator.h"

static void 
animal_tiger_predator_interface_init(AnimalPredatorInterface *iface);

Функцию animal_tiger_predator_interface_init необходимо объявить в начале файла, так как она понадобится для следующего макроса.

Заменим макрос G_DEFINE_TYPE на аналогичный, но с более широкими возможностями. Как видите, тут добавился новый «аргумент», который может содержать ряд макросов, раскрывающихся в произвольный код. В данном случае мы поместили туда макрос G_IMPLEMENT_INTERFACE.

G_DEFINE_TYPE_WITH_CODE (AnimalTiger, animal_tiger, ANIMAL_TYPE_CAT,
			         G_IMPLEMENT_INTERFACE (ANIMAL_TYPE_PREDATOR,
						animal_tiger_predator_interface_init))

Добавим две функции, реализующие «прототипы» из классовой структуры интерфейса:

static void
animal_tiger_predator_hunt(AnimalTiger* self)
{
	printf("Tiger hunts. Beware!\n");
}

static void
animal_tiger_predator_eat_meat(AnimalTiger* self, int quantity)
{
	printf("Tiger eats %d kg of meat.\n", quantity);
}

Наконец, создадим функцию-конструктор реализации нашего интерфейса, где мы присваиваем указателям из интерфейсной структуры конкретные значения, определённые выше:

static void
animal_tiger_predator_interface_init(AnimalPredatorInterface* iface)
{
	iface->hunt = animal_tiger_predator_hunt;
	iface->eat_meat = animal_tiger_predator_eat_meat;
}

Поглядим, как это работает:

#include "animaltiger.h"
#include "animalpredator.h"

int 
main(int argc, char** argv)
{
	AnimalTiger* tiger = animal_tiger_new();
	animal_predator_hunt(tiger);
	animal_predator_eat_meat(tiger, 100500);
}

Собираем, запускаем:

First instance of AnimalCat was created.
The first instance of the object that implements AnimalPredator interface was created
First instance of AnimalTiger was created.
Little cat was born.
Tiger cub was born.
Tiger hunts. Beware!
Tiger eats 100500 kg of meat.

Возможно ли реализация нескольких интерфейсов сразу? Да, разумеется. Вот реальный код из GIO, где объект реализует сразу три интерфейса:

static void initable_iface_init       (GInitableIface *initable_iface);
static void async_initable_iface_init (GAsyncInitableIface *async_initable_iface);
static void dbus_object_manager_interface_init (GDBusObjectManagerIface *iface);

G_DEFINE_TYPE_WITH_CODE (GDBusObjectManagerClient, g_dbus_object_manager_client, G_TYPE_OBJECT,
                         G_ADD_PRIVATE (GDBusObjectManagerClient)
                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
                         G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init)
G_IMPLEMENT_INTERFACE (G_TYPE_DBUS_OBJECT_MANAGER, dbus_object_manager_interface_init))

В таком случае понадобятся три отдельные функции _interface_init. Обратите внимание, последние три макроса не разделяются запятыми, это один «аргумент».

А как обстоят дела с наследованием интерфейсов? Здесь принцип также схож с применяющимся в Java: эту задачу решает механизм пререквезитов — зависимостей типов и объектов между собой. Например, для того, чтобы реализовать интерфейс AnimalPredator, понадобится реализовать ещё и интерфейс Eater или быть унаследованным от объекта, скажем, Mammal. Однако, рассмотрение этих нюансов грозит резко увеличить объём статьи, поэтому оставим его за рамками этого текста.
Tags:
Hubs:
+10
Comments2

Articles

Change theme settings