Вы натолкнулись на интересную деталь, которая существует благодаря языку C. Функции в C строго типизированы, и поэтому, строго говоря, вам нужно иметь функции для работы с каждым возможным типом обратного вызова, что-то вроде следующего кошмара:
g_signal_connect_callback_void__void(GObject *object, gchar *signal,
void (*callback)(GObject *, gpointer), gpointer data);
g_signal_connect_callback_void__guint(GObject *object, gchar *signal,
void (*callback)(GObject *, guint, gpointer), gpointer data);
g_signal_connect_callback_gboolean__gdkevent(GObject *object, gchar *signal,
gboolean (*callback)(GObject *, GdkEvent *, gpointer), gpointer data);
К счастью, две особенности языка C позволяют избежать этого беспорядка.
- Указатели функций гарантированно имеют одинаковый размер, независимо от типа возвращаемого значения и аргументов функции.
- соглашение о вызовах C (технически это деталь реализации, зависящая от компилятора и архитектуры!)
Поскольку все указатели на функции имеют одинаковый размер, их можно привести к void (*callback)(void)
, для чего GCallback
является определением типа. GCallback
используется во всех API платформы GLib для обратных вызовов, которые могут иметь переменные числа и типы аргументов. Вот почему вы должны преобразовать child_test_set_age
в GCallback
в приведенном выше примере кода.
Но даже если вы можете передавать указатели на функции, как будто они все одинаковые, как убедиться, что функции действительно получают свои аргументы? Вот для чего существует соглашение о вызовах C. Компилятор генерирует код таким образом, что вызывающий объект помещает аргументы функции в стек, функция считывает аргументы из стека, но не извлекает их из стека, а когда возвращается, вызывающий объект извлекает аргументы обратно из стека. Таким образом, вызывающая сторона может передать другое количество аргументов, чем ожидает функция, пока функция может найти все аргументы, к которым она пытается получить доступ!
Давайте проиллюстрируем это на вашем примере: вызов метода child_test_set_age(ChildTest *childTest, guint ageIn, GError **error)
. Предположим, что на вашей платформе указатели и целые числа имеют одинаковый размер, и я пропущу некоторые детали, чтобы понять общую идею.
Вызывающий помещает аргументы в стек:
+------------+
| &childTest | arg1
+------------+
| 25 | arg2
+------------+
| NULL | arg3
+------------+
...и вызывает функцию. Функция получает этот стек и ищет там свои аргументы:
+------------+
| &childTest | ChildTest *childTest
+------------+
| 25 | guint ageIn
+------------+
| NULL | GError **error
+------------+
Все отлично. Затем функция возвращается, и вызывающая сторона извлекает аргументы из стека.
Теперь, однако, если вы зададите своей функции тип, отличный от сигнатуры DBus в XML, скажем, child_test_set_age(ChildTest *childTest, guint ageIn, guint otherNumberIn, GError **error)
, скажем, одни и те же аргументы помещаются в стек, но ваша функция интерпретирует их по-разному:
+------------+
| &childTest | ChildTest *childTest ...OK so far
+------------+
| 25 | guint ageIn ...still OK
+------------+
| NULL | guint otherNumberIn ...will be 0 if you try to read it, but OK
+------------+
| undefined | GError **error ...will be garbage!
| behavior |
| land!! |
| ... |
Первые два параметра в порядке. Третий, поскольку DBus не знает, что вы ожидаете еще guint
, будет преобразованием GError **
в guint
. Если вам повезло, что этот указатель был NULL
, тогда otherNumberIn
будет равно 0, но в противном случае это будет ячейка памяти, приведенная к целому числу: мусор.
Четвертый параметр особенно опасен, потому что он попытается прочитать что-то со стека, а вы понятия не имеете, что там находится. Поэтому, если вы попытаетесь получить доступ к указателю error
, вы, вероятно, сломаете свою программу из-за segfault.
Однако, если вам удастся выполнить функцию без segfault, то вызывающая программа аккуратно вытолкнет три аргумента из стека после возврата функции, и все вернется в нормальное русло. Вот почему ваша программа все еще работает.
Параметры «out» реализованы с использованием аргументов-указателей, поэтому они не работают; функция почти гарантированно записывает в недопустимый адрес памяти, потому что указатели являются мусором.
Таким образом, ваша функция может иметь другую подпись, если выполняются следующие условия:
- ваш компилятор использует соглашение о вызовах C
- ваша функция имеет то же количество аргументов (или меньше), чем ожидает вызывающая сторона
- ваши аргументы имеют тот же размер, что и аргументы, которые вызывающий абонент ожидает нажать
- ваши типы аргументов имеют смысл при приведении аргументов, которые вызывающая сторона ожидает нажать
person
ptomato
schedule
15.03.2014