[common] add MessageBackedArray class (#12341)

This commit adds `MessageBackedArray`, which implements a dynamic
array backed by a `Message` for data storage.

The new array class supports:
- Pushing new elements to the end.
- Reading and writing elements at specific indices.
- Searching in the array to find matching entries.
- Iterating over array elements.
- Clearing the array and freeing the underlying message.

Unit tests are added in `tests/unit/test_msg_backed_array.cpp` to
verify its functionality.
This commit is contained in:
Abtin Keshavarzian
2026-02-09 11:36:53 -08:00
committed by GitHub
parent 1b523d7adf
commit 62a2d794d4
4 changed files with 515 additions and 0 deletions
+1
View File
@@ -461,6 +461,7 @@ openthread_core_files = [
"common/logging.hpp",
"common/message.cpp",
"common/message.hpp",
"common/msg_backed_array.hpp",
"common/non_copyable.hpp",
"common/notifier.cpp",
"common/notifier.hpp",
+293
View File
@@ -0,0 +1,293 @@
/*
* Copyright (c) 2026, The OpenThread Authors.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @file
* This file includes definitions for a generic array using `Message` for data storage.
*/
#ifndef OT_CORE_COMMON_MSG_BACKED_ARRAY_HPP_
#define OT_CORE_COMMON_MSG_BACKED_ARRAY_HPP_
#include "openthread-core-config.h"
#include "common/code_utils.hpp"
#include "common/error.hpp"
#include "common/locator.hpp"
#include "common/message.hpp"
#include "common/non_copyable.hpp"
#include "common/numeric_limits.hpp"
namespace ot {
/**
* Represents a dynamic array of generic type using `Message` for data storage.
*
* @tparam Type The array element type.
* @tparam kMaxSize Specifies the max array size (maximum number of elements in the array).
*/
template <typename Type, uint16_t kMaxSize> class MessageBackedArray : public InstanceLocator, private NonCopyable
{
static_assert(kMaxSize != 0, "MessageBackedArray `kMaxSize` cannot be zero");
public:
static constexpr uint16_t kInvalidIndex = NumericLimits<uint16_t>::kMax; ///< Invalid Index.
/**
* Represent an entry in the array along with index.
*/
class IndexedEntry : public Type
{
friend class MessageBackedArray;
public:
/**
* Gets the index.
*
* @returns The index.
*/
uint16_t GetIndex(void) const { return mArrayIndex; }
/**
* Initializes the entry for iteration (see `ReadNext()`).
*/
void InitForIteration(void) { mArrayIndex = kInvalidIndex; }
/**
* Sets the index to the invalid value (`kInvalidIndex`).
*/
void SetIndexToInvalid(void) { mArrayIndex = kInvalidIndex; }
/**
* Indicates whether the index is set to the invalid value (`kInvalidIndex`).
*
* @retval TRUE The index is invalid.
* @retval FALSE The index is valid.
*/
bool IsIndexInvalid(void) const { return mArrayIndex == kInvalidIndex; }
private:
uint16_t mArrayIndex;
};
/**
* Initializes the array as empty.
*
* @param[in] aInstance The OpenThread instance.
*/
MessageBackedArray(Instance &aInstance)
: InstanceLocator(aInstance)
, mMessage(nullptr)
{
}
/**
* Destructor.
*/
~MessageBackedArray(void) { Clear(); }
/**
* Clears the array.
*
* This frees the underlying message used for storage.
*/
void Clear(void)
{
if (mMessage != nullptr)
{
mMessage->Free();
mMessage = nullptr;
}
}
/**
* Returns the current array length (number of elements in the array).
*
* @returns The array length.
*/
uint16_t GetLength(void) const { return (mMessage == nullptr) ? 0 : mMessage->GetLength() / sizeof(Type); }
/**
* Indicates whether the array is full.
*
* @retval TRUE The array is full.
* @retval FALSE The array is not full.
*/
bool IsFull(void) const { return GetLength() == kMaxSize; }
/**
* Reads an element from the array at a given index.
*
* @param[in] aIndex The index to read from.
* @param[out] aEntry A reference to a variable to output the read element.
*
* @retval kErrorNone Successfully read the element.
* @retval kErrorNotFound The @p aIndex is out of bounds (beyond current array length).
*/
Error ReadAt(uint16_t aIndex, Type &aEntry) const
{
Error error = kErrorNotFound;
VerifyOrExit(mMessage != nullptr);
SuccessOrExit(mMessage->Read(aIndex * sizeof(Type), aEntry));
error = kErrorNone;
exit:
return error;
}
/**
* Reads an element from the array.
*
* @param[in,out] aIndexedEntry A reference to an `IndexedEntry`.
*
* @retval kErrorNone Successfully read the element.
* @retval kErrorNotFound The `GetIndex()` in `aIndexedEntry` is out of bounds (beyond current array length).
*/
Error Read(IndexedEntry &aIndexedEntry) const { return ReadAt(aIndexedEntry.GetIndex(), aIndexedEntry); }
/**
* Writes an element to the array at a given index.
*
* @param[in] aIndex The index to write to.
* @param[in] aEntry The value to write.
*
* @retval kErrorNone Successfully wrote the element.
* @retval kErrorInvalidArgs The @p aIndex is out of bounds (beyond current array length).
*/
Error WriteAt(uint16_t aIndex, const Type &aEntry)
{
Error error = kErrorInvalidArgs;
VerifyOrExit(aIndex < GetLength());
mMessage->Write(aIndex * sizeof(Type), aEntry);
error = kErrorNone;
exit:
return error;
}
/**
* Writes an element to the array at a given index.
*
* @param[in] aIndexedEntry A reference to an `IndexedEntry` specifying both the index and the element.
*
* @retval kErrorNone Successfully wrote the element.
* @retval kErrorInvalidArgs The `GetIndex()` in @p aIndexedEntry is out of bounds (beyond current array length).
*/
Error Write(const IndexedEntry &aIndexedEntry) { return WriteAt(aIndexedEntry.GetIndex(), aIndexedEntry); }
/**
* Appends a new entry to the end of the array.
*
* @param[in] aEntry The new entry to push back.
*
* @retval kErrorNone Successfully pushed back @p aEntry to the end of the array.
* @retval kErrorNoBufs Could not allocate buffer to grow the array or array reached its max size.
*/
Error Push(const Type &aEntry)
{
Error error = kErrorNoBufs;
uint16_t msgLen;
VerifyOrExit(GetLength() < kMaxSize);
if (mMessage == nullptr)
{
mMessage = Get<MessagePool>().Allocate(Message::kTypeOther);
VerifyOrExit(mMessage != nullptr);
}
msgLen = mMessage->GetLength();
if (mMessage->Append(aEntry) != kErrorNone)
{
IgnoreError(mMessage->SetLength(msgLen));
ExitNow();
}
error = kErrorNone;
exit:
return error;
}
/**
* Searches within the array to find the first entry matching a set of conditions.
*
* To check that an entry matches, the `Matches()` method is invoked on each `Type` entry in the array. The
* `Matches()` method with the same set of `Args` input types should be provided by the `Type` class accordingly:
*
* bool Type::Matches(const Args &...) const
*
* @param[out] aIndexedEntry A reference to an `IndexedEntry` to return the matched entry if found.
* @param[in] aArgs The args to pass to `Matches()`.
*
* @retval kErrorNone A matching entry was found. @p aIndexedEntry is updated.
* @retval kErrorNotFound Could not find a matching entry in the array. @p aIndexedEntry may still be updated!
*/
template <typename... Args> Error FindMatching(IndexedEntry &aIndexedEntry, Args &&...aArgs) const
{
Error error = kErrorNotFound;
for (aIndexedEntry.InitForIteration(); ReadNext(aIndexedEntry) == kErrorNone;)
{
if (aIndexedEntry.Matches(aArgs...))
{
error = kErrorNone;
break;
}
}
return error;
}
/**
* Iterates through the entries in the array, reading the next entry.
*
* To start the iteration from the start of the array, @p aIndexedEntry MUST be first initialized using the
* `InitForIteration()` method.
*
* @param[in,out] aIndexedEntry A reference to an `IndexedEntry` to update.
*
* @retval kErrorNone Iterated to the next entry. @p aIndexedEntry is updated.
* @retval kErrorNotFound Reached end of the array.
*/
Error ReadNext(IndexedEntry &aIndexedEntry) const
{
aIndexedEntry.mArrayIndex++;
return Read(aIndexedEntry);
}
private:
Message *mMessage;
};
} // namespace ot
#endif // OT_CORE_COMMON_MSG_BACKED_ARRAY_HPP_
+1
View File
@@ -238,6 +238,7 @@ ot_unit_test(meshcop)
ot_unit_test(message)
ot_unit_test(message_queue)
ot_unit_test(mle)
ot_unit_test(msg_backed_array)
ot_unit_test(multicast_listeners_table)
ot_unit_test(nat64)
ot_unit_test(ndproxy_table)
+220
View File
@@ -0,0 +1,220 @@
/*
* Copyright (c) 2026, The OpenThread Authors.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdarg.h>
#include <string.h>
#include "test_platform.h"
#include <openthread/config.h>
#include "common/clearable.hpp"
#include "common/equatable.hpp"
#include "common/msg_backed_array.hpp"
#include "instance/instance.hpp"
#include "test_util.hpp"
namespace ot {
struct Entry : public Clearable<Entry>, public Equatable<Entry>
{
static constexpr uint8_t kStringSize = 32;
Entry(void) { Clear(); }
Entry(bool aFlag, uint32_t aValue, const char *aString)
{
Clear();
mFlag = aFlag;
mValue = aValue;
SuccessOrQuit(StringCopy(mString, aString));
}
bool Matches(bool aFlag) const { return mFlag == aFlag; }
bool Matches(uint16_t aValue) const { return mValue == aValue; }
bool Matches(bool aFlag, uint16_t aValue) const { return Matches(aFlag) && Matches(aValue); }
bool Matches(const char *aString) const { return StringMatch(mString, aString); }
bool mFlag;
uint32_t mValue;
char mString[kStringSize];
};
void TestMsgBackedArray(void)
{
static constexpr uint16_t kMaxSize = 4;
using EntryArray = MessageBackedArray<Entry, kMaxSize>;
Instance *instance = testInitInstance();
EntryArray array(*instance);
EntryArray::IndexedEntry entry;
Entry entry0(false, 0x1234, "Entry A");
Entry entry1(false, 0x5678, "Second Entry");
Entry entry2(true, 0xfedc, "");
Entry entry3(true, 0x1234, "4");
Entry entry4(true, 0x9876, "Replace");
// Initial state when array is empty
VerifyOrQuit(array.GetLength() == 0);
VerifyOrQuit(array.ReadAt(0, entry) == kErrorNotFound);
VerifyOrQuit(array.WriteAt(0, entry0) == kErrorInvalidArgs);
VerifyOrQuit(array.FindMatching(entry, true) == kErrorNotFound);
entry.InitForIteration();
VerifyOrQuit(array.ReadNext(entry) == kErrorNotFound);
// Array with one entry
SuccessOrQuit(array.Push(entry0));
VerifyOrQuit(array.GetLength() == 1);
SuccessOrQuit(array.ReadAt(0, entry));
VerifyOrQuit(entry == entry0);
VerifyOrQuit(array.ReadAt(1, entry) == kErrorNotFound);
VerifyOrQuit(array.FindMatching(entry, true) == kErrorNotFound);
SuccessOrQuit(array.FindMatching(entry, false));
VerifyOrQuit(entry.GetIndex() == 0);
VerifyOrQuit(entry == entry0);
SuccessOrQuit(array.FindMatching(entry, "Entry A"));
VerifyOrQuit(entry.GetIndex() == 0);
VerifyOrQuit(entry == entry0);
entry.InitForIteration();
SuccessOrQuit(array.ReadNext(entry));
VerifyOrQuit(entry.GetIndex() == 0);
VerifyOrQuit(entry == entry0);
VerifyOrQuit(array.ReadNext(entry) == kErrorNotFound);
// Array with four entries.
SuccessOrQuit(array.Push(entry1));
SuccessOrQuit(array.Push(entry2));
SuccessOrQuit(array.Push(entry3));
VerifyOrQuit(array.GetLength() == 4);
SuccessOrQuit(array.ReadAt(0, entry));
VerifyOrQuit(entry == entry0);
SuccessOrQuit(array.ReadAt(1, entry));
VerifyOrQuit(entry == entry1);
SuccessOrQuit(array.ReadAt(2, entry));
VerifyOrQuit(entry == entry2);
SuccessOrQuit(array.ReadAt(3, entry));
VerifyOrQuit(entry == entry3);
VerifyOrQuit(array.ReadAt(4, entry) == kErrorNotFound);
SuccessOrQuit(array.FindMatching(entry, true));
VerifyOrQuit(entry.GetIndex() == 2);
VerifyOrQuit(entry == entry2);
SuccessOrQuit(array.FindMatching(entry, false));
VerifyOrQuit(entry.GetIndex() == 0);
VerifyOrQuit(entry == entry0);
SuccessOrQuit(array.FindMatching(entry, true, 0x1234));
VerifyOrQuit(entry.GetIndex() == 3);
VerifyOrQuit(entry == entry3);
entry.InitForIteration();
SuccessOrQuit(array.ReadNext(entry));
VerifyOrQuit(entry.GetIndex() == 0);
VerifyOrQuit(entry == entry0);
SuccessOrQuit(array.ReadNext(entry));
VerifyOrQuit(entry.GetIndex() == 1);
VerifyOrQuit(entry == entry1);
SuccessOrQuit(array.ReadNext(entry));
VerifyOrQuit(entry.GetIndex() == 2);
VerifyOrQuit(entry == entry2);
SuccessOrQuit(array.ReadNext(entry));
VerifyOrQuit(entry.GetIndex() == 3);
VerifyOrQuit(entry == entry3);
VerifyOrQuit(array.ReadNext(entry) == kErrorNotFound);
// Overwrite entry at index 1
SuccessOrQuit(array.WriteAt(1, entry4));
VerifyOrQuit(array.GetLength() == 4);
SuccessOrQuit(array.ReadAt(0, entry));
VerifyOrQuit(entry == entry0);
SuccessOrQuit(array.ReadAt(1, entry));
VerifyOrQuit(entry == entry4);
SuccessOrQuit(array.ReadAt(2, entry));
VerifyOrQuit(entry == entry2);
SuccessOrQuit(array.ReadAt(3, entry));
VerifyOrQuit(entry == entry3);
VerifyOrQuit(array.ReadAt(4, entry) == kErrorNotFound);
// Overwrite last entry
SuccessOrQuit(array.WriteAt(3, entry1));
VerifyOrQuit(array.GetLength() == 4);
SuccessOrQuit(array.ReadAt(0, entry));
VerifyOrQuit(entry == entry0);
SuccessOrQuit(array.ReadAt(1, entry));
VerifyOrQuit(entry == entry4);
SuccessOrQuit(array.ReadAt(2, entry));
VerifyOrQuit(entry == entry2);
SuccessOrQuit(array.ReadAt(3, entry));
VerifyOrQuit(entry == entry1);
VerifyOrQuit(array.ReadAt(4, entry) == kErrorNotFound);
// Overwrite out of bound
VerifyOrQuit(array.WriteAt(4, entry4) == kErrorInvalidArgs);
// Array at its max size
VerifyOrQuit(array.Push(entry4) == kErrorNoBufs);
// Clearing array
array.Clear();
VerifyOrQuit(array.GetLength() == 0);
VerifyOrQuit(array.ReadAt(0, entry) == kErrorNotFound);
VerifyOrQuit(array.WriteAt(0, entry0) == kErrorInvalidArgs);
testFreeInstance(instance);
}
} // namespace ot
int main(void)
{
ot::TestMsgBackedArray();
printf("All tests passed\n");
return 0;
}