-
Notifications
You must be signed in to change notification settings - Fork 39
Generic skeleton implementation - Only with Event and Black Box Test with IPC_bridge App #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…l partial restart handling implementation.
7e7c369 to
809cecd
Compare
|
@mina-hamdi it looks like you have conflicts that require manual resolution. Could you please fix them? |
Signed-off-by: Abhishek GOYAL <[email protected]>
hi @LittleHuba : confilct resolved |
|
hi @LittleHuba , @hoe-jo , @castler 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(). |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>>( |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 ...
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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"....
bemerybmw
left a comment
There was a problem hiding this 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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>), |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this moved?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>>(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
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