Why does GObject method still get called even if callback arguments don't match those in XML?

两盒软妹~` 提交于 2019-12-01 09:26:47

You've hit on an interesting detail that exists because of the C language. Functions in C are strongly typed, and so strictly speaking you'd have to have functions to deal with every single possible type of callback, something like the following nightmare:

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);

Luckily two features of the C language make it possible to avoid that mess.

  • Function pointers are guaranteed to be the same size, no matter the return type and arguments of the function.
  • The C calling convention (technically a compiler-dependent and architecture-dependent implementation detail!)

Since function pointers are all the same size, it's OK to cast them all to void (*callback)(void), which is what GCallback is a typedef for. GCallback is used in all GLib-platform API for callbacks that can have variable numbers and types of arguments. That's why you have to cast child_test_set_age to GCallback in your code sample above.

But even if you can pass function pointers around as if they're all the same, how do you make sure the functions actually get their arguments? That's what the C calling convention is for. The compiler generates code such that the caller pushes the function's arguments on to the stack, the function reads the arguments from the stack but doesn't pop them, and when it returns, the caller pops the arguments back off the stack. So the caller can push a different number of arguments than the function expects, as long as the function can find all the arguments it tries to access!

Let's illustrate that with your example: calling the method child_test_set_age(ChildTest *childTest, guint ageIn, GError **error). Let's assume pointers and integers are the same size on your platform, and I'll skip over some details in order to get the general idea across.

The caller puts the arguments onto the stack:

+------------+
| &childTest |   arg1
+------------+
| 25         |   arg2
+------------+
| NULL       |   arg3
+------------+

...and calls the function. The function gets that stack and looks for its arguments there:

+------------+
| &childTest |   ChildTest *childTest
+------------+
| 25         |   guint ageIn
+------------+
| NULL       |   GError **error
+------------+

Everything is fine. Then the function returns and the caller pops the arguments off the stack.

Now, however, if you give your function a different type than the DBus signature in the XML, let's say child_test_set_age(ChildTest *childTest, guint ageIn, guint otherNumberIn, GError **error), let's say the same arguments get pushed onto the stack, but your function interprets them differently:

+------------+
| &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!!     |
| ...        |

The first two parameters are fine. The third one, since DBus doesn't know you're expecting another guint, is going to be the GError ** cast to a guint. If you're lucky enough that that pointer was NULL, then otherNumberIn will be equal to 0, but otherwise it'll be a memory location cast to an integer: garbage.

The fourth parameter is especially dangerous, because it will try to read something off the stack, and you have no idea what's on there. So if you try to access the error pointer, you'll probably crash your program with a segfault.

However, if you manage to get through the function without a segfault, then the caller will neatly pop three arguments off the stack after the function returns and everything will be back to normal. So that's why your program still seems to work.

The "out" parameters are implemented using pointer arguments, and so that's why they don't work; the function is almost guaranteed to write to an invalid memory address because the pointers are garbage.

In summary, your function can have a different signature if the following conditions hold:

  • your compiler uses the C calling convention
  • your function has the same number of arguments (or fewer) than the caller expects it to
  • your arguments are each the same size as the arguments the caller expects to push
  • your argument types make sense when casting the arguments the caller expects to push
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!