summaryrefslogtreecommitdiffstats
path: root/xpcom/threads/WinHandleWatcher.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--xpcom/threads/WinHandleWatcher.cpp306
1 files changed, 306 insertions, 0 deletions
diff --git a/xpcom/threads/WinHandleWatcher.cpp b/xpcom/threads/WinHandleWatcher.cpp
new file mode 100644
index 0000000000..d475993925
--- /dev/null
+++ b/xpcom/threads/WinHandleWatcher.cpp
@@ -0,0 +1,306 @@
+/* -*- 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 <windows.h>
+#include <threadpoolapiset.h>
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ThreadSafety.h"
+#include "mozilla/WinHandleWatcher.h"
+
+#include "nsCOMPtr.h"
+#include "nsIRunnable.h"
+#include "nsISerialEventTarget.h"
+#include "nsISupportsImpl.h"
+#include "nsITargetShutdownTask.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsThreadUtils.h"
+
+mozilla::LazyLogModule sHWLog("HandleWatcher");
+
+namespace mozilla {
+namespace details {
+struct WaitHandleDeleter {
+ void operator()(PTP_WAIT waitHandle) {
+ MOZ_LOG(sHWLog, LogLevel::Debug, ("Closing PTP_WAIT %p", waitHandle));
+ ::CloseThreadpoolWait(waitHandle);
+ }
+};
+} // namespace details
+using WaitHandlePtr = UniquePtr<TP_WAIT, details::WaitHandleDeleter>;
+
+// HandleWatcher::Impl
+//
+// The backing implementation of HandleWatcher is a PTP_WAIT, an OS-threadpool
+// wait-object. Windows doesn't actually create a new thread per wait-object;
+// OS-threadpool threads are assigned to wait-objects only when their associated
+// handle become signaled -- although explicit documentation of this fact is
+// somewhat obscurely placed. [1]
+//
+// Throughout this class, we use manual locking and unlocking guarded by Clang's
+// thread-safety warnings, rather than scope-based lock-guards. See `Replace()`
+// for an explanation and justification.
+//
+// [1]https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects#remarks
+class HandleWatcher::Impl final : public nsITargetShutdownTask {
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ public:
+ Impl() = default;
+
+ private:
+ ~Impl() { MOZ_ASSERT(IsStopped()); }
+
+ struct Data {
+ // The watched handle and its callback.
+ HANDLE handle;
+ RefPtr<nsIEventTarget> target;
+ nsCOMPtr<nsIRunnable> runnable;
+
+ // Handle to the threadpool wait-object.
+ WaitHandlePtr waitHandle;
+ // A pointer to ourselves, notionally owned by the wait-object.
+ RefPtr<Impl> self;
+
+ // (We can't actually do this because a) it has annoying consequences in
+ // C++20 thanks to P1008R1, and b) Clang just ignores it anyway.)
+ //
+ // ~Data() MOZ_EXCLUDES(mMutex) = default;
+ };
+
+ mozilla::Mutex mMutex{"HandleWatcher::Impl"};
+ Data mData MOZ_GUARDED_BY(mMutex) = {};
+
+ // Callback from OS threadpool wait-object.
+ static void CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE, void* ctx,
+ PTP_WAIT aWaitHandle,
+ TP_WAIT_RESULT aResult) {
+ static_cast<Impl*>(ctx)->OnWaitCompleted(aWaitHandle, aResult);
+ }
+
+ void OnWaitCompleted(PTP_WAIT aWaitHandle, TP_WAIT_RESULT aResult)
+ MOZ_EXCLUDES(mMutex) {
+ MOZ_ASSERT(aResult == WAIT_OBJECT_0);
+
+ mMutex.Lock();
+ // If this callback is no longer the active callback, skip out.
+ // All cleanup is someone else's problem.
+ if (aWaitHandle != mData.waitHandle.get()) {
+ MOZ_LOG(sHWLog, LogLevel::Debug,
+ ("Recv'd already-stopped callback: HW %p | PTP_WAIT %p", this,
+ aWaitHandle));
+ mMutex.Unlock();
+ return;
+ }
+
+ // Take our self-pointer so that we release it on exit.
+ RefPtr<Impl> self = std::move(mData.self);
+
+ MOZ_LOG(sHWLog, LogLevel::Info,
+ ("Recv'd callback: HW %p | handle %p | target %p | PTP_WAIT %p",
+ this, mData.handle, mData.target.get(), aWaitHandle));
+
+ // This may fail if (for example) `mData.target` is being shut down, but we
+ // have not yet received the shutdown callback.
+ mData.target->Dispatch(mData.runnable.forget());
+ Replace(Data{});
+ }
+
+ public:
+ static RefPtr<Impl> Create(HANDLE aHandle, nsIEventTarget* aTarget,
+ already_AddRefed<nsIRunnable> aRunnable) {
+ auto impl = MakeRefPtr<Impl>();
+ bool const ok [[maybe_unused]] =
+ impl->Watch(aHandle, aTarget, std::move(aRunnable));
+ MOZ_ASSERT(ok);
+ return impl;
+ }
+
+ private:
+ bool Watch(HANDLE aHandle, nsIEventTarget* aTarget,
+ already_AddRefed<nsIRunnable> aRunnable) MOZ_EXCLUDES(mMutex) {
+ MOZ_ASSERT(aHandle);
+ MOZ_ASSERT(aTarget);
+
+ RefPtr<nsIEventTarget> target(aTarget);
+
+ WaitHandlePtr waitHandle{
+ ::CreateThreadpoolWait(&WaitCallback, this, nullptr)};
+ if (!waitHandle) {
+ return false;
+ }
+
+ {
+ mMutex.Lock();
+
+ nsresult const ret = aTarget->RegisterShutdownTask(this);
+ if (NS_FAILED(ret)) {
+ mMutex.Unlock();
+ return false;
+ }
+
+ MOZ_LOG(sHWLog, LogLevel::Info,
+ ("Setting callback: HW %p | handle %p | target %p | PTP_WAIT %p",
+ this, aHandle, aTarget, waitHandle.get()));
+
+ // returns `void`; presumably always succeeds given a successful
+ // `::CreateThreadpoolWait()`
+ ::SetThreadpoolWait(waitHandle.get(), aHandle, nullptr);
+ // After this point, you must call `FlushWaitHandle(waitHandle.get())`
+ // before destroying the wait handle. (Note that this must be done while
+ // *not* holding `mMutex`!)
+
+ Replace(Data{.handle = aHandle,
+ .target = std::move(target),
+ .runnable = aRunnable,
+ .waitHandle = std::move(waitHandle),
+ .self = this});
+ }
+
+ return true;
+ }
+
+ void TargetShutdown() MOZ_EXCLUDES(mMutex) override final {
+ mMutex.Lock();
+
+ MOZ_LOG(sHWLog, LogLevel::Debug,
+ ("Target shutdown: HW %p | handle %p | target %p | PTP_WAIT %p",
+ this, mData.handle, mData.target.get(), mData.waitHandle.get()));
+
+ // Clear mData.target, since there's no need to unregister the shutdown task
+ // anymore. Hold onto it until we release the mutex, though, to avoid any
+ // reentrancy issues.
+ //
+ // This is more for internal consistency than safety: someone has to be
+ // shutting `target` down, and that someone isn't us, so there's necessarily
+ // another reference out there. (Although decrementing the refcount might
+ // still have arbitrary effects if someone's been excessively clever with
+ // nsISupports::Release...)
+ auto const oldTarget = std::move(mData.target);
+ Replace(Data{});
+ // (Static-assert that the mutex has indeed been released.)
+ ([&]() MOZ_EXCLUDES(mMutex) {})();
+ }
+
+ public:
+ void Stop() MOZ_EXCLUDES(mMutex) {
+ mMutex.Lock();
+ Replace(Data{});
+ }
+
+ bool IsStopped() MOZ_EXCLUDES(mMutex) {
+ mozilla::MutexAutoLock lock(mMutex);
+ return !mData.handle;
+ }
+
+ private:
+ // Throughout this class, we use manual locking and unlocking guarded by
+ // Clang's thread-safety warnings, rather than scope-based lock-guards. This
+ // is largely driven by `Replace()`, below, which performs both operations
+ // which require the mutex to be held and operations which require it to not
+ // be held, and therefore must explicitly sequence the mutex release.
+ //
+ // These explicit locks, unlocks, and annotations are both alien to C++ and
+ // offensively tedious; but they _are_ still checked for state consistency at
+ // scope boundaries. (The concerned reader is invited to test this by
+ // deliberately removing an `mMutex.Unlock()` call from anywhere in the class
+ // and viewing the resultant compiler diagnostics.)
+ //
+ // A more principled, or at least differently-principled, implementation might
+ // create a scope-based lock-guard and pass it to `Replace()` to dispose of at
+ // the proper time. Alas, it cannot be communicated to Clang's thread-safety
+ // checker that such a guard is associated with `mMutex`.
+ //
+ void Replace(Data&& aData) MOZ_CAPABILITY_RELEASE(mMutex) {
+ // either both handles are NULL, or neither is
+ MOZ_ASSERT(!!aData.handle == !!aData.waitHandle);
+
+ if (mData.handle) {
+ MOZ_LOG(sHWLog, LogLevel::Info,
+ ("Stop callback: HW %p | handle %p | target %p | PTP_WAIT %p",
+ this, mData.handle, mData.target.get(), mData.waitHandle.get()));
+ }
+
+ if (mData.target) {
+ mData.target->UnregisterShutdownTask(this);
+ }
+
+ // Extract the old data and insert the new -- but hold onto the old data for
+ // now. (See [1] and [2], below.)
+ Data oldData = std::exchange(mData, std::move(aData));
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Release the mutex.
+ mMutex.Unlock();
+ ////////////////////////////////////////////////////////////////////////////
+
+ // [1] `oldData.self` will be unset if the old callback already ran (or if
+ // there was no old callback in the first place). If it's set, though, we
+ // need to explicitly clear out the wait-object first.
+ if (oldData.self) {
+ MOZ_ASSERT(oldData.waitHandle);
+ FlushWaitHandle(oldData.waitHandle.get());
+ }
+
+ // [2] oldData also includes several other reference-counted pointers. It's
+ // possible that these may be the last pointer to something, so releasing
+ // them may have arbitrary side-effects -- like calling this->Stop(), which
+ // will try to reacquire the mutex.
+ //
+ // Now that we've released the mutex, we can (implicitly) release them all
+ // here.
+ }
+
+ // Either confirm as complete or cancel any callbacks on aWaitHandle. Block
+ // until this is done. (See documentation for ::CloseThreadpoolWait().)
+ void FlushWaitHandle(PTP_WAIT aWaitHandle) MOZ_EXCLUDES(mMutex) {
+ ::SetThreadpoolWait(aWaitHandle, nullptr, nullptr);
+ // This might block on `OnWaitCompleted()`, so we can't hold `mMutex` here.
+ ::WaitForThreadpoolWaitCallbacks(aWaitHandle, TRUE);
+ // ::CloseThreadpoolWait() itself is the caller's responsibility.
+ }
+};
+
+NS_IMPL_ISUPPORTS(HandleWatcher::Impl, nsITargetShutdownTask)
+
+//////
+// HandleWatcher member function implementations
+
+HandleWatcher::HandleWatcher() : mImpl{} {}
+HandleWatcher::~HandleWatcher() {
+ if (mImpl) {
+ MOZ_ASSERT(mImpl->IsStopped());
+ mImpl->Stop(); // just in case, in release
+ }
+}
+
+HandleWatcher::HandleWatcher(HandleWatcher&&) noexcept = default;
+HandleWatcher& HandleWatcher::operator=(HandleWatcher&&) noexcept = default;
+
+void HandleWatcher::Watch(HANDLE aHandle, nsIEventTarget* aTarget,
+ already_AddRefed<nsIRunnable> aRunnable) {
+ auto impl = Impl::Create(aHandle, aTarget, std::move(aRunnable));
+ MOZ_ASSERT(impl);
+
+ if (mImpl) {
+ mImpl->Stop();
+ }
+ mImpl = std::move(impl);
+}
+
+void HandleWatcher::Stop() {
+ if (mImpl) {
+ mImpl->Stop();
+ }
+}
+
+bool HandleWatcher::IsStopped() { return !mImpl || mImpl->IsStopped(); }
+
+} // namespace mozilla