Revisiting signal handlers once more
Posted on November 14, 2021, under Programming, Whisker Menu
This is a bit of a long post about a purely technical change inside of Whisker Menu. I quite enjoyed solving it, but the change is invisible to end users. All code is under the GNU GPL version 2 or later, same as Whisker Menu.
The adventure begins
I have been reasonably content with how I handle connecting GLib signals to C++ member functions in Whisker Menu. A while ago I even switched back to C++11 and variadic templates as that standard is old enough now that everybody supports it quite well. It makes the code much cleaner behind the scenes.
Recently, though, I was inspired to revisit the signal handlers in Whisker Menu after discovering that the other Xfce plugins written in C++ use lambdas. That was an intriguing idea and got me thinking about how I could do it myself. After all, I like using lambdas in my Qt projects.
Why use lambdas if I am okay with the current code? Well, lambdas let you capture variables, so I don’t need to write extra functions to bind variables. Putting the signal handling code directly next to the signal itself makes it more clear what will happen in response to that signal. And, for the most part, my signal handlers are tiny and having to scroll around in the document to add or modify them is frustrating (especially the settings dialog, which has a ton of one-line signal handlers).
There are still some cases where a lambda will call a separate function, but through the wonders of inlining those calls will frequently be replaced by the compiler with the function they are calling. In fact, looking at the assembly output, the entire lambda is usually inlined in the end, so it is almost like having a C signal handler. But I am getting ahead of myself.
So! How to connect a lambda to a GLib signal?
The first solution
The simplest approach doesn’t require extra shared code!
class Example
{
public:
Example()
{
GtkWidget* widget = gtk_button_new();
g_signal_connect(G_OBJECT(widget), "button-press-event",
G_CALLBACK(
+[](GtkWidget*, GdkEvent*, gpointer user_data) -> gboolean
{
Example* example = static_cast<Example*>(user_data);
… do stuff …
return GDK_EVENT_PROPAGATE;
}
),
this);
}
};
Yeah, no. Static casts to get an equivalent "this" pointer" and no binding variables. Yikes. It works, but I don’t think it is very readable. Definitely not something I would want to work with for all the signal handlers in Whisker Menu. This provides nothing over the current approach, and it has definite drawbacks.
Templates to the rescue!
It wasn’t hard to modify my current signal handler helper code to end up with something that stored and called lambdas, but it unfortunately could not deduce the types for me. So I ended up having to write the types out twice. Yuck. It was a good start, but I needed to sit on it for a few days.
enum class Connect
{
Default = 0,
After = G_CONNECT_AFTER
};
template<typename R, typename... Args, typename Sender, typename Func>
gulong connect(Sender instance, const gchar* detailed_signal, Func func, Connect flags = Connect::Default)
{
class Slot
{
Func m_func;
public:
Slot(Func func) :
m_func(func)
{
}
static R invoke(Args... args, gpointer user_data)
{
return static_cast<Slot*>(user_data)->m_func(args...);
}
static void destroy(gpointer data, GClosure*)
{
delete static_cast<Slot*>(data);
}
};
return g_signal_connect_data(G_OBJECT(instance),
detailed_signal,
G_CALLBACK(&Slot::invoke),
new Slot(func),
&Slot::destroy,
GConnectFlags(flags));
}
class Example()
{
Example()
{
GtkWidget* widget = gtk_button_new();
connect<gboolean, GtkWidget*, GdkEvent*>(widget, "button-press-event",
[](GtkWidget*, GdkEvent*) -> gboolean
{
… do stuff …
return GDK_EVENT_PROPAGATE;
});
}
};
Solved!
I was certain that the compiler had to be able to find the types itself. Maybe I could make the Slot
class a partial specialization? That would allow me to not specify the types in the template of the connect()
function. But how to get the compiler to determine the types for the partial specialization?
After some research, I figured it out. Use decltype
on the operator()
function of the lambda! Since lambdas are basically functors built into the language, they all have an operator()
function that is called behind the scenes to make the magic work. And decltype
will get me the return type and argument types!
enum class Connect
{
Default = 0,
After = G_CONNECT_AFTER
};
template<typename Func, typename T>
class Slot
{
};
template<typename Func, typename R, typename T, typename... Args>
class Slot<Func, R(T::*)(Args...) const>
{
Func m_func;
public:
Slot(Func func) :
m_func(func)
{
}
static R invoke(Args... args, gpointer user_data)
{
return static_cast<Slot*>(user_data)->m_func(args...);
}
static void destroy(gpointer data, GClosure*)
{
delete static_cast<Slot*>(data);
}
};
template<typename Sender, typename Func>
gulong connect(Sender instance, const gchar* detailed_signal, Func func, Connect flags = Connect::Default)
{
typedef Slot<Func, decltype(&Func::operator())> Receiver;
return g_signal_connect_data(G_OBJECT(instance),
detailed_signal,
G_CALLBACK(&Receiver::invoke),
new Receiver(func),
&Receiver::destroy,
GConnectFlags(flags));
}
class Example()
{
Example()
{
GtkWidget* widget = gtk_button_new();
connect(widget, "button-press-event",
[](GtkWidget*, GdkEvent*) -> gboolean
{
… do stuff …
return GDK_EVENT_PROPAGATE;
});
}
};
Astute readers have probably noticed that I am seemingly unnecessarily specifying the return type of the lambdas. It is true that C++ will happily deduce it for me, but I want to make sure that the function signatures exactly match the signal handlers since GLib casts everything to void*
.
This was a lot of fun to figure out, and I am quite happy with it. Much happier than I was with the old signal handler code!