2014-12-08 22:45:37 +00:00
|
|
|
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
|
|
/* vim:set ts=2 sw=2 sts=2 et cindent: */
|
|
|
|
/* 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/. */
|
|
|
|
|
|
|
|
#if !defined(MediaPromise_h_)
|
|
|
|
#define MediaPromise_h_
|
|
|
|
|
|
|
|
#include "prlog.h"
|
|
|
|
|
2014-12-09 01:19:05 +00:00
|
|
|
#include "nsTArray.h"
|
|
|
|
#include "nsThreadUtils.h"
|
|
|
|
|
2014-12-08 22:45:37 +00:00
|
|
|
#include "mozilla/DebugOnly.h"
|
|
|
|
#include "mozilla/Maybe.h"
|
|
|
|
#include "mozilla/Mutex.h"
|
|
|
|
#include "mozilla/Monitor.h"
|
|
|
|
|
|
|
|
/* Polyfill __func__ on MSVC for consumers to pass to the MediaPromise API. */
|
|
|
|
#ifdef _MSC_VER
|
|
|
|
#define __func__ __FUNCTION__
|
|
|
|
#endif
|
|
|
|
|
2014-12-09 01:19:05 +00:00
|
|
|
class nsIEventTarget;
|
2014-12-08 22:45:37 +00:00
|
|
|
namespace mozilla {
|
|
|
|
|
|
|
|
extern PRLogModuleInfo* gMediaPromiseLog;
|
|
|
|
|
|
|
|
#define PROMISE_LOG(x, ...) \
|
|
|
|
MOZ_ASSERT(gMediaPromiseLog); \
|
|
|
|
PR_LOG(gMediaPromiseLog, PR_LOG_DEBUG, (x, ##__VA_ARGS__))
|
|
|
|
|
2014-12-09 01:19:05 +00:00
|
|
|
class MediaTaskQueue;
|
|
|
|
namespace detail {
|
|
|
|
|
|
|
|
nsresult DispatchMediaPromiseRunnable(MediaTaskQueue* aQueue, nsIRunnable* aRunnable);
|
|
|
|
nsresult DispatchMediaPromiseRunnable(nsIEventTarget* aTarget, nsIRunnable* aRunnable);
|
|
|
|
|
|
|
|
} // namespace detail
|
|
|
|
|
2014-12-08 22:45:37 +00:00
|
|
|
/*
|
|
|
|
* 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).
|
|
|
|
*
|
|
|
|
* By default, resolve and reject callbacks are always invoked on the same thread
|
|
|
|
* where Then() was invoked.
|
|
|
|
*/
|
|
|
|
template<typename T> class MediaPromiseHolder;
|
|
|
|
template<typename ResolveValueT, typename RejectValueT>
|
|
|
|
class MediaPromise
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
typedef ResolveValueT ResolveValueType;
|
|
|
|
typedef RejectValueT RejectValueType;
|
|
|
|
|
|
|
|
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaPromise)
|
2014-12-10 22:49:09 +00:00
|
|
|
explicit MediaPromise(const char* aCreationSite)
|
2014-12-08 22:45:37 +00:00
|
|
|
: mCreationSite(aCreationSite)
|
|
|
|
, mMutex("MediaPromise Mutex")
|
|
|
|
{
|
|
|
|
PROMISE_LOG("%s creating MediaPromise (%p)", mCreationSite, this);
|
|
|
|
}
|
|
|
|
|
2014-12-16 09:52:57 +00:00
|
|
|
static nsRefPtr<MediaPromise<ResolveValueT, RejectValueT>>
|
|
|
|
CreateAndResolve(ResolveValueType aResolveValue, const char* aResolveSite)
|
|
|
|
{
|
|
|
|
nsRefPtr<MediaPromise<ResolveValueT, RejectValueT>> p =
|
|
|
|
new MediaPromise<ResolveValueT, RejectValueT>(aResolveSite);
|
|
|
|
p->Resolve(aResolveValue, aResolveSite);
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
|
|
|
|
static nsRefPtr<MediaPromise<ResolveValueT, RejectValueT>>
|
|
|
|
CreateAndReject(RejectValueType aRejectValue, const char* aRejectSite)
|
|
|
|
{
|
|
|
|
nsRefPtr<MediaPromise<ResolveValueT, RejectValueT>> p =
|
|
|
|
new MediaPromise<ResolveValueT, RejectValueT>(aRejectSite);
|
|
|
|
p->Reject(aRejectValue, aRejectSite);
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
|
2014-12-08 22:45:37 +00:00
|
|
|
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
|
2014-12-09 01:19:05 +00:00
|
|
|
* resolved or rejected, a {Resolve,Reject}Runnable is dispatched, which
|
|
|
|
* invokes the resolve/reject method and then deletes the ThenValue.
|
2014-12-08 22:45:37 +00:00
|
|
|
*/
|
|
|
|
class ThenValueBase
|
|
|
|
{
|
|
|
|
public:
|
2014-12-09 01:19:05 +00:00
|
|
|
class ResolveRunnable : public nsRunnable
|
2014-12-08 22:45:37 +00:00
|
|
|
{
|
|
|
|
public:
|
2014-12-09 01:19:05 +00:00
|
|
|
ResolveRunnable(ThenValueBase* aThenValue, ResolveValueType aResolveValue)
|
2014-12-08 22:45:37 +00:00
|
|
|
: mThenValue(aThenValue)
|
2014-12-22 08:20:30 +00:00
|
|
|
, mResolveValue(aResolveValue) {}
|
2014-12-08 22:45:37 +00:00
|
|
|
|
2014-12-09 01:19:05 +00:00
|
|
|
~ResolveRunnable()
|
|
|
|
{
|
|
|
|
MOZ_ASSERT(!mThenValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
NS_IMETHODIMP Run()
|
|
|
|
{
|
|
|
|
PROMISE_LOG("ResolveRunnable::Run() [this=%p]", this);
|
|
|
|
mThenValue->DoResolve(mResolveValue);
|
|
|
|
|
|
|
|
delete mThenValue;
|
|
|
|
mThenValue = nullptr;
|
|
|
|
return NS_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
ThenValueBase* mThenValue;
|
|
|
|
ResolveValueType mResolveValue;
|
|
|
|
};
|
|
|
|
|
|
|
|
class RejectRunnable : public nsRunnable
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
RejectRunnable(ThenValueBase* aThenValue, RejectValueType aRejectValue)
|
2014-12-08 22:45:37 +00:00
|
|
|
: mThenValue(aThenValue)
|
2014-12-22 08:20:30 +00:00
|
|
|
, mRejectValue(aRejectValue) {}
|
2014-12-08 22:45:37 +00:00
|
|
|
|
2014-12-09 01:19:05 +00:00
|
|
|
~RejectRunnable()
|
2014-12-08 22:45:37 +00:00
|
|
|
{
|
|
|
|
MOZ_ASSERT(!mThenValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
NS_IMETHODIMP Run()
|
|
|
|
{
|
2014-12-09 01:19:05 +00:00
|
|
|
PROMISE_LOG("RejectRunnable::Run() [this=%p]", this);
|
|
|
|
mThenValue->DoReject(mRejectValue);
|
2014-12-08 22:45:37 +00:00
|
|
|
|
|
|
|
delete mThenValue;
|
|
|
|
mThenValue = nullptr;
|
|
|
|
return NS_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
ThenValueBase* mThenValue;
|
2014-12-09 01:19:05 +00:00
|
|
|
RejectValueType mRejectValue;
|
2014-12-08 22:45:37 +00:00
|
|
|
};
|
|
|
|
|
2014-12-10 22:49:09 +00:00
|
|
|
explicit ThenValueBase(const char* aCallSite) : mCallSite(aCallSite)
|
2014-12-08 22:45:37 +00:00
|
|
|
{
|
|
|
|
MOZ_COUNT_CTOR(ThenValueBase);
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void Dispatch(MediaPromise *aPromise) = 0;
|
|
|
|
|
|
|
|
protected:
|
2014-12-09 01:19:05 +00:00
|
|
|
// This may only be deleted by {Resolve,Reject}Runnable::Run.
|
2014-12-08 22:45:37 +00:00
|
|
|
virtual ~ThenValueBase() { MOZ_COUNT_DTOR(ThenValueBase); }
|
|
|
|
|
|
|
|
virtual void DoResolve(ResolveValueType aResolveValue) = 0;
|
|
|
|
virtual void DoReject(RejectValueType aRejectValue) = 0;
|
|
|
|
|
|
|
|
const char* mCallSite;
|
|
|
|
};
|
|
|
|
|
2014-12-12 22:22:23 +00:00
|
|
|
/*
|
|
|
|
* We create two overloads for invoking Resolve/Reject Methods so as to
|
|
|
|
* make the resolve/reject value argument "optional".
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Avoid confusing the compiler when the callback accepts T* but the ValueType
|
|
|
|
// is nsRefPtr<T>. See bug 1109954 comment 6.
|
|
|
|
template <typename T>
|
|
|
|
struct NonDeduced
|
|
|
|
{
|
|
|
|
typedef T type;
|
|
|
|
};
|
|
|
|
|
|
|
|
template<typename ThisType, typename ValueType>
|
|
|
|
static void InvokeCallbackMethod(ThisType* aThisVal, void(ThisType::*aMethod)(ValueType),
|
|
|
|
typename NonDeduced<ValueType>::type aValue)
|
|
|
|
{
|
|
|
|
((*aThisVal).*aMethod)(aValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
template<typename ThisType, typename ValueType>
|
|
|
|
static void InvokeCallbackMethod(ThisType* aThisVal, void(ThisType::*aMethod)(), ValueType aValue)
|
|
|
|
{
|
|
|
|
((*aThisVal).*aMethod)();
|
|
|
|
}
|
|
|
|
|
2014-12-08 22:45:37 +00:00
|
|
|
template<typename TargetType, typename ThisType,
|
|
|
|
typename ResolveMethodType, typename RejectMethodType>
|
|
|
|
class ThenValue : public ThenValueBase
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
ThenValue(TargetType* aResponseTarget, ThisType* aThisVal,
|
|
|
|
ResolveMethodType aResolveMethod, RejectMethodType aRejectMethod,
|
|
|
|
const char* aCallSite)
|
|
|
|
: ThenValueBase(aCallSite)
|
|
|
|
, mResponseTarget(aResponseTarget)
|
|
|
|
, mThisVal(aThisVal)
|
|
|
|
, mResolveMethod(aResolveMethod)
|
|
|
|
, mRejectMethod(aRejectMethod) {}
|
|
|
|
|
|
|
|
void Dispatch(MediaPromise *aPromise) MOZ_OVERRIDE
|
|
|
|
{
|
|
|
|
aPromise->mMutex.AssertCurrentThreadOwns();
|
|
|
|
MOZ_ASSERT(!aPromise->IsPending());
|
|
|
|
bool resolved = aPromise->mResolveValue.isSome();
|
|
|
|
nsRefPtr<nsRunnable> runnable =
|
2014-12-09 01:19:05 +00:00
|
|
|
resolved ? static_cast<nsRunnable*>(new (typename ThenValueBase::ResolveRunnable)(this, aPromise->mResolveValue.ref()))
|
|
|
|
: static_cast<nsRunnable*>(new (typename ThenValueBase::RejectRunnable)(this, aPromise->mRejectValue.ref()));
|
2014-12-08 22:45:37 +00:00
|
|
|
PROMISE_LOG("%s Then() call made from %s [Runnable=%p, Promise=%p, ThenValue=%p]",
|
|
|
|
resolved ? "Resolving" : "Rejecting", ThenValueBase::mCallSite,
|
|
|
|
runnable.get(), aPromise, this);
|
2014-12-09 01:19:05 +00:00
|
|
|
DebugOnly<nsresult> rv = detail::DispatchMediaPromiseRunnable(mResponseTarget, runnable);
|
2014-12-08 22:45:37 +00:00
|
|
|
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
virtual void DoResolve(ResolveValueType aResolveValue)
|
|
|
|
{
|
2014-12-12 22:22:23 +00:00
|
|
|
InvokeCallbackMethod(mThisVal.get(), mResolveMethod, aResolveValue);
|
2014-12-08 22:45:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
virtual void DoReject(RejectValueType aRejectValue)
|
|
|
|
{
|
2014-12-12 22:22:23 +00:00
|
|
|
InvokeCallbackMethod(mThisVal.get(), mRejectMethod, aRejectValue);
|
2014-12-08 22:45:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
virtual ~ThenValue() {}
|
|
|
|
|
|
|
|
private:
|
|
|
|
nsRefPtr<TargetType> mResponseTarget;
|
|
|
|
nsRefPtr<ThisType> mThisVal;
|
|
|
|
ResolveMethodType mResolveMethod;
|
|
|
|
RejectMethodType mRejectMethod;
|
|
|
|
};
|
|
|
|
public:
|
|
|
|
|
|
|
|
template<typename TargetType, typename ThisType,
|
|
|
|
typename ResolveMethodType, typename RejectMethodType>
|
|
|
|
void Then(TargetType* aResponseTarget, const char* aCallSite, ThisType* aThisVal,
|
|
|
|
ResolveMethodType aResolveMethod, RejectMethodType aRejectMethod)
|
|
|
|
{
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
ThenValueBase* thenValue = new ThenValue<TargetType, ThisType, ResolveMethodType,
|
|
|
|
RejectMethodType>(aResponseTarget, aThisVal,
|
|
|
|
aResolveMethod, aRejectMethod,
|
|
|
|
aCallSite);
|
|
|
|
PROMISE_LOG("%s invoking Then() [this=%p, thenValue=%p, aThisVal=%p, isPending=%d]",
|
|
|
|
aCallSite, this, thenValue, aThisVal, (int) IsPending());
|
|
|
|
if (!IsPending()) {
|
|
|
|
thenValue->Dispatch(this);
|
|
|
|
} else {
|
|
|
|
mThenValues.AppendElement(thenValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-09 01:19:05 +00:00
|
|
|
void ChainTo(already_AddRefed<MediaPromise> aChainedPromise, const char* aCallSite)
|
|
|
|
{
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
nsRefPtr<MediaPromise> chainedPromise = aChainedPromise;
|
|
|
|
PROMISE_LOG("%s invoking Chain() [this=%p, chainedPromise=%p, isPending=%d]",
|
|
|
|
aCallSite, this, chainedPromise.get(), (int) IsPending());
|
|
|
|
if (!IsPending()) {
|
|
|
|
ForwardTo(chainedPromise);
|
|
|
|
} else {
|
|
|
|
mChainedPromises.AppendElement(chainedPromise);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-08 22:45:37 +00:00
|
|
|
void Resolve(ResolveValueType aResolveValue, const char* aResolveSite)
|
|
|
|
{
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
MOZ_ASSERT(IsPending());
|
|
|
|
PROMISE_LOG("%s resolving MediaPromise (%p created at %s)", aResolveSite, this, mCreationSite);
|
|
|
|
mResolveValue.emplace(aResolveValue);
|
|
|
|
DispatchAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Reject(RejectValueType aRejectValue, const char* aRejectSite)
|
|
|
|
{
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
MOZ_ASSERT(IsPending());
|
|
|
|
PROMISE_LOG("%s rejecting MediaPromise (%p created at %s)", aRejectSite, this, mCreationSite);
|
|
|
|
mRejectValue.emplace(aRejectValue);
|
|
|
|
DispatchAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
bool IsPending() { return mResolveValue.isNothing() && mRejectValue.isNothing(); }
|
|
|
|
void DispatchAll()
|
|
|
|
{
|
|
|
|
mMutex.AssertCurrentThreadOwns();
|
2014-12-10 22:03:56 +00:00
|
|
|
for (size_t i = 0; i < mThenValues.Length(); ++i) {
|
2014-12-08 22:45:37 +00:00
|
|
|
mThenValues[i]->Dispatch(this);
|
2014-12-10 22:03:56 +00:00
|
|
|
}
|
2014-12-08 22:45:37 +00:00
|
|
|
mThenValues.Clear();
|
2014-12-09 01:19:05 +00:00
|
|
|
|
|
|
|
for (size_t i = 0; i < mChainedPromises.Length(); ++i) {
|
|
|
|
ForwardTo(mChainedPromises[i]);
|
|
|
|
}
|
|
|
|
mChainedPromises.Clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ForwardTo(MediaPromise* aOther)
|
|
|
|
{
|
|
|
|
MOZ_ASSERT(!IsPending());
|
|
|
|
if (mResolveValue.isSome()) {
|
|
|
|
aOther->Resolve(mResolveValue.ref(), "<chained promise>");
|
|
|
|
} else {
|
|
|
|
aOther->Reject(mRejectValue.ref(), "<chained promise>");
|
|
|
|
}
|
2014-12-08 22:45:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
~MediaPromise()
|
|
|
|
{
|
|
|
|
PROMISE_LOG("MediaPromise::~MediaPromise [this=%p]", this);
|
|
|
|
MOZ_ASSERT(!IsPending());
|
2014-12-09 01:19:05 +00:00
|
|
|
MOZ_ASSERT(mThenValues.IsEmpty());
|
|
|
|
MOZ_ASSERT(mChainedPromises.IsEmpty());
|
2014-12-08 22:45:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const char* mCreationSite; // For logging
|
|
|
|
Mutex mMutex;
|
|
|
|
Maybe<ResolveValueType> mResolveValue;
|
|
|
|
Maybe<RejectValueType> mRejectValue;
|
|
|
|
nsTArray<ThenValueBase*> mThenValues;
|
2014-12-09 01:19:05 +00:00
|
|
|
nsTArray<nsRefPtr<MediaPromise>> mChainedPromises;
|
2014-12-08 22:45:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Class to encapsulate a promise for a particular role. Use this as the member
|
|
|
|
* variable for a class whose method returns a promise.
|
|
|
|
*/
|
|
|
|
template<typename PromiseType>
|
|
|
|
class MediaPromiseHolder
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
MediaPromiseHolder()
|
|
|
|
: mMonitor(nullptr) {}
|
|
|
|
|
|
|
|
~MediaPromiseHolder() { MOZ_ASSERT(!mPromise); }
|
|
|
|
|
|
|
|
already_AddRefed<PromiseType> Ensure(const char* aMethodName) {
|
|
|
|
if (mMonitor) {
|
|
|
|
mMonitor->AssertCurrentThreadOwns();
|
|
|
|
}
|
|
|
|
if (!mPromise) {
|
|
|
|
mPromise = new PromiseType(aMethodName);
|
|
|
|
}
|
|
|
|
nsRefPtr<PromiseType> p = mPromise;
|
|
|
|
return p.forget();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Provide a Monitor that should always be held when accessing this instance.
|
|
|
|
void SetMonitor(Monitor* aMonitor) { mMonitor = aMonitor; }
|
|
|
|
|
|
|
|
bool IsEmpty()
|
|
|
|
{
|
|
|
|
if (mMonitor) {
|
|
|
|
mMonitor->AssertCurrentThreadOwns();
|
|
|
|
}
|
|
|
|
return !mPromise;
|
|
|
|
}
|
|
|
|
|
2014-12-09 01:19:05 +00:00
|
|
|
already_AddRefed<PromiseType> Steal()
|
|
|
|
{
|
|
|
|
if (mMonitor) {
|
|
|
|
mMonitor->AssertCurrentThreadOwns();
|
|
|
|
}
|
|
|
|
|
|
|
|
nsRefPtr<PromiseType> p = mPromise;
|
|
|
|
mPromise = nullptr;
|
|
|
|
return p.forget();
|
|
|
|
}
|
|
|
|
|
2014-12-08 22:45:37 +00:00
|
|
|
void Resolve(typename PromiseType::ResolveValueType aResolveValue,
|
|
|
|
const char* aMethodName)
|
|
|
|
{
|
|
|
|
if (mMonitor) {
|
|
|
|
mMonitor->AssertCurrentThreadOwns();
|
|
|
|
}
|
|
|
|
MOZ_ASSERT(mPromise);
|
|
|
|
mPromise->Resolve(aResolveValue, aMethodName);
|
|
|
|
mPromise = nullptr;
|
|
|
|
}
|
|
|
|
|
2014-12-09 01:19:05 +00:00
|
|
|
void ResolveIfExists(typename PromiseType::ResolveValueType aResolveValue,
|
|
|
|
const char* aMethodName)
|
|
|
|
{
|
|
|
|
if (!IsEmpty()) {
|
|
|
|
Resolve(aResolveValue, aMethodName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-08 22:45:37 +00:00
|
|
|
void Reject(typename PromiseType::RejectValueType aRejectValue,
|
|
|
|
const char* aMethodName)
|
|
|
|
{
|
|
|
|
if (mMonitor) {
|
|
|
|
mMonitor->AssertCurrentThreadOwns();
|
|
|
|
}
|
|
|
|
MOZ_ASSERT(mPromise);
|
|
|
|
mPromise->Reject(aRejectValue, aMethodName);
|
|
|
|
mPromise = nullptr;
|
|
|
|
}
|
|
|
|
|
2014-12-09 01:19:05 +00:00
|
|
|
void RejectIfExists(typename PromiseType::RejectValueType aRejectValue,
|
|
|
|
const char* aMethodName)
|
|
|
|
{
|
|
|
|
if (!IsEmpty()) {
|
|
|
|
Reject(aRejectValue, aMethodName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-08 22:45:37 +00:00
|
|
|
private:
|
|
|
|
Monitor* mMonitor;
|
|
|
|
nsRefPtr<PromiseType> mPromise;
|
|
|
|
};
|
|
|
|
|
|
|
|
#undef PROMISE_LOG
|
|
|
|
|
|
|
|
} // namespace mozilla
|
|
|
|
|
|
|
|
#endif
|