Blog

CMake Build Trees Are Disposable

My KDE 4 build kept compiling code for a library I had just disabled. I checked the option twice, added a loud message to CMakeLists.txt, and still got the old generated header. The compiler was not being stubborn. I was looking at leftovers.

The first clue was CMakeCache.txt: the build directory had been configured against another source checkout. The second was a generated config.h whose timestamp did not move when I reran CMake. An earlier configure had put a copy in the source tree, and that directory appeared first in the include path.

make clean did not help. It removed compiled output known to the generated makefiles; it did not remove the cache, old makefiles, or a header generated by an obsolete rule. That distinction cost me most of the afternoon.

The useful diagnosis was simple. I configured a brand-new build directory from the same checkout and compared the generated command line and config.h. The fresh tree had the option I expected. At that point there was no reason to repair the old tree one cache entry at a time, so I discarded it and configured again.

I also removed the generated header from the source tree and made its destination unambiguous:

configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/config.h
)

Now the binary include directory supplies config.h, and a fresh checkout cannot accidentally inherit one. If the clean build disagrees with the old build, I check the old cache’s source path, generated-file timestamps, and include order before blaming CMake detection.

Build trees are cheap. An hour spent negotiating with stale generated CMake files is not.

Network State Is Not a Boolean

My test applet announced “offline” while NetworkManager was still associating with an access point. A second later it announced “online.” Technically energetic, socially useless.

I had reduced state to a boolean. The mechanism has transitions: unavailable, disconnected, preparing, configuring, connected, and failure are meaningfully different to a user and to an application deciding whether to retry.

The applet now maps backend states into three presentation states:

enum Connectivity {
    Offline,
    Connecting,
    Online
};

It preserves the detailed state in the adapter for diagnostics, but the paint code does not understand every NetworkManager value. Solid provides the abstraction boundary; the widget receives only what it can honestly display.

I prefer reacting to state-change notifications rather than polling every second. Polling wastes work and can still miss a short transition. The caveat is startup: the client must query initial state before relying on later notifications, or it may wait forever for a change that already happened.

“Online” also means only that the connection mechanism considers a network active. It does not guarantee a working name server, reachable destination, or paid hotel login. The applet should report connection state, not certify the Internet.

Three states are less satisfyingly binary. They are also less wrong, an increasingly fashionable property in software.

D-Bus 1.0 and Boring Interfaces

D-Bus 1.0 arrived last week, so I replaced a small experimental IPC call today and discovered that my “simple” interface exposed an internal class name, an internal enum, and optimism about call order.

The wire has four names worth keeping straight: a bus name identifies the service, an object path identifies an object, an interface groups methods and signals, and a member names the operation. Treating all four as one application string works until the service grows a second object.

For a thumbnail service I settled on deliberately plain values:

service:   org.kde.Thumbnailer
path:      /Thumbnailer
interface: org.kde.Thumbnailer
method:    Queue(string url, int width, int height)
signal:    Ready(string url, string file)

The exact names may change; the useful constraint is that callers do not need my C++ headers or object layout. Strings and integers cross the process boundary predictably. Errors should cross as named errors, not magic negative dimensions.

A method call can be synchronous, but that does not make blocking the GUI wise. Thumbnail generation may involve disk access or a broken file. The caller should queue work and react to completion, while also handling service disappearance. Processes are allowed to crash independently; that is among their principal features.

I prefer small, boring bus interfaces with operations phrased in domain terms. They are easier to inspect and less likely to preserve an accidental implementation forever. The caveat is round-trip cost and versioning. Turning every getter into a remote call creates slow, chatty code, and changing a published signature later can break clients I do not control.

Introspection is useful during development, but readable metadata does not replace a written contract. Callers still need to know whether requests are idempotent, which errors are expected, and how long returned object paths remain valid.

For KDE 4 porting, D-Bus 1.0 gives us a stable point to build against, not permission to export every object. I am writing the interface first, testing it from a separate process, and only then attaching the current implementation. If the test needs a private header, the boundary has failed.

Today’s service returns one error instead of hanging when the source file vanishes. This is less exciting than transparent desktop integration, but substantially more useful when transparent desktop integration loses a file.

Plasma Data Before Decoration

I changed the weather prototype’s frame today and accidentally made it fetch the forecast again. The border and the network request had become neighbours in one class, then naturally began borrowing each other’s tools.

The experiment now separates a data source from the visualisation. The source produces named values and announces updates. The applet chooses how to present them. Fake data is enough to exercise the boundary:

QVariantMap WeatherSource::snapshot() const
{
    QVariantMap data;
    data.insert("condition", "cloudy");
    data.insert("temperature", 12);
    data.insert("location", "Porto Alegre");
    return data;
}

The applet receives a complete snapshot instead of reading half-updated fields one by one. This matters if the source later performs work asynchronously: a temperature from the new report paired with yesterday’s condition is internally consistent only in avant-garde meteorology.

The visual item invalidates its content when data changes. Theme or frame changes invalidate geometry and decoration, but do not restart the source. Keeping those paths separate avoids needless work and gives the containment freedom to alter presentation.

For this little applet, pullable snapshots plus one update notification beat a long series of property-specific signals. A new field can appear without adding another connection, and a newly created applet can request current state immediately. Copying a large map for frequent updates would be wasteful, though, and weakly typed keys invite spelling mistakes. This fits small desktop data, not every stream.

Error data travels in the same snapshot rather than through a modal dialog. The applet can then show stale values with a warning, hide them, or offer a retry according to the space and purpose of that visualisation.

There are unresolved questions. Sources need lifetimes, update intervals, error states, and perhaps sharing between several applets. A clock should not require sixty private timer objects merely because sixty visualisations exist. On the other hand, premature sharing can couple applets through one source’s policy.

This remains a Plasma prototype, not evidence of a completed KDE 4 shell. The useful result is that I can replace the rectangle with text, or the text with a graph, without changing how forecast data arrives.

The forecast itself still says cloudy. At least this time it did not download new clouds when I changed the border colour.

Const Does Not Mean Concurrent

A cache lookup crashed under load today even though every worker called a const method. That sentence sounds reassuring only if const is mistaken for a locking primitive.

The method updated a mutable cache:

QImage ImageStore::image(const QString &key) const
{
    if (!m_cache.contains(key))
        m_cache.insert(key, loadImage(key));
    return m_cache.value(key);
}

const prevents ordinary modification through the public type. It says nothing about simultaneous calls, and mutable explicitly allows internal state to change. Two processors can both inspect and modify the cache, corrupting its internal bookkeeping.

I first added a mutex around the whole operation. That was correct but serialised disk loading, so unrelated images waited behind one slow file. The improved version checks under lock, loads outside it, then locks again to publish:

QImage ImageStore::image(const QString &key) const
{
    m_mutex.lock();
    if (m_cache.contains(key)) {
        QImage image = m_cache.value(key);
        m_mutex.unlock();
        return image;
    }
    m_mutex.unlock();

    QImage image = loadImage(key);

    m_mutex.lock();
    if (!m_cache.contains(key))
        m_cache.insert(key, image);
    else
        image = m_cache.value(key);
    m_mutex.unlock();
    return image;
}

Duplicate loading is possible, but publication is safe and the common cached path is short. For expensive loads I might track in-progress keys with a wait condition; that complexity needs measurements first.

Implicitly shared Qt values make returning an image cheap in the usual case, but they do not make shared storage safe for concurrent mutation. Copy-on-write protects value semantics, not arbitrary access patterns.

The lock itself belongs beside the state it protects. Passing the cache to helper code without the mutex invites a second access path that quietly ignores the rule. I am documenting the invariant as “all cache lookup and publication occurs while holding m_mutex,” then checking each method against that sentence.

I prefer immutable inputs and per-job results when using several cores. Shared caches should be small, explicitly locked, and treated as an optimisation rather than the foundation of correctness. The caveat is memory and copying: some data is too large to duplicate, so careful shared ownership remains necessary.

The failure appeared only on the dual-core machine and only after several minutes. That does not make it a rare bug. It makes the single-core test machine an excellent accomplice.