Skip to content
Snippets Groups Projects
handle.hpp 6.9 KiB
Newer Older
Nicolas Pope's avatar
Nicolas Pope committed
/**
 * @file handle.hpp
 * @copyright Copyright (c) 2020 University of Turku, MIT License
 * @author Nicolas Pope
 */

#pragma once

#include <ftl/threads.hpp>
#include <ftl/exception.hpp>
#include <functional>
#include <unordered_map>

namespace ftl {

struct Handle;
struct BaseHandler {
	virtual void remove(const Handle &)=0;

	virtual void removeUnsafe(const Handle &)=0;

	inline Handle make_handle(BaseHandler*, int);

	protected:
	std::mutex mutex_;
	int id_=0;
};

/**
 * A `Handle` is used to manage registered callbacks, allowing them to be
 * removed safely whenever the `Handle` instance is destroyed.
 */
struct [[nodiscard]] Handle {
	friend struct BaseHandler;

	/**
	 * Cancel the callback and invalidate the handle.
	 */
	inline void cancel() { if (handler_) handler_->remove(*this); handler_ = nullptr; }

	inline void innerCancel() { if (handler_) handler_->removeUnsafe(*this); handler_ = nullptr; }

	inline int id() const { return id_; }

	Handle() : handler_(nullptr), id_(0) {}

	Handle(const Handle &)=delete;
	Handle &operator=(const Handle &)=delete;

	inline Handle(Handle &&h) : handler_(nullptr) {
		if (handler_) handler_->remove(*this);
		handler_ = h.handler_;
		h.handler_ = nullptr;
		id_ = h.id_;
	}

	inline Handle &operator=(Handle &&h) {
		if (handler_) handler_->remove(*this);
		handler_ = h.handler_;
		h.handler_ = nullptr;
		id_ = h.id_;
		return *this;
	}

	inline ~Handle() {
		if (handler_) {
			handler_->remove(*this);
		}
	}

	private:
	BaseHandler *handler_;
	int id_;

	Handle(BaseHandler *h, int id) : handler_(h), id_(id) {}
};

/**
 * This class is used to manage callbacks. The template parameters are the
 * arguments to be passed to the callback when triggered. This class is already
 * thread-safe.
 *
 * POSSIBLE BUG:	On destruction any remaining handles will be left with
 * 					dangling pointer to Handler.
 */
template <typename ...ARGS>
struct Handler : BaseHandler {
	Handler() {}
	~Handler() {
		// Ensure all thread pool jobs are done
		while (jobs_ > 0 && ftl::pool.size() > 0) std::this_thread::sleep_for(std::chrono::milliseconds(2));
Nicolas Pope's avatar
Nicolas Pope committed
	}

	/**
	 * Add a new callback function. It returns a `Handle` object that must
	 * remain in scope, the destructor of the `Handle` will remove the callback.
	 */
	Handle on(const std::function<bool(ARGS...)> &f) {
		std::unique_lock<std::mutex> lk(mutex_);
		int id = id_++;
		callbacks_[id] = f;
		return make_handle(this, id);
	}

	/**
	 * Safely trigger all callbacks. Note that `Handler` is locked when
	 * triggering so callbacks cannot make modifications to it or they will
	 * lock up. To remove a callback, return false from the callback, else
	 * return true.
	 */
	void trigger(ARGS ...args) {
		bool hadFault = false;
Nicolas Pope's avatar
Nicolas Pope committed
		std::unique_lock<std::mutex> lk(mutex_);
		for (auto i=callbacks_.begin(); i!=callbacks_.end(); ) {
			bool keep = true;
			try {
				keep = i->second(args...);
			} catch(...) {
				hadFault = true;
			}
Nicolas Pope's avatar
Nicolas Pope committed
			if (!keep) i = callbacks_.erase(i);
			else ++i;
		}
		if (hadFault) throw FTL_Error("Callback exception");
Nicolas Pope's avatar
Nicolas Pope committed
	}

	/**
	 * Call all the callbacks in another thread. The callbacks are done in a
	 * single thread, not in parallel.
	 */
	void triggerAsync(ARGS ...args) {
Nicolas Pope's avatar
Nicolas Pope committed
		ftl::pool.push([this, args...](int id) {
			bool hadFault = false;
Nicolas Pope's avatar
Nicolas Pope committed
			std::unique_lock<std::mutex> lk(mutex_);
			for (auto i=callbacks_.begin(); i!=callbacks_.end(); ) {
				bool keep = true;
				try {
					keep = i->second(args...);
				} catch (...) {
					hadFault = true;
				}
Nicolas Pope's avatar
Nicolas Pope committed
				if (!keep) i = callbacks_.erase(i);
				else ++i;
			}
			--jobs_;
			if (hadFault) throw FTL_Error("Callback exception");
Nicolas Pope's avatar
Nicolas Pope committed
		});
	}

	/**
	 * Each callback is called in its own thread job. Note: the return value
	 * of the callback is ignored in this case and does not allow callback
	 * removal via the return value.
	 */
	void triggerParallel(ARGS ...args) {
		std::unique_lock<std::mutex> lk(mutex_);
		jobs_ += callbacks_.size();
		for (auto i=callbacks_.begin(); i!=callbacks_.end(); ++i) {
			ftl::pool.push([this, f = i->second, args...](int id) {
				try {
					f(args...);
Nicolas Pope's avatar
Nicolas Pope committed
				} catch (const ftl::exception &e) {
					--jobs_;
					throw e;
				}
				--jobs_;
			});
		}
	}

	/**
	 * Remove a callback using its `Handle`. This is equivalent to allowing the
	 * `Handle` to be destroyed or cancelled.
	 */
	void remove(const Handle &h) override {
		{
			std::unique_lock<std::mutex> lk(mutex_);
			callbacks_.erase(h.id());
		}
		// Make sure any possible call to removed callback has finished.
		while (jobs_ > 0 && ftl::pool.size() > 0) std::this_thread::sleep_for(std::chrono::milliseconds(2));
Nicolas Pope's avatar
Nicolas Pope committed
	}

	void removeUnsafe(const Handle &h) override {
		callbacks_.erase(h.id());
		// Make sure any possible call to removed callback has finished.
		while (jobs_ > 0 && ftl::pool.size() > 0) std::this_thread::sleep_for(std::chrono::milliseconds(2));
	}

	void clear() {
		callbacks_.clear();
Nicolas Pope's avatar
Nicolas Pope committed
	}

	private:
	std::unordered_map<int, std::function<bool(ARGS...)>> callbacks_;
	std::atomic_int jobs_=0;
};

/**
 * This class is used to manage callbacks. The template parameters are the
 * arguments to be passed to the callback when triggered. This class is already
 * thread-safe. Note that this version only allows a single callback at a time
 * and throws an exception if multiple are added without resetting.
 */
template <typename ...ARGS>
struct SingletonHandler : BaseHandler {
	/**
	 * Add a new callback function. It returns a `Handle` object that must
	 * remain in scope, the destructor of the `Handle` will remove the callback.
	 */
	[[nodiscard]] Handle on(const std::function<bool(ARGS...)> &f) {
		std::unique_lock<std::mutex> lk(mutex_);
		if (callback_) throw FTL_Error("Callback already bound");
		callback_ = f;
		return make_handle(this, id_++);
	}

	/**
	 * Safely trigger all callbacks. Note that `Handler` is locked when
	 * triggering so callbacks cannot make modifications to it or they will
	 * lock up. To remove a callback, return false from the callback, else
	 * return true.
	 */
	bool trigger(ARGS ...args) {
		std::unique_lock<std::mutex> lk(mutex_);
		if (callback_) {
			bool keep = callback_(std::forward<ARGS>(args)...);
			if (!keep) callback_ = nullptr;
			return keep;
		} else {
			return false;
		}
		//} catch (const std::exception &e) {
		//	LOG(ERROR) << "Exception in callback: " << e.what();
		//}
	}

	/**
	 * Remove a callback using its `Handle`. This is equivalent to allowing the
	 * `Handle` to be destroyed or cancelled. If the handle does not match the
	 * currently bound callback then the callback is not removed.
	 */
	void remove(const Handle &h) override {
		std::unique_lock<std::mutex> lk(mutex_);
		if (h.id() == id_-1) callback_ = nullptr;
	}

	void removeUnsafe(const Handle &h) override {
		if (h.id() == id_-1) callback_ = nullptr;
	}

	void reset() { callback_ = nullptr; }

	operator bool() const { return (bool)callback_; }

	private:
	std::function<bool(ARGS...)> callback_;
};

}

ftl::Handle ftl::BaseHandler::make_handle(BaseHandler *h, int id) {
	return ftl::Handle(h, id);
}