1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 #include <threadpoolapiset.h>
10 #include "mozilla/AlreadyAddRefed.h"
11 #include "mozilla/Assertions.h"
12 #include "mozilla/Logging.h"
13 #include "mozilla/Mutex.h"
14 #include "mozilla/RefPtr.h"
15 #include "mozilla/ThreadSafety.h"
16 #include "mozilla/WinHandleWatcher.h"
19 #include "nsIRunnable.h"
20 #include "nsISerialEventTarget.h"
21 #include "nsISupportsImpl.h"
22 #include "nsITargetShutdownTask.h"
23 #include "nsIWeakReferenceUtils.h"
24 #include "nsThreadUtils.h"
26 mozilla::LazyLogModule
sHWLog("HandleWatcher");
30 struct WaitHandleDeleter
{
31 void operator()(PTP_WAIT waitHandle
) {
32 MOZ_LOG(sHWLog
, LogLevel::Debug
, ("Closing PTP_WAIT %p", waitHandle
));
33 ::CloseThreadpoolWait(waitHandle
);
36 } // namespace details
37 using WaitHandlePtr
= UniquePtr
<TP_WAIT
, details::WaitHandleDeleter
>;
39 // HandleWatcher::Impl
41 // The backing implementation of HandleWatcher is a PTP_WAIT, an OS-threadpool
42 // wait-object. Windows doesn't actually create a new thread per wait-object;
43 // OS-threadpool threads are assigned to wait-objects only when their associated
44 // handle become signaled -- although explicit documentation of this fact is
45 // somewhat obscurely placed. [1]
47 // Throughout this class, we use manual locking and unlocking guarded by Clang's
48 // thread-safety warnings, rather than scope-based lock-guards. See `Replace()`
49 // for an explanation and justification.
51 // [1]https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects#remarks
52 class HandleWatcher::Impl final
: public nsITargetShutdownTask
{
53 NS_DECL_THREADSAFE_ISUPPORTS
59 ~Impl() { MOZ_ASSERT(IsStopped()); }
62 // The watched handle and its callback.
64 RefPtr
<nsIEventTarget
> target
;
65 nsCOMPtr
<nsIRunnable
> runnable
;
67 // Handle to the threadpool wait-object.
68 WaitHandlePtr waitHandle
;
69 // A pointer to ourselves, notionally owned by the wait-object.
72 // (We can't actually do this because a) it has annoying consequences in
73 // C++20 thanks to P1008R1, and b) Clang just ignores it anyway.)
75 // ~Data() MOZ_EXCLUDES(mMutex) = default;
78 mozilla::Mutex mMutex
{"HandleWatcher::Impl"};
79 Data mData
MOZ_GUARDED_BY(mMutex
) = {};
81 // Callback from OS threadpool wait-object.
82 static void CALLBACK
WaitCallback(PTP_CALLBACK_INSTANCE
, void* ctx
,
84 TP_WAIT_RESULT aResult
) {
85 static_cast<Impl
*>(ctx
)->OnWaitCompleted(aWaitHandle
, aResult
);
88 void OnWaitCompleted(PTP_WAIT aWaitHandle
, TP_WAIT_RESULT aResult
)
89 MOZ_EXCLUDES(mMutex
) {
90 MOZ_ASSERT(aResult
== WAIT_OBJECT_0
);
93 // If this callback is no longer the active callback, skip out.
94 // All cleanup is someone else's problem.
95 if (aWaitHandle
!= mData
.waitHandle
.get()) {
96 MOZ_LOG(sHWLog
, LogLevel::Debug
,
97 ("Recv'd already-stopped callback: HW %p | PTP_WAIT %p", this,
103 // Take our self-pointer so that we release it on exit.
104 RefPtr
<Impl
> self
= std::move(mData
.self
);
106 MOZ_LOG(sHWLog
, LogLevel::Info
,
107 ("Recv'd callback: HW %p | handle %p | target %p | PTP_WAIT %p",
108 this, mData
.handle
, mData
.target
.get(), aWaitHandle
));
110 // This may fail if (for example) `mData.target` is being shut down, but we
111 // have not yet received the shutdown callback.
112 mData
.target
->Dispatch(mData
.runnable
.forget());
117 static RefPtr
<Impl
> Create(HANDLE aHandle
, nsIEventTarget
* aTarget
,
118 already_AddRefed
<nsIRunnable
> aRunnable
) {
119 auto impl
= MakeRefPtr
<Impl
>();
120 bool const ok
[[maybe_unused
]] =
121 impl
->Watch(aHandle
, aTarget
, std::move(aRunnable
));
127 bool Watch(HANDLE aHandle
, nsIEventTarget
* aTarget
,
128 already_AddRefed
<nsIRunnable
> aRunnable
) MOZ_EXCLUDES(mMutex
) {
132 RefPtr
<nsIEventTarget
> target(aTarget
);
134 WaitHandlePtr waitHandle
{
135 ::CreateThreadpoolWait(&WaitCallback
, this, nullptr)};
143 nsresult
const ret
= aTarget
->RegisterShutdownTask(this);
144 if (NS_FAILED(ret
)) {
149 MOZ_LOG(sHWLog
, LogLevel::Info
,
150 ("Setting callback: HW %p | handle %p | target %p | PTP_WAIT %p",
151 this, aHandle
, aTarget
, waitHandle
.get()));
153 // returns `void`; presumably always succeeds given a successful
154 // `::CreateThreadpoolWait()`
155 ::SetThreadpoolWait(waitHandle
.get(), aHandle
, nullptr);
156 // After this point, you must call `FlushWaitHandle(waitHandle.get())`
157 // before destroying the wait handle. (Note that this must be done while
158 // *not* holding `mMutex`!)
160 Replace(Data
{.handle
= aHandle
,
161 .target
= std::move(target
),
162 .runnable
= aRunnable
,
163 .waitHandle
= std::move(waitHandle
),
170 void TargetShutdown() MOZ_EXCLUDES(mMutex
) override final
{
173 MOZ_LOG(sHWLog
, LogLevel::Debug
,
174 ("Target shutdown: HW %p | handle %p | target %p | PTP_WAIT %p",
175 this, mData
.handle
, mData
.target
.get(), mData
.waitHandle
.get()));
177 // Clear mData.target, since there's no need to unregister the shutdown task
178 // anymore. Hold onto it until we release the mutex, though, to avoid any
179 // reentrancy issues.
181 // This is more for internal consistency than safety: someone has to be
182 // shutting `target` down, and that someone isn't us, so there's necessarily
183 // another reference out there. (Although decrementing the refcount might
184 // still have arbitrary effects if someone's been excessively clever with
185 // nsISupports::Release...)
186 auto const oldTarget
= std::move(mData
.target
);
188 // (Static-assert that the mutex has indeed been released.)
189 ([&]() MOZ_EXCLUDES(mMutex
) {})();
193 void Stop() MOZ_EXCLUDES(mMutex
) {
198 bool IsStopped() MOZ_EXCLUDES(mMutex
) {
199 mozilla::MutexAutoLock
lock(mMutex
);
200 return !mData
.handle
;
204 // Throughout this class, we use manual locking and unlocking guarded by
205 // Clang's thread-safety warnings, rather than scope-based lock-guards. This
206 // is largely driven by `Replace()`, below, which performs both operations
207 // which require the mutex to be held and operations which require it to not
208 // be held, and therefore must explicitly sequence the mutex release.
210 // These explicit locks, unlocks, and annotations are both alien to C++ and
211 // offensively tedious; but they _are_ still checked for state consistency at
212 // scope boundaries. (The concerned reader is invited to test this by
213 // deliberately removing an `mMutex.Unlock()` call from anywhere in the class
214 // and viewing the resultant compiler diagnostics.)
216 // A more principled, or at least differently-principled, implementation might
217 // create a scope-based lock-guard and pass it to `Replace()` to dispose of at
218 // the proper time. Alas, it cannot be communicated to Clang's thread-safety
219 // checker that such a guard is associated with `mMutex`.
221 void Replace(Data
&& aData
) MOZ_CAPABILITY_RELEASE(mMutex
) {
222 // either both handles are NULL, or neither is
223 MOZ_ASSERT(!!aData
.handle
== !!aData
.waitHandle
);
226 MOZ_LOG(sHWLog
, LogLevel::Info
,
227 ("Stop callback: HW %p | handle %p | target %p | PTP_WAIT %p",
228 this, mData
.handle
, mData
.target
.get(), mData
.waitHandle
.get()));
232 mData
.target
->UnregisterShutdownTask(this);
235 // Extract the old data and insert the new -- but hold onto the old data for
236 // now. (See [1] and [2], below.)
237 Data oldData
= std::exchange(mData
, std::move(aData
));
239 ////////////////////////////////////////////////////////////////////////////
240 // Release the mutex.
242 ////////////////////////////////////////////////////////////////////////////
244 // [1] `oldData.self` will be unset if the old callback already ran (or if
245 // there was no old callback in the first place). If it's set, though, we
246 // need to explicitly clear out the wait-object first.
248 MOZ_ASSERT(oldData
.waitHandle
);
249 FlushWaitHandle(oldData
.waitHandle
.get());
252 // [2] oldData also includes several other reference-counted pointers. It's
253 // possible that these may be the last pointer to something, so releasing
254 // them may have arbitrary side-effects -- like calling this->Stop(), which
255 // will try to reacquire the mutex.
257 // Now that we've released the mutex, we can (implicitly) release them all
261 // Either confirm as complete or cancel any callbacks on aWaitHandle. Block
262 // until this is done. (See documentation for ::CloseThreadpoolWait().)
263 void FlushWaitHandle(PTP_WAIT aWaitHandle
) MOZ_EXCLUDES(mMutex
) {
264 ::SetThreadpoolWait(aWaitHandle
, nullptr, nullptr);
265 // This might block on `OnWaitCompleted()`, so we can't hold `mMutex` here.
266 ::WaitForThreadpoolWaitCallbacks(aWaitHandle
, TRUE
);
267 // ::CloseThreadpoolWait() itself is the caller's responsibility.
271 NS_IMPL_ISUPPORTS(HandleWatcher::Impl
, nsITargetShutdownTask
)
274 // HandleWatcher member function implementations
276 HandleWatcher::HandleWatcher() : mImpl
{} {}
277 HandleWatcher::~HandleWatcher() {
279 MOZ_ASSERT(mImpl
->IsStopped());
280 mImpl
->Stop(); // just in case, in release
284 HandleWatcher::HandleWatcher(HandleWatcher
&&) noexcept
= default;
285 HandleWatcher
& HandleWatcher::operator=(HandleWatcher
&&) noexcept
= default;
287 void HandleWatcher::Watch(HANDLE aHandle
, nsIEventTarget
* aTarget
,
288 already_AddRefed
<nsIRunnable
> aRunnable
) {
289 auto impl
= Impl::Create(aHandle
, aTarget
, std::move(aRunnable
));
295 mImpl
= std::move(impl
);
298 void HandleWatcher::Stop() {
304 bool HandleWatcher::IsStopped() { return !mImpl
|| mImpl
->IsStopped(); }
306 } // namespace mozilla