C, Type Systems, GLib, and API Design
I was working on xfce4-session, and encountered a weird segfault that only happened in one particular case: while I was testing some new code in xfwl4 that restarts xfce4-session if it crashes.
I tested the feature by manually killing xfce4-session, but then it
would actually crash once it restarted (and then get restarted, and
crash again, and get restarted, and…). The crash was confusing. In
the function that handles when an app wants to register over D-Bus, I’d
added code to iterate over the elements of a
GQueue of running
session-managed applications to ensure that a new app wasn’t trying to
register with the same ID that a previous app had used. I was seeing
garbage inside the client instances, and the first list item’s next
pointer was 0x1, which… well, not great.
On top of that, I couldn’t reproduce the crash with any diagnostic
tooling: when running under valgrind, gcc’s address sanitizer, or even
gdb (with a watchpoint enabled), there would be no crash. Even if I
started xfce4-session manually, running it in a while true bash loop,
so it would continually restart on exit, I couldn’t get it to crash. So
it was timing-dependent, great.
I spent a ton of time digging through the core file with gdb, a ton of time just staring at the code, and then tossed it at GLM 5.3 Flash to see if it could make sense of it. The LLM added more logging and I dutifully helped it gather more data. After an hour or so of this, it seemed to have mostly given up.
I was tired, and it was already well past a reasonable hour to be awake (I’d been out with friends earlier, and was working on some code as a way of winding down for the night), so I went to bed, hoping the answer would come to me after sleeping, as often is the case for me. But by the time I decided to work on it again the next day, I still had no answers.
So I told the LLM to summarize what we’d learned in a markdown file, and then handed that to a new session with the non-Flash version of GLM 5.3, assuming it would be more capable. And so it was. It churned on the problem for about five minutes, and told me I was using the wrong API to fetch the list head pointer to start the iteration. (And, amusingly, bluntly affirmed that whoever was working on it before and wrote the handoff document was completely wrong about what the problem might be.)
A GQueue is a GList
under the hood (the queue structure is just a list head pointer, tail
pointer, and length). It has some functions for interacting with it and
fetching the head or tail. I was using
g_queue_peek_head(),
which I thought would return the GList pointer, but returns the data
pointer of whatever is stored in the queue head’s list struct. What I
really wanted was
g_queue_peek_head_link().
🤦🏻♂ 🤦🏻♂ 🤦🏻♂
I blame:
Myself, of course. I wrote the code, and I have an auto-completion
plugin set up in vim, and probably even noticed the other completions
after typing g_list_peek_head, but ignored them and/or thought they
weren’t relevant to what I was doing, even one with “link” in the name.
But I also blame:
C’s weak type system, and type coercion rules. The return value of
g_queue_peek_head() is gpointer (aka void *), and C lets you
assign void pointers to any other pointer type, without a cast or
warning. I was assigning it to a GList *, and the compiler was fine
with that.
C’s lack of generics1. g_queue_peek_head() returns void *
because it doesn’t (and can’t) know or track what type you’ve actually
put into it. If it could, I would have gotten an error when trying to
assign a XfsmClient * to a GList *.
GLib’s inconsistent API. I reached for g_queue_peek_head() because I
am very familiar with GList, and am not super familiar with GQueue.
GList has:
g_list_nth(), which returns theGList *node at the nth position, andg_list_nth_data(), which returns the data in the list at the nth position.
So I assumed that GQueue would have g_queue_peek_head() (it does),
which would return the GList pointer (it doesn’t) and
q_queue_peek_head_data() (it doesn’t), which would return the data
pointer. But no, instead it’s g_queue_peek_head_link() and
g_queue_peek_head().2
The first two are among the many reasons why after 25+ years, I don’t enjoy working with C code anymore, and prefer something like Rust. It has a strong type system and generics, and type confusion is much harder, to the point of being essentially impossible in code like this.
Anyhow, speaking of unreasonable hours to be awake, I should go and… not be awake.
-
Yes, I know about
_Generic. It’s very limited, often awkward to use, and not really relevant here. You can’t build a truly generic data type out of it, because you need to write out all the “generic” types you plan to use with it ahead of time, when declaring it. That’s impossible for a library to do; of course it has no idea what random users of the library will use it for. ↩ -
GQueuealso has direct analogues to theGListfunctions I mentioned,g_queue_peek_nth()andg_queue_peek_nth_link(). That’s consistent with the otherGQueuefunctions, but not withGList. I actually do thinkGQueue’s names are better! But I think consistency is more important. ↩