Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef XPCOM_THREADS_MOZPROMISE_H_
#define XPCOM_THREADS_MOZPROMISE_H_
#include <type_traits>
#include <utility>
#include "mozilla/Attributes.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/Logging.h"
#include "mozilla/Maybe.h"
#include "mozilla/Monitor.h"
#include "mozilla/Mutex.h"
#include "mozilla/RefPtr.h"
#include "mozilla/StaticString.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Variant.h"
#include "nsIDirectTaskDispatcher.h"
#include "nsISerialEventTarget.h"
#include "nsTArray.h"
#include "nsThreadUtils.h"
#ifdef MOZ_WIDGET_ANDROID
# include "mozilla/jni/GeckoResultUtils.h"
#endif
#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
# define PROMISE_DEBUG
#endif
#ifdef PROMISE_DEBUG
# define PROMISE_ASSERT MOZ_RELEASE_ASSERT
#else
# define PROMISE_ASSERT(...) \
do { \
} while (0)
#endif
#if DEBUG
# include "nsPrintfCString.h"
#endif
namespace mozilla {
namespace dom {
class Promise;
}
extern LazyLogModule gMozPromiseLog;
#define PROMISE_LOG(x, ...) \
MOZ_LOG(gMozPromiseLog, mozilla::LogLevel::Debug, (x, ##__VA_ARGS__))
namespace detail {
template <typename F>
struct MethodTraitsHelper : MethodTraitsHelper<decltype(&F::operator())> {};
template <typename ThisType, typename Ret, typename... ArgTypes>
struct MethodTraitsHelper<Ret (ThisType::*)(ArgTypes...)> {
using ReturnType = Ret;
static const size_t ArgSize = sizeof...(ArgTypes);
};
template <typename ThisType, typename Ret, typename... ArgTypes>
struct MethodTraitsHelper<Ret (ThisType::*)(ArgTypes...) const> {
using ReturnType = Ret;
static const size_t ArgSize = sizeof...(ArgTypes);
};
template <typename ThisType, typename Ret, typename... ArgTypes>
struct MethodTraitsHelper<Ret (ThisType::*)(ArgTypes...) volatile> {
using ReturnType = Ret;
static const size_t ArgSize = sizeof...(ArgTypes);
};
template <typename ThisType, typename Ret, typename... ArgTypes>
struct MethodTraitsHelper<Ret (ThisType::*)(ArgTypes...) const volatile> {
using ReturnType = Ret;
static const size_t ArgSize = sizeof...(ArgTypes);
};
template <typename T>
struct MethodTrait : MethodTraitsHelper<std::remove_reference_t<T>> {};
} // namespace detail
template <typename T>
using MethodReturnType = typename detail::MethodTrait<T>::ReturnType;
template <typename MethodType>
constexpr bool TakesAnyArguments =
detail::MethodTrait<MethodType>::ArgSize != 0;
template <typename ResolveValueT, typename RejectValueT, bool IsExclusive>
class MozPromise;
template <typename T>
constexpr bool IsMozPromise = false;
template <typename ResolveValueT, typename RejectValueT, bool IsExclusive>
constexpr bool
IsMozPromise<MozPromise<ResolveValueT, RejectValueT, IsExclusive>> = true;
/*
* A promise manages an asynchronous request that may or may not be able to be
* fulfilled immediately. When an API returns a promise, the consumer may attach
* callbacks to be invoked (asynchronously, on a specified thread) when the
* request is either completed (resolved) or cannot be completed (rejected).
* Whereas JS promise callbacks are dispatched from Microtask checkpoints,
* MozPromises resolution/rejection make a normal round-trip through the event
* loop, which simplifies their ordering semantics relative to other native
* code.
*
* MozPromises attempt to mirror the spirit of JS Promises to the extent that
* is possible (and desirable) in C++. While the intent is that MozPromises
* feel familiar to programmers who are accustomed to their JS-implemented
* cousin, we don't shy away from imposing restrictions and adding features that
* make sense for the use cases we encounter.
*
* A MozPromise is ThreadSafe, and may be ->Then()ed on any thread. The Then()
* call accepts resolve and reject callbacks, and returns a magic object which
* will be implicitly converted to a MozPromise::Request or a MozPromise object
* depending on how the return value is used. The magic object serves several
* purposes for the consumer.
*
* (1) When converting to a MozPromise::Request, it allows the caller to
* cancel the delivery of the resolve/reject value if it has not already
* occurred, via Disconnect() (this must be done on the target thread to
* avoid racing).
*
* (2) When converting to a MozPromise (which is called a completion promise),
* it allows promise chaining so ->Then() can be called again to attach
* more resolve and reject callbacks. If the resolve/reject callback
* returns a new MozPromise, that promise is chained to the completion
* promise, such that its resolve/reject value will be forwarded along
* when it arrives. If the resolve/reject callback returns void, the
* completion promise is resolved/rejected with the same value that was
* passed to the callback.
*
* The MozPromise APIs skirt traditional XPCOM convention by returning nsRefPtrs
* (rather than already_AddRefed) from various methods. This is done to allow
* elegant chaining of calls without cluttering up the code with intermediate
* variables, and without introducing separate API variants for callers that
* want a return value (from, say, ->Then()) from those that don't.
*
* When IsExclusive is true, the MozPromise does a release-mode assertion that
* there is at most one call to either Then(...) or ChainTo(...).
*/
class MozPromiseRefcountable {
public:
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MozPromiseRefcountable)
protected:
virtual ~MozPromiseRefcountable() = default;
};
class MozPromiseBase : public MozPromiseRefcountable {
public:
virtual void AssertIsDead() = 0;
};
template <typename T>
class MozPromiseHolder;
template <typename T>
class MozPromiseRequestHolder;
template <typename ResolveValueT, typename RejectValueT, bool IsExclusive>
class MozPromise : public MozPromiseBase {
static const uint32_t sMagic = 0xcecace11;
// Return a |T&&| to enable move when IsExclusive is true or
// a |const T&| to enforce copy otherwise.
template <typename T,
typename R = std::conditional_t<IsExclusive, T&&, const T&>>
static R MaybeMove(T& aX) {
return static_cast<R>(aX);
}
public:
using ResolveValueType = ResolveValueT;
using RejectValueType = RejectValueT;
class ResolveOrRejectValue {
public:
template <typename ResolveValueType_>
void SetResolve(ResolveValueType_&& aResolveValue) {
MOZ_ASSERT(IsNothing());
mValue = Storage(VariantIndex<ResolveIndex>{},
std::forward<ResolveValueType_>(aResolveValue));
}
template <typename RejectValueType_>
void SetReject(RejectValueType_&& aRejectValue) {
MOZ_ASSERT(IsNothing());
mValue = Storage(VariantIndex<RejectIndex>{},
std::forward<RejectValueType_>(aRejectValue));
}
template <typename ResolveValueType_>
static ResolveOrRejectValue MakeResolve(ResolveValueType_&& aResolveValue) {
ResolveOrRejectValue val;
val.SetResolve(std::forward<ResolveValueType_>(aResolveValue));
return val;
}
template <typename RejectValueType_>
static ResolveOrRejectValue MakeReject(RejectValueType_&& aRejectValue) {
ResolveOrRejectValue val;
val.SetReject(std::forward<RejectValueType_>(aRejectValue));
return val;
}
bool IsResolve() const { return mValue.template is<ResolveIndex>(); }
bool IsReject() const { return mValue.template is<RejectIndex>(); }
bool IsNothing() const { return mValue.template is<NothingIndex>(); }
const ResolveValueType& ResolveValue() const {
return mValue.template as<ResolveIndex>();
}
ResolveValueType& ResolveValue() {
return mValue.template as<ResolveIndex>();
}
const RejectValueType& RejectValue() const {
return mValue.template as<RejectIndex>();
}
RejectValueType& RejectValue() { return mValue.template as<RejectIndex>(); }
private:
enum { NothingIndex, ResolveIndex, RejectIndex };
using Storage = Variant<Nothing, ResolveValueType, RejectValueType>;
Storage mValue = Storage(VariantIndex<NothingIndex>{});
};
protected:
// MozPromise is the public type, and never constructed directly. Construct
// a MozPromise::Private, defined below.
MozPromise(StaticString aCreationSite, bool aIsCompletionPromise)
: mCreationSite(aCreationSite),
mMutex("MozPromise Mutex"),
mHaveRequest(false),
mIsCompletionPromise(aIsCompletionPromise)
#ifdef PROMISE_DEBUG
,
mMagic4(&mMutex)
#endif
{
PROMISE_LOG("%s creating MozPromise (%p)", mCreationSite.get(), this);
}
public:
// MozPromise::Private allows us to separate the public interface (upon which
// consumers of the promise may invoke methods like Then()) from the private
// interface (upon which the creator of the promise may invoke Resolve() or
// Reject()). APIs should create and store a MozPromise::Private (usually
// via a MozPromiseHolder), and return a MozPromise to consumers.
//
// NB: We can include the definition of this class inline once B2G ICS is
// gone.
class Private;
template <typename ResolveValueType_>
[[nodiscard]] static RefPtr<MozPromise> CreateAndResolve(
ResolveValueType_&& aResolveValue, StaticString aResolveSite) {
static_assert(std::is_convertible_v<ResolveValueType_, ResolveValueT>,
"Resolve() argument must be implicitly convertible to "
"MozPromise's ResolveValueT");
RefPtr<typename MozPromise::Private> p =
new MozPromise::Private(aResolveSite);
p->Resolve(std::forward<ResolveValueType_>(aResolveValue), aResolveSite);
return p;
}
template <typename RejectValueType_>
[[nodiscard]] static RefPtr<MozPromise> CreateAndReject(
RejectValueType_&& aRejectValue, StaticString aRejectSite) {
static_assert(std::is_convertible_v<RejectValueType_, RejectValueT>,
"Reject() argument must be implicitly convertible to "
"MozPromise's RejectValueT");
RefPtr<typename MozPromise::Private> p =
new MozPromise::Private(aRejectSite);
p->Reject(std::forward<RejectValueType_>(aRejectValue), aRejectSite);
return p;
}
template <typename ResolveOrRejectValueType_>
[[nodiscard]] static RefPtr<MozPromise> CreateAndResolveOrReject(
ResolveOrRejectValueType_&& aValue, StaticString aSite) {
RefPtr<typename MozPromise::Private> p = new MozPromise::Private(aSite);
p->ResolveOrReject(std::forward<ResolveOrRejectValueType_>(aValue), aSite);
return p;
}
using AllPromiseType = MozPromise<CopyableTArray<ResolveValueType>,
RejectValueType, IsExclusive>;
using AllSettledPromiseType =
MozPromise<CopyableTArray<ResolveOrRejectValue>, bool, IsExclusive>;
private:
class AllPromiseHolder : public MozPromiseRefcountable {
public:
explicit AllPromiseHolder(size_t aDependentPromises)
: mPromise(new typename AllPromiseType::Private(__func__)),
mOutstandingPromises(aDependentPromises) {
MOZ_ASSERT(aDependentPromises > 0);
mResolveValues.SetLength(aDependentPromises);
}
template <typename ResolveValueType_>
void Resolve(size_t aIndex, ResolveValueType_&& aResolveValue) {
if (!mPromise) {
// Already rejected.
return;
}
mResolveValues[aIndex].emplace(
std::forward<ResolveValueType_>(aResolveValue));
if (--mOutstandingPromises == 0) {
nsTArray<ResolveValueType> resolveValues;
resolveValues.SetCapacity(mResolveValues.Length());
for (auto&& resolveValue : mResolveValues) {
resolveValues.AppendElement(std::move(resolveValue.ref()));
}
mPromise->Resolve(std::move(resolveValues), __func__);
mPromise = nullptr;
mResolveValues.Clear();
}
}
template <typename RejectValueType_>
void Reject(RejectValueType_&& aRejectValue) {
if (!mPromise) {
// Already rejected.
return;
}
mPromise->Reject(std::forward<RejectValueType_>(aRejectValue), __func__);
mPromise = nullptr;
mResolveValues.Clear();
}
AllPromiseType* Promise() { return mPromise; }
private:
nsTArray<Maybe<ResolveValueType>> mResolveValues;
RefPtr<typename AllPromiseType::Private> mPromise;
size_t mOutstandingPromises;
};
// Trying to pass ResolveOrRejectValue by value fails static analysis checks,
// so we need to use either a const& or an rvalue reference, depending on
// whether IsExclusive is true or not.
using ResolveOrRejectValueParam =
std::conditional_t<IsExclusive, ResolveOrRejectValue&&,
const ResolveOrRejectValue&>;
using ResolveValueTypeParam =
std::conditional_t<IsExclusive, ResolveValueType&&,
const ResolveValueType&>;
using RejectValueTypeParam =
std::conditional_t<IsExclusive, RejectValueType&&,
const RejectValueType&>;
class AllSettledPromiseHolder : public MozPromiseRefcountable {
public:
explicit AllSettledPromiseHolder(size_t aDependentPromises)
: mPromise(new typename AllSettledPromiseType::Private(__func__)),
mOutstandingPromises(aDependentPromises) {
MOZ_ASSERT(aDependentPromises > 0);
mValues.SetLength(aDependentPromises);
}
void Settle(size_t aIndex, ResolveOrRejectValueParam aValue) {
if (!mPromise) {
// Already rejected.
return;
}
mValues[aIndex].emplace(MaybeMove(aValue));
if (--mOutstandingPromises == 0) {
nsTArray<ResolveOrRejectValue> values;
values.SetCapacity(mValues.Length());
for (auto&& value : mValues) {
values.AppendElement(std::move(value.ref()));
}
mPromise->Resolve(std::move(values), __func__);
mPromise = nullptr;
mValues.Clear();
}
}
AllSettledPromiseType* Promise() { return mPromise; }
private:
nsTArray<Maybe<ResolveOrRejectValue>> mValues;
RefPtr<typename AllSettledPromiseType::Private> mPromise;
size_t mOutstandingPromises;
};
public:
[[nodiscard]] static RefPtr<AllPromiseType> All(
nsISerialEventTarget* aProcessingTarget,
nsTArray<RefPtr<MozPromise>>& aPromises) {
if (aPromises.Length() == 0) {
return AllPromiseType::CreateAndResolve(
CopyableTArray<ResolveValueType>(), __func__);
}
RefPtr<AllPromiseHolder> holder = new AllPromiseHolder(aPromises.Length());
RefPtr<AllPromiseType> promise = holder->Promise();
for (size_t i = 0; i < aPromises.Length(); ++i) {
aPromises[i]->Then(
aProcessingTarget, __func__,
[holder, i](ResolveValueTypeParam aResolveValue) -> void {
holder->Resolve(i, MaybeMove(aResolveValue));
},
[holder](RejectValueTypeParam aRejectValue) -> void {
holder->Reject(MaybeMove(aRejectValue));
});
}
return promise;
}
[[nodiscard]] static RefPtr<AllSettledPromiseType> AllSettled(
nsISerialEventTarget* aProcessingTarget,
nsTArray<RefPtr<MozPromise>>& aPromises) {
if (aPromises.Length() == 0) {
return AllSettledPromiseType::CreateAndResolve(
CopyableTArray<ResolveOrRejectValue>(), __func__);
}
RefPtr<AllSettledPromiseHolder> holder =
new AllSettledPromiseHolder(aPromises.Length());
RefPtr<AllSettledPromiseType> promise = holder->Promise();
for (size_t i = 0; i < aPromises.Length(); ++i) {
aPromises[i]->Then(aProcessingTarget, __func__,
[holder, i](ResolveOrRejectValueParam aValue) -> void {
holder->Settle(i, MaybeMove(aValue));
});
}
return promise;
}
class Request : public MozPromiseRefcountable {
public:
virtual void Disconnect() = 0;
protected:
Request() : mComplete(false), mDisconnected(false) {}
virtual ~Request() = default;
bool mComplete;
bool mDisconnected;
};
protected:
/*
* A ThenValue tracks a single consumer waiting on the promise. When a
* consumer invokes promise->Then(...), a ThenValue is created. Once the
* Promise is resolved or rejected, a {Resolve,Reject}Runnable is dispatched,
* which invokes the resolve/reject method and then deletes the ThenValue.
*/
class ThenValueBase : public Request {
friend class MozPromise;
static const uint32_t sMagic = 0xfadece11;
public:
class ResolveOrRejectRunnable final
: public PrioritizableCancelableRunnable {
public:
ResolveOrRejectRunnable(ThenValueBase* aThenValue, MozPromise* aPromise)
: PrioritizableCancelableRunnable(
aPromise->mPriority,
"MozPromise::ThenValueBase::ResolveOrRejectRunnable"),
mThenValue(aThenValue),
mPromise(aPromise) {
MOZ_DIAGNOSTIC_ASSERT(!mPromise->IsPending());
}
~ResolveOrRejectRunnable() {
if (mThenValue) {
mThenValue->AssertIsDead();
}
}
NS_IMETHOD Run() override {
PROMISE_LOG("ResolveOrRejectRunnable::Run() [this=%p]", this);
mThenValue->DoResolveOrReject(mPromise->Value());
mThenValue = nullptr;
mPromise = nullptr;
return NS_OK;
}
nsresult Cancel() override { return Run(); }
private:
RefPtr<ThenValueBase> mThenValue;
RefPtr<MozPromise> mPromise;
};
ThenValueBase(nsISerialEventTarget* aResponseTarget, StaticString aCallSite)
: mResponseTarget(aResponseTarget), mCallSite(aCallSite) {
MOZ_ASSERT(aResponseTarget);
}
#ifdef PROMISE_DEBUG
~ThenValueBase() {
mMagic1 = 0;
mMagic2 = 0;
}
#endif
void AssertIsDead() {
PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic);
// We want to assert that this ThenValues is dead - that is to say, that
// there are no consumers waiting for the result. In the case of a normal
// ThenValue, we check that it has been disconnected, which is the way
// that the consumer signals that it no longer wishes to hear about the
// result. If this ThenValue has a completion promise (which is mutually
// exclusive with being disconnectable), we recursively assert that every
// ThenValue associated with the completion promise is dead.
if (MozPromiseBase* p = CompletionPromise()) {
p->AssertIsDead();
} else {
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
if (MOZ_UNLIKELY(!Request::mDisconnected)) {
MOZ_CRASH_UNSAFE_PRINTF(
"MozPromise::ThenValue created from '%s' destroyed without being "
"either disconnected, resolved, or rejected (dispatchRv: %s)",
mCallSite.get(),
mDispatchRv ? GetStaticErrorName(*mDispatchRv)
: "not dispatched");
}
#endif
}
}
void Dispatch(MozPromise* aPromise) {
PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic);
aPromise->mMutex.AssertCurrentThreadOwns();
MOZ_ASSERT(!aPromise->IsPending());
nsCOMPtr<nsIRunnable> r = new ResolveOrRejectRunnable(this, aPromise);
PROMISE_LOG(
"%s Then() call made from %s [Runnable=%p, Promise=%p, ThenValue=%p] "
"%s dispatch",
aPromise->mValue.IsResolve() ? "Resolving" : "Rejecting",
mCallSite.get(), r.get(), aPromise, this,
aPromise->mUseSynchronousTaskDispatch ? "synchronous"
: aPromise->mUseDirectTaskDispatch ? "directtask"
: "normal");
if (aPromise->mUseSynchronousTaskDispatch &&
mResponseTarget->IsOnCurrentThread()) {
PROMISE_LOG("ThenValue::Dispatch running task synchronously [this=%p]",
this);
r->Run();
return;
}
if (aPromise->mUseDirectTaskDispatch &&
mResponseTarget->IsOnCurrentThread()) {
PROMISE_LOG(
"ThenValue::Dispatch dispatch task via direct task queue [this=%p]",
this);
nsCOMPtr<nsIDirectTaskDispatcher> dispatcher =
do_QueryInterface(mResponseTarget);
if (dispatcher) {
SetDispatchRv(dispatcher->DispatchDirectTask(r.forget()));
return;
}
NS_WARNING(
nsPrintfCString(
"Direct Task dispatching not available for thread \"%s\"",
PR_GetThreadName(PR_GetCurrentThread()))
.get());
MOZ_DIAGNOSTIC_ASSERT(
false,
"mResponseTarget must implement nsIDirectTaskDispatcher for direct "
"task dispatching");
}
// Promise consumers are allowed to disconnect the Request object and
// then shut down the thread or task queue that the promise result would
// be dispatched on. So we unfortunately can't assert that promise
// dispatch succeeds. :-(
// We do record whether or not it succeeds so that if the ThenValueBase is
// then destroyed and it was not disconnected, we can include that
// information in the assertion message.
SetDispatchRv(mResponseTarget->Dispatch(r.forget()));
}
void Disconnect() override {