/* -*- 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 nsTArray_h__ # error "Don't include this file directly" #endif // NOTE: We don't use MOZ_COUNT_CTOR/MOZ_COUNT_DTOR to perform leak checking of // nsTArray_base objects intentionally for the following reasons: // * The leak logging isn't as useful as other types of logging, as // nsTArray_base is frequently relocated without invoking a constructor, such // as when stored within another nsTArray. This means that // XPCOM_MEM_LOG_CLASSES cannot be used to identify specific leaks of nsTArray // objects. // * The nsTArray type is layout compatible with the ThinVec crate with the // correct flags, and ThinVec does not currently perform leak logging. // This means that if a large number of arrays are transferred between Rust // and C++ code using ThinVec, for example within another ThinVec, they // will not be logged correctly and might appear as e.g. negative leaks. // * Leaks which have been found thanks to the leak logging added by this // type have often not been significant, and/or have needed to be // circumvented using some other mechanism. Most leaks found with this type // in them also include other types which will continue to be tracked. template nsTArray_base::nsTArray_base() : mHdr(EmptyHdr()) {} template nsTArray_base::~nsTArray_base() { if (!HasEmptyHeader() && !UsesAutoArrayBuffer()) { Alloc::Free(mHdr); } } template nsTArray_base::nsTArray_base(const nsTArray_base&) : mHdr(EmptyHdr()) { // Actual copying happens through nsTArray_CopyEnabler, we just need to do the // initialization of mHdr. } template nsTArray_base& nsTArray_base::operator=(const nsTArray_base&) { // Actual copying happens through nsTArray_CopyEnabler, so do nothing here (do // not copy mHdr). return *this; } template const nsTArrayHeader* nsTArray_base::GetAutoArrayBufferUnsafe( size_t aElemAlign) const { // Assuming |this| points to an nsAutoArray, we want to get a pointer to // mAutoBuf. So just cast |this| to nsAutoArray* and read &mAutoBuf! const void* autoBuf = &reinterpret_cast, 1>*>(this) ->mAutoBuf; // If we're on a 32-bit system and aElemAlign is 8, we need to adjust our // pointer to take into account the extra alignment in the auto array. static_assert( sizeof(void*) != 4 || (MOZ_ALIGNOF(mozilla::AlignedElem<8>) == 8 && sizeof(AutoTArray, 1>) == sizeof(void*) + sizeof(nsTArrayHeader) + 4 + sizeof(mozilla::AlignedElem<8>)), "auto array padding wasn't what we expected"); // We don't support alignments greater than 8 bytes. MOZ_ASSERT(aElemAlign <= 4 || aElemAlign == 8, "unsupported alignment."); if (sizeof(void*) == 4 && aElemAlign == 8) { autoBuf = reinterpret_cast(autoBuf) + 4; } return reinterpret_cast(autoBuf); } template bool nsTArray_base::UsesAutoArrayBuffer() const { if (!mHdr->mIsAutoArray) { return false; } // This is nuts. If we were sane, we'd pass aElemAlign as a parameter to // this function. Unfortunately this function is called in nsTArray_base's // destructor, at which point we don't know value_type's alignment. // // We'll fall on our face and return true when we should say false if // // * we're not using our auto buffer, // * aElemAlign == 4, and // * mHdr == GetAutoArrayBuffer(8). // // This could happen if |*this| lives on the heap and malloc allocated our // buffer on the heap adjacent to |*this|. // // However, we can show that this can't happen. If |this| is an auto array // (as we ensured at the beginning of the method), GetAutoArrayBuffer(8) // always points to memory owned by |*this|, because (as we assert below) // // * GetAutoArrayBuffer(8) is at most 4 bytes past GetAutoArrayBuffer(4), // and // * sizeof(nsTArrayHeader) > 4. // // Since AutoTArray always contains an nsTArrayHeader, // GetAutoArrayBuffer(8) will always point inside the auto array object, // even if it doesn't point at the beginning of the header. // // Note that this means that we can't store elements with alignment 16 in an // nsTArray, because GetAutoArrayBuffer(16) could lie outside the memory // owned by this AutoTArray. We statically assert that value_type's // alignment is 8 bytes or less in AutoTArray. static_assert(sizeof(nsTArrayHeader) > 4, "see comment above"); #ifdef DEBUG ptrdiff_t diff = reinterpret_cast(GetAutoArrayBuffer(8)) - reinterpret_cast(GetAutoArrayBuffer(4)); MOZ_ASSERT(diff >= 0 && diff <= 4, "GetAutoArrayBuffer doesn't do what we expect."); #endif return mHdr == GetAutoArrayBuffer(4) || mHdr == GetAutoArrayBuffer(8); } // defined in nsTArray.cpp bool IsTwiceTheRequiredBytesRepresentableAsUint32(size_t aCapacity, size_t aElemSize); template template typename ActualAlloc::ResultTypeProxy nsTArray_base::ExtendCapacity(size_type aLength, size_type aCount, size_type aElemSize) { mozilla::CheckedInt newLength = aLength; newLength += aCount; if (!newLength.isValid()) { return ActualAlloc::FailureResult(); } return this->EnsureCapacity(newLength.value(), aElemSize); } template template typename ActualAlloc::ResultTypeProxy nsTArray_base::EnsureCapacityImpl( size_type aCapacity, size_type aElemSize) { MOZ_ASSERT(aCapacity > mHdr->mCapacity, "Should have been checked by caller (EnsureCapacity)"); // If the requested memory allocation exceeds size_type(-1)/2, then // our doubling algorithm may not be able to allocate it. // Additionally, if it exceeds uint32_t(-1) then we couldn't fit in the // Header::mCapacity member. Just bail out in cases like that. We don't want // to be allocating 2 GB+ arrays anyway. if (!IsTwiceTheRequiredBytesRepresentableAsUint32(aCapacity, aElemSize)) { ActualAlloc::SizeTooBig((size_t)aCapacity * aElemSize); return ActualAlloc::FailureResult(); } size_t reqSize = sizeof(Header) + aCapacity * aElemSize; if (HasEmptyHeader()) { // Malloc() new data Header* header = static_cast(ActualAlloc::Malloc(reqSize)); if (!header) { return ActualAlloc::FailureResult(); } header->mLength = 0; header->mCapacity = aCapacity; header->mIsAutoArray = 0; mHdr = header; return ActualAlloc::SuccessResult(); } // We increase our capacity so that the allocated buffer grows exponentially, // which gives us amortized O(1) appending. Below the threshold, we use // powers-of-two. Above the threshold, we grow by at least 1.125, rounding up // to the nearest MiB. const size_t slowGrowthThreshold = 8 * 1024 * 1024; size_t bytesToAlloc; if (reqSize >= slowGrowthThreshold) { size_t currSize = sizeof(Header) + Capacity() * aElemSize; size_t minNewSize = currSize + (currSize >> 3); // multiply by 1.125 bytesToAlloc = reqSize > minNewSize ? reqSize : minNewSize; // Round up to the next multiple of MiB. const size_t MiB = 1 << 20; bytesToAlloc = MiB * ((bytesToAlloc + MiB - 1) / MiB); } else { // Round up to the next power of two. bytesToAlloc = mozilla::RoundUpPow2(reqSize); } Header* header; if (UsesAutoArrayBuffer() || !RelocationStrategy::allowRealloc) { // Malloc() and copy header = static_cast(ActualAlloc::Malloc(bytesToAlloc)); if (!header) { return ActualAlloc::FailureResult(); } RelocationStrategy::RelocateNonOverlappingRegionWithHeader( header, mHdr, Length(), aElemSize); if (!UsesAutoArrayBuffer()) { ActualAlloc::Free(mHdr); } } else { // Realloc() existing data header = static_cast(ActualAlloc::Realloc(mHdr, bytesToAlloc)); if (!header) { return ActualAlloc::FailureResult(); } } // How many elements can we fit in bytesToAlloc? size_t newCapacity = (bytesToAlloc - sizeof(Header)) / aElemSize; MOZ_ASSERT(newCapacity >= aCapacity, "Didn't enlarge the array enough!"); header->mCapacity = newCapacity; mHdr = header; return ActualAlloc::SuccessResult(); } // We don't need use Alloc template parameter specified here because failure to // shrink the capacity will leave the array unchanged. template void nsTArray_base::ShrinkCapacity( size_type aElemSize, size_t aElemAlign) { if (HasEmptyHeader() || UsesAutoArrayBuffer()) { return; } if (mHdr->mLength >= mHdr->mCapacity) { // should never be greater than... return; } size_type length = Length(); if (IsAutoArray() && GetAutoArrayBuffer(aElemAlign)->mCapacity >= length) { Header* header = GetAutoArrayBuffer(aElemAlign); // Move the data, but don't copy the header to avoid overwriting mCapacity. header->mLength = length; RelocationStrategy::RelocateNonOverlappingRegion(header + 1, mHdr + 1, length, aElemSize); nsTArrayFallibleAllocator::Free(mHdr); mHdr = header; return; } if (length == 0) { MOZ_ASSERT(!IsAutoArray(), "autoarray should have fit 0 elements"); nsTArrayFallibleAllocator::Free(mHdr); mHdr = EmptyHdr(); return; } size_type newSize = sizeof(Header) + length * aElemSize; Header* newHeader; if (!RelocationStrategy::allowRealloc) { // Malloc() and copy. newHeader = static_cast(nsTArrayFallibleAllocator::Malloc(newSize)); if (!newHeader) { return; } RelocationStrategy::RelocateNonOverlappingRegionWithHeader( newHeader, mHdr, Length(), aElemSize); nsTArrayFallibleAllocator::Free(mHdr); } else { // Realloc() existing data. newHeader = static_cast(nsTArrayFallibleAllocator::Realloc(mHdr, newSize)); if (!newHeader) { return; } } mHdr = newHeader; mHdr->mCapacity = length; } template void nsTArray_base::ShrinkCapacityToZero( size_type aElemSize, size_t aElemAlign) { MOZ_ASSERT(mHdr->mLength == 0); if (HasEmptyHeader() || UsesAutoArrayBuffer()) { return; } const bool isAutoArray = IsAutoArray(); nsTArrayFallibleAllocator::Free(mHdr); if (isAutoArray) { mHdr = GetAutoArrayBufferUnsafe(aElemAlign); mHdr->mLength = 0; } else { mHdr = EmptyHdr(); } } template template void nsTArray_base::ShiftData(index_type aStart, size_type aOldLen, size_type aNewLen, size_type aElemSize, size_t aElemAlign) { if (aOldLen == aNewLen) { return; } // Determine how many elements need to be shifted size_type num = mHdr->mLength - (aStart + aOldLen); // Compute the resulting length of the array mHdr->mLength += aNewLen - aOldLen; if (mHdr->mLength == 0) { ShrinkCapacityToZero(aElemSize, aElemAlign); } else { // Maybe nothing needs to be shifted if (num == 0) { return; } // Perform shift (change units to bytes first) aStart *= aElemSize; aNewLen *= aElemSize; aOldLen *= aElemSize; char* baseAddr = reinterpret_cast(mHdr + 1) + aStart; RelocationStrategy::RelocateOverlappingRegion( baseAddr + aNewLen, baseAddr + aOldLen, num, aElemSize); } } template template void nsTArray_base::SwapFromEnd(index_type aStart, size_type aCount, size_type aElemSize, size_t aElemAlign) { // This method is part of the implementation of // nsTArray::SwapRemoveElement{s,}At. For more information, read the // documentation on that method. if (aCount == 0) { return; } // We are going to be removing aCount elements. Update our length to point to // the new end of the array. size_type oldLength = mHdr->mLength; mHdr->mLength -= aCount; if (mHdr->mLength == 0) { // If we have no elements remaining in the array, we can free our buffer. ShrinkCapacityToZero(aElemSize, aElemAlign); return; } // Determine how many elements we need to move from the end of the array into // the now-removed section. This will either be the number of elements which // were removed (if there are more elements in the tail of the array), or the // entire tail of the array, whichever is smaller. size_type relocCount = std::min(aCount, mHdr->mLength - aStart); if (relocCount == 0) { return; } // Move the elements which are now stranded after the end of the array back // into the now-vacated memory. index_type sourceBytes = (oldLength - relocCount) * aElemSize; index_type destBytes = aStart * aElemSize; // Perform the final copy. This is guaranteed to be a non-overlapping copy // as our source contains only still-valid entries, and the destination // contains only invalid entries which need to be overwritten. MOZ_ASSERT(sourceBytes >= destBytes, "The source should be after the destination."); MOZ_ASSERT(sourceBytes - destBytes >= relocCount * aElemSize, "The range should be nonoverlapping"); char* baseAddr = reinterpret_cast(mHdr + 1); RelocationStrategy::RelocateNonOverlappingRegion( baseAddr + destBytes, baseAddr + sourceBytes, relocCount, aElemSize); } template template typename ActualAlloc::ResultTypeProxy nsTArray_base::InsertSlotsAt(index_type aIndex, size_type aCount, size_type aElemSize, size_t aElemAlign) { if (MOZ_UNLIKELY(aIndex > Length())) { mozilla::detail::InvalidArrayIndex_CRASH(aIndex, Length()); } if (!ActualAlloc::Successful( this->ExtendCapacity(Length(), aCount, aElemSize))) { return ActualAlloc::FailureResult(); } // Move the existing elements as needed. Note that this will // change our mLength, so no need to call IncrementLength. ShiftData(aIndex, 0, aCount, aElemSize, aElemAlign); return ActualAlloc::SuccessResult(); } // nsTArray_base::IsAutoArrayRestorer is an RAII class which takes // |nsTArray_base &array| in its constructor. When it's destructed, it ensures // that // // * array.mIsAutoArray has the same value as it did when we started, and // * if array has an auto buffer and mHdr would otherwise point to // sEmptyTArrayHeader, array.mHdr points to array's auto buffer. template nsTArray_base::IsAutoArrayRestorer:: IsAutoArrayRestorer(nsTArray_base& aArray, size_t aElemAlign) : mArray(aArray), mElemAlign(aElemAlign), mIsAuto(aArray.IsAutoArray()) {} template nsTArray_base::IsAutoArrayRestorer::~IsAutoArrayRestorer() { // Careful: We don't want to set mIsAutoArray = 1 on sEmptyTArrayHeader. if (mIsAuto && mArray.HasEmptyHeader()) { // Call GetAutoArrayBufferUnsafe() because GetAutoArrayBuffer() asserts // that mHdr->mIsAutoArray is true, which surely isn't the case here. mArray.mHdr = mArray.GetAutoArrayBufferUnsafe(mElemAlign); mArray.mHdr->mLength = 0; } else if (!mArray.HasEmptyHeader()) { mArray.mHdr->mIsAutoArray = mIsAuto; } } template template typename ActualAlloc::ResultTypeProxy nsTArray_base::SwapArrayElements( nsTArray_base& aOther, size_type aElemSize, size_t aElemAlign) { // EnsureNotUsingAutoArrayBuffer will set mHdr = sEmptyTArrayHeader even if we // have an auto buffer. We need to point mHdr back to our auto buffer before // we return, otherwise we'll forget that we have an auto buffer at all! // IsAutoArrayRestorer takes care of this for us. IsAutoArrayRestorer ourAutoRestorer(*this, aElemAlign); typename nsTArray_base::IsAutoArrayRestorer otherAutoRestorer(aOther, aElemAlign); // If neither array uses an auto buffer which is big enough to store the // other array's elements, then ensure that both arrays use malloc'ed storage // and swap their mHdr pointers. if ((!UsesAutoArrayBuffer() || Capacity() < aOther.Length()) && (!aOther.UsesAutoArrayBuffer() || aOther.Capacity() < Length())) { if (!EnsureNotUsingAutoArrayBuffer(aElemSize) || !aOther.template EnsureNotUsingAutoArrayBuffer( aElemSize)) { return ActualAlloc::FailureResult(); } Header* temp = mHdr; mHdr = aOther.mHdr; aOther.mHdr = temp; return ActualAlloc::SuccessResult(); } // Swap the two arrays by copying, since at least one is using an auto // buffer which is large enough to hold all of the aOther's elements. We'll // copy the shorter array into temporary storage. // // (We could do better than this in some circumstances. Suppose we're // swapping arrays X and Y. X has space for 2 elements in its auto buffer, // but currently has length 4, so it's using malloc'ed storage. Y has length // 2. When we swap X and Y, we don't need to use a temporary buffer; we can // write Y straight into X's auto buffer, write X's malloc'ed buffer on top // of Y, and then switch X to using its auto buffer.) if (!ActualAlloc::Successful( EnsureCapacity(aOther.Length(), aElemSize)) || !Allocator::Successful( aOther.template EnsureCapacity(Length(), aElemSize))) { return ActualAlloc::FailureResult(); } // The EnsureCapacity calls above shouldn't have caused *both* arrays to // switch from their auto buffers to malloc'ed space. MOZ_ASSERT(UsesAutoArrayBuffer() || aOther.UsesAutoArrayBuffer(), "One of the arrays should be using its auto buffer."); size_type smallerLength = XPCOM_MIN(Length(), aOther.Length()); size_type largerLength = XPCOM_MAX(Length(), aOther.Length()); void* smallerElements; void* largerElements; if (Length() <= aOther.Length()) { smallerElements = Hdr() + 1; largerElements = aOther.Hdr() + 1; } else { smallerElements = aOther.Hdr() + 1; largerElements = Hdr() + 1; } // Allocate temporary storage for the smaller of the two arrays. We want to // allocate this space on the stack, if it's not too large. Sounds like a // job for AutoTArray! (One of the two arrays we're swapping is using an // auto buffer, so we're likely not allocating a lot of space here. But one // could, in theory, allocate a huge AutoTArray on the heap.) AutoTArray temp; if (!ActualAlloc::Successful(temp.template EnsureCapacity( smallerLength * aElemSize, sizeof(uint8_t)))) { return ActualAlloc::FailureResult(); } RelocationStrategy::RelocateNonOverlappingRegion( temp.Elements(), smallerElements, smallerLength, aElemSize); RelocationStrategy::RelocateNonOverlappingRegion( smallerElements, largerElements, largerLength, aElemSize); RelocationStrategy::RelocateNonOverlappingRegion( largerElements, temp.Elements(), smallerLength, aElemSize); // Swap the arrays' lengths. MOZ_ASSERT((aOther.Length() == 0 || !HasEmptyHeader()) && (Length() == 0 || !aOther.HasEmptyHeader()), "Don't set sEmptyTArrayHeader's length."); size_type tempLength = Length(); // Avoid writing to EmptyHdr, since it can trigger false // positives with TSan. if (!HasEmptyHeader()) { mHdr->mLength = aOther.Length(); } if (!aOther.HasEmptyHeader()) { aOther.mHdr->mLength = tempLength; } return ActualAlloc::SuccessResult(); } template template void nsTArray_base::MoveInit( nsTArray_base& aOther, size_type aElemSize, size_t aElemAlign) { // This method is similar to SwapArrayElements, but specialized for the case // where the target array is empty with no allocated heap storage. It is // provided and used to simplify template instantiation and enable better code // generation. MOZ_ASSERT(Length() == 0); MOZ_ASSERT(Capacity() == 0 || (IsAutoArray() && UsesAutoArrayBuffer())); // EnsureNotUsingAutoArrayBuffer will set mHdr = sEmptyTArrayHeader even if we // have an auto buffer. We need to point mHdr back to our auto buffer before // we return, otherwise we'll forget that we have an auto buffer at all! // IsAutoArrayRestorer takes care of this for us. IsAutoArrayRestorer ourAutoRestorer(*this, aElemAlign); typename nsTArray_base::IsAutoArrayRestorer otherAutoRestorer(aOther, aElemAlign); // If neither array uses an auto buffer which is big enough to store the // other array's elements, then ensure that both arrays use malloc'ed storage // and swap their mHdr pointers. if ((!IsAutoArray() || Capacity() < aOther.Length()) && !aOther.UsesAutoArrayBuffer()) { mHdr = aOther.mHdr; aOther.mHdr = EmptyHdr(); return; } // Move the data by copying, since at least one has an auto // buffer which is large enough to hold all of the aOther's elements. EnsureCapacity(aOther.Length(), aElemSize); // The EnsureCapacity calls above shouldn't have caused *both* arrays to // switch from their auto buffers to malloc'ed space. MOZ_ASSERT(UsesAutoArrayBuffer() || aOther.UsesAutoArrayBuffer(), "One of the arrays should be using its auto buffer."); RelocationStrategy::RelocateNonOverlappingRegion(Hdr() + 1, aOther.Hdr() + 1, aOther.Length(), aElemSize); // Swap the arrays' lengths. MOZ_ASSERT((aOther.Length() == 0 || !HasEmptyHeader()) && (Length() == 0 || !aOther.HasEmptyHeader()), "Don't set sEmptyTArrayHeader's length."); // Avoid writing to EmptyHdr, since it can trigger false // positives with TSan. if (!HasEmptyHeader()) { mHdr->mLength = aOther.Length(); } if (!aOther.HasEmptyHeader()) { aOther.mHdr->mLength = 0; } } template template void nsTArray_base::MoveConstructNonAutoArray( nsTArray_base& aOther, size_type aElemSize, size_t aElemAlign) { // We know that we are not an (Copyable)AutoTArray and we know that we are // empty, so don't use SwapArrayElements which doesn't know either of these // facts and is very complex. if (aOther.IsEmpty()) { return; } // aOther might be an (Copyable)AutoTArray though, and it might use its inline // buffer. const bool otherUsesAutoArrayBuffer = aOther.UsesAutoArrayBuffer(); if (otherUsesAutoArrayBuffer) { // Use nsTArrayInfallibleAllocator regardless of Alloc because this is // called from a move constructor, which cannot report an error to the // caller. aOther.template EnsureNotUsingAutoArrayBuffer( aElemSize); } const bool otherIsAuto = otherUsesAutoArrayBuffer || aOther.IsAutoArray(); mHdr = aOther.mHdr; // We might write to mHdr, so ensure it's not the static empty header. aOther // shouldn't have been empty if we get here anyway. MOZ_ASSERT(!HasEmptyHeader()); if (otherIsAuto) { mHdr->mIsAutoArray = false; aOther.mHdr = aOther.GetAutoArrayBufferUnsafe(aElemAlign); aOther.mHdr->mLength = 0; } else { aOther.mHdr = aOther.EmptyHdr(); } } template template bool nsTArray_base::EnsureNotUsingAutoArrayBuffer( size_type aElemSize) { if (UsesAutoArrayBuffer()) { // If you call this on a 0-length array, we'll set that array's mHdr to // sEmptyTArrayHeader, in flagrant violation of the AutoTArray invariants. // It's up to you to set it back! (If you don't, the AutoTArray will // forget that it has an auto buffer.) if (Length() == 0) { mHdr = EmptyHdr(); return true; } size_type size = sizeof(Header) + Length() * aElemSize; Header* header = static_cast(ActualAlloc::Malloc(size)); if (!header) { return false; } RelocationStrategy::RelocateNonOverlappingRegionWithHeader( header, mHdr, Length(), aElemSize); header->mCapacity = Length(); mHdr = header; } return true; }