From c10ecaa97bc1ba06abd64336410e268a89f1a1b9 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Mon, 22 Mar 2021 16:00:43 -0400 Subject: [PATCH] Port a simple linked-list buffer-backed heap implementation to chip (#5434) --- src/lib/support/BUILD.gn | 2 + src/lib/support/PrivateHeap.cpp | 283 +++++++++++++++++++++ src/lib/support/PrivateHeap.h | 66 +++++ src/lib/support/tests/BUILD.gn | 1 + src/lib/support/tests/TestPrivateHeap.cpp | 288 ++++++++++++++++++++++ 5 files changed, 640 insertions(+) create mode 100644 src/lib/support/PrivateHeap.cpp create mode 100644 src/lib/support/PrivateHeap.h create mode 100644 src/lib/support/tests/TestPrivateHeap.cpp diff --git a/src/lib/support/BUILD.gn b/src/lib/support/BUILD.gn index 20010d9b111d92..fd456498c175a7 100644 --- a/src/lib/support/BUILD.gn +++ b/src/lib/support/BUILD.gn @@ -79,6 +79,8 @@ static_library("support") { "PersistedCounter.h", "Pool.cpp", "Pool.h", + "PrivateHeap.cpp", + "PrivateHeap.h", "RandUtils.cpp", "RandUtils.h", "ReturnMacros.h", diff --git a/src/lib/support/PrivateHeap.cpp b/src/lib/support/PrivateHeap.cpp new file mode 100644 index 00000000000000..e906792cb518b5 --- /dev/null +++ b/src/lib/support/PrivateHeap.cpp @@ -0,0 +1,283 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * Copyright 2019 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PrivateHeap.h" + +#include +#include + +namespace { + +constexpr uint32_t kInvalidHeapBlockSize = 0xFFFFFFFF; +constexpr int32_t kHeapBlockInUse = 0x01; +constexpr int32_t kHeapBlockFree = 0x10; +constexpr int32_t kInvalidHeaderState = 0xff; + +using internal::PrivateHeapBlockHeader; + +// this makes life easier, no need to add align offsets. +static_assert(sizeof(PrivateHeapBlockHeader) % kPrivateHeapAllocationAlignment == 0, "Invalid block size."); + +uint32_t ComputeHeapBlockChecksum(const PrivateHeapBlockHeader * header) +{ + uint32_t checksum = header->prevBytes; + checksum *= 31; + checksum += header->nextBytes; + checksum *= 31; + checksum += header->state; + return checksum; +} + +// Advances the heap block to the next value +PrivateHeapBlockHeader * NextHeader(PrivateHeapBlockHeader * start) +{ + if (start->nextBytes == kInvalidHeapBlockSize) + { + return nullptr; + } + + if (start->checksum != ComputeHeapBlockChecksum(start)) + { + ChipLogError(Support, "Corrupted heap: checksum is invalid"); + chipDie(); + } + + return reinterpret_cast(reinterpret_cast(start) + sizeof(PrivateHeapBlockHeader) + + start->nextBytes); +} + +// Advances the heap block to the previous value +PrivateHeapBlockHeader * PreviousHeader(PrivateHeapBlockHeader * start) +{ + if (start->prevBytes == kInvalidHeapBlockSize) + { + return nullptr; + } + + if (start->checksum != ComputeHeapBlockChecksum(start)) + { + ChipLogError(Support, "Corrupted heap: checksum is invalid"); + chipDie(); + } + + return reinterpret_cast(reinterpret_cast(start) - sizeof(PrivateHeapBlockHeader) - + start->prevBytes); +} + +void ValidateHeader(const PrivateHeapBlockHeader * header) +{ + if (header->state != kHeapBlockFree && header->state != kHeapBlockInUse) + { + ChipLogError(Support, "Invalid header state (neither free nor in use) at %p", header); + chipDie(); + } + + if (header->checksum != ComputeHeapBlockChecksum(header)) + { + ChipLogError(Support, "Corrupted heap: checksum is invalid at %p", header); + chipDie(); + } +} + +} // namespace + +extern "C" void PrivateHeapInit(void * heap, size_t size) +{ + if (heap == nullptr) + { + ChipLogError(Support, "Cannot initialize null heap"); + chipDie(); + } + + if (size < 2 * sizeof(PrivateHeapBlockHeader)) + { + ChipLogError(Support, "Insufficient space in private heap"); + chipDie(); + } + + if (reinterpret_cast(heap) % kPrivateHeapAllocationAlignment != 0) + { + ChipLogError(Support, "Invalid alignment for private heap initialization"); + chipDie(); + } + + PrivateHeapBlockHeader * header = reinterpret_cast(heap); + + header->prevBytes = kInvalidHeapBlockSize; + header->nextBytes = static_cast(size - 2 * sizeof(PrivateHeapBlockHeader)); + header->state = kHeapBlockFree; + header->checksum = ComputeHeapBlockChecksum(header); + + header = NextHeader(header); + header->nextBytes = kInvalidHeapBlockSize; + header->prevBytes = static_cast(size - 2 * sizeof(PrivateHeapBlockHeader)); + header->state = kHeapBlockFree; // does not matter really + header->checksum = ComputeHeapBlockChecksum(header); +} + +extern "C" void * PrivateHeapAlloc(void * heap, size_t size) +{ + PrivateHeapBlockHeader * header = reinterpret_cast(heap); + + // we allocate aligned, no matter what + if (size % kPrivateHeapAllocationAlignment != 0) + { + size += kPrivateHeapAllocationAlignment - (size % kPrivateHeapAllocationAlignment); + } + + for (; header != nullptr; header = NextHeader(header)) + { + ValidateHeader(header); + + if (header->nextBytes == kInvalidHeapBlockSize) + { + continue; + } + + if (header->state != kHeapBlockFree) + { + continue; // not free + } + + if (header->nextBytes < size) + { + continue; // insufficient space + } + + if (header->nextBytes - size < sizeof(PrivateHeapBlockHeader) + kPrivateHeapAllocationAlignment) + { + // allocate the entire block + header->state = kHeapBlockInUse; + header->checksum = ComputeHeapBlockChecksum(header); + } + else + { + // splits the block into two + // + // +--------+ +--------+ +------+ + // | header | ---> | middle | ---> | next | + // +--------+ +--------+ +------+ + // + PrivateHeapBlockHeader * next = NextHeader(header); + PrivateHeapBlockHeader * middle = + reinterpret_cast(reinterpret_cast(header + 1) + size); + + // middle is a new block + middle->nextBytes = static_cast(header->nextBytes - size - sizeof(PrivateHeapBlockHeader)); + middle->prevBytes = static_cast(size); + middle->state = kHeapBlockFree; + middle->checksum = ComputeHeapBlockChecksum(middle); + + // fix up the header + header->nextBytes = static_cast(size); + header->state = kHeapBlockInUse; + header->checksum = ComputeHeapBlockChecksum(header); + + // fix up the final block + if (next != nullptr) + { + next->prevBytes = middle->nextBytes; + next->checksum = ComputeHeapBlockChecksum(next); + } + } + + // we can now use the header + return header + 1; // data right after the header + } + + // no space found + return nullptr; +} + +extern "C" void PrivateHeapFree(void * ptr) +{ + if (ptr == nullptr) + { + // freeing NULL pointers is always acceptable and a noop + return; + } + + PrivateHeapBlockHeader * header = + reinterpret_cast(static_cast(ptr) - sizeof(PrivateHeapBlockHeader)); + + ValidateHeader(header); + header->state = kHeapBlockFree; + header->checksum = ComputeHeapBlockChecksum(header); + + // Merge with previous + // + // +-------+ +--------+ + // | other | ----- nextBytes -----> | header | + // +-------+ +--------+ + // + PrivateHeapBlockHeader * other = PreviousHeader(header); + if (other != nullptr && other->state == kHeapBlockFree && other->nextBytes != kInvalidHeapBlockSize) + { + // includes the free bytes in this block in the previous + other->nextBytes += static_cast(header->nextBytes + sizeof(PrivateHeapBlockHeader)); + other->checksum = ComputeHeapBlockChecksum(other); + header->state = kInvalidHeaderState; + header = other; + + // fixes up the next block + other = NextHeader(header); + if (other != nullptr) + { + other->prevBytes = header->nextBytes; + other->checksum = ComputeHeapBlockChecksum(other); + } + } + + // Merge with next + // + // +--------+ +-------+ + // | header | ----- nextBytes -----> | other | + // +--------+ +-------+ + // + other = NextHeader(header); + if (other != nullptr && other->state == kHeapBlockFree && other->nextBytes != kInvalidHeapBlockSize) + { + // includes the free bytes in the next block + other->state = kInvalidHeaderState; + header->nextBytes += static_cast(other->nextBytes + sizeof(PrivateHeapBlockHeader)); + header->checksum = ComputeHeapBlockChecksum(header); + + // fixes up the next block + other = NextHeader(header); + if (other != nullptr) + { + other->prevBytes = header->nextBytes; + other->checksum = ComputeHeapBlockChecksum(other); + } + } +} + +extern "C" void PrivateHeapDump(void * top) +{ + PrivateHeapBlockHeader * header = reinterpret_cast(top); + + ChipLogProgress(Support, "========= HEAP ==========="); + while (header->nextBytes != kInvalidHeapBlockSize) + { + intptr_t offset = reinterpret_cast(header) - reinterpret_cast(top); + ChipLogProgress(Support, " %ld: size: %d, state: %d", static_cast(offset), static_cast(header->nextBytes), + static_cast(header->state)); + + header = NextHeader(header); + } +} diff --git a/src/lib/support/PrivateHeap.h b/src/lib/support/PrivateHeap.h new file mode 100644 index 00000000000000..e62c64b6913fba --- /dev/null +++ b/src/lib/support/PrivateHeap.h @@ -0,0 +1,66 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * Copyright 2019 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Initializes the heap (set start and end blocks) +void PrivateHeapInit(void * heap, size_t size); + +// Allocates a new block on the specified heap +void * PrivateHeapAlloc(void * heap, size_t size); + +// Marks the specified block as free +void PrivateHeapFree(void * ptr); + +void PrivateHeapDump(void * ptr); + +#ifdef __cplusplus +} // extern "C" + +namespace internal { + +// Heap structure, exposed for tests +// +// +---------+---------+-----+ +---------+-----------+ +// | prev: 0 | next: n | ... | .... .... | prev: n | next: ... | +// +---------+---------+-----+ +---------+-----------+ +// +struct PrivateHeapBlockHeader +{ + uint32_t prevBytes; + uint32_t nextBytes; + uint32_t state; + uint32_t checksum; // super-basic attempt to detect errors +}; + +} // namespace internal + +constexpr size_t kPrivateHeapAllocationAlignment = std::alignment_of::value; + +#endif // ifdef __cplusplus diff --git a/src/lib/support/tests/BUILD.gn b/src/lib/support/tests/BUILD.gn index 5f191899eb1ca8..6e872a3d75504b 100644 --- a/src/lib/support/tests/BUILD.gn +++ b/src/lib/support/tests/BUILD.gn @@ -30,6 +30,7 @@ chip_test_suite("tests") { "TestCHIPMem.cpp", "TestErrorStr.cpp", "TestPool.cpp", + "TestPrivateHeap.cpp", "TestSafeInt.cpp", "TestSafeString.cpp", "TestScopedBuffer.cpp", diff --git a/src/lib/support/tests/TestPrivateHeap.cpp b/src/lib/support/tests/TestPrivateHeap.cpp new file mode 100644 index 00000000000000..75aec86531b14d --- /dev/null +++ b/src/lib/support/tests/TestPrivateHeap.cpp @@ -0,0 +1,288 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * Copyright 2019 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include + +namespace { + +constexpr size_t kBlockHeaderSize = sizeof(internal::PrivateHeapBlockHeader); + +// Splitting block tests assume we know the size +static_assert(kBlockHeaderSize == 16, "Test assumes block size of 16"); + +// helper class for allocating things +template +class PrivateHeapAllocator +{ +public: + PrivateHeapAllocator() { PrivateHeapInit(mHeap.buffer, kSize); } + void * HeapAlloc(size_t size) { return PrivateHeapAlloc(mHeap.buffer, size); } + void HeapFree(void * buffer) { PrivateHeapFree(buffer); } + +private: + struct alignas(kPrivateHeapAllocationAlignment) + { + uint8_t buffer[kSize]; + } mHeap; +}; + +void SingleHeapAllocAndFree(nlTestSuite * inSuite, void * inContext) +{ + PrivateHeapAllocator<16 + 2 * kBlockHeaderSize> allocator; + + NL_TEST_ASSERT(inSuite, nullptr == allocator.HeapAlloc(17)); // insufficient size + void * ptr = allocator.HeapAlloc(16); + NL_TEST_ASSERT(inSuite, nullptr != ptr); + NL_TEST_ASSERT(inSuite, nullptr == allocator.HeapAlloc(1)); // insufficient size + memset(ptr, 0xab, 16); + allocator.HeapFree(ptr); + + // allocate different sizes on this heap, see how that goes + for (size_t i = 1; i < 17; ++i) + { + ptr = allocator.HeapAlloc(i); + NL_TEST_ASSERT(inSuite, nullptr != ptr); + NL_TEST_ASSERT(inSuite, nullptr == allocator.HeapAlloc(17 - i)); // insufficient size + allocator.HeapFree(ptr); + } +} + +void SplitHeapAllocAndFree(nlTestSuite * inSuite, void * inContext) +{ + PrivateHeapAllocator<128> allocator; + // allocator state: + // 96 + + void * p1 = allocator.HeapAlloc(30); + NL_TEST_ASSERT(inSuite, nullptr != p1); + // allocator state: + // 32 48 + + void * p2 = allocator.HeapAlloc(4); + NL_TEST_ASSERT(inSuite, nullptr != p2); + // allocator state: + // 32 8 24 + + allocator.HeapFree(p1); + // allocator state: + // 32 8 24 + + allocator.HeapFree(p2); + // allocator state: + // 96 + + p1 = allocator.HeapAlloc(90); + NL_TEST_ASSERT(inSuite, nullptr != p1); + allocator.HeapFree(p1); +} + +void FreeMergeNext(nlTestSuite * inSuite, void * inContext) +{ + PrivateHeapAllocator<5 * 16> allocator; + + void * p1 = allocator.HeapAlloc(16); + void * p2 = allocator.HeapAlloc(16); + + NL_TEST_ASSERT(inSuite, nullptr != p1); + NL_TEST_ASSERT(inSuite, nullptr != p2); + NL_TEST_ASSERT(inSuite, nullptr == allocator.HeapAlloc(1)); + + memset(p1, 0xab, 16); + memset(p2, 0xcd, 16); + + // freeing 1,2 should clear space + allocator.HeapFree(p1); + allocator.HeapFree(p2); + + p1 = allocator.HeapAlloc(3 * 16); + NL_TEST_ASSERT(inSuite, nullptr != p1); + allocator.HeapFree(p1); +} + +void FreeMergePrevious(nlTestSuite * inSuite, void * inContext) +{ + PrivateHeapAllocator<5 * 16> allocator; + + void * p1 = allocator.HeapAlloc(16); + void * p2 = allocator.HeapAlloc(16); + + NL_TEST_ASSERT(inSuite, nullptr != p1); + NL_TEST_ASSERT(inSuite, nullptr != p2); + NL_TEST_ASSERT(inSuite, nullptr == allocator.HeapAlloc(1)); + + memset(p1, 0xab, 16); + memset(p2, 0xcd, 16); + + // freeing 2,1 should clear space + allocator.HeapFree(p2); + allocator.HeapFree(p1); + p1 = allocator.HeapAlloc(3 * 16); + NL_TEST_ASSERT(inSuite, nullptr != p1); + allocator.HeapFree(p1); +} + +void FreeMergePreviousAndNext(nlTestSuite * inSuite, void * inContext) +{ + + PrivateHeapAllocator<7 * 16> allocator; + + void * p1 = allocator.HeapAlloc(16); + void * p2 = allocator.HeapAlloc(16); + void * p3 = allocator.HeapAlloc(16); + + NL_TEST_ASSERT(inSuite, nullptr != p1); + NL_TEST_ASSERT(inSuite, nullptr != p2); + NL_TEST_ASSERT(inSuite, nullptr != p3); + NL_TEST_ASSERT(inSuite, nullptr == allocator.HeapAlloc(1)); + + memset(p1, 0xab, 16); + memset(p2, 0xcd, 16); + memset(p3, 0xef, 16); + + allocator.HeapFree(p1); + allocator.HeapFree(p3); + // we have 2 slots of size 16 available now + NL_TEST_ASSERT(inSuite, nullptr == allocator.HeapAlloc(17)); + + // Freeing p2 makes enoug space + allocator.HeapFree(p2); + p1 = allocator.HeapAlloc(5 * 16); + NL_TEST_ASSERT(inSuite, nullptr != p1); + allocator.HeapFree(p1); +} + +void MultipleMerge(nlTestSuite * inSuite, void * inContext) +{ + PrivateHeapAllocator<32 * kBlockHeaderSize> allocator; + + // 31 blocks available for alloc + void * p1 = allocator.HeapAlloc(2 * kBlockHeaderSize); // uses up 3 blocks + void * p2 = allocator.HeapAlloc(5 * kBlockHeaderSize); // uses up 6 blocks + void * p3 = allocator.HeapAlloc(8 * kBlockHeaderSize); // uses up 9 blocks + void * p4 = allocator.HeapAlloc(1 * kBlockHeaderSize); // uses up 2 blocks + void * p5 = allocator.HeapAlloc(7 * kBlockHeaderSize); // uses up 8 blocks + void * p6 = allocator.HeapAlloc(2 * kBlockHeaderSize); // uses up 2 (last given) + + NL_TEST_ASSERT(inSuite, nullptr != p1); + NL_TEST_ASSERT(inSuite, nullptr != p2); + NL_TEST_ASSERT(inSuite, nullptr != p3); + NL_TEST_ASSERT(inSuite, nullptr != p4); + NL_TEST_ASSERT(inSuite, nullptr != p5); + NL_TEST_ASSERT(inSuite, nullptr != p6); + + allocator.HeapFree(p3); + allocator.HeapFree(p4); + // 10 blocks available (9 from p3 without HDR and 2 from p4 + HDR) + p3 = allocator.HeapAlloc(10 * kBlockHeaderSize); + NL_TEST_ASSERT(inSuite, nullptr != p3); + NL_TEST_ASSERT(inSuite, nullptr == allocator.HeapAlloc(1)); // full + + allocator.HeapFree(p6); + allocator.HeapFree(p5); + allocator.HeapFree(p3); + allocator.HeapFree(p2); + allocator.HeapFree(p1); + + p1 = allocator.HeapAlloc(30 * kBlockHeaderSize); + NL_TEST_ASSERT(inSuite, nullptr != p1); + allocator.HeapFree(p1); +} + +void ForwardFreeAndRealloc(nlTestSuite * inSuite, void * inContext) +{ + constexpr int kNumBlocks = 16; + PrivateHeapAllocator<(2 * kNumBlocks + 1) * kBlockHeaderSize> allocator; + void * ptrs[kNumBlocks]; + + for (int i = 0; i < kNumBlocks; ++i) + { + ptrs[i] = allocator.HeapAlloc(kBlockHeaderSize); + NL_TEST_ASSERT(inSuite, nullptr != ptrs[i]); + memset(ptrs[i], 0xab, kBlockHeaderSize); + } + + // heap looks like: + /// |HDR| 16 |HDR| 16 |HDR| ..... |HDR| 16 |HDR| + + // free each block from the start and re-allocate into a bigger block + for (size_t i = 1; i < kNumBlocks; ++i) + { + allocator.HeapFree(ptrs[0]); + allocator.HeapFree(ptrs[i]); + + ptrs[0] = allocator.HeapAlloc((1 + 2 * i) * kBlockHeaderSize); + NL_TEST_ASSERT(inSuite, nullptr != ptrs[0]); + } + allocator.HeapFree(ptrs[0]); +} + +void BackwardFreeAndRealloc(nlTestSuite * inSuite, void * inContext) +{ + constexpr int kNumBlocks = 16; + PrivateHeapAllocator<(2 * kNumBlocks + 1) * kBlockHeaderSize> allocator; + void * ptrs[kNumBlocks]; + + for (int i = 0; i < kNumBlocks; ++i) + { + ptrs[i] = allocator.HeapAlloc(kBlockHeaderSize); + NL_TEST_ASSERT(inSuite, nullptr != ptrs[i]); + memset(ptrs[i], 0xab, kBlockHeaderSize); + } + + // heap looks like: + /// |HDR| 16 |HDR| 16 |HDR| ..... |HDR| 16 |HDR| + + // free each block from the send and re-allocate into a bigger block + for (size_t i = 1; i < kNumBlocks; ++i) + { + allocator.HeapFree(ptrs[kNumBlocks - 1]); + allocator.HeapFree(ptrs[kNumBlocks - i - 1]); + + ptrs[kNumBlocks - 1] = allocator.HeapAlloc((1 + 2 * i) * kBlockHeaderSize); + NL_TEST_ASSERT(inSuite, nullptr != ptrs[kNumBlocks - 1]); + } + allocator.HeapFree(ptrs[kNumBlocks - 1]); +} + +const nlTest sTests[] = { + NL_TEST_DEF("SingleHeapAllocAndFree", SingleHeapAllocAndFree), // + NL_TEST_DEF("SplitHeapAllocAndFree", SplitHeapAllocAndFree), // + NL_TEST_DEF("FreeMergeNext", FreeMergeNext), // + NL_TEST_DEF("FreeMergePrevious", FreeMergePrevious), // + NL_TEST_DEF("FreeMergePreviousAndNext", FreeMergePreviousAndNext), // + NL_TEST_DEF("MultipleMerge", MultipleMerge), // + NL_TEST_DEF("ForwardFreeAndRealloc", ForwardFreeAndRealloc), // + NL_TEST_DEF("BackwardFreeAndRealloc", BackwardFreeAndRealloc), // + NL_TEST_SENTINEL() // +}; + +} // namespace + +int TestPrivateHeap(void) +{ + nlTestSuite theSuite = { "PrivateHeap", sTests, nullptr, nullptr }; + nlTestRunner(&theSuite, nullptr); + return nlTestRunnerStats(&theSuite); +} + +CHIP_REGISTER_TEST_SUITE(TestPrivateHeap)