// zlib open source license // // Copyright (c) 2024 to 2025 David Forsgren Piuva // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // // 3. This notice may not be removed or altered from any source // distribution. #ifndef DFPSR_HANDLE #define DFPSR_HANDLE #include "heap.h" #include enum class AllocationInitialization { Uninitialized, // Used when the data will be instantly overwritten. Zeroed, // Used for trivial data types. Constructed // Used for a few objects. }; namespace dsr { template class Handle { private: // The internal pointer that reference counting is added to. // Must be allocated using heap_allocate, so that it can be freed using heap_free when the use count reaches zero. T *data = nullptr; #ifdef SAFE_POINTER_CHECKS // The identity that should match the allocation header's identity. uint64_t allocationIdentity = 0; inline void validate() const { if (this->data != nullptr) { // Heap allocations are shared with all threads, so we only need to check the identity. AllocationHeader *header = heap_getHeader(this->data); if (header->allocationIdentity != this->allocationIdentity) { impl_throwIdentityMismatch(header->allocationIdentity, this->allocationIdentity); } } } #endif public: // Default construct an empty handle. Handle() {} // Assigns a debug name to the handled heap allocation. // Returns the handle by reference to allow call chaining: // return handle_create(some, arguments).setName("data for something specific"); // return buffer_create(size).setName("data for something specific"); // Should be trivially optimized away by the compiler in release mode. inline Handle &setName(const char *name) { #ifdef SAFE_POINTER_CHECKS heap_setAllocationName(this->data, name); #endif return *this; } // Construct from pointer. // Pre-condition: data is the data allocated with heap_allocate. #ifdef SAFE_POINTER_CHECKS Handle(T* data, uint64_t allocationIdentity) noexcept : data(data) { this->allocationIdentity = allocationIdentity; if (this->data != nullptr) { heap_increaseUseCount(this->data); } this->validate(); } inline uint64_t getAllocationIdentity() const { return this->allocationIdentity; } #else Handle(T* data) noexcept : data(data) { if (this->data != nullptr) { heap_increaseUseCount(this->data); } } #endif // Copy constructor. Handle(const Handle &other) noexcept : data(other.getUnsafe()) { if (this->data != nullptr) { heap_increaseUseCount(this->data); } #ifdef SAFE_POINTER_CHECKS this->allocationIdentity = other.getAllocationIdentity(); this->validate(); #endif } // Copy constructor with static cast. template Handle(const Handle &other) noexcept : data(static_cast(other.getUnsafe())) { if (this->data != nullptr) { heap_increaseUseCount(this->data); } #ifdef SAFE_POINTER_CHECKS this->allocationIdentity = other.getAllocationIdentity(); this->validate(); #endif } // Move constructor. Handle(Handle &&other) noexcept : data(other.takeOwnership()) { #ifdef SAFE_POINTER_CHECKS this->allocationIdentity = other.getAllocationIdentity(); this->validate(); #endif } // Move constructor with static cast. template Handle(Handle &&other) noexcept : data(static_cast(other.takeOwnership())) { #ifdef SAFE_POINTER_CHECKS this->allocationIdentity = other.getAllocationIdentity(); this->validate(); #endif } // Assignment. Handle& operator = (const Handle &other) { #ifdef SAFE_POINTER_CHECKS this->validate(); this->allocationIdentity = other.getAllocationIdentity(); #endif if (this->data != other.getUnsafe()) { // Decrease any old use count. if (this->data != nullptr) { heap_decreaseUseCount(this->data); } this->data = other.data; // Increase any new use count. if (this->data != nullptr) { heap_increaseUseCount(this->data); } } return *this; } // Assignment with static cast. template Handle& operator = (const Handle &other) { #ifdef SAFE_POINTER_CHECKS this->validate(); this->allocationIdentity = other.getAllocationIdentity(); #endif if (this->data != other.getUnsafe()) { // Decrease any old use count. if (this->data != nullptr) { heap_decreaseUseCount(this->data); } this->data = static_cast(other.data); // Increase any new use count. if (this->data != nullptr) { heap_increaseUseCount(this->data); } } return *this; } // Move assignment. Handle& operator = (Handle &&other) { T* inherited = other.takeOwnership(); #ifdef SAFE_POINTER_CHECKS this->validate(); this->allocationIdentity = other.getAllocationIdentity(); #endif if (this->data != inherited) { // Decrease any old use count. if (this->data != nullptr) { heap_decreaseUseCount(this->data); } this->data = inherited; } return *this; } // Move assignment with static cast. template Handle& operator = (Handle &&other) { T* inherited = static_cast(other.takeOwnership()); #ifdef SAFE_POINTER_CHECKS this->validate(); this->allocationIdentity = other.getAllocationIdentity(); #endif if (this->data != inherited) { // Decrease any old use count. if (this->data != nullptr) { heap_decreaseUseCount(this->data); } this->data = inherited; } return *this; } // Destructor. ~Handle() { if (this->data != nullptr) { #ifdef SAFE_POINTER_CHECKS this->validate(); #endif heap_decreaseUseCount(this->data); } } // Take ownership of the returned pointer from this handle. inline T* takeOwnership() { T* result = this->data; this->data = nullptr; return result; } // Check if the handle is null, using explicit syntax to explain the code. inline bool isNull() const { return this->data == nullptr; } // Check if the handle points to anything, using explicit syntax to explain the code. inline bool isNotNull() const { return this->data != nullptr; } // Access content through the handle using the -> operator. inline T* operator ->() const { #ifdef SAFE_POINTER_CHECKS if (this->data == nullptr) { impl_throwNullException(); } this->validate(); #endif return this->data; } // Returns the allocation's used size in bytes. inline uintptr_t getUsedSize() const { if (this->data == nullptr) { return 0; } else { return heap_getUsedSize(this->data); } } // Get the number of elements by dividing the total size with the element size. inline uintptr_t getElementCount() const { if (this->data == nullptr) { return 0; } else { // When sizeof(T) is a power of two, this unsigned integer division will be optimized into a bit shift by the compiler. return heap_getUsedSize(this->data) / sizeof(T); } } // Get a SafePointer to the data, which is used temporarity to iterate over the content with bound checks in debug mode but no overhead in release mode. // Alignment decides how many additional bytes of padding that should be possible to access for SIMD operations. template SafePointer getSafe(const char * name) const { if (this->data == nullptr) { // A null handle returns a null pointer. return SafePointer(); } else { #ifdef SAFE_POINTER_CHECKS AllocationHeader *header = heap_getHeader(this->data); return SafePointer(header, this->allocationIdentity, name, (V*)this->data, heap_getPaddedSize(this->data)); #else return SafePointer(name, (V*)this->data); #endif } } // Get an unsafe pointer. inline T* getUnsafe() const { #ifdef SAFE_POINTER_CHECKS this->validate(); #endif return this->data; } // Get a reference. inline T& getReference() const { #ifdef SAFE_POINTER_CHECKS if (this->data == nullptr) { impl_throwNullException(); } this->validate(); #endif return *(this->data); } // Get the use count. inline uintptr_t getUseCount() const { #ifdef SAFE_POINTER_CHECKS this->validate(); #endif return this->data ? heap_getUseCount(this->data) : 0; } }; // Construct a new Handle using the heap allocator and begin reference counting. // The object is aligned by DSR_MAXIMUM_ALIGNMENT. template static Handle handle_create(ARGS&&...args) { // Reset the memory to zero before construction, in case that something was forgotten. // TODO: Should debug mode set the memory to a deterministic pattern to simplify detection of uninitialized variables? UnsafeAllocation allocation = heap_allocate(sizeof(T), true); // Construction from pointer increases the allocation's use count to 1. #ifdef SAFE_POINTER_CHECKS Handle result((T*)(allocation.data), allocation.header->allocationIdentity); #else Handle result((T*)(allocation.data)); #endif if (result.isNull()) { impl_throwAllocationFailure(); } else { new (result.getUnsafe()) T(std::forward(args)...); if (!std::is_trivially_destructible::value) { heap_setAllocationDestructor(result.getUnsafe(), HeapDestructor([](void *toDestroy, void *externalResource) { // Destroy one object. ((T*)toDestroy)->~T(); })); } } return std::move(result.setName("Nameless handle object")); } // Construct an array of objects with a shared handle pointing to the first element. // The first element is aligned by DSR_MAXIMUM_ALIGNMENT and the rest are following directly according to sizeof(T). // This allow tight packing of data for SIMD vectorization, because aligning with a SIMD vector would be pointless if each vector only contained one useful lane. // Pre-condition: // sizeof(T) % alignof(T) == 0 template static Handle handle_createArray(AllocationInitialization initialization, uintptr_t elementCount, ARGS&&...args) { UnsafeAllocation allocation = heap_allocate(sizeof(T) * elementCount, initialization == AllocationInitialization::Zeroed); // Construction from pointer increases the allocation's use count to 1. #ifdef SAFE_POINTER_CHECKS Handle result((T*)(allocation.data), allocation.header->allocationIdentity); #else Handle result((T*)(allocation.data)); #endif if (result.isNull()) { impl_throwAllocationFailure(); } else { if (initialization == AllocationInitialization::Constructed) { for (uintptr_t i = 0; i < elementCount; i++) { new (result.getUnsafe() + i) T(std::forward(args)...); } } if (!std::is_trivially_destructible::value) { heap_setAllocationDestructor(result.getUnsafe(), HeapDestructor([](void *toDestroy, void *externalResource) { // Calculate the number of elements from the size. uintptr_t elementCount = heap_getUsedSize(toDestroy) / sizeof(T); // Destroy each element. for (uintptr_t i = 0; i < elementCount; i++) { ((T*)toDestroy)[i].~T(); } })); } } return std::move(result.setName("Nameless handle array")); } // Dynamic casting of handles. // Attempts to cast from a base class to a specific class inheriting from the old type. // OLD_TYPE does not have to be stated explicitly in the call, because it is provided by oldHandle. // Example: // Handle = handle_dynamicCast(handle_create(1, 2, 3)); // Pre-condition: // The old handle must refer to a single element or nullptr, no arrays allowed. // Post-condition: // Returns oldHandle dynamically casted to NEW_TYPE. // Returns an empty handle if the conversion failed. template Handle handle_dynamicCast(const Handle &oldHandle) { #ifdef SAFE_POINTER_CHECKS return Handle(dynamic_cast(oldHandle.getUnsafe()), oldHandle.getAllocationIdentity()); #else return Handle(dynamic_cast(oldHandle.getUnsafe())); #endif } } #endif