/*** Copyright (C) 2024 Jamie Reece Wilson (a/k/a "Reece"). All rights reserved. File: DestructionWatch.hpp Date: 2024-01-22 Author: Reece ***/ #pragma once namespace Aurora::Utility { struct ADestructionWatcher; struct DestructionWatch { inline DestructionWatch(); inline ~DestructionWatch(); // If your (optionally abstract) ADestructionWatcher requires access to the parent object, it is highly recommened to call RemoveAll within the destructor of the parent class. // Otherwise, you must maintain a strict ctor/dtor order of your classes layout. inline void RemoveAll(); private: friend struct ADestructionWatcher; Threading::Waitables::FutexWaitable mutex; ADestructionWatcher *pFirstWatcher {}; AuList *pListpWatches {}; AuUInt32 uDtorCalls {}; inline void RemoveWatcher(ADestructionWatcher *pWatcher); inline void RemoveWatcherCall(ADestructionWatcher *pWatcher); }; struct ADestructionWatcher { inline ADestructionWatcher(); inline ~ADestructionWatcher(); inline void Observe(DestructionWatch *pWatch); inline void Observe(DestructionWatch &pWatch); inline bool IsObservedAlive(); inline bool IsObservedDead(); inline ADestructionWatcher(ADestructionWatcher &&move); inline ADestructionWatcher(const ADestructionWatcher ©); inline ADestructionWatcher &operator =(ADestructionWatcher &&move); inline ADestructionWatcher &operator =(const ADestructionWatcher ©); protected: // Note that since the parent object is in the midst of destruction; you should not be manually calling the destroy operations of it or its' children. // C++ construction and the inverse destruction order are well defined. // In the case of extending DestructionWatch as a base class, our constructor should be invoked first, and therefore our destructor will be invoked last. // In the case of inserting DestructionWatch as a class member, you must guard resource access via synchronization primitives placed in proper construction/destruction order. inline virtual void OnParentDestroy() { } inline virtual void OnDestroy() { // You *might* be able to access the parent object at this point in time. // // It is legal to access the memory under both OnParentDestroy(), and OnDestroy() in the case that IsObservedAlive() return true; however, accessing the parents // member fields shall be restricted pursuant to the inverse of C++s construction order. It is possible for multithreaded code to be in the midst of calling // the destruction chain before we are aware of the destruction condition. Shared pointers work around this by invoking private std::enable_shared_from_this // members at the time of construction and time of control block release. It *should* be safe to access HANDLEs, raw c pointers, etc without any special logic, // on the account that the parent object will not have released its' memory until we're done; same applies to OnParentDestroy(). // // ** To further harden against out of order destruction, it is advisable but not required, to call DestructionWatch::RemoveAll() in the parents destructor. ** // ** Alternatively, you must be hyper-aware of the C++ destruction order. ** } public: inline void Destroy() { if (!DoUnderLock(&ADestructionWatcher::OnDestroy, this)) { this->OnDestroy(); } } private: friend struct DestructionWatch; friend class Threading::LockGuard; inline void OnSelfDTOR(); inline void OnDTOR(); inline void Lock(); inline void Unlock(); public: template AuConditional_t, void>, bool, AuOptional>> DoUnderLock(const T &callable, Args && ...args) { { AU_LOCK_GUARD(this->selfLock); if (!this->pParent) { if constexpr (AuIsSame_v, void>) { return false; } else { return AuOptional> {}; } } AuAtomicAdd(&this->pParent->uDtorCalls, 1u); } { AU_LOCK_GUARD(this); if constexpr (AuIsSame_v, void>) { AuInvoke(callable, AuForward(args)...); return true; } else { return AuInvoke(callable, AuForward(args)...); } } } private: Threading::Waitables::FutexWaitable selfLock; DestructionWatch *pParent {}; }; DestructionWatch::DestructionWatch() { } DestructionWatch::~DestructionWatch() { this->RemoveAll(); } void DestructionWatch::RemoveAll() { { AU_LOCK_GUARD(this->mutex); if (auto pWatcher = AuExchange(this->pFirstWatcher, nullptr)) { this->RemoveWatcherCall(pWatcher); } if (auto pWatchList = AuExchange(this->pListpWatches, nullptr)) { for (const auto pWatcher : *pWatchList) { this->RemoveWatcherCall(pWatcher); } delete pWatchList; } } { AuUInt32 uOld {}; while ((uOld = AuAtomicLoad(&this->uDtorCalls)) != 0) { Aurora::Threading::WaitOnAddress(&this->uDtorCalls, &uOld, sizeof(uOld), 0); } } } void DestructionWatch::RemoveWatcher(ADestructionWatcher *pWatcher) { AU_LOCK_GUARD(this->mutex); if (this->pFirstWatcher == pWatcher) { this->pFirstWatcher = nullptr; return; } if (this->pListpWatches) { AuTryRemove(*this->pListpWatches, pWatcher); } } void DestructionWatch::RemoveWatcherCall(ADestructionWatcher *pWatcher) { pWatcher->OnDTOR(); } ADestructionWatcher::ADestructionWatcher() { } ADestructionWatcher::~ADestructionWatcher() { this->OnSelfDTOR(); } ADestructionWatcher::ADestructionWatcher(ADestructionWatcher &&move) { AU_LOCK_GUARD(move.selfLock); if (auto pParent = AuExchange(move.pParent, nullptr)) { pParent->RemoveWatcher(&move); this->Observe(pParent); } } ADestructionWatcher::ADestructionWatcher(const ADestructionWatcher ©) { AU_LOCK_GUARD(copy.selfLock); if (auto &pParent = copy.pParent) { this->Observe(pParent); } } ADestructionWatcher &ADestructionWatcher::operator =(ADestructionWatcher &&move) { AU_LOCK_GUARD(move.selfLock); if (auto pParent = AuExchange(move.pParent, nullptr)) { pParent->RemoveWatcher(&move); this->Observe(pParent); } return *this; } ADestructionWatcher &ADestructionWatcher::operator =(const ADestructionWatcher ©) { AU_LOCK_GUARD(copy.selfLock); if (auto &pParent = copy.pParent) { this->Observe(pParent); } return *this; } void ADestructionWatcher::Observe(DestructionWatch *pWatch) { AU_LOCK_GUARD(pWatch->mutex); if (!pWatch->pFirstWatcher) { pWatch->pFirstWatcher = this; } else { if (!pWatch->pListpWatches) { pWatch->pListpWatches = new AuRemovePointer_tpListpWatches)>(); } pWatch->pListpWatches->push_back(this); } this->pParent = pWatch; } void ADestructionWatcher::Observe(DestructionWatch &pWatch) { this->Observe(&pWatch); } bool ADestructionWatcher::IsObservedAlive() { return bool(this->pParent); } bool ADestructionWatcher::IsObservedDead() { return !bool(this->pParent); } void ADestructionWatcher::OnSelfDTOR() { AU_LOCK_GUARD(this->selfLock); if (auto pParent = AuExchange(this->pParent, nullptr)) { this->OnDestroy(); pParent->RemoveWatcher(this); } else { this->OnDestroy(); } } void ADestructionWatcher::OnDTOR() { { AU_LOCK_GUARD(this->selfLock); this->pParent = nullptr; } this->OnParentDestroy(); } void ADestructionWatcher::Lock() { } void ADestructionWatcher::Unlock() { if (AuAtomicSub(&this->pParent->uDtorCalls, 1u) == 0) { Aurora::Threading::WakeOnAddress(&this->pParent->uDtorCalls); } } }