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 лежит система 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)


  1. norlin
    03.02.2018 17:26
    -1

    Безотносительно статьи, я правильно понимаю, что на КДПВ котик взламывает комп, бесконтактно передавая сигналы из лапы в клавиатуру и потом передавая полученные данные через хвост куда-то ещё?


  1. justhabrauser
    03.02.2018 18:26

    «привязки к GLib/GTK+»: Glib != GTK


    1. potustoronnimv Автор
      04.02.2018 00:05

      Glib != GTK

      В таком виде да. Но GTK+ построен на системе GObject, которая является частью GLib, так же как построены на GObject GStreamer, GSettings, Pango, GIO, ATK и многие другие, более высокоуровневые системные компоненты.


  1. technic93
    03.02.2018 22:00

    И чем это лучше обычных плюсов?


    1. apro
      03.02.2018 23:02

      И чем это лучше обычных плюсов?

      Насколько я понимаю, основное преимущество C (с точки зрения разработчиков gtk+), над остальными языками это ABI.
      C C++ проблема даже использовать библиотеку скомпилированную одним компилятором,
      из библиотеки/программы собранной другим компилятором C++. А чего уж говорить о взаимодействии с другими языками программирования.


    1. potustoronnimv Автор
      03.02.2018 23:57

      Хотя бы из соображений удобства создания биндингов для более высокоуровневых языков. Кроме того, значительный пласт приложений под UNIX-системы использует фреймворк GLib/GTK+ в чистом виде.


  1. biseptol
    04.02.2018 00:27

    «Убъемся, напишем глючный небезопасный С++ на макросах и указателях на чистом С, лишь бы не писать на C++»


    1. Kobalt_x
      04.02.2018 00:28

      как там у c++ со стандартным ABI во всех компиляторах?


      1. biseptol
        04.02.2018 01:10

        Да лучше уж С-шные обертки экспортировать, чем этот ужас.

        Сорри, у меня легкая степень PTSD, я писал плагины для GStreamer-а пару лет назад, до сих пор рябит перед глазами.


        1. pixelcube
          04.02.2018 02:14

          Если хочется красоты с GLib, то есть Vala.
          GStreamer плагины на нем хорошо пишутся, исходного кода раза в четыре меньше.


        1. apro
          04.02.2018 02:40
          +1

          Да лучше уж С-шные обертки экспортировать, чем этот ужас.

          А как сделать-то? Захочешь например чтобы могли наследовать объекты в других языках и вот вместо наследования C++:
          struct Derive { struct Base base; };
          захочешь чтобы виртуальные функции могли переопределять в других языках и вместо virtual у тебя указатели на функции, вместо неявного this передается
          явно указатель на структуру, и сколько c++ от c++ останется?


          1. biseptol
            04.02.2018 02:52

            А зачем это все? Может в архитектуре что-нибудь исправить, если приходится протаскивать наследование, да еще и с virtual методами в другие языки?


  1. Lsh
    04.02.2018 13:24

    А что вы скажете про Vala? Насколько оно живое и насколько стабильное? Можно ли на Vala написать что-то, что потом будет использоваться в других языках через биндинги?


    1. potustoronnimv Автор
      05.02.2018 10:24

      Vala вполне живой и развитый проект, очень удобный и простой C#-подобный язык, но компилируется в нативный код. Изначально конвертировался препроцессором в си-код, после чего компилировался любым си-компилятором. Вообще, по хорошему, Vala — просто обёртка вокруг GObject, эдакий синтаксический сахар.


  1. bamovetz
    07.02.2018 08:55

    Самый большой грех основателей open-source и в частности создателей Линукс в том что они, являясь ярыми сишниками, проигнорировали весь труд Страуструпа и компании. Они даже готово были переизобрести Си с классами вместо использования С++. В GLibs очень яркий пример — гигантский проект, реализация GUI — идеально ложится на ООП в С++. Но нет мы же пишем только на Си. Придумаем свои классы на неотлаживаемых макросах и будем гордиться. А потом когда уже макросы начнут доставать сделаем свой язык Vala. Но С++ использовать религия не позволяет.


    1. potustoronnimv Автор
      07.02.2018 09:13

      Изначально, возможно, их можно было упрекнуть в этом. На данный момент, GLib и набор библиотек, простроенных на ней, являются сишным ядром, а разработка может вестись на более высокоуровневых языках, в том числе и C++ (gtkmm и glibmm — биндинги к C++).

      Что касается Vala, тут вы сравниваете совершенно разнородные сущности. У Vala вполне конкретная ниша — прикладные десктопные приложения, в первую очередь для GNOME и GTK-based окружений. У Vala очень простой и дружелюбный синтаксис, обширная стандартная библиотека, делающая построение десктопных приложений простым и приятным процессом. Вне этой ниши использовать Vala большого смысла нет, в общем-то. Это не конкурент C++, скорее, его можно сравнивать со Swift — язык конкретный платформы и фреймворков.