QCoro 0.11.0 Release Announcement

QCoro 0.11.0 Release Announcement

A long over-due release which has accumulated a bunch of bugfixes but also some fancy new features…read on!

As always, big thanks to everyone who reported issues and contributed to QCoro. Your help is much appreciated!

QCoro::LazyTask<T>

The biggest new features in this release is the brand-new QCoro::LazyTask<T>. It’s a new return type that you can use for your coroutines. It differs from QCoro::Task<T> in that, as the name suggest, the coroutine is evaluated lazily. What that means is when you call a coroutine that returns LazyTask, it will return imediately without executing the body of the coroutine. The body will be executed only once you co_await on the returned LazyTask object.

This is different from the behavior of QCoro::Task<T>, which is eager, meaning that it will start executing the body immediately when called (like a regular function call).

QCoro::LazyTask<int> myWorker()
{
    qDebug() << "Starting worker";
    co_return 42;
}

QCoro::Task<> mainCoroutine()
{
    qDebug() << "Creating worker";
    const auto task = myWorker();
    qDebug() << "Awaiting on worker";
    const auto result = co_await task;
    // do something with the result
}

This will result in the following output:

mainCoroutine(): Creating worker
mainCoroutine(): Awaiting on worker
myWorker(): Starting worker

If myWorker() were a QCoro::Task<T> as we know it, the output would look like this:

mainCoroutine(): Creating worker
myWorker(): Starting worker
mainCoroutine(): Awaiting on worker

The fact that the body of a QCoro::LazyTask<T> coroutine is only executed when co_awaited has one very important implication: it must not be used for Qt slots, Q_INVOKABLEs or, in general, for any coroutine that may be executed directly by the Qt event loop. The reason is, that the Qt event loop is not aware of coroutines (or QCoro), so it will never co_await on the returned QCoro::LazyTask object - which means that the code inside the coroutine would never get executed. This is the reason why the good old QCoro::Task<T> is an eager coroutine - to ensure the body of the coroutine gets executed even when called from the Qt event loop and not co_awaited.

For more details, see the documentation of QCoro::LazyTask<T>.

Defined Semantics for Awaiting Default-Constructed and Moved-From Tasks

This is something that wasn’t clearely defined until now (both in the docs and in the code), which is what happens when you try to co_await on a default-constructed QCoro::Task<T> (or QCoro::LazyTask<T>):

co_await QCoro::Task<>(); // will hang indefinitely!

Previously this would trigger a Q_ASSERT in debug build and most likely a crash in production build. Starting with QCoro 0.11, awaiting such task will print a qWarning() and will hang indefinitely.

The same applies to awaiting a moved-from task, which is identical to a default-constructed task:

QCoro::LazyTask<int> task = myTask();
handleTask(std::move(task));

co_await task; // will hang indefinitely!`

Compiler Support

We have dropped official support for older compilers. Since QCoro 0.11, the officially supported compilers are:

  • GCC >= 11
  • Clang >= 15
  • MSVC >= 19.40 (Visual Studio 17 2022)
  • AppleClang >= 15 (Xcode 15.2)

QCoro might still compile or work with older versions of those compilers, but we no longer test it and do not guarantee that it will work correctly.

The reason is that coroutine implementation in older versions of GCC and clang were buggy and behaved differently than they do in newer versions, so making sure that QCoro behaves correctly across wide range of compilers was getting more difficult as we implemented more and more complex and advanced features.

Other Features and Changes

A coroutine-friendly version of QFuture::takeResult() is now available in the form of QCoroFuture::takeResult() when building QCoro against Qt 6 (#217).

QCoro::waitFor(QCoro::Task<T>) no longer requires that the task return type T is default-constructible (#223, Joey Richey)

Bugfixes

  • Suppress Clang error when building against Android NDK <= 25 (#204, Daniel Vrátil)
  • Fixed missing QtGui dependency in QCoroQuick module (#209, Andreas Sturmlechner)
  • Fixed QCoroIODevice::write() always returning 0 instead of bytes written (#211, Daniel Vrátil)
  • Fixed unchecked std::optional access in QCoroIODevice::write
  • Fixed awaiting on signal emission with qCoro() would resume the awaiter in the sender’s thread context (#213, Daniel Vrátil)
  • Fixed build wilth clang 18 due to missing #include <exception> (#220, Micah Terhaar)
  • Fixed crash when QNetworkAccessManager is destroyed from a coroutine awaiting on a network reply (#231, Daniel Vrátil)

Full changelog

See changelog on Github

Support

If you enjoy using QCoro, consider supporting its development on GitHub Sponsors or buy me a coffee on Ko-fi (after all, more coffee means more code, right?).

KDE PIM Sprint 2024 Report

In 2021 I decided to take a break from contributing to KDE, since I felt that I’ve been losing motivation and energy to contribute for a while… But I’ve been slowly getting back to hacking on KDE stuff for the past year, which ended in me going to Toulouse this year to attend the annual KDE PIM Sprint, my first in 5 years.

I’m very happy to say that we have /a lot/ going on in PIM, and even though not everything is in the best shape and the community is quite small (there were only four of us at the sprint), we have great plans for the future, and I’m happy to be part of it.

Day 0

The sprint was officially supposed to start on Saturday, but everyone arrived already on Friday, so why wait? We wrote down the topics to discuss, put them on a whiteboard and got to it.

Whiteboard with all discussion topics

We’ve managed to discuss some pretty important topics - how we want to proceed with deprecation and removal of some components, how to improve our test coverage or how to improve indexing and much much more.

I arrived to the sprint with two big topics to discuss: milestones and testing:

Milestones

The idea is to create milestones for all our bigger efforts that we work (or want to work) on. The milestones should be concrete goals that are achievable within a reasonable time frame and have clear definition of done. Each milestones should then be split to smaller tasks that can be tackled by individuals. We hope that this will help to make KDE PIM more attractive to new contributors, who can now clearly see what is being worked on and can find very concrete, bite-sized tasks to work on.

As a result, we took all the ongoing tasks and turned most of them into milestones in Gitlab. It’s still very much work in progress, we still need to break down many milestones to smaller tasks, but the general ideas are out there.

E2E Testing of Resources

Akonadi Resources provide “bridge” between Akonadi Server and individual services, like IMAP servers, DAV servers, Google Calendar etc. But we have no tests to verify that our Resources can talk to the services and vice versa. The plan is to create a testing framework (in Python) so that we can have automated nightly tests to verify that e.g. IMAP resource interfaces properly with common IMAP server implementations, including major proprietary ones like Gmail or Office365. We want to achieve decent coverage for all our resources. This is a big project, but I think it’s a very exciting one as it includes not just programming, but also figuring out and building some infrastructure to run e.g. Dovecot, NextCloud and others in a Docker to test against.

Day 1

On Saturday we started quite early, all the delicious french pastry is not going to eat itself, is it? After breakfast we continued with discussions, we dicussed tags support, how to improve our PR. But we also managed to produce some code. I implemented syncing of iCal categories with Akonadi tags, so the tags are becoming more useful. I also prepared Akonadi to be cleanly handle planned deprecation and retirement of KJots, KNotes and their acompanying resources, as well as planned removal of the Akonadi Kolab Resource (in favor of using IMAP+DAV).

One of the tasks I want to look into is improving how we do database transactions in the Akonadi Server. To get some data out of it, I shoved Prometheus exporter into Akonadi, hooked it up to a local Prometheus service, thrown together a Grafana dashboard, and here we are:

Grafana dashboard

We decided to order some pizzas for dinner and stayed at the venue hacking until nearly 11 o’clock.

Day 2

On the last day of the sprint we wrapped up on the discussions and focused on actually implementing some of the ideas. I spent most of the time extending the Migration agent to extract tags from all existing events and todos already stored in Akonadi and helped to create some of the milestones on the Gitlab board. We also came up with a plan for KDE PIM BoF on this years Akademy, where we want to present out progress on the respective milestones and to give a chance to contributors to learn what are the biggest hurdles they are facing when trying to contribute to KDE PIM and how we can help make it easier for them to get involved.

Conclusion

I think it was a very productive sprint and I am really excited to be involved in PIM again. Can’t wait to meet up with everyone again on Akademy in September.

Go check out Kevin’s and Carl’s reports to see what else have they been up to during the sprint.

Did some of the milestones caught your eye, or do you have have any questions? Come talk to us in our matrix channel.

Finally, many thanks to Kevin for organizing the sprint, Étincelle Coworking for providing us with nice and spacious venue and KDE e.V. for supporting us on travel.

Finally, if you like such meetings to happen in the future so that we can push forward your favorite software, please consider making a tax-deductible donation to the KDE e.V. foundation.

QCoro 0.10.0 Release Announcement

QCoro 0.10.0 Release Announcement

Thank you to everyone who reported issues and contributed to QCoro. Your help is much appreciated!

Support for awaiting Qt signals with QPrivateSignal

Qt has a feature where signals can be made “private” (in the sense that only class that defines the signal can emit it) by appending QPrivateSignal argument to the signal method:

class MyObject : public QObject {
    Q_OBJECT
...
Q_SIGNALS:
    void error(int code, const QString &message, QPrivateSignal);
};

QPrivateSignal is a type that is defined inside the Q_OBJECT macro, so it’s private and as such only MyObject class can emit the signal, since only MyObject can instantiate QPrivateSignal:

void MyObject::handleError(int code, const QString &message)
{
    Q_EMIT error(code, message, QPrivateSignal{});
}

QCoro has a feature that makes it possible to co_await a signal emission and returns the signals arguments as a tuple:


MyObject myObject;
const auto [code,  message] = co_await qCoro(&myObject, &MyObject::handleError);

While it was possible to co_await a “private” signal previously, it would get return the QPrivateSignal value as an additional value in the result tuple and on some occasions would not compile at all.

In QCoro 0.10, we can detect the QPrivateSignal argument and drop it inside QCoro so that it does not cause trouble and does not clutter the result type.

Achieving this wasn’t simple, as it’s not really possible to detect the type (because it’s private), e.g. code like this would fail to compile, because we are not allowed to refer to Obj::QPrivateSignal, since that type is private to Obj.

template<typename T, typename Obj>
constexpr bool is_qprivatesignal = std::same_as_v<T, typename Obj::QPrivateSignal>;

After many different attempts we ended up abusing __PRETTY_FUNCTION__ (and __FUNCSIG__ on MSVC) and checking whether the function’s name contains QPrivateSignal string in the expected location. It’s a whacky hack, but hey - if it works, it’s not stupid :). And thanks to improvements in compile-time evaluation in C++20, the check is evaluated completely at compile-time, so there’s no runtime overhead of obtaining current source location and doing string comparisons.

Source Code Reorganization (again!)

Big part of QCoro are template classes, so there’s a lot of code in headers. In my opinion, some of the files (especially qcorotask.h) were getting hard to read and navigate and it made it harder to just see the API of the class (like you get with non-template classes), which is what users of a library are usually most interested in.

Therefore I decided to move definitions into separated files, so that they don’t clutter the main include files.

This change is completely source- and binary-compatible, so QCoro users don’t have to make any changes to their code. The only difference is that the main QCoro headers are much prettier to look at now.

Bugfixes

  • QCoro::waitFor() now re-throws exceptions (#172, Daniel Vrátil)
  • Replaced deprecated QWebSocket::error with QWbSocket::errorOccured in QCoroWebSockets module (#174, Marius P)
  • Fix QCoro::connect() not working with lambdas (#179, Johan Brüchert)
  • Fix library name postfix for qmake compatibilty (#192, Shantanu Tushar)
  • Fix std::coroutine_traits isn't a class template error with LLVM 16 (#196, Rafael Sadowski)

Full changelog

See changelog on Github

QCoro 0.8.0 Release Announcement

QCoro 0.8.0 Release Announcement

This is a rather small release with only two new features and one small improvement.

Big thank you to Xstrahl Inc. who sponsored development of new features included in this release and of QCoro in general.

And as always, thank you to everyone who reported issues and contributed to QCoro. Your help is much appreciated!

The original release announcement on qcoro.dvratil.cz.

Improved QCoro::waitFor()

Up until this version, QCoro::waitFor() was only usable for QCoro::Task<T>. Starting with QCoro 0.8.0, it is possible to use it with any type that satisfies the Awaitable concept. The concept has also been fixed to satisfies not just types with the await_resume(), await_suspend() and await_ready() member functions, but also types with member operator co_await() and non-member operator co_await() functions.

QCoro::sleepFor() and QCoro::sleepUntil()

Working both on QCoro codebase as well as some third-party code bases using QCoro it’s clear that there’s a usecase for a simple coroutine that will sleep for specified amount of time (or until a specified timepoint). It is especially useful in tests, where simulating delays, especially in asynchronous code is common.

Previously I used to create small coroutines like this:

QCoro::Task<> timer(std::chrono::milliseconds timeout) {
    QTimer timer;
    timer.setSingleShot(true);
    timer.start(timeout);
    co_await timer;
}

Now we can do the same simply by using QCoro::sleepFor().

Read the documentation for QCoro::sleepFor() and QCoro::sleepUntil() for more details.

QCoro::moveToThread()

A small helper coroutine that allows a piece of function to be executed in the context of another thread.

void App::runSlowOperation(QThread *helperThread) {
    // Still on the main thread
    ui->statusLabel.setText(tr("Running"));

    const QString input = ui->userInput.text();

    co_await QCoro::moveToThread(helperThread);
    // Now we are running in the context of the helper thread, the main thread is not blocked

    // It is safe to use `input` which was created in another thread
    doSomeComplexCalculation(input);

    // Move the execution back to the main thread
    co_await QCoro::moveToThread(this->thread());
    // Runs on the main thread again
    ui->statusLabel.setText(tr("Done"));
}

Read the documentation for QCoro::moveToThread for more details.

Full changelog

See changelog on Github

QCoro 0.7.0 Release Announcement

QCoro 0.7.0 Release Announcement

The major new feature in this release is initial QML support, contributed by Jonah Brüchert. Jonah also contributed QObject::connect helper and a coroutine version of QQuickImageProvider. As always, this release includes some smaller enhancements and bugfixes, you can find a full list of them on the Github release page.

As always, big thank you to everyone who report issues and contributed to QCoro. Your help is much appreciated!

Initial QML Support

Jonah Brüchert has contributed initial support for QML. Unfortunately, we cannot extend the QML engine to support the async and await keywords from ES8, but we can make it possible to set a callback from QML that should be called when the coroutine finishes.

The problem with QCoro::Task is that it is a template class so it cannot be registered into the QML type system and used from inside QML. The solution that Jonach has come up with is to introduce QCoro::QmlTask class, which can wrap any awaitable (be it QCoro::Task or any generic awaitable type) and provides a then() method that can be called from QML and that takes a JavaScript function as its only argument. The function will be invoked by QCoro::QmlTask when the wrapped awaitable has finished.

The disadvantage of this approach is that in order to expose a class that uses QCoro::Task<T> as return types of its member functions into QML, we need to create a wrapper class that converts those return types to QCoro::QmlTask.

Luckily, we should be able to provide a smoother user experience when using QCoro in QML for Qt6 in a future QCoro release.

class QmlCoroTimer: public QObject {
    Q_OBJECT
public:
    explicit QmlCoroTimer(QObject *parent = nullptr)
        : QObject(parent)
    {}

    Q_INVOCABLE QCoro::QmlTask start(int milliseconds) {
        // Implicitly wraps QCoro::Task<> into QCoro::QmlTask
        return waitFor(milliseconds);
    }

private:
    // A simple coroutine that co_awaits a timer timeout
    QCoro::Task<> waitFor(int milliseconds) {
        QTimer timer;
        timer.start(milliseconds);
        co_await timer;
    }
};

...
QCoro::Qml::registerTypes();
qmlRegisterType<QmlCoroTimer>("cz.dvratil.qcoro.example", 0, 1);
import cz.dvratil.qcoro.example 1.0

Item {

    QmlCoroTimer {
        id: timer
    }

    Component.onCompleted: () {
        // Attaches a callback to be called when the QmlCoroTimer::waitFor()
        // coroutine finishes.
        timer.start(1000).then(() => {
            console.log("1 second elapsed!");
        });
    }
}

Read the documentation for QCoro::QmlTask for more details.

QCoro::connect Helper

The QCoro::connect() helper is similar to QObject::connect() - except you you pass in a QCoro::Task<T> instead of a sender and signal pointers. While using the .then() continuation can achieve similar results, the main difference is that QCoro::connect() takes a pointer to a context (receiver) QObject. If the receiver is destroyed before the connected QCoro::Task<T> finishes, the slot is not invoked.

void MyClass::buttonClicked() {
    QCoro::Task<QByteArray> task = sendNetworkRequest();
    // If this object is deleted before the `task` completes,
    // the slot is not invoked.
    QCoro::connect(std::move(task), this, &handleNetworkReply);
}

See the QCoro documentation for more details.

Full changelog

See changelog on Github

QCoro 0.6.0 Release Announcement

I’m pleased to announce release 0.6.0 of QCoro, a library that allows using C++20 coroutines with Qt. This release brings several major new features alongside a bunch of bugfixes and improvements inside QCoro.

The four major features are:

  • Generator support
  • New QCoroWebSockets module
  • Deprecated task.h
  • Clang-cl and apple-clang support

🎉 Starting with 0.6.0 I no longer consider this library to be experimental (since clearly the experiment worked :-)) and its API to be stable enough for general use. 🎉

As always, big thank you to everyone who report issues and contributed to QCoro. Your help is much appreciated!

Generator support

Unlike regular functions (or QCoro::Task<>-based coroutines) which can only ever produce at most single result (through return or co_return statement), generators can yield results repeatedly without terminating. In QCoro we have two types of generators: synchronous and asynchronous. Synchronous means that the generator produces each value synchronously. In QCoro those are implemented as QCoro::Generator<T>:

// A generator that produces a sequence of numbers from 0 to `end`.
QCoro::Generator<int> sequence(int end) {
    for (int i = 0; i <= end; ++i) {
        // Produces current value of `i` and suspends.
        co_yield i;
    }
    // End the iterator
}

int sumSequence(int end) {
    int sum = 0;
    // Loops over the returned Generator, resuming the generator on each iterator
    // so it can produce a value that we then consume.
    for (int value : sequence(end)) {
        sum += value;
    }
    return sum;
}

The Generator interface implements begin() and end() methods which produce an iterator-like type. When the iterator is incremented, the generator is resumed to yield a value and then suspended again. The iterator-like interface is not mandated by the C++ standard (the C++ standard provides no requirements for generators), but it is an intentional design choice, since it makes it possible to use the generators with existing language constructs as well as standard-library and Qt features.

You can find more details about synchronous generators in the QCoro::Generator<T> documentation.

Asynchronous generators work in a similar way, but they produce value asynchronously, that is the result of the generator must be co_awaited by the caller.

QCoro::AsyncGenerator<QUrl> paginator(const QUrl &baseUrl) {
  QUrl pageUrl = baseUrl;
  Q_FOREVER {
    pageUrl = co_await getNextPage(pageUrl); // co_awaits next page URL
    if (pageUrl.isNull()) { // if empty, we reached the last page
      break; // leave the loop
    }
    co_yield pageUrl; // finally, yield the value and suspend
  }
  // end the generator
}

QCoro::AsyncGenerator<QString> pageReader(const QUrl &baseUrl) {
  // Create a new generator
  auto generator = paginator(baseUrl);
  // Wait for the first value
  auto it = co_await generator.begin();
  auto end = generator.end();
  while (it != end) { // while the `it` iterator is valid...
    // Asynchronously retrieve the page content
    const auto content = co_await fetchPageContent(*it);
    // Yield it to the caller, then suspend
    co_yield content;
    // When resumed, wait for the paginator generator to produce another value
    co_await ++it;
  }
}

QCoro::Task<> downloader(const QUrl &baseUrl) {
  int page = 1;
  // `QCORO_FOREACH` is like `Q_FOREACH` for asynchronous iterators
  QCORO_FOREACH(const QString &page, pageReader(baseUrl)) {
    // When value is finally produced, write it to a file
    QFile file(QStringLiteral("page%1.html").arg(page));
    file.open(QIODevice::WriteOnly);
    file.write(page);
    ++page;
  }
}

Async generators also have begin() and end() methods which provide an asynchronous iterator-like types. For one, the begin() method itself is a coroutine and must be co_awaited to obtain the initial iterator. The increment operation of the iterator must then be co_awaited as well to obtain the iterator for the next value. Unfortunately, asynchronous iterator cannot be used with ranged-based for loops, so QCoro provides QCORO_FOREACH macro to make using asynchronous generators simpler.

Read the documentation for QCoro::AsyncGenerator<T> for more details.

New QCoroWebSockets module

The QCoroWebSockets module provides QCoro wrappers for QWebSocket and QWebSocketServer classes to make them usable with coroutines. Like the other modules, it’s a standalone shared or static library that you must explicitly link against in order to be able to use it, so you don’t have to worry that QCoro would pull websockets dependency into your project if you don’t want to.

QCoro::Task<> ChatApp::handleNotifications(const QUrl &wsServer) {
  if (!co_await qCoro(mWebSocket).open(wsServer)) {
    qWarning() << "Failed to open websocket connection to" << wsServer << ":" << mWebSocket->errorString();
    co_return;
  }
  qDebug() << "Connected to" << wsServer;

  // Loops whenever a message is received until the socket is disconnected
  QCORO_FOREACH(const QString &rawMessage, qCoro(mWebSocket).textMessages()) {
    const auto message = parseMessage(rawMessage);
    switch (message.type) {
      case MessageType::ChatMessage:
        handleChatMessage(message);
        break;
      case MessageType::PresenceChange:
        handlePresenceChange(message);
        break;
      case MessageType::Invalid:
        qWarning() << "Received an invalid message:" << message.error;
        break;
    }
  }
}

The textMessages() methods returns an asynchronous generator, which yields the message whenever it arrives. The messages are received and enqueued as long as the generator object exists. The difference between using a generator and just co_awaiting the next emission of the QWebSocket::textMessage() signal is that the generator holds a connection to the signal for its entire lifetime, so no signal emission is lost. If we were only co_awaiting a singal emission, any message that is received before we start co_awaiting again after handling the current message would be lost.

You can find more details about the QCoroWebSocket and QCoroWebSocketSever in the QCoro’s websocket module documentation.

You can build QCoro without the WebSockets module by passing -DQCORO_WITH_QTWEBSOCKETS=OFF to CMake.

Deprecated tasks.h header

The task.h header and it’s camelcase variant Task been deprecated in QCoro 0.6.0 in favor of qcorotask.h (and QCoroTask camelcase version). The main reasons are to avoid such a generic name in a library and to make the name consistent with the rest of QCoro’s public headers which all start with qcoro (or QCoro) prefix.

The old header is still present and fully functional, but including it will produce a warning that you should port your code to use qcorotask.h. You can suppress the warning by defining QCORO_NO_WARN_DEPRECATED_TASK_H in the compiler definitions:

CMake:

add_compiler_definitions(QCORO_NO_WARN_DEPRECATED_TASK_H)

QMake

DEFINES += QCORO_NO_WARN_DEPRECATED_TASK_H

The header file will be removed at some point in the future, at latest in the 1.0 release.

You can also pass -DQCORO_DISABLE_DEPRECATED_TASK_H=ON to CMake when compiling QCoro to prevent it from installing the deprecated task.h header.

Clang-cl and apple-clang support

The clang compiler is fully supported by QCoro since 0.4.0. This version of QCoro intruduces supports for clang-cl and apple-clang.

Clang-cl is a compiler-driver that provides MSVC-compatible command line options, allowing to use clang and LLVM as a drop-in replacement for the MSVC toolchain.

Apple-clang is the official build of clang provided by Apple on MacOS, which may be different from the upstream clang releases.

Full changelog

  • Enable exceptions when compiling with clang-cl (#90, #91)
  • Add option to generate code coverage report (commit 0f0408c)
  • Lower CMake requirement to 3.18.4 (commit deb80c1)
  • Add support for clang-cl (#84, #86)
  • Avoid identifiers that begin with underscore and uppercase letter (#83)
  • Add mising <chrono> include (#82
  • New module: QCoroWebSockets (#75, #88, #89)
  • Add QCoroFwd header with forward-declarations of relevant types (#71)
  • Deprecate task.h header file in favor of qcorotask.h (#70)
  • Fix installing export headers (#77)
  • Introduce support for generator coroutines (#69)
  • QCoro is now build with “modern Qt” compile definitions (#66)
  • Export QCoro wrapper classes (#63, #65)
  • Extended CI to include MSVC, apple-clang and multiple version of gcc and clang-cl (#60, #61)
  • Fixed build with apple-clang

Download

You can download QCoro 0.6.0 here or check the latest sources on QCoro GitHub.

More About QCoro

If you are interested in learning more about QCoro, go read the documentation, look at the first release announcement, which contains a nice explanation and example or watch recording of my talk about C++20 coroutines and QCoro this years’ Akademy.

QCoro 0.5.0 Release Announcement

After another few months I’m happy to announce a new release of QCoro, which brings several new features and a bunch of bugfixes.

  • .then() continuation for Task<T>
  • All asynchronous operations now return Task<T>
  • Timeouts for many operations
  • Support for QThread

.then() continuation for Task

Sometimes it’s not possible to co_await a coroutine - usually because you need to integrate with a 3rd party code that is not coroutine-ready. A good example might be implementing QAbstractItemModel, where none of the virtual methods are coroutines and thus it’s not possible to use co_await in them.

To still make it possible to all coroutines from such code, QCoro::Task<T> now has a new method: .then(), which allows attaching a continuation callback that will be invoked by QCoro when the coroutine represented by the Task finishes.

void notACoroutine() {
    someCoroutineReturningQString().then([](const QString &result) {
        // Will be invoked when the someCoroutine() finishes.
        // The result of the coroutine is passed as an argument to the continuation.
    });
}

The continuation itself might be a coroutine, and the result of the .then() member function is again a Task<R> (where R is the return type of the continuation callback), so it is possible to chain multiple continuations as well as co_awaiting the entire chain.

All asynchronous operations now return Task<T>

Up until now each operation from the QCoro wrapper types returned a special awaitable - for example, QCoroIODevice::read() returned QCoro::detail::QCoroIODevice::ReadOperation. In most cases users of QCoro do not need to concern themselves with that type, since they can still directly co_await the returned awaitable.

However, it unnecessarily leaks implementation details of QCoro into public API and it makes it harded to return a coroutine from a non-coroutine function.

As of QCoro 0.5.0, all the operations now return Task<T>, which makes the API consistent. As a secondary effect, all the operations can have a chained continuation using the .then() continuation, as described above.

Timeout support for many operations

Qt doesn’t allow specifying timeout for many operations, because they are typically non-blocking. But the timeout makes sense in most QCoro cases, because they are combination of wait + the non-blocking operation. Let’s take QIODevice::read() for example: the Qt version doesn’t have any timeout, because the call will never block - if there’s nothing to read, it simply returns an empty QByteArray.

On the other hand, QCoroIODevice::read() is an asynchronous operation, because under to hood, it’s a coroutine that asynchronously calls a sequence of

device->waitForReadyRead();
device->read();

Since QIODevice::waitForReadyRead() takes a timeout argument, it makes sense for QCoroIODevice::read() to also take (an optional) timeout argument. This and many other operations have gained support for timeout.

Support for QThread

It’s been a while since I added a new wrapper for a Qt class, so QCoro 0.5.0 adds wrapper for QThread. It’s now possible to co_await thread start and end:

std::unique_ptr<QThread> thread(QThread::create([]() {
    ...
});
ui->setLabel(tr("Starting thread...");
thread->start();
co_await qCoro(thread)->waitForStarted();
ui->setLabel(tr("Calculating..."));
co_await qCoro(thread)->waitForFinished();
ui->setLabel(tr("Finished!"));

Full changelog

  • .then() continuation for Task<T> (#39)
  • Fixed namespace scoping (#45)
  • Fixed QCoro::waitFor() getting stuck when coroutine returns synchronously (#46)
  • Fixed -pthread usage in CMake (#47)
  • Produce QMake config files (.pri) for each module (commit e215616)
  • Fix build on platforms where -latomic must be linked explicitly (#52)
  • Return Task<T> from all operations (#54)
  • Add QCoro wrapper for QThread (commit 832d931)
  • Many documentation updates

Thanks to everyone who contributed to QCoro!


Download

You can download QCoro 0.5.0 here or check the latest sources on QCoro GitHub.

More About QCoro

If you are interested in learning more about QCoro, go read the documentation, look at the first release announcement, which contains a nice explanation and example or watch recording of my talk about C++20 coroutines and QCoro this years’ Akademy.

QCoro 0.4.0 Release Announcement

It took a few months, but there’s a new release of QCoro with some new cool features. This change contains a breaking change in CMake, wich requires QCoro users to adjust their CMakeLists.txt. I sincerely hope this is the last breaking change for a very long time.

Major highlights in this release:

  • Co-installability of Qt5 and Qt6 builds of QCoro
  • Complete re-work of CMake configuration
  • Support for compiling QCoro with Clang against libstdc++

Co-installability of Qt5 and Qt6 builds of QCoro

This change mostly affects packagers of QCoro. It is now possible to install both Qt5 and Qt6 versions of QCoro alongside each other without conflicting files. The shared libraries now contain the Qt version number in their name (e.g. libQCoro6Core.so) and header files are also located in dedicated subdirectories (e.g. /usr/include/qcoro6/{qcoro,QCoro}). User of QCoro should not need to do any changes to their codebase.

Complete re-work of CMake configuration

This change affects users of QCoro, as they will need to adjust CMakeLists.txt of their projects. First, depending on whether they want to use Qt5 or Qt6 version of QCoro, a different package must be used. Additionally, list of QCoro components to use must be specified:

find_package(QCoro5 REQUIRED COMPONENTS Core Network DBus)

Finally, the target names to use in target_link_libraries have changed as well:

  • QCoro::Core
  • QCoro::Network
  • QCoro::DBus

The version-less QCoro namespace can be used regardless of whether using Qt5 or Qt6 build of QCoro. QCoro5 and QCoro6 namespaces are available as well, in case users need to combine both Qt5 and Qt6 versions in their codebase.

This change brings QCoro CMake configuration system to the same style and behavior as Qt itself, so it should now be easier to use QCoro, especially when supporting both Qt5 and Qt6.

Support for compiling QCoro with Clang against libstdc++

Until now, when the Clang compiler was detected, QCoro forced usage of LLVM’s libc++ standard library. Coroutine support requires tight co-operation between the compiler and standard library. Because Clang still considers their coroutine support experimental it expects all coroutine-related types in standard library to be located in std::experimental namespace. In GNU’s libstdc++, coroutines are fully supported and thus implemented in the std namespace. This requires a little bit of extra glue, which is now in place.

Full changelog

  • QCoro can now be built with Clang against libstdc++ (#38, #22)
  • Qt5 and Qt6 builds of QCoro are now co-installable (#36, #37)
  • Fixed early co_return not resuming the caller (#24, #35)
  • Fixed QProcess example (#34)
  • Test suite has been improved and extended (#29, #31)
  • Task move assignment operator checks for self-assignment (#27)
  • QCoro can now be built as a subdirectory inside another CMake project (#25)
  • Fixed QCoroCore/qcorocore.h header (#23)
  • DBus is disabled by default on Windows, Mac and Android (#21)

Thanks to everyone who contributed to QCoro!


Download

You can download QCoro 0.4.0 here or check the latest sources on QCoro GitHub.

More About QCoro

If you are interested in learning more about QCoro, go read the documentation, look at the first release announcement, which contains a nice explanation and example or watch recording of my talk about C++20 coroutines and QCoro this years’ Akademy.

QCoro 0.2.0 Release Announcement

Just about a month after the first official release of QCoro, a library that provides C++ coroutine support for Qt, here’s 0.2.0 with some big changes. While the API is backwards compatible, users updating from 0.1.0 will have to adjust their #include statements when including QCoro headers.

QCoro 0.2.0 brings the following changes:

Library modularity

The code has been reorganized into three modules (and thus three standalone libraries): QCoroCore, QCoroDBus and QCoroNetwork. QCoroCore contains the elementary QCoro tools (QCoro::Task, qCoro() wrapper etc.) and coroutine support for some QtCore types. The QCoroDBus module contains coroutine support for types from the QtDBus module and equally the QCoroNetwork module contains coroutine support for types from the QtNetwork module. The latter two modules are also optional, the library can be built without them. It also means that an application that only uses let’s say QtNetwork and has no DBus dependency will no longer get QtDBus pulled in through QCoro, as long as it only links against libQCoroCore and libQCoroNetwork. The reorganization will also allow for future support of additional Qt modules.

Headers clean up

The include headers in QCoro we a bit of a mess and in 0.2.0 they all got a unified form. All public header files now start with qcoro (e.g. qcorotimer.h, qcoronetworkreply.h etc.), and QCoro also provides CamelCase headers now. Thus users should simply do #include <QCoroTimer> if they want coroutine support for QTimer.

The reorganization of headers makes QCoro 0.2.0 incompatible with previous versions and any users of QCoro will have to update their #include statements. I’m sorry about this extra hassle, but with this brings much needed sanity into the header organization and naming scheme.

Docs update

The documentation has been updated to reflect the reorganization as well as some internal changes. It should be easier to understand now and hopefully will make it easier for users to start with QCoro now.

Internal API cleanup and code de-duplication

Historically, certain types types which can be directly co_awaited with QCoro, for instance QTimer has their coroutine support implemented differently than types that have multiple asynchronous operations and thus have a coroutine-friendly wrapper classes (like QIODevice and it’s QCoroIODevice wrapper). In 0.2.0 I have unified the code so that even the coroutine support for simple types like QTimer are implemented through wrapper classes (so there’s QCoroTimer now)


Download

You can download QCoro 0.2.0 here or check the latest sources on QCoro GitHub.

More About QCoro

If you are interested in learning more about QCoro, go read the documentation, look at the first release announcement, which contains a nice explanation and example or watch recording of my talk about C++20 coroutines and QCoro this years’ Akademy.

Initial release of QCoro

I’m happy to announce first release of QCoro, a library that provides C++ coroutine support for Qt.

You can download QCoro 0.1.0 here or check the latest sources on QCoro GitHub.

I have talked about QCoro (and C++ coroutines in general) recently at KDE Akademy, you can view the recording of my talk on YouTube.

In general, QCoro provides coroutine support for various asynchronous operations provided by Qt. Since Qt doesn’t support coroutines by default, QCoro provides the necessary “glue” between native Qt types and the C++ coroutine machinery, making it possible to use Qt types with coroutines easily.

QCoro provides coroutine support for asynchronous operations of QIODevice, QNetworkReply, QProcess, QDBusPendingReply, QTimer and more. Take a look at the documentation for detailed description and list of all currently supported Qt types.

A brief example from our documentation that demonstrates how using coroutines makes handling asynchronous operations in Qt simpler:

This is a (simplified) example of how we do network requests with Qt normally, using signals and slots:

QNetworkAccessManager *manager = new QNetworkAccessManager(this);
QNetworkReply *reply = manager->get(url);
connect(reply, &QNetworkReply::finished, this,
        [this, reply]() {
            const auto data = reply->readAll();
            doSomethingWithData(data);
            reply->deleteLater();
        });

And this is the same code, written using C++ coroutines:

QNetworkAccessManager networkAccessManager;
QNetworkReply *reply = co_await networkAccessManager.get(url);
const auto data = reply->readAll();
doSomethingWithData(data);
reply->deleteLater();

The co_await keyword here is the key here: it asynchronously waits for the reply to finish. During the wait, the execution returns to the caller, which could be the Qt event loop, which means that even if this code looks synchronous, in fact it won’t block the event loop while keeping the code simple to read and understand.