Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions score/mw/com/impl/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ cc_library(
deps = [
":receive_handler_registration_changed_handler",
":skeleton_event_binding",
"@score_baselibs//score/memory:data_type_size_info",
"@score_baselibs//score/result",
],
)
Expand Down
11 changes: 9 additions & 2 deletions score/mw/com/impl/bindings/lola/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ cc_library(
"generic_skeleton_event.h",
],
features = COMPILER_WARNING_FEATURES,
implementation_deps = [
"@score_baselibs//score/memory:data_type_size_info",
],
tags = ["FFI"],
visibility = [
"//score/mw/com/impl:__subpackages__",
Expand Down Expand Up @@ -747,8 +750,11 @@ cc_library(
"//score/mw/com/impl:__subpackages__",
],
deps = [
":control_slot_types",
"//score/memory/shared:managed_memory_resource",
"//score/memory/shared:offset_ptr",
"//score/memory/shared:types",
"@score_baselibs//score/containers:dynamic_array",
"@score_baselibs//score/memory:data_type_size_info",
],
)

Expand Down Expand Up @@ -1271,7 +1277,8 @@ cc_unit_test(
features = COMPILER_WARNING_FEATURES,
deps = [
":event_data_storage",
"@score_baselibs//score/containers:dynamic_array",
"//score/memory/shared:new_delete_delegate_resource",
"@score_baselibs//score/memory:data_type_size_info",
"@score_baselibs//score/mw/log",
],
)
Expand Down
76 changes: 76 additions & 0 deletions score/mw/com/impl/bindings/lola/event_data_storage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,79 @@
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/
#include "score/mw/com/impl/bindings/lola/event_data_storage.h"

#include <score/assert.hpp>
#include <score/utility.hpp>

#include <limits>

namespace score::mw::com::impl::lola
{

EventDataStorage::EventDataStorage(memory::shared::ManagedMemoryResource& resource,
SlotIndexType number_of_slots,
memory::DataTypeSizeInfo event_sample_size_info)
: number_of_slots_(number_of_slots),
sample_size_info_(event_sample_size_info),
memory_resource_(resource),
type_erased_data_slots_(nullptr),
type_erased_data_slots_storage_size_(0)
{
// Guard against an overflow when calculating the total number of bytes needed for the raw slot-array. Without
// this check, an overflowing multiplication would silently wrap around to a much smaller value than what is
// actually required, leading to an undersized allocation and, subsequently, out-of-bounds accesses when the
// slots are used (see GetTypeErasedDataSlot()).
SCORE_LANGUAGE_FUTURECPP_PRECONDITION_PRD_MESSAGE(
(event_sample_size_info.Size() == 0U) ||
(number_of_slots <= (std::numeric_limits<std::size_t>::max() / event_sample_size_info.Size())),
"Overflow while calculating the total size of the raw event-data slot-array.");
const auto storage_bytes_needed = number_of_slots * event_sample_size_info.Size();

// The alignment used here must match exactly the alignment CalculateServiceDataStorageShmSize() assumes for this
// allocation (see service_data_storage.cpp), i.e. event_sample_size_info.Alignment(). Using a different (e.g.
// hardcoded, stricter) alignment here would make the analytically calculated shm-size wrong (too small).
void* const type_erased_data_slots_start =
memory_resource_.allocate(storage_bytes_needed, event_sample_size_info.Alignment());
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD(nullptr != type_erased_data_slots_start);
type_erased_data_slots_ = static_cast<std::byte*>(type_erased_data_slots_start);
type_erased_data_slots_storage_size_ = storage_bytes_needed;
}

EventDataStorage::~EventDataStorage()
{
if (type_erased_data_slots_ != nullptr)
{
memory_resource_.deallocate(type_erased_data_slots_.get(), type_erased_data_slots_storage_size_);
}
}

void* EventDataStorage::GetTypeErasedDataSlot(SlotIndexType index, size_t data_size) const
{
SCORE_LANGUAGE_FUTURECPP_PRECONDITION_PRD(index < number_of_slots_);

SCORE_LANGUAGE_FUTURECPP_PRECONDITION_PRD(data_size == sample_size_info_.Size());
const auto element_offset = data_size * index;

// we apply the required bounds-checking:
// Verify, that the start of the type-erased storage is still within bounds
auto* const slots_start_address = type_erased_data_slots_.get();
// Verify, that the complete slot to be accessed in the type-erased storage is still within bounds.
const auto slot_last_byte_offset_ptr =
type_erased_data_slots_ + decltype(type_erased_data_slots_)::difference_type(element_offset + data_size - 1U);
score::cpp::ignore = slot_last_byte_offset_ptr.get();

// In our architecture we have a one-to-one mapping between pointers and integral values.
// The preconditions above guarantee that element_address will always point inside type_erased_data_slots_.
// Therefore, casting between integers and pointers is well-defined in this case.
// NOLINTNEXTLINE(score-banned-function) see above
auto* const element_address = memory::shared::AddOffsetToPointer(slots_start_address, element_offset);

return element_address;
}

SlotIndexType EventDataStorage::GetNumberOfSlots() const
{
return number_of_slots_;
}

} // namespace score::mw::com::impl::lola
42 changes: 35 additions & 7 deletions score/mw/com/impl/bindings/lola/event_data_storage.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,52 @@
#ifndef SCORE_MW_COM_IMPL_BINDINGS_LOLA_EVENT_DATA_STORAGE_H
#define SCORE_MW_COM_IMPL_BINDINGS_LOLA_EVENT_DATA_STORAGE_H

#include "score/containers/dynamic_array.h"
#include "score/memory/shared/polymorphic_offset_ptr_allocator.h"
#include "score/memory/data_type_size_info.h"
#include "score/memory/shared/managed_memory_resource.h"
#include "score/memory/shared/offset_ptr.h"
#include "score/mw/com/impl/bindings/lola/control_slot_types.h"

#include <scoped_allocator>
#include <cstddef>

namespace score::mw::com::impl::lola
{

/// \brief Container for storing the actual data of a LoLa Event
/// \brief Container for storing the actual data of a LoLa event (resp. field) within shared-memory.
///
/// \details This container will be accessed in parallel by multiple threads. The access must be synchronized via the
/// EventDataControl block. The idea is that a producer first needs to claim an event slot, then change the data within
/// the storage and then mark the slot as ready (similar for a consumer). This enables us cache optimized access of
/// these data structures. The overall contract will be abstracted for the end-user anyhow, so the separation into two
/// classes should be no problem.
template <typename SampleType>
using EventDataStorage = score::containers::
DynamicArray<SampleType, std::scoped_allocator_adaptor<memory::shared::PolymorphicOffsetPtrAllocator<SampleType>>>;
class EventDataStorage final
{
public:
EventDataStorage(memory::shared::ManagedMemoryResource& resource,
SlotIndexType number_of_slots,
memory::DataTypeSizeInfo event_sample_size_info);

~EventDataStorage();

/// \brief Returns a pointer to the type-erased data slot at the given index.
/// \details This access also does a complete bounds-check to verify that the returned raw-pointer is within the
/// bounds as well as the end-address (returned pointer plus data_size).
/// \param data_size The size of the data slot. This is used to verify, that the callers size expectation matches
/// the size of the event data type, the EventDataStorage was constructed with.
/// @return A pointer to the type-erased data slot.
void* GetTypeErasedDataSlot(SlotIndexType index, size_t data_size) const;

SlotIndexType GetNumberOfSlots() const;

private:
SlotIndexType number_of_slots_;
memory::DataTypeSizeInfo sample_size_info_;
memory::shared::ManagedMemoryResource& memory_resource_;

memory::shared::OffsetPtr<std::byte> type_erased_data_slots_;

/// size of type_erased_data_slots_ storage in bytes. This is equal to number_of_slots_ * sample_size_info_.Size()
std::size_t type_erased_data_slots_storage_size_;
};

} // namespace score::mw::com::impl::lola

Expand Down
154 changes: 141 additions & 13 deletions score/mw/com/impl/bindings/lola/event_data_storage_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,156 @@
********************************************************************************/
#include "score/mw/com/impl/bindings/lola/event_data_storage.h"

#include "score/containers/dynamic_array.h"
#include "score/memory/data_type_size_info.h"
#include "score/memory/shared/new_delete_delegate_resource.h"

#include <score/utility.hpp>

#include <gtest/gtest.h>
#include <type_traits>

#include <cstddef>
#include <cstdint>
#include <cstring>
#include <limits>
#include <vector>

namespace score::mw::com::impl::lola
{
namespace
{

TEST(EventDataStorageTest, EventDataStorageUsesADynamicArrayToRepresentSlots)
const std::uint64_t kMemoryResourceId{42U};
constexpr SlotIndexType kNumberOfSlots{4U};

/// \brief A trivial dummy type that requires std::max_align_t alignment.
/// \details Used (in addition to plain integral types) as one of the sample types EventDataStorage is typed-tested
/// with, to make sure EventDataStorage also correctly handles the "worst case" alignment requirement.
struct MaxAlignedDummyStruct
{
std::byte byte_member;
std::max_align_t max_align_member;
};

/// \brief Fills every byte of value with pattern, so that two values created with a differing pattern are guaranteed
/// to compare unequal (see BytesEqual()), regardless of TypeParam's actual member layout.
template <typename T>
T MakeValue(const std::uint8_t pattern)
{
T value{};
std::memset(&value, pattern, sizeof(T));
return value;
}

/// \brief Compares lhs and rhs byte-by-byte.
/// \details We cannot rely on operator== being defined for every TypeParam (e.g. MaxAlignedDummyStruct doesn't define
/// one), so we compare the raw bytes instead.
template <typename T>
bool BytesEqual(const T& lhs, const T& rhs)
{
return std::memcmp(&lhs, &rhs, sizeof(T)) == 0;
}

/// \brief Templated test fixture that constructs a real EventDataStorage for TypeParam, sized/aligned according to
/// TypeParam's actual memory::DataTypeSizeInfo.
template <typename T>
class EventDataStorageTypedTest : public ::testing::Test
{
protected:
memory::shared::NewDeleteDelegateMemoryResource memory_resource_{kMemoryResourceId};
memory::DataTypeSizeInfo sample_size_info_{sizeof(T), alignof(T)};
EventDataStorage unit_{memory_resource_, kNumberOfSlots, sample_size_info_};
};

using SampleTypes = ::testing::Types<std::uint16_t, std::uint64_t, MaxAlignedDummyStruct>;
TYPED_TEST_SUITE(EventDataStorageTypedTest, SampleTypes, );

TYPED_TEST(EventDataStorageTypedTest, GetTypeErasedDataSlotReturnsCorrectlyAlignedAndWritableSlotForEveryIndex)
{
// Given an EventDataStorage constructed for TypeParam (see fixture)

for (SlotIndexType slot_index = 0U; slot_index < kNumberOfSlots; ++slot_index)
{
// When retrieving the type-erased pointer to the data slot at slot_index
void* const type_erased_slot = this->unit_.GetTypeErasedDataSlot(slot_index, sizeof(TypeParam));

// Then the returned pointer is non-null and correctly aligned for TypeParam
ASSERT_NE(type_erased_slot, nullptr);
EXPECT_EQ(reinterpret_cast<std::uintptr_t>(type_erased_slot) % alignof(TypeParam), 0U);

// and casting it to a TypeParam* and writing/reading a value through it works without crashing (e.g. due to
// an alignment violation) and yields back the very same value that was written.
auto* const typed_slot = static_cast<TypeParam*>(type_erased_slot);
const TypeParam value_to_write = MakeValue<TypeParam>(static_cast<std::uint8_t>(slot_index + 1U));
*typed_slot = value_to_write;

EXPECT_TRUE(BytesEqual(*typed_slot, value_to_write));
}
}

TYPED_TEST(EventDataStorageTypedTest, DataSlotsOfDifferentIndicesDoNotOverlap)
{
// Given an EventDataStorage constructed for TypeParam (see fixture)

// When writing a distinct typed-value into every one of its data slots
std::vector<TypeParam> written_values{};
for (SlotIndexType slot_index = 0U; slot_index < kNumberOfSlots; ++slot_index)
{
const TypeParam value = MakeValue<TypeParam>(static_cast<std::uint8_t>(slot_index + 1U));
written_values.push_back(value);

auto* const typed_slot =
static_cast<TypeParam*>(this->unit_.GetTypeErasedDataSlot(slot_index, sizeof(TypeParam)));
*typed_slot = value;
}

// Then every data slot still contains the very same distinct value that was written to it, i.e. writing to one
// slot did not corrupt/overlap the contents of another slot.
for (SlotIndexType slot_index = 0U; slot_index < kNumberOfSlots; ++slot_index)
{
auto* const typed_slot =
static_cast<TypeParam*>(this->unit_.GetTypeErasedDataSlot(slot_index, sizeof(TypeParam)));
EXPECT_TRUE(BytesEqual(*typed_slot, written_values[slot_index]));
}
}

TEST(EventDataStorageDeathTest, GetTypeErasedDataSlotTerminatesOnDataSizeMismatch)
{
// Given an EventDataStorage constructed for a std::uint32_t sample type
memory::shared::NewDeleteDelegateMemoryResource memory_resource{kMemoryResourceId};
const memory::DataTypeSizeInfo sample_size_info{sizeof(std::uint32_t), alignof(std::uint32_t)};
EventDataStorage unit{memory_resource, kNumberOfSlots, sample_size_info};

// When requesting a data slot with a data_size that does not match the sample type's actual size
// Then the program terminates, since the caller's size expectation doesn't match the storage's sample size.
EXPECT_DEATH(unit.GetTypeErasedDataSlot(0U, sizeof(std::uint32_t) + 1U), ".*");
}

TEST(EventDataStorageDeathTest, GetTypeErasedDataSlotTerminatesOnOutOfBoundsIndex)
{
// Our detailed design (aas/lib/memory/design/shared_memory/OffsetPtrDesign.md#dynamic-array-considerations)
// requires that we use a DynamicArray to represent our slots so that bounds checking is done.
using DummySampleType = int;
static_assert(
std::is_same_v<
score::containers::DynamicArray<
DummySampleType,
std::scoped_allocator_adaptor<memory::shared::PolymorphicOffsetPtrAllocator<DummySampleType>>>,
EventDataStorage<DummySampleType>>,
"EventDataControl should use a dynamic array to represent slots.");
// Given an EventDataStorage constructed for a std::uint32_t sample type with kNumberOfSlots slots
memory::shared::NewDeleteDelegateMemoryResource memory_resource{kMemoryResourceId};
const memory::DataTypeSizeInfo sample_size_info{sizeof(std::uint32_t), alignof(std::uint32_t)};
EventDataStorage unit{memory_resource, kNumberOfSlots, sample_size_info};

// When requesting a data slot with an index that is out of bounds
// Then the program terminates.
EXPECT_DEATH(unit.GetTypeErasedDataSlot(kNumberOfSlots, sizeof(std::uint32_t)), ".*");
}

TEST(EventDataStorageDeathTest, ConstructionTerminatesOnRawSlotArraySizeOverflow)
{
// Given a number_of_slots and a per-sample memory::DataTypeSizeInfo whose sizes, when multiplied to calculate
// the total size of the raw event-data slot-array, overflow std::size_t
memory::shared::NewDeleteDelegateMemoryResource memory_resource{kMemoryResourceId};
constexpr std::size_t alignment{alignof(std::max_align_t)};
const memory::DataTypeSizeInfo overflowing_sample_size_info{
(std::numeric_limits<std::size_t>::max() / alignment) * alignment, alignment};
constexpr SlotIndexType number_of_slots{2U};

// When constructing an EventDataStorage from this sizing information
// Then the program terminates, since calculating the total raw slot-array size would silently overflow.
EXPECT_DEATH(
score::cpp::ignore = (EventDataStorage{memory_resource, number_of_slots, overflowing_sample_size_info}), ".*");
}

} // namespace
Expand Down
13 changes: 4 additions & 9 deletions score/mw/com/impl/bindings/lola/event_meta_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
#define SCORE_MW_COM_IMPL_BINDINGS_LOLA_EVENT_META_INFO_H

#include "score/memory/data_type_size_info.h"
#include "score/memory/shared/offset_ptr.h"

namespace score::mw::com::impl::lola
{
Expand All @@ -23,22 +22,18 @@ namespace score::mw::com::impl::lola
/// \details Normally proxies/skeletons or "user code" dealing with an event, know its properties. This info is
/// provided and placed into shared-memory for the GenericProxy use-case, where a proxy connects to a provided
/// service based on only deployment info, NOT having any knowledge about the exact data type of the event.
/// Currently, the only "meta-info" needed is DataTypeSizeInfo. However, we wrap it into EventMetaInfo to be
/// prepared for future extensions.
class EventMetaInfo
{
public:
EventMetaInfo(const memory::DataTypeSizeInfo data_type_info,
const memory::shared::OffsetPtr<void> event_slots_raw_array)
: data_type_info_(data_type_info), event_slots_raw_array_(event_slots_raw_array)
{
}
EventMetaInfo(const memory::DataTypeSizeInfo data_type_info) : data_type_info_(data_type_info) {}

// Suppress "AUTOSAR C++14 M11-0-1" rule findings. This rule states: "Member data in non-POD class types shall
// be private.". There are no class invariants to maintain which could be violated by directly accessing member
// be private". There are no class invariants to maintain which could be violated by directly accessing member
// variables.
// coverity[autosar_cpp14_m11_0_1_violation]
memory::DataTypeSizeInfo data_type_info_;
// coverity[autosar_cpp14_m11_0_1_violation]
memory::shared::OffsetPtr<void> event_slots_raw_array_;
};

} // namespace score::mw::com::impl::lola
Expand Down
Loading
Loading