В отличие от других схожих проектов, GObject отличают архитектурные особенности, целью которых является лёгкая и прозрачная реализация привязок библиотек, написанных с применением чистого Си и GObject, к другим языкам программирования, в том числе с динамической типизацией и управлением памятью при помощи сборщика мусора. Именно этим объясняется некоторое ощущение переусложнённости, которое может возникнуть у программиста, приступившего к знакомству с GObject API. Тем не менее, эта система очень продуманная и логичная, так что проблем с пониманием всего изложенного ниже у программиста, знакомого с C++ или Java, возникнуть не должно.
Данная статья иллюстрирует самые основы работы с объектной системой типов GLib.
На самом базовом уровне системы типов GObject лежит система GType, реализующая доступное в run-time описание всех возможных типов, которыми оперирует программист. Эта система выступает своеобразным «клеем», связывающим код на Си с кодом на других языках, имеющих привязки к библиотекам, построенным на использовании динамической системы типов GLib.
Система типов GObject поддерживает одиночное наследование (а с учётом концепции Java-подобных интерфейсов, и более сложные концептуальные решения), инкапсуляцию, виртуальные функции, так называемые свойства, схожие с полями С++, но обладающие при этом большим количеством дополнительных возможностей, а также систему сигналов, позволяющих создавать эффективные и развитые конструкции в рамках событийно-ориентированной парадигмы.
Система типов GObject состоит из трёх основных сущностей — фундаментальных неистанцируемых типов вроде gchar (аналога char из чистого Си), gponter (аналога указателя типа 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!
Комментарии (16)
justhabrauser
03.02.2018 18:26«привязки к GLib/GTK+»: Glib != GTK
potustoronnimv Автор
04.02.2018 00:05Glib != GTK
В таком виде да. Но GTK+ построен на системе GObject, которая является частью GLib, так же как построены на GObject GStreamer, GSettings, Pango, GIO, ATK и многие другие, более высокоуровневые системные компоненты.
technic93
03.02.2018 22:00И чем это лучше обычных плюсов?
apro
03.02.2018 23:02И чем это лучше обычных плюсов?
Насколько я понимаю, основное преимущество
C
(с точки зрения разработчиковgtk+
), над остальными языками это ABI.
CC++
проблема даже использовать библиотеку скомпилированную одним компилятором,
из библиотеки/программы собранной другим компиляторомC++
. А чего уж говорить о взаимодействии с другими языками программирования.
potustoronnimv Автор
03.02.2018 23:57Хотя бы из соображений удобства создания биндингов для более высокоуровневых языков. Кроме того, значительный пласт приложений под UNIX-системы использует фреймворк GLib/GTK+ в чистом виде.
biseptol
04.02.2018 00:27«Убъемся, напишем глючный небезопасный С++ на макросах и указателях на чистом С, лишь бы не писать на C++»
Kobalt_x
04.02.2018 00:28как там у c++ со стандартным ABI во всех компиляторах?
biseptol
04.02.2018 01:10Да лучше уж С-шные обертки экспортировать, чем этот ужас.
Сорри, у меня легкая степень PTSD, я писал плагины для GStreamer-а пару лет назад, до сих пор рябит перед глазами.pixelcube
04.02.2018 02:14Если хочется красоты с GLib, то есть Vala.
GStreamer плагины на нем хорошо пишутся, исходного кода раза в четыре меньше.
apro
04.02.2018 02:40+1Да лучше уж С-шные обертки экспортировать, чем этот ужас.
А как сделать-то? Захочешь например чтобы могли наследовать объекты в других языках и вот вместо наследования C++:
struct Derive { struct Base base; };
захочешь чтобы виртуальные функции могли переопределять в других языках и вместоvirtual
у тебя указатели на функции, вместо неявногоthis
передается
явно указатель на структуру, и сколькоc++
отc++
останется?biseptol
04.02.2018 02:52А зачем это все? Может в архитектуре что-нибудь исправить, если приходится протаскивать наследование, да еще и с virtual методами в другие языки?
Lsh
04.02.2018 13:24А что вы скажете про Vala? Насколько оно живое и насколько стабильное? Можно ли на Vala написать что-то, что потом будет использоваться в других языках через биндинги?
potustoronnimv Автор
05.02.2018 10:24Vala вполне живой и развитый проект, очень удобный и простой C#-подобный язык, но компилируется в нативный код. Изначально конвертировался препроцессором в си-код, после чего компилировался любым си-компилятором. Вообще, по хорошему, Vala — просто обёртка вокруг GObject, эдакий синтаксический сахар.
bamovetz
07.02.2018 08:55Самый большой грех основателей open-source и в частности создателей Линукс в том что они, являясь ярыми сишниками, проигнорировали весь труд Страуструпа и компании. Они даже готово были переизобрести Си с классами вместо использования С++. В GLibs очень яркий пример — гигантский проект, реализация GUI — идеально ложится на ООП в С++. Но нет мы же пишем только на Си. Придумаем свои классы на неотлаживаемых макросах и будем гордиться. А потом когда уже макросы начнут доставать сделаем свой язык Vala. Но С++ использовать религия не позволяет.
potustoronnimv Автор
07.02.2018 09:13Изначально, возможно, их можно было упрекнуть в этом. На данный момент, GLib и набор библиотек, простроенных на ней, являются сишным ядром, а разработка может вестись на более высокоуровневых языках, в том числе и C++ (gtkmm и glibmm — биндинги к C++).
Что касается Vala, тут вы сравниваете совершенно разнородные сущности. У Vala вполне конкретная ниша — прикладные десктопные приложения, в первую очередь для GNOME и GTK-based окружений. У Vala очень простой и дружелюбный синтаксис, обширная стандартная библиотека, делающая построение десктопных приложений простым и приятным процессом. Вне этой ниши использовать Vala большого смысла нет, в общем-то. Это не конкурент C++, скорее, его можно сравнивать со Swift — язык конкретный платформы и фреймворков.
norlin
Безотносительно статьи, я правильно понимаю, что на КДПВ котик взламывает комп, бесконтактно передавая сигналы из лапы в клавиатуру и потом передавая полученные данные через хвост куда-то ещё?