The compositor boots. The module loads. The log says “activated.” Everything looks correct.

Then the screen goes black and you’re staring at a login prompt.

The Setup

A plugin system. Modules are loaded as shared objects, registered into a manager, and dispatched through GObject interfaces. The manager tracks the real module instances internally – but exposes them to the outside world through a metadata struct. Name, description, version. Read-only. Safe.

The API signature tells you exactly what you’re getting:

Returns: (element-type ModuleInfo): list of module info

Not module instances. Module info.

The Mistake

New feature: after loading modules, iterate them and pass each one its configuration. Simple loop. Get the list, walk it, call configure.

The code looked clean. It compiled without warnings. The types even had similar shapes – both are GObject-derived, both have a name field somewhere in their layout. The C type system doesn’t complain when you cast a pointer to a different pointer.

But the memory layouts are completely different. ModuleInfo is a simple boxed struct with three string pointers. The real module instance has a GObject header, private data, vtable pointers, interface implementations. When you call a virtual method on the info struct, the vtable lookup reads garbage from where the GObject class pointer should be.

Instant segfault. No recovery.

The Fix

The answer wasn’t to expose the real module instances – that breaks the encapsulation the API was designed to provide. Instead, the configure dispatch moves inside the manager, where it has direct access to the internal array of actual instances.

One new function. The caller passes in the config table. The manager walks its own modules and calls configure on each. No type confusion possible.

The Lesson

API boundaries exist for a reason. When a function returns metadata objects instead of the real thing, that’s a design choice. It’s telling you: “you don’t need the real instances, and you shouldn’t have them.”

When you find yourself casting the return value to get around that boundary, stop. The boundary is the message. Work with the API, not around it.

In languages with stronger type systems, this would be a compile error. In C with GObject, you get a black screen and a trip back to the display manager. The type system tried to tell us. We just had to listen.