// Copyright (c) 2023 Robin Davies // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #pragma once #include #include #include #include namespace pipedal { // thread-safe javascript-like future. template class Promise; template class PromiseInner; template using ResolveFunction = std::function; using RejectFunction = std::function; template using WorkerFunction = std::function, RejectFunction reject)>; using CatchFunction = std::function; class PromiseInnerBase { public: static int64_t allocationCount; // not thread-safe. for testing purposes only. public: virtual void Cancel() = 0; protected: virtual ~PromiseInnerBase() { if (this->inputPromise) { this->inputPromise->ReleaseRef(); } } void AddRef() { std::lock_guard lock{mutex}; ++referenceCount; } void ReleaseRef() { bool deleteMe = false; { std::lock_guard lock{mutex}; if (referenceCount == 0) { throw std::logic_error("Reference count blown."); } --referenceCount; deleteMe = (referenceCount == 0); // can't do it while holding the mutex. } if (deleteMe) { delete this; } } void SetInputPromise(PromiseInnerBase *promise) { PromiseInnerBase *oldPromise; { std::lock_guard lock{mutex}; oldPromise = this->inputPromise; this->inputPromise = promise; } if (promise) { promise->AddRef(); } if (oldPromise) { oldPromise->ReleaseRef(); } } protected: PromiseInnerBase *inputPromise = nullptr; int referenceCount = 0; std::mutex mutex; }; inline int64_t PromiseInnerBase::allocationCount = 0; template class PromiseInner : PromiseInnerBase { private: friend class Promise; using ResolveFunction = pipedal::ResolveFunction; using WorkerFunction = pipedal::WorkerFunction; using ThenFunction = std::function; PromiseInner() { ++allocationCount; referenceCount = 1; // one for the owner, none for the worker (yet) } void Work(WorkerFunction work) { { std::lock_guard lock{mutex}; if (cancelled) return; ++referenceCount; this->worker = work; this->hasWorker = true; } try { worker( [this](const T &value) { this->Resolve(value); }, [this](const std::string &message) { this->Reject(message); }); } catch (const std::exception &e) { Reject(e.what()); } } PromiseInner(WorkerFunction work) { ++allocationCount; referenceCount = 2; // one for the owner, one for the worker. this->worker = work; this->hasWorker = true; try { worker( [this](const T &value) { this->Resolve(value); }, [this](const std::string &message) { this->Reject(message); }); } catch (const std::exception &e) { Reject(e.what()); } } ~PromiseInner() { delete conditionVariable; --allocationCount; } T Get() { while (true) { std::unique_lock lock{mutex}; if (this->resolved || this->cancelled) break; if (conditionVariable != nullptr) { conditionVariable = new std::condition_variable(); } conditionVariable->wait(lock); } ReleaseFunctions(); if (cancelled) { throw std::logic_error("Cancelled."); } if (rejected) { throw std::logic_error(catchMessage); } return resolvedValue; } void Resolve(const T &value) { resolvedValue = value; bool fireResult = false; std::condition_variable *conditionVariable = nullptr; bool releaseRef = false; { std::lock_guard lock{mutex}; if (!resolved) { resolved = true; releaseRef = true; if (hasThenFunction && !cancelled) { fireResult = true; } conditionVariable = this->conditionVariable; } } if (fireResult) { thenFunction(resolvedValue); ReleaseFunctions(); thenFunction = nullptr; catchFunction = nullptr; } if (conditionVariable) { conditionVariable->notify_all(); } if (releaseRef) { ReleaseRef(); } } void Reject(const std::string &message) { bool fireCatch = false; bool releaseRef = false; std::condition_variable *conditionVariable = nullptr; { std::lock_guard lock{mutex}; if (!resolved) { releaseRef = hasWorker; resolved = true; rejected = true; this->catchMessage = message; if (hasCatchFunction && !cancelled) { fireCatch = true; } conditionVariable = this->conditionVariable; resolved = true; } } if (fireCatch) { catchFunction(this->catchMessage); ReleaseFunctions(); } if (conditionVariable) { conditionVariable->notify_all(); } if (releaseRef) { ReleaseRef(); } } virtual void Cancel() { std::condition_variable *conditionVariable = nullptr; { std::lock_guard lock{mutex}; if (!cancelled) { cancelled = true; thenFunction = nullptr; catchFunction = nullptr; conditionVariable = this->conditionVariable; if (inputPromise) { inputPromise->Cancel(); SetInputPromise(nullptr); } } } ReleaseFunctions(); if (conditionVariable) { conditionVariable->notify_all(); } } bool IsCancelled() { std::lock_guard lock{mutex}; return cancelled; } void Then(ThenFunction &&handler) { bool fireThen; { std::lock_guard lock{mutex}; if (hasThenFunction) { throw std::logic_error("Then handler already set."); } hasThenFunction = true; thenFunction = std::move(handler); fireThen = resolved && (!rejected) && (!cancelled); } if (fireThen) { thenFunction(resolvedValue); ReleaseFunctions(); } } void Then(const ThenFunction &handler) { ThenFunction fn = handler; Then(std::move(fn)); } void Catch(CatchFunction handler) { bool fireCatch; { std::lock_guard lock{mutex}; if (hasCatchFunction) { throw std::logic_error("Catch handler already set."); } if (!hasThenFunction) { ++referenceCount; } hasCatchFunction = true; catchFunction = handler; fireCatch = resolved && rejected && !cancelled; } if (fireCatch) { catchFunction(this->catchMessage); ReleaseFunctions(); } } bool IsReady() const { std::lock_guard lock{mutex}; return this->resolved; } private: void ReleaseFunctions() { // Most importantly, release capture variables that reference a promise. thenFunction = nullptr; catchFunction = nullptr; worker = nullptr; SetInputPromise(nullptr); } private: bool hasWorker = false; bool cancelled = false; bool resolved = false; bool rejected = false; WorkerFunction worker; std::condition_variable *conditionVariable = nullptr; bool hasThenFunction = false; ThenFunction thenFunction; bool hasCatchFunction = false; CatchFunction catchFunction; T resolvedValue; std::string catchMessage; }; template class Promise { public: using ResolveFunction = PromiseInner::ResolveFunction; using RejectFunction = pipedal::RejectFunction; using WorkerFunction = PromiseInner::WorkerFunction; using ThenFunction = PromiseInner::ThenFunction; using CatchFunction = pipedal::CatchFunction; Promise() { p = new PromiseInner(); } Promise(const Promise &other) { p = other.p; p->AddRef(); } Promise(Promise &&other) { this->p = other.p; other.p = nullptr; } Promise(WorkerFunction worker) { p = new PromiseInner(worker); } ~Promise() { if (p) p->ReleaseRef(); } Promise(Promise &other) { other.p->AddRef(); this->p = other.p; } Promise Then(std::function &&thenFn) { p->Then(std::move(thenFn)); return Promise(*this); } Promise Then(const std::function &thenFn) { std::function t(thenFn); p->Then(std::move(t)); return Promise(*this); } template Promise Then(std::function result, RejectFunction reject)> thenFn) { Promise t{}; t.SetInputPromise(this->p); this->Then([t, thenFn](const T &value) mutable { t.Work([thenFn, value](pipedal::ResolveFunction resolveU, RejectFunction rejectU) mutable { thenFn(value, resolveU, rejectU); }); }) .Catch([t](const std::string &message) mutable { t.Reject(message); }); return t; } Promise Catch(const CatchFunction &catchFn) { p->Catch(catchFn); return Promise(*this); } Promise &operator=(Promise &&other) { std::swap(this->p, other.p); return *this; } Promise &operator=(const Promise &other) { this->p = other.p; p->AddRef(); return *this; } T Get() { return p->Get(); } class Test { // not thread-safe. for testing purposes only. int GetAllocationCount() { return PromiseInnerBase::allocationCount; } }; void Work(std::function workFunction) { p->Work(workFunction); } void Reject(const std::string &message) { p->Reject(message); } void SetInputPromise(PromiseInnerBase *inputPromise) { p->SetInputPromise(inputPromise); } private: PromiseInner *p = nullptr; }; }