The idea of having a container for a value that is not yet present, but will be available sometime in the future is a powerful one. Unlike call-callbacks, it decouples the asynchronous process invocation from the result handler. This allows creating nicer asynchronous APIs than the usual approaches.

The Qt library provides a container like this – the QFuture<T>. And, at the same time, it provides incompatible special-cases containers like QDBusReply<T> and other Q*Reply classes. Initially, QFuture was meant to be used only in the QtConcurrent library, and was designed to suit that need – to wrap operations executed concurrently on multiple threads. It was moved to QtCore in 5.x, but apart from the rellocation, it did not receive much love, and its design stayed the same.

Issues with waiting

In order to do something with the result, you need to wait for the future to arrive. It you do not want to block your main thread, you need to wait for the future like this:

auto watcher = new QFutureWatcher<SomeType>();
               // remember to free later

connect(watcher, QFutureWatcherBase::finished,
        … slot to connect to …);
connect(watcher, QFutureWatcherBase::canceled,
        … slot to connect to …);

This makes using QFuture a pain. I’ve been creating wrappers for this on multiple occasions, and realised that I do it often enough to warrant creating a new library.

Issues with creation

Another issue with QFuture is that Qt does not really use it. The previously mentioned classes like QDBusReply are a perfect example. Imagine you are writing an interface library for some DBus service.

You can make it blocking

QString serviceVersion() const;

which is very bad since the service might take a long time to respond.

You can make it asynchronous by returning QDBusReply

QDBusReply<QString> serviceVersion() const;

which exposes the implementation detail via the API. Also a very bad approach.

Or, you can split it into a request slot and a response signal

void fetchServiceVersion() const;
Q_SIGNALS: void serviceVersionRetrieved(const QString &version);

which is plain ugly to write and use.

If the QFuture<QString> is used instead, you get exactly what you need – it is asynchronous, it hides all the implementation details (the user does not know whether it uses DBus, executes an external process and returns its output, etc.) and it is not ugly.

The problem is that Qt does not provide a way to allow you to convert these different future-like constructs to QFuture. This is the second reason behind the creation of the AsynQt framework.

What is AsynQt?

The aims of the framework are:

  • To provide wrappers for common Qt future-like classes
  • To add methods for easier manipulation of QFutures

Wrappers

So far, I’ve implemented wrappers for the DBus and QProcess.

You can convert the QDBusReply<T> to a QFuture<T> simply by using the makeFuture template function, or you can invoke a DBus method and get the QFuture as the result immediately:

QFuture<QStirng> future =
    AsynQt::DBus::asyncCall<QString>(interface, method, arg1, …)

QProcess wrapper is a bit trickier because it has one special feature. Apart from the process, it receives a function that allows you to extract the information you want to return from the process, instead of returning the QFuture<QProcess*>.

Examples are worth more than words:

QFuture<int> exitCodeFuture =
    makeFuture(process, [] (QProcess *p) {
        return p->exitCode();
    });

QFuture<QByteArray> outputFuture =
    makeFuture(process, [] (QProcess *p) {
        return p->readAllStandardOutput();
    });

There are also a few convenience methods for creating simpler futures – makeReadyFuture that creates a future which already contains a value, makeCanceledFuture that creates a canceled future and makeDelayedFuture which creates a future that will contain the specified value after the specified duration of time.

auto ready    = makeReadyFuture(10);
auto canceled = makeCanceledFuture<QString>();
auto delayed  = makeDelayedFuture(42, 30s); // with c++17 std::chrono

And more will come.

Transformations

Now, all of the above would not be that useful if we still had to use the usual QFuture API. For this reason, a few new methods are provided – transform, flatten, cast, continueWith. I’ll leave the explanation of these for later (when I add a couple more different ones that I consider necessary). At this time, just a few examples:

QFuture<int> answer = meaningOfLife()
    // answer will eventually contain 42

QFuture<QString> text = transform(answer, toText)
    // text will eventually contain the result of toText(42)

QFuture<QByteArray> future =
    AsynQt::Process::getOutput("echo", { "Hello KDE" });
    // will eventually contain QByteArray("Hello KDE\n")

QFuture<QString> castFuture =
    AsynQt::qfuture_cast<QString>(future);
    // will eventually contain QString("Hello KDE\n")

Pipe or range syntax

The library also supports the pipe syntax. Instead of calling a transformation on a future directly, you can also send the future through a pipe to the transformation.

QFuture<QString> future = meaningOfLife() | transform(toText)
    // text will eventually contain the result of toText(42)

QFuture<QByteArray> future =
    AsynQt::Process::getOutput("echo", { "Hello KDE" }) | cast<QString>();
    // will eventually contain QString("Hello KDE\n")

Status

It is under heavy development. The aforementioned things work, but the API might not be stable still. I’m still deciding whether some of the names are the best possible (cast<T> vs cast_to<T> vs convert_to<T> vs as<T>), etc.

It is implemented as mostly a header-only library, I’m still wondering whether I ought to remove the ‘mostly’ part.

It should be compilable by any compiler that supports decltype, static_assert, lambdas and similar. Nothing fancy, but I am not able to test whether it can be compiled on windows at this time.

The code is in my scratch space at git@git.kde.org:scratch/ivan/libasynqt