Skip to content

Conversation

@Abhishek2581
Copy link
Contributor

@Abhishek2581 Abhishek2581 commented Jan 15, 2026

Hi @crimson11
As discussed earler Rasing a pull request containing the Event with black-box tests through Example Application.

Rest will done in second pull request(First week of Feb) which includes the complete Event functionality documentation along with tests for both Event and Field

@mina-hamdi mina-hamdi force-pushed the generic_skeleton_implementation branch 2 times, most recently from 7e7c369 to 809cecd Compare January 15, 2026 12:54
@LittleHuba
Copy link
Contributor

@mina-hamdi it looks like you have conflicts that require manual resolution. Could you please fix them?

@Abhishek2581
Copy link
Contributor Author

@mina-hamdi it looks like you have conflicts that require manual resolution. Could you please fix them?

hi @LittleHuba : confilct resolved

@Abhishek2581
Copy link
Contributor Author

hi @LittleHuba , @hoe-jo , @castler
As discussed in the ticket, this pull request represents the first iteration of the generic Skeleton for Events, including black-box tests via the example application. Kindly review and provide feedback.

A follow-up pull request is planned for the beginning of Feb to add Field support, tests, and Event documentation, incorporating feedback from this review


/// @brief Adds a type-erased event to the skeleton.
///
/// This must be called before OfferService().
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to avoid this statefulness?

For example Create() or OfferService() could get an additional argument Span<tuple<string_view, SizeInfo>> events which is a complete list of events to be used.

Then there is no need for the user to keep an eye on what is allowed in the documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your point — avoiding this statefulness would be an improvement. Passing the complete list of events in a single call removes the need for users to reason about call order or rely on documentation to understand what is allowed at each stage.

We are planning to move event configuration into Create() and pass the full set of events upfront via a std::span. This allows the service instance and its events to be defined atomically and validated in one place, while keeping OfferService() as a pure activation step.

Regarding the event descriptor type, we currently lean slightly towards using a small named struct (e.g. EventInfo { std::string_view name; SizeInfo size_info; }) rather than a tuple. The main reason is that this models the event description as a single logical unit and makes the intent explicit in the type, rather than relying on positional semantics. That said, a tuple would also work mechanically.

Before finalizing this, we’d like to check with you whether you prefer the struct-based descriptor or the tuple<string_view, SizeInfo> approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

structs are completely fine. Actually I usually avoid tuples, because you cannot give its members names.


if (!instance_identifier_result.has_value())
{
score::mw::log::LogFatal("lola") << "Failed to resolve instance identifier from instance specifier";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is code, which is like this in SkeletonWrapperClass, but it is soon to be changed! Because it makes no sense to terminate here as it is a simple failure, the user handing over an `InstanceSpecifier' for which no mapping is in the config!
Instead of terminating an error code 'kInstanceIDCouldNotBeResolved' (this code already exists!) shall be returned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with you


/// \brief Template specialization of SampleAllocateePtr for void.
template <>
class SampleAllocateePtr<void>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is a "full" specialization really needed? I did not check in-depth ... but in case of SamplePtr we also did only "SFINAE remove" via std::enable_if the operator* in case of the void-case ... so couldn't we have done it here symmetrically?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good observation, and the comparison with SamplePtr is valid.
However, for SampleAllocateePtr the need for a full specialization is not an interface-level issue (such as disabling operator*), but a type-instantiation and object-lifetime issue caused by the internal representation.

The primary template of SampleAllocateePtr contains the following member:
std::variant< score::cpp::blank, lola::SampleAllocateePtr<SampleType>, std::unique_ptr<SampleType>> internal_;

For non-void SampleType, the std::unique_ptr alternative is required to support non-LoLa/test bindings and provides correct ownership semantics.

When SampleType = void, instantiating the primary template would require instantiating std::unique_ptr. This is fundamentally problematic:

  • std::unique_ptr assumes ownership of a complete object type and performs destruction via its deleter.
  • For T = void, there is no complete object type, and deletion requires a complete object type.
  • As a consequence, std::unique_ptr does not represent a valid owning abstraction in this context.

This failure occurs during class template instantiation, not during overload resolution. Even if all dereferencing operators were conditionally removed via SFINAE, the class would already be ill-formed because std::variant requires all alternative types to be well-formed and destructible.

SFINAE cannot be applied here because:

  • it only applies to substitution in dependent contexts (e.g. function templates),
  • the std::unique_ptr alternative is part of the class layout itself,
  • and std::variant must be fully instantiated, including its destructor and move operations, regardless of which alternative is active at runtime.

In contrast, SamplePtr only needed SFINAE because its internal representation does not include an owning type that becomes invalid for void; the issue there is limited to disabling invalid operations such as dereferencing.

Therefore, a full specialization for SampleAllocateePtr is required to:

  • remove the invalid std::unique_ptr alternative at the type level,
  • ensure that all variant alternatives are well-formed and destructible,
  • maintain correct ownership semantics,
  • and clearly express the reduced contract for the void case (no dereference, no owning test pointer).

While it would technically be possible to avoid a full specialization by conditionally replacing std::unique_ptr inside the std::variant, this would introduce additional dummy alternatives and require extra visitor handling to preserve invariants. That approach would obscure the ownership model and rely on subtle template indirection rather than expressing the constraints explicitly at the type level.

By using a dedicated specialization for SampleAllocateePtr, the invalid owning alternative is removed entirely, the variant remains minimal and well-formed, and the reduced contract for void is enforced directly by the type system. This makes the design more robust and easier to reason about in the long term.

Please let me know if you see a viable alternative that avoids the specialization without introducing an invalid variant alternative or weakening the ownership semantics.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I guess, we were a bit "lazy" in the context of SampleAllocateePtr ... with adding just std::unique_ptr<T> to the variant to care for test cases ... which now falls on our feet, when introducing a void-specialization!
Please now follow the same approach as for the SamplePtr, which for testing purposes adds a specific mock-binding SampleAllocateePtr to the variant. This would be a mock_binding::SampleAllocateePtr<SampleType>

You can implement is according to mock_binding::SamplePtr<SampleType>, which adds a distinct deleter to the underlying std::unique_ptr, so that class template instantiation (impl::SampleAllocateePtr) works.

The thing is, that in case you are then implementing tests on a mock/mock-binding base, you need to be concise and hand over the "right" deleter! Example, where/how it is done, you find here.
I.e. in the testing case for a GenericProxyEvent or GenericSkeletonEvent, you exactly know the type (being non-void), with which you want to set-up/populate your test. There you can then instantiate the mock_binding::SamplePtr<SampleType> resp. mock_binding::SampleAllocateePtr<SampleType> (being aliases to std::unique_ptr) with the correct deleter as shown in my link!


#include <cstddef>

namespace score::mw::com
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be score::mw::com::impl? Most likely it will later also be used on the public interface ... then you would also introduce an alias in types.h

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with you

SizeInfo struct should be moved into the score::mw::com::impl namespace to accurately reflect its location and intended usage as an internal detail

namespace score::mw::com
{
/// @brief A struct to hold size and alignment information for generic type-erased data.
struct SizeInfo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have DataTypeMetaInfo ... this seems to be identical/redundant? E.g. you should think about re-using it ... in this case it is also fine imho, if you move the existing DataTypeMetaInfo from impl::lola to just impl.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out — there is overlap and we should avoid redundant “size + alignment” carriers where possible.

That said, lola::DataTypeMetaInfo is currently not a drop-in replacement for score::mw::com::SizeInfo for two reasons:

Different scope/semantics: DataTypeMetaInfo lives in score::mw::com::impl::lola and is treated as LoLa-binding internal metadata. SizeInfo is already used as a general-purpose representation (e.g. DynamicType::mwcom_size_info_) and is not LoLa-specific.

Different representation: DataTypeMetaInfo::align_of_ is std::uint8_t, while SizeInfo::alignment is size_t. In valid C++ the alignment requirement can exceed 255 (e.g. via alignas(N)), so storing alignment in uint8_t risks truncation. Even if our current platforms don’t hit this, size_t is the safer generic type and matches how we treat alignment elsewhere.

Given that, I agree we should consolidate. Two options:

Option A (preferred): keep SizeInfo as the canonical type for generic size/alignment and update LoLa code to use it (or an impl::SizeInfo), then deprecate/remove lola::DataTypeMetaInfo.

Option B: move/rename DataTypeMetaInfo to impl and change align_of_ to size_t (and potentially rename fields for consistency), then use that everywhere.

I leaned toward Option A since SizeInfo is already used by DynamicType and uses the robust size_t representation. Let me know which consolidation direction you prefer and I’ll align the changes accordingly.

size_t sample_size,
size_t sample_alignment) noexcept
{
auto* data_storage = storage_resource_->construct<EventDataStorage<std::uint8_t>>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes this EventDataStorage (which is an alias for DynamicArray) really sense here .... I mean, what is the added value of the DynamicArray here, if the slots/elements within the DynamicArray do not even represent the "real" events ... and you have to do pointer-arithmetic anyhow?
Apart from this, when creating a DynamicArray<uint8_t>, you get potential WRONG alignment! Because it will allocate starting on a one-byte aligned address, which might be wrong, when your sampl_alignmentis larger!

I.e. why not simply:
void* data_storage = storage_resource_->allocate(sample_size * element_properties.number_of_slots, sample_alignment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally Agree with you

// The at() method on EventDataStorage (which is a DynamicArray) correctly handles
// the pointer arithmetic based on its template type (std::uint8_t).
// We multiply by size_info_.size here because the underlying storage is a byte array.
void* data_ptr = &data_storage->at(static_cast<std::uint64_t>(slot.GetIndex()) * size_info_.size);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this cast to std::uint64_t necessary? However, if you follow my comment for Skeleton::CreateEventDataFromOpenedSharedMemory(), then this code here will change ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice point and for sure after change in Skeleton::CreateEventDataFromOpenedSharedMemory() as per your feedback this code will change to something like this

void* base_ptr = data_storage_.get();
void* data_ptr = static_caststd::uint8_t*(base_ptr) + (static_caststd::uint64_t(slot.GetIndex()) * size_info_.size);

but still i am thinking we need type casting in uint64_t because

slot.GetIndex() returns SlotIndexType which is uint16_t. In C++, when we do arithmetic with small integer types like uint16_t, the compiler first promotes them to a larger “normal” integer type (typically int or unsigned int) before doing the multiplication. Then, because size_info_.size is size_t, the final multiplication ends up being done after a sequence of implicit conversions. Which exact intermediate type is used can depend on the platform (32-bit vs 64-bit), and it’s not obvious from reading the code.

By explicitly casting the index first, we force the multiplication to happen in 64-bit arithmetic from the start. This makes the intent clearer and avoids any risk of unexpected overflow due to intermediate type promotions.

Whats your view?

return skeleton_base_.binding_.get();
}

bool IsOffered() const
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% convinced to use the SkeletonBaseView pattern here:
You want the GenericSkeleton to be able to access this member service_offered_flag_ of its base class.
So I would rather make it protected then.
The usage of SkeletonBaseView in the existing code is to "allow" classes outside of SkeletonBase class hierarchy to access non-public elements ... but here it is a bit different ... a subclass should be able to access this member (if it needs) without the SkeletonBaseView "trick"....

Copy link
Contributor

@bemerybmw bemerybmw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't done a full review, just left a few comments after glancing over the PR.

class SkeletonBinding;


class GenericSkeleton : public SkeletonBase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class should have a docstring.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes i'll do this

auto binding = SkeletonBindingFactory::Create(identifier);
if (!binding)
{
return MakeUnexpected(ComErrc::kBindingFailure);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add an error log message.

Copy link
Contributor Author

@Abhishek2581 Abhishek2581 Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice point, I'll do this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general point, all header files should have a corresponding .cpp file.


/// @brief Overload for generic skeletons (which require a SizeInfo).
template <typename SkeletonServiceElementBinding, typename SkeletonServiceElement, ServiceElementType element_type>
auto CreateSkeletonServiceElement(const InstanceIdentifier& identifier,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just have CreateGenericSkeletonServiceElement rather than overloading CreateSkeletonServiceElement?

const std::string_view service_element_name) noexcept
-> std::unique_ptr<SkeletonServiceElementBinding>
{
SCORE_LANGUAGE_FUTURECPP_ASSERT_PRD_MESSAGE((!std::is_same_v<SkeletonServiceElement, lola::GenericSkeletonEvent>),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static_assert so that it's checked at compile time?

SkeletonBase& parent,
const std::string_view service_element_name) noexcept
const std::string_view service_element_name,
const score::cpp::optional<std::reference_wrapper<const SizeInfo>>& size_info) noexcept
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const score::cpp::optional<std::reference_wrapper<const SizeInfo>>& size_info) noexcept
const score::cpp::optional<SizeInfo> size_info) noexcept

SizeInfo is small so I wouldn't worry about passing by reference (to keep the signature more readable).


TEST_F(GenericSkeletonTest, CreateWithInstanceSpecifier)
{
auto instance_specifier = InstanceSpecifier::Create("a/b/c").value();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit tests should follow the Given, when, then structure. (E.g. https://martinfowler.com/bliki/GivenWhenThen.html)

public:
virtual Result<score::Blank> Send(lola::ControlSlotCompositeIndicator control_slot_indicator) noexcept = 0;

virtual Result<lola::SampleAllocateePtr<void>> Allocate() noexcept = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code within mw::com::impl is binding independent. So we should pass SampleAllocateePtr (from impl/plumbing) instead of lola::SampleAllocateePtr.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have correctly identified a violation of the architectural layering in code. I completely agree with assessment. I 'll do necessary change

return service_data_storage;
}

std::optional<memory::shared::LockFile> CreateOrOpenServiceInstanceExistenceMarkerFile(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this moved?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanksm
It was a mistake from my side , i will revert this

return BindingType::kLoLa;
};

std::pair<score::memory::shared::OffsetPtr<void>, EventDataControlComposite> RegisterGeneric(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these new functions should have docstrings

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing out ,i will do

class GenericSkeletonEventBinding : public SkeletonEventBindingBase
{
public:
virtual Result<score::Blank> Send(lola::ControlSlotCompositeIndicator control_slot_indicator) noexcept = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This signature makes no sense. GenericSkeletonEventBinding is the interface every binding shall support. It can't have a lola-binding specific argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've made a great point, thank you for catching that. I completely agree that the binding-independent layer shouldn't have alola-binding specific argument..

I will correct it.

{

template <typename EventType>
void PrepareOfferImpl(EventType& event)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you are trying to re-use impl. where possible between SkeletonEvent and GenericSkeletonEvent ...
I would propose to achieve this "symmetrically" to the proxy-side, by introducing a SkeletonEventCommon ... which should then be aggregated by SkeletonEvent and GenericSkeletonEvent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great Point i;ll do necessary changes

return MakeUnexpected(ComErrc::kNotOffered);
}

auto* const lola_sample_ptr = SampleAllocateePtrView<void>{sample}.As<lola::SampleAllocateePtr<void>>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this hardcoded expectation, that SampleAllocateePtr is a lola variant? We are on the binding independent layer here, which needs to deal with all potential variants of SampleAllocateePtr

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've made a great point, thank you for catching that. I completely agree that the binding-independent layer shouldn't have a hardcoded dependency.

I will correct it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants