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
QFuture
s
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
Are you aware of the work Daniel Vratil and I did on KAsync? There seems to be quite a bit of overlap in terms of a more composable async API, although the design is quite different. We did a rather poor job in communicating how it works, but the akonadi next codebase [2] relies on it rather heavily.
I think we'll eventually want to go over the API of kasync anyways, and better QFuture support (instead of our own custom future that we have right now), would be a good idea anyways.
[1] http://api.kde.org/playground-api/libs-apidocs/kasync/html/namespaceKAsync.html,
kde:kasync
[2] kde:akonadi-next