Bug 1293922 - TimerUtils for easier-to-use timers - r?froydnj
TimerUtils provides a number of classes that help with managing timers, from a
simple handle class managing an nsCOMPtr<nsITimer> to classes that accept
lambda callbacks and can safely auto-cancel themselves (if needed) on
destruction.
MozReview-Commit-ID: 5GzZlvhZOs2
new file mode 100644
--- /dev/null
+++ b/xpcom/glue/TimerUtils.cpp
@@ -0,0 +1,111 @@
+/* -*- 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/. */
+
+#include "TimerUtils.h"
+
+#include "nsComponentManagerUtils.h"
+
+nsresult
+TimerHandle::Create()
+{
+ nsresult error;
+ mTimerCOMPtr = do_CreateInstance(NS_TIMER_CONTRACTID, &error);
+ if (MOZ_UNLIKELY(NS_FAILED(error))) {
+ return error;
+ }
+ if (MOZ_UNLIKELY(!mTimerCOMPtr)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+// Create a timer instance if there isn't already one.
+nsresult
+TimerHandle::Ensure()
+{
+ if (!mTimerCOMPtr) {
+ return Create();
+ }
+ return NS_OK;
+}
+
+nsresult
+TimerHandle::Cancel(void)
+{
+ if (mTimerCOMPtr) {
+ return mTimerCOMPtr->Cancel();
+ }
+ return NS_OK;
+}
+
+void
+TimerHandle::Release(void)
+{
+ mTimerCOMPtr = nullptr;
+}
+
+nsresult
+TimerHandle::CancelAndRelease(void)
+{
+ if (mTimerCOMPtr) {
+ nsresult rv = mTimerCOMPtr->Cancel();
+ mTimerCOMPtr = nullptr;
+ return rv;
+ }
+ return NS_OK;
+}
+
+nsresult
+TimerHandleWithFunction::Init(uint32_t aDelay_ms,
+ uint32_t aType,
+ mozilla::function<void(void)>&& aF)
+{
+ nsresult rv = Ensure();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ return rv;
+ }
+ return get()->InitWithCallback(
+ new Callback(mozilla::Move(aF)),
+ aDelay_ms, aType);
+}
+
+enum CallbackSituation
+{
+ TimerFired,
+ ResourceDestroyed
+};
+
+nsresult
+TimerHandleWithFunction::InitWithCleanup(uint32_t aDelay_ms,
+ uint32_t aType,
+ mozilla::function<void(CallbackSituation)>&& aF)
+{
+ nsresult rv = Ensure();
+ if (MOZ_UNLIKELY(NS_FAILED(rv))) {
+ return rv;
+ }
+ return get()->InitWithCallback(
+ new CallbackAndCleaner(mozilla::Move(aF)),
+ aDelay_ms, aType);
+}
+
+NS_IMPL_ISUPPORTS(TimerHandleWithFunction::Callback, nsITimerCallback)
+
+NS_IMETHODIMP
+TimerHandleWithFunction::Callback::Notify(nsITimer* aTimer)
+{
+ mF();
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(TimerHandleWithFunction::CallbackAndCleaner, nsITimerCallback)
+
+NS_IMETHODIMP
+TimerHandleWithFunction::CallbackAndCleaner::Notify(nsITimer* aTimer)
+{
+ mF(TimerFired);
+ return NS_OK;
+}
new file mode 100644
--- /dev/null
+++ b/xpcom/glue/TimerUtils.h
@@ -0,0 +1,143 @@
+/* -*- 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 TimerUtils_h___
+#define TimerUtils_h___
+
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+
+#include "mozilla/Function.h"
+
+// Encapsulates an nsCOMPtr<nsITimer>, with utility methods to create and access
+// the timer.
+class TimerHandle
+{
+public:
+ TimerHandle() = default;
+ TimerHandle(const TimerHandle&) = default;
+ TimerHandle& operator=(const TimerHandle&) = default;
+ TimerHandle(TimerHandle&&) = default;
+ TimerHandle& operator=(TimerHandle&&) = default;
+ ~TimerHandle() = default;
+
+ // Create a timer instance, override previous one if any.
+ nsresult Create();
+ // Create a timer instance if there isn't already one.
+ nsresult Ensure();
+ // Cancel timer, if any.
+ nsresult Cancel();
+ // Release timer from this handle.
+ void Release();
+ // If there is a timer, cancel it and then release it from this handle.
+ nsresult CancelAndRelease();
+
+ // nsCOMPtr-like API.
+ nsITimer* get() const { return mTimerCOMPtr.get(); }
+ explicit operator bool() const { return get(); }
+ nsITimer* operator->() const
+ {
+ MOZ_ASSERT(get() != 0,
+ "You can't dereference a NULL TimerHandle with operator->().");
+ return get();
+ }
+ nsITimer& operator*() const
+ {
+ MOZ_ASSERT(get() != 0,
+ "You can't dereference a NULL TimerHandle with operator*().");
+ return *get();
+ }
+
+private:
+ nsCOMPtr<nsITimer> mTimerCOMPtr;
+};
+
+// Timer handle that will cancel its timer when destroyed.
+// Useful when storing a timer in a class, when that timer callback works on
+// that class, to avoid UAF.
+class AutoCancelingTimerHandle : public TimerHandle
+{
+public:
+ AutoCancelingTimerHandle() = default;
+ AutoCancelingTimerHandle(const AutoCancelingTimerHandle&) = default;
+ AutoCancelingTimerHandle& operator=(const AutoCancelingTimerHandle&) = default;
+ AutoCancelingTimerHandle(AutoCancelingTimerHandle&&) = default;
+ AutoCancelingTimerHandle& operator=(AutoCancelingTimerHandle&&) = default;
+ ~AutoCancelingTimerHandle() { Cancel(); }
+};
+
+// Timer handle that takes a function object (e.g.: lambda) as callback.
+class TimerHandleWithFunction : public TimerHandle
+{
+public:
+ TimerHandleWithFunction() = default;
+ TimerHandleWithFunction(const TimerHandleWithFunction&) = default;
+ TimerHandleWithFunction& operator=(const TimerHandleWithFunction&) = default;
+ TimerHandleWithFunction(TimerHandleWithFunction&&) = default;
+ TimerHandleWithFunction& operator=(TimerHandleWithFunction&&) = default;
+ ~TimerHandleWithFunction() = default;
+
+ // Start a timer, and call aF() whenever the timer fires.
+ nsresult Init(uint32_t aDelay_ms,
+ uint32_t aType, // TYPE_* enum from nsITimer
+ mozilla::function<void(void)>&& aF);
+
+ enum CallbackSituation
+ {
+ TimerFired,
+ ResourceDestroyed
+ };
+
+ // Start a timer, aF will be called if/when the timer fires, and also when
+ // the timer resource is destroyed (e.g., when the timer is first cancelled,
+ // or destroyed).
+ nsresult InitWithCleanup(uint32_t aDelay_ms,
+ uint32_t aType, // TYPE_* enum from nsITimer
+ mozilla::function<void(CallbackSituation)>&& aF);
+
+private:
+ class Callback final : public nsITimerCallback
+ {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ template <typename AF>
+ explicit Callback(AF&& aF) : mF(mozilla::Forward<AF>(aF)) {}
+ private:
+ virtual ~Callback() = default;
+ mozilla::function<void(void)> mF;
+ };
+
+ class CallbackAndCleaner final : public nsITimerCallback
+ {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ template <typename AF>
+ explicit CallbackAndCleaner(AF&& aF) : mF(mozilla::Forward<AF>(aF)) {}
+ private:
+ virtual ~CallbackAndCleaner()
+ {
+ mF(ResourceDestroyed);
+ }
+ mozilla::function<void(CallbackSituation)> mF;
+ };
+};
+
+// Auto-canceling timer handle that takes a function object (e.g.: lambda) as
+// callback.
+class AutoCancelingTimerHandleWithFunction : public TimerHandleWithFunction
+{
+public:
+ AutoCancelingTimerHandleWithFunction() = default;
+ AutoCancelingTimerHandleWithFunction(const AutoCancelingTimerHandleWithFunction&) = default;
+ AutoCancelingTimerHandleWithFunction& operator=(const AutoCancelingTimerHandleWithFunction&) = default;
+ AutoCancelingTimerHandleWithFunction(AutoCancelingTimerHandleWithFunction&&) = default;
+ AutoCancelingTimerHandleWithFunction& operator=(AutoCancelingTimerHandleWithFunction&&) = default;
+ ~AutoCancelingTimerHandleWithFunction() { Cancel(); }
+};
+
+#endif // TimerUtils_h___
--- a/xpcom/glue/moz.build
+++ b/xpcom/glue/moz.build
@@ -75,16 +75,17 @@ EXPORTS.mozilla += [
'EnumeratedArrayCycleCollection.h',
'FileUtils.h',
'GenericFactory.h',
'IntentionalCrash.h',
'Monitor.h',
'Mutex.h',
'Observer.h',
'ReentrantMonitor.h',
+ 'TimerUtils.h',
]
include('objs.mozbuild')
UNIFIED_SOURCES += xpcom_gluens_src_cppsrcs
UNIFIED_SOURCES += xpcom_glue_src_cppsrcs
UNIFIED_SOURCES += [
--- a/xpcom/glue/objs.mozbuild
+++ b/xpcom/glue/objs.mozbuild
@@ -25,16 +25,17 @@ xpcom_glue_src_lcppsrcs = [
'nsMemory.cpp',
'nsQuickSort.cpp',
'nsTArray.cpp',
'nsThreadUtils.cpp',
'nsTObserverArray.cpp',
'nsVersionComparator.cpp',
'nsWeakReference.cpp',
'PLDHashTable.cpp',
+ 'TimerUtils.cpp',
]
xpcom_glue_src_cppsrcs = [
'/xpcom/glue/%s' % s for s in xpcom_glue_src_lcppsrcs
]
xpcom_gluens_src_lcppsrcs = [
'BlockingResourceBase.cpp',
--- a/xpcom/tests/TestTimers.cpp
+++ b/xpcom/tests/TestTimers.cpp
@@ -2,16 +2,17 @@
/* 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/. */
#include "TestHarness.h"
#include "nsIThread.h"
#include "nsITimer.h"
+#include "mozilla/TimerUtils.h"
#include "nsCOMPtr.h"
#include "nsComponentManagerUtils.h"
#include "nsServiceManagerUtils.h"
#include "nsThreadUtils.h"
#include "prinrval.h"
#include "prmon.h"
#include "prthread.h"
@@ -442,25 +443,147 @@ FuzzTestTimers()
"Timed out waiting for all timers to pop");
PR_Sleep(PR_MillisecondsToInterval(10));
}
}
return NS_OK;
}
+nsresult
+TestTimerUtils_TimerHandleWithFunction()
+{
+ AutoCreateAndDestroyReentrantMonitor newMon;
+ NS_ENSURE_TRUE(newMon, NS_ERROR_OUT_OF_MEMORY);
+
+ AutoTestThread testThread;
+ NS_ENSURE_TRUE(testThread, NS_ERROR_OUT_OF_MEMORY);
+
+ TimerHandleWithFunction timer;
+ nsresult rv;
+ rv = timer.Ensure();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIEventTarget* target = static_cast<nsIEventTarget*>(testThread);
+
+ rv = timer->SetTarget(target);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIThread* notifiedThread = nullptr;
+
+ nsIThread** threadPtr = ¬ifiedThread;
+ ReentrantMonitor* monPtr = newMon;
+ bool cleanedUp = false;
+ rv =
+ timer.InitWithCleanup(
+ 2000,
+ nsITimer::TYPE_ONE_SHOT,
+ [threadPtr, monPtr, &cleanedUp]
+ (TimerHandleWithFunction::CallbackSituation aSituation)
+ {
+ if (aSituation == TimerHandleWithFunction::TimerFired) {
+ MOZ_ASSERT(threadPtr, "Callback was not supposed to be called!");
+ nsCOMPtr<nsIThread> current(do_GetCurrentThread());
+
+ ReentrantMonitorAutoEnter mon(*monPtr);
+
+ MOZ_ASSERT(!*threadPtr, "Timer called back more than once!");
+ *threadPtr = current;
+
+ mon.Notify();
+ } else {
+ MOZ_ASSERT(aSituation == TimerHandleWithFunction::ResourceDestroyed);
+ cleanedUp = true;
+ }
+ });
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ReentrantMonitorAutoEnter mon(*newMon);
+ while (!notifiedThread) {
+ mon.Wait();
+ }
+ NS_ENSURE_TRUE(notifiedThread == testThread, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(cleanedUp, NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+nsresult
+TestTimerUtils_AutoCancelingTimerHandleWithFunction()
+{
+ AutoCreateAndDestroyReentrantMonitor newMon;
+ NS_ENSURE_TRUE(newMon, NS_ERROR_OUT_OF_MEMORY);
+
+ AutoTestThread testThread;
+ NS_ENSURE_TRUE(testThread, NS_ERROR_OUT_OF_MEMORY);
+
+ nsIThread* notifiedThread = nullptr;
+ bool cleanedUp = false;
+
+ {
+ AutoCancelingTimerHandleWithFunction timer;
+ nsresult rv;
+ rv = timer.Ensure();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIEventTarget* target = static_cast<nsIEventTarget*>(testThread);
+
+ rv = timer->SetTarget(target);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIThread** threadPtr = ¬ifiedThread;
+ ReentrantMonitor* monPtr = newMon;
+ rv =
+ timer.InitWithCleanup(
+ 2000,
+ nsITimer::TYPE_ONE_SHOT,
+ [threadPtr, monPtr, &cleanedUp]
+ (TimerHandleWithFunction::CallbackSituation aSituation)
+ {
+ if (aSituation == TimerHandleWithFunction::TimerFired) {
+ MOZ_ASSERT(threadPtr, "Callback was not supposed to be called!");
+ nsCOMPtr<nsIThread> current(do_GetCurrentThread());
+
+ ReentrantMonitorAutoEnter mon(*monPtr);
+
+ MOZ_ASSERT(!*threadPtr, "Timer called back more than once!");
+ *threadPtr = current;
+
+ mon.Notify();
+ } else {
+ MOZ_ASSERT(aSituation == TimerHandleWithFunction::ResourceDestroyed);
+ cleanedUp = true;
+ }
+ });
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_FALSE(notifiedThread == testThread, NS_ERROR_FAILURE);
+ NS_ENSURE_FALSE(cleanedUp, NS_ERROR_FAILURE);
+
+ // End of scope for 'AutoCancelingTimerHandleWithFunction timer',
+ // should destroy timer *before* it has fired!
+ }
+
+ NS_ENSURE_FALSE(notifiedThread == testThread, NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(cleanedUp, NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
int main(int argc, char** argv)
{
ScopedXPCOM xpcom("TestTimers");
NS_ENSURE_FALSE(xpcom.failed(), 1);
static TestFuncPtr testsToRun[] = {
TestTargetedTimers,
TestTimerWithStoppedTarget,
- FuzzTestTimers
+ FuzzTestTimers,
+ TestTimerUtils_TimerHandleWithFunction,
+ TestTimerUtils_AutoCancelingTimerHandleWithFunction
};
static uint32_t testCount = sizeof(testsToRun) / sizeof(testsToRun[0]);
for (uint32_t i = 0; i < testCount; i++) {
nsresult rv = testsToRun[i]();
NS_ENSURE_SUCCESS(rv, 1);
}