Writing an EventLoop in C++. Few requirements, from a user perspective:
- Must be simple to use (as simple as the Javascript event loop)
- No type casting, no complex registering of event handler. Plain and simple API.
- Event handlers have the proper event type passed to them as reference.
- Should be able to register handlers and inject events in event handler themselves (prevent race conditions)
- User can define events by just creating a class that derives from Event. There should be no need to implement a function or do anything in that class (POCO).
- header only
From a maintainer perspective:
- As static as possible (avoid dynamic cast, dynamic allocation, pointers...)
- As efficient as possible (as few object copy as possible)
#include <algorithm>
#include <condition_variable>
#include <functional>
#include <iostream>
#include <map>
#include <mutex>
#include <queue>
#include <thread>
#include <typeindex>
// Pre-declaration
class Message;
class Event {};
// Represent a function that will be called with an event.
typedef std::function<void(Event&)> EventHandler;
// Utility function for our Event map.
template<typename T>
auto get_event_type() -> std::type_index {
return std::type_index(typeid(T));
}
// User defined event. POCO!
class KeyDownEvent: public Event {
public:
KeyDownEvent(int _key): key(_key) {}
int key;
};
// Another user defined event.
class TimeoutEvent: public Event {
public:
TimeoutEvent() {}
};
// A message is an event and its associated event handler. Designed inspired by
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop.
class Message {
public:
template <typename EventType>
Message(EventType &&event, EventHandler event_handler) :
m_event(std::make_unique<EventType>(std::move(event))),
m_event_handler(event_handler) {}
template <typename EventType>
Message(Message&& message) :
m_event(std::move(message.m_event)),
m_event_handler(message.m_event_handler) {}
std::unique_ptr<Event> m_event;
EventHandler m_event_handler;
};
class EventLoop {
public:
[[ noreturn ]] void run() {
while (true) {
std::unique_lock<std::mutex> lock(m_message_queue_mutex);
// Wait for an event
m_event_cv.wait(lock, [&]{ return m_message_queue.size() > 0; });
// Retrieve the injected event
Message message = std::move(m_message_queue.front());
m_message_queue.pop();
// Unlock before notify, is this necessary? Where did I saw that?
lock.unlock();
m_event_cv.notify_one();
// Call the event listener
message.m_event_handler(*message.m_event.get());
}
}
template<class EventType>
void add_event_listener(std::function<void(EventType&)> event_listener) {
std::lock_guard<std::mutex> guard(m_event_listeners_mutex);
std::type_index event_type = get_event_type<EventType>();
// If the event_type as no listener yet...
if (m_event_listeners.find(event_type) == m_event_listeners.end()) {
// ... create the vector of listeners
m_event_listeners[event_type] = {
[&event_listener](Event& e) { event_listener(static_cast<EventType&>(e)); }
};
} else {
// ... or append to the existing one.
m_event_listeners[event_type].push_back([&event_listener](Event& e) {
event_listener(static_cast<EventType&>(e));
});
}
}
// Convert an event to a message to be handled by the event loop.
template <class EventType, typename ...EventParamsType>
void inject_event(EventParamsType&&... event_params) {
std::lock_guard<std::mutex> listener_guard(m_event_listeners_mutex);
std::vector<EventHandler>& listeners = m_event_listeners[get_event_type<EventType>()];
std::lock_guard<std::mutex> event_guard(m_message_queue_mutex);
std::for_each(listeners.begin(), listeners.end(), [this, &event_params...](EventHandler &listener) {
EventType event = EventType(std::forward<EventParamsType>(event_params)...);
m_message_queue.emplace(std::move(event), listener);
});
m_event_cv.notify_one();
}
private:
std::queue<Message> m_message_queue;
std::mutex m_message_queue_mutex;
std::condition_variable m_event_cv;
std::map<std::type_index, std::vector<EventHandler>> m_event_listeners;
std::mutex m_event_listeners_mutex;
};
Could be used this way:
int main() {
EventLoop eventLoop;
std::thread mainThread(&EventLoop::run, &eventLoop);
// Register event handlers
eventLoop.add_event_listener<KeyDownEvent>([](KeyDownEvent& event) {
std::cout << "*" << event.key << "*" << std::endl;
});
eventLoop.add_event_listener<TimeoutEvent>([](TimeoutEvent&) {
std::cout << "timeout!" << std::endl;
});
do {
int key = getchar();
if (key != 10) {
eventLoop.inject_event<KeyDownEvent>(key);
}
} while (true);
}
This EventLoop
is basically an event Reactor
.
this->
? \$\endgroup\$