palacaze / sigslot

A simple C++14 signal-slots implementation

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

sigslot as Qt replacement: QSignalSpy

opened this issue · comments

I'm currently in the progress of splitting out libraries from some Qt projects. These libraries then are supposed to have no Qt dependency anymore.

For that I'm looking for a replacement for Qt's signal-slot system. I first thought of libsigc++ because it's used by other large open source projects but I'm scoping and I liked sigslot's results in this test.

The Qt projects I'm working on often use in unit tests QSignalSpy what is a helpful little tool to record if signals were emitted and the carried arguments. It also can block (or retry in a separate Qt event loop) until a signal is received and fails after a timeout if no signal is received.

I assume something like this would be possible with sigslot too. But before I begin to write a small utility class for it downstream would you be interested in providing such a class in sigslot directly?

I could imagine a single sigslot::spy class that takes a sigslot::signal argument and records all signal activations in a simple vector with provided argument values.

Hi,
This seems easy enough to implement and would be a handy tool for unit testing.
Should the spy be templated over the signal arguments? That would be easier to use.

If I understand correctly, this is something along the lines of this?

template <typename L, typename... T>
class spy : public std::vector<std::tuple<T...>> {
public:
    explicit spy(signal_base<L, T...> &sig)
        : m_sig(sig)
    {
        sig.connect([this] (T &&... t) {
             this->emplace_back(t...);
             /* notify wait */
        });
    }
    auto& signal() { return m_sig; }

    bool wait(int timeout) { /* start a thread? */ }

private:
    signal_base<L, T...> &m_sig;
};

Yea, that looks very nice. And it's great how compact it is. std::vector could be also composited but subclassing gives direct access to all its functionality and a spy should really be used in tests only anyway.

I like the variadic template args to tuple flow. 👍

For QSignalSpy a call to wait() according to docs starts a separate event loop and returns only after the signal was received. So I guess that would translate here as you put in the comment to starting a new thread until the spy.size() increases and then joining back again.

I assume the spy will be used in a multi-threaded context, wait() being a blocking call?

QSignalSpy creates an event loop whenever wait() gets invoked. This loop is responsible for dispatching the signals to the slots in Qt, so timers and signals created before the wait() in the same thread can be used and will be received by the slots as expected. Sigslot cannot take on this role, we need to either enforce all the logic to be tested to happen in another thread, or build a tool tailored for a specific scenario.

Can you explain how the spy would be used? A concrete example would be nice.

I assume the spy will be used in a multi-threaded context, wait() being a blocking call?

Yes.

QSignalSpy creates an event loop whenever wait() gets invoked. This loop is responsible for dispatching the signals to the slots in Qt, so timers and signals created before the wait() in the same thread can be used and will be received by the slots as expected. Sigslot cannot take on this role, we need to either enforce all the logic to be tested to happen in another thread, or build a tool tailored for a specific scenario.

Can you explain how the spy would be used? A concrete example would be nice.

A typical example from my unit tests would be this.

It is about a Wayland client connecting to a Wayland server (via a Wayland socket).

There are two threads/event loops at work, the Wayland server sits in the main thread of the test/QApplication, the Wayland client sits in a second QThread (the ConnectionThread object is not a thread itself, but a normal QObject moved to the created QThread). We ask to establishConnection on the client and then connectedSpy.wait() on the main thread until the request was received via the Wayland protocol.

Bottom line is that the client is pretty independent in all of that, being in a separate thread and just sending some Wayland protocol request via socket. But the server is in the main thread and as you noticed that can't be directly ported since when we block on the spy we still process events in the main event loop/thread.

I don't directly know how this would port over to a signal-slot system without this integrated event-loops, would need to try out with some prototype probably. Or do you have an idea?

There is no obvious solution, I do not think implementing a generic event-loop agnostic spy is a good idea. Generally speaking, the wait() method should start or defer the wait to the event-loop running in the current thread.

If you keep Qt's event loop, the best bet would be to make wait() duplicate the logic of QSignalSpy by creating a QtEventLoop with a timeout.

In the meantime, I pushed a toy signal spy in a new test in the "spy' branch:
https://github.com/palacaze/sigslot/blob/spy/test/spy.cpp

I do not know if this is relevant to the issue at hand.

There is no obvious solution, I do not think implementing a generic event-loop agnostic spy is a good idea. Generally speaking, the wait() method should start or defer the wait to the event-loop running in the current thread.

Yes, that's true. Maybe a small API with a callback to integrate with an event loop could be possible? I'm not sure it's worth it though. Would need to check it out with some prototype first.

If you keep Qt's event loop, the best bet would be to make wait() duplicate the logic of QSignalSpy by creating a QtEventLoop with a timeout.

Long term plan is to move away from Qt's event loop in library contexts and make integration with it only optional. But I currently do not yet know enough about the topic to say what that would mean in detail for usage of a library like sigslot.

In the meantime, I pushed a toy signal spy in a new test in the "spy' branch:
https://github.com/palacaze/sigslot/blob/spy/test/spy.cpp

Thanks, hopefully I find some time soon to try out sigslot in one of my smaller libraries and then I'll come back to this branch.

Hi, I tried your implementation for some of my tests, here is my feedback:

  • sigslot::spy should be event loop agnostic
  • sigslot::spy::wait should be virtual, with default implementation returning false.

The whole point of sigslot is to run in an environement without Qt event loop, but to easily integrate with, (or any other event loop). So I think it should be the user responsability to customize his sigslot::spy to match his event loop. The readme/example should provide some code snippet.

Your template implementation is really nice and allow simple unit tests in mono thread environnement:

sigslot::signal<int, std::string> sig;
auto spy = sigslot::make_spy(sig);
sig(1, "test")

ASSERT_EQ(spy .size(), 1);
const auto [paramInt, paramString] = spy.front();
ASSERT_EQ(paramInt, 1);
ASSERT_EQ(paramString, "test");
spy.clear();

Maybe it could be nice to add a takeFirst/takeAt/takeLast api, like in QList.

Thank you for the feedback. I actually use an improved and thread-safe variant of the sigslot::spy to wait for signals in production code. This lets me transform async code into sync code for instance.

Looking at the spy implementation, almost nothing would be reused for another event loop, so this may be kind of useless to strive for an even loop agnostic version.

Realistically, I only use Qt and boost ASIO event loops in my own code. Providing concrete implementations for those two might make more sense.

I'm using asio too, when not using qt.
Do you have any open source example of your usage with asio?

I do not, but basically signals are used to asynchronously emit results from a network request, and I use a SignalWaiter to transform this asynchronous emission into a synchronous blocking call.

class Client {
    /// Emits results received from the network asynchronously
    sigslot::signal<Result> results;

    //// Wait for a result with a timeout
    std::optional<Result> getResult(int timout_ms = 10000)
    {
        SignalWaiter waiter(results);
        if (waiter.wait(timeout_ms)) {
            return waiter.front();
        }
        return std::nullopt;
    }
};

I'm currently in the progress of splitting out libraries from some Qt projects. These libraries then are supposed to have no Qt dependency anymore.

For that I'm looking for a replacement for Qt's signal-slot system. I first thought of libsigc++ because it's used by other large open source projects but I'm scoping and I liked sigslot's results in this test.

The Qt projects I'm working on often use in unit tests QSignalSpy what is a helpful little tool to record if signals were emitted and the carried arguments. It also can block (or retry in a separate Qt event loop) until a signal is received and fails after a timeout if no signal is received.

I assume something like this would be possible with sigslot too. But before I begin to write a small utility class for it downstream would you be interested in providing such a class in sigslot directly?

I could imagine a single sigslot::spy class that takes a sigslot::signal argument and records all signal activations in a simple vector with provided argument values.

I'm looking for a replacement for Qt's signal-slot system too. I tried to implement simple one base on this sigslot.

https://github.com/ouxianghui/signal-slot-cpp