Как обеспечить большую гибкость для пользователей?

Сигналы GObject могут использоваться для обеспечения большей гибкости чем механизм уведомления об изменении файла рассмотренный в предыдущем примере. Одна из ключевых идей - сделать процесс записи данных в файл частью процесса эмиссии сигнала позволив пользователю получать уведомления либо перед либо после записи данных в файл.

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

Первый шаг реализации этой идеи - изменить сигнатуру сигнала: мы должны разместить буфер для записи и его размер. Для этого мы используем собственный маршаллер который будет сгенерирован с помощью специальной утилиты glib. Таким образом создаём файл с именем marshall.list который содержит следующую строку:

VOID:POINTER,UINT

и используем Makefile поставляемый в sample/signal/Makefile для генерации файла с именем maman-file-complex-marshall.c. Этот C файл включаем в maman-file-complex.c.

Как только маршаллер создан, мы регистрируем сигнал и его маршаллер в функции class_init объекта MamanFileComplex (полный исходный код этого объекта включён в sample/signal/maman-file-complex.{h|c}):

GClosure *default_closure; GType param_types[2]; default_closure = g_cclosure_new (G_CALLBACK (default_write_signal_handler), (gpointer)0xdeadbeaf /* user_data */, NULL /* destroy_data */); param_types[0] = G_TYPE_POINTER; param_types[1] = G_TYPE_UINT; klass->write_signal_id = g_signal_newv ("write", G_TYPE_FROM_CLASS (g_class), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, default_closure /* class closure */, NULL /* accumulator */, NULL /* accu_data */, maman_file_complex_VOID__POINTER_UINT, G_TYPE_NONE /* return_type */, 2 /* n_params */, param_types /* param_types */);

Код показанный выше создаёт замыкание которое содержит код полной записи файла. Это замыкание регистрируется как значение по умолчанию class_closure вновь созданного сигнала.

Конечно, вы должны полностью реализовать код для замыкания по умолчанию, так как я создал только скелет:

static void default_write_signal_handler (GObject *obj, guint8 *buffer, guint size, gpointer user_data) { g_assert (user_data == (gpointer)0xdeadbeaf); /* Здесь мы вызываем реальную запись файла. */ g_print ("default signal handler: 0x%x %u\n", buffer, size); }

Наконец, код клиента должен вызвать функцию maman_file_complex_write которая переключает эмиссию сигнала:

void maman_file_complex_write (MamanFileComplex *self, guint8 *buffer, guint size) { /* trigger event */ g_signal_emit (self, MAMAN_FILE_COMPLEX_GET_CLASS (self)->write_signal_id, 0, /* details */ buffer, size); }

Клиентский код (представленный ниже и в sample/signal/test.c) может теперь подключать обработчики сигнала перед и после завершения записи файла: так как обработчик сигнала по умолчанию, который делает саму запись, выполняется в течение фазы RUN_LAST эмиссии сигнала, он будет выполнен после всех обработчиков подключенных с помощью g_signal_connect и перед обработчиками подключенными с помощью g_signal_connect_after. Если вы намерены написать GObject который издаёт сигналы, я рекомендовал бы вам создавать все ваши сигналы с G_SIGNAL_RUN_LAST чтобы пользователи могли иметь максимальную гибкость при получении события. Здесь мы объединили его с G_SIGNAL_NO_RECURSE и G_SIGNAL_NO_HOOKS чтобы гарантировать что пользователи не будут делать ничего сверхъестественного с нашим GObject. Я строго советую вам делать тоже самое если вы действительно не знаете почему (если бы вы знали внутреннюю работу GSignal вы бы это не читали).

static void complex_write_event_before (GObject *file, guint8 *buffer, guint size, gpointer user_data) { g_assert (user_data == NULL); g_print ("Complex Write event before: 0x%x, %u\n", buffer, size); } static void complex_write_event_after (GObject *file, guint8 *buffer, guint size, gpointer user_data) { g_assert (user_data == NULL); g_print ("Complex Write event after: 0x%x, %u\n", buffer, size); } static void test_file_complex (void) { guint8 buffer[100]; GObject *file; file = g_object_new (MAMAN_FILE_COMPLEX_TYPE, NULL); g_signal_connect (G_OBJECT (file), "write", (GCallback)complex_write_event_before, NULL); g_signal_connect_after (G_OBJECT (file), "write", (GCallback)complex_write_event_after, NULL); maman_file_complex_write (MAMAN_FILE_COMPLEX (file), buffer, 50); g_object_unref (G_OBJECT (file)); }

Код выше генерирует следующий вывод на моей машине:

Complex Write event before: 0xbfffe280, 50 default signal handler: 0xbfffe280 50 Complex Write event after: 0xbfffe280, 50

Как большинство людей делают тоже самое с меньшим количеством кода

По многим историческим причинам связанным с тем что предок GObject используется для работы в GTK+ версий 1.x, есть намного более простой[17] способ создания сигнала с обработчиком по умолчанию, чем создавать замыкание вручную и использовать g_signal_newv.

Например, g_signal_new может использоваться для создания сигнала который использует обработчик по умолчанию сохранённый в структуре класса объекта. Конкретнее, структура класса содержит указатель функции который доступен в течение эмиссии сигнала для вызова обработчика по умолчанию, а пользователь как ожидается обеспечит для g_signal_new смещение сначала классовой сструктуры для указания функции.[18]

Следующий код показывает декларацию классовой сструктуры MamanFileSimple которая содержит указатель write функции.

struct _MamanFileSimpleClass { GObjectClass parent; guint write_signal_id; /* обработчик сигнала по умолчанию */ void (*write) (MamanFileSimple *self, guint8 *buffer, guint size); };

Указатель write функции инициализированной в функции class_init объекта для default_write_signal_handler:

static void maman_file_simple_class_init (gpointer g_class, gpointer g_class_data) { GObjectClass *gobject_class = G_OBJECT_CLASS (g_class); MamanFileSimpleClass *klass = MAMAN_FILE_SIMPLE_CLASS (g_class); klass->write = default_write_signal_handler;

Наконец, сигнал создаётся с помощью g_signal_new в той же функции class_init:

klass->write_signal_id = g_signal_new ("write", G_TYPE_FROM_CLASS (g_class), G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, G_STRUCT_OFFSET (MamanFileSimpleClass, write), NULL /* accumulator */, NULL /* accu_data */, maman_file_complex_VOID__POINTER_UINT, G_TYPE_NONE /* return_type */, 2 /* n_params */, G_TYPE_POINTER, G_TYPE_UINT);

Примечателен здесь 4-ый аргумент функции: он рассчитывается с помощью макроса G_STRUCT_OFFSET указывая смещение члена write от начала классовой сструктуры MamanFileSimpleClass.[19]

Не смотря на то, что полный код для этого обработчика по умолчанию выглядит меньше clutered как показано в sample/signal/maman-file-simple.{h|c}, он содержит много тонкостей. Основная тонкость которую должен знать каждый - сигнатура обработчика по умолчанию созданного таким способом не имеет аргумента user_data: default_write_signal_handler в sample/signal/maman-file-complex.c отличается от sample/signal/maman-file-simple.c.

Если вы не знаете какой метод использовать, я советовал бы вам второй который вызывает g_signal_new а не g_signal_newv: он лучше для написания кода который похож на большую часть кода GTK+/GObject чем делать это собственным способом. Однако теперь вы знаете как это сделать.



[17] Я лично думаю что этот метод ужасно запутанный: он добавляет новую неопределённость которая излишне усложняет код. Однако, раз этот метод широко используется во всём коде GTK+ и GObject, читатель должен об этом знать. Причина по которой этот метод используется в GTK+ связана с фактом что предок GObject не обеспечивал других способов создания сигналов с обработчиками по умолчанию. Некоторые люди пытались оправдать этот способ тем что он быстрее и лучше (У меня большие сомнения по поводу утверждения о быстроте. Честно говоря, и фраза о "лучше" тоже большая для меня загадка ;-). Я думаю что большинство копируют похожий код и не задумываются над этим. Вероятно лучше оставить эти специфичные пустяки в области хакерских легенд...

[18] Я хотел бы заметить что причина по которой обработчик сигнала по умолчанию везде именуется как class_closure заключается в том что фактически это действительно указатель функции хранящийся в классовой структуре.

[19] GSignal использует это смещение для создания специальной оболочки замыкания которая сначала получает целевой указатель функции перед её вызовом.