diff --git a/SilKit/IntegrationTests/CMakeLists.txt b/SilKit/IntegrationTests/CMakeLists.txt index 0eb1b3a59..ce8c48f1c 100644 --- a/SilKit/IntegrationTests/CMakeLists.txt +++ b/SilKit/IntegrationTests/CMakeLists.txt @@ -177,6 +177,10 @@ add_silkit_test_to_executable(SilKitIntegrationTests SOURCES ITest_PubHistory.cpp ) +add_silkit_test_to_executable(SilKitIntegrationTests + SOURCES ITest_CapiServiceDiscovery.cpp +) + # RPC add_silkit_test_to_executable(SilKitInternalIntegrationTests @@ -203,6 +207,10 @@ add_silkit_test_to_executable(SilKitIntegrationTests SOURCES ITest_NetSimFlexRay.cpp ) +add_silkit_test_to_executable(SilKitIntegrationTests + SOURCES ITest_NetSimServiceDiscovery.cpp +) + # ============================================================ # Integration Tests based on Demo Applications diff --git a/SilKit/IntegrationTests/Hourglass/CMakeLists.txt b/SilKit/IntegrationTests/Hourglass/CMakeLists.txt index d22ee24b2..76fa5e1ed 100644 --- a/SilKit/IntegrationTests/Hourglass/CMakeLists.txt +++ b/SilKit/IntegrationTests/Hourglass/CMakeLists.txt @@ -114,3 +114,10 @@ add_silkit_test_to_executable(SilKitHourglassTests LIBS S_Test_Hourglass ) + +add_silkit_test_to_executable(SilKitHourglassTests + SOURCES + Test_HourglassServiceDiscovery.cpp + LIBS + S_Test_Hourglass +) diff --git a/SilKit/IntegrationTests/Hourglass/MockCapi.cpp b/SilKit/IntegrationTests/Hourglass/MockCapi.cpp index ccedbf562..40e13c09b 100644 --- a/SilKit/IntegrationTests/Hourglass/MockCapi.cpp +++ b/SilKit/IntegrationTests/Hourglass/MockCapi.cpp @@ -748,6 +748,22 @@ extern "C" workflowConfiguration); } + // ServiceDiscovery + + SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_Create( + SilKit_Experimental_ServiceDiscovery** outServiceDiscovery, SilKit_Participant* participant) + { + return globalCapi->SilKit_Experimental_ServiceDiscovery_Create(outServiceDiscovery, participant); + } + + SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler( + SilKit_Experimental_ServiceDiscovery* serviceDiscovery, void* context, + SilKit_Experimental_ServiceDiscoveryHandler_t handler) + { + return globalCapi->SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, context, + handler); + } + // Participant SilKit_ReturnCode SilKitCALL SilKit_Participant_Create(SilKit_Participant** outParticipant, diff --git a/SilKit/IntegrationTests/Hourglass/MockCapi.hpp b/SilKit/IntegrationTests/Hourglass/MockCapi.hpp index 1b863ea8b..888b36e92 100644 --- a/SilKit/IntegrationTests/Hourglass/MockCapi.hpp +++ b/SilKit/IntegrationTests/Hourglass/MockCapi.hpp @@ -396,6 +396,15 @@ class MockCapi (SilKit_Experimental_SystemController * systemController, const SilKit_WorkflowConfiguration* workflowConfiguration)); + // ServiceDiscovery + + MOCK_METHOD(SilKit_ReturnCode, SilKit_Experimental_ServiceDiscovery_Create, + (SilKit_Experimental_ServiceDiscovery * *outServiceDiscovery, SilKit_Participant* participant)); + + MOCK_METHOD(SilKit_ReturnCode, SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler, + (SilKit_Experimental_ServiceDiscovery * serviceDiscovery, void* context, + SilKit_Experimental_ServiceDiscoveryHandler_t handler)); + // Participant MOCK_METHOD(SilKit_ReturnCode, SilKit_Participant_Create, diff --git a/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp b/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp new file mode 100644 index 000000000..163a675fc --- /dev/null +++ b/SilKit/IntegrationTests/Hourglass/Test_HourglassServiceDiscovery.cpp @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "silkit/capi/SilKit.h" + +#include "silkit/SilKit.hpp" +#include "silkit/detail/impl/ThrowOnError.hpp" +#include "silkit/experimental/participant/ParticipantExtensions.hpp" +#include "silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp" +#include "silkit/experimental/serviceDiscovery/string_utils.hpp" + +#include "MockCapiTest.hpp" + +#include +#include + +namespace { + +using testing::_; +using testing::DoAll; +using testing::Return; +using testing::SaveArg; +using testing::SetArgPointee; + +namespace SD = SilKit::Experimental::ServiceDiscovery; +namespace Detail = SilKit::DETAIL_SILKIT_DETAIL_NAMESPACE_NAME; + +using ServiceDiscoveryWrapper = Detail::Impl::Experimental::ServiceDiscovery::ServiceDiscovery; + +class Test_HourglassServiceDiscovery : public SilKitHourglassTests::MockCapiTest +{ +public: + SilKit_Participant* mockParticipant{reinterpret_cast(uintptr_t(0x12345678))}; + SilKit_Experimental_ServiceDiscovery* mockServiceDiscovery{ + reinterpret_cast(uintptr_t(0x45362718))}; + + Test_HourglassServiceDiscovery() + { + ON_CALL(capi, SilKit_Experimental_ServiceDiscovery_Create(_, _)) + .WillByDefault(DoAll(SetArgPointee<0>(mockServiceDiscovery), Return(SilKit_ReturnCode_SUCCESS))); + } +}; + +TEST_F(Test_HourglassServiceDiscovery, SilKit_Experimental_ServiceDiscovery_Create) +{ + Detail::Impl::Participant participant{mockParticipant}; + + EXPECT_CALL(capi, SilKit_Experimental_ServiceDiscovery_Create(_, mockParticipant)); + + auto* serviceDiscovery = Detail::Experimental::Participant::CreateServiceDiscovery(&participant); + EXPECT_NE(serviceDiscovery, nullptr); +} + +TEST_F(Test_HourglassServiceDiscovery, SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler) +{ + ServiceDiscoveryWrapper serviceDiscovery{mockParticipant}; + + void* capturedContext{nullptr}; + SilKit_Experimental_ServiceDiscoveryHandler_t capturedHandler{nullptr}; + EXPECT_CALL(capi, SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(mockServiceDiscovery, _, _)) + .WillOnce(DoAll(SaveArg<1>(&capturedContext), SaveArg<2>(&capturedHandler), + Return(SilKit_ReturnCode_SUCCESS))); + + serviceDiscovery.SetServiceDiscoveryHandler([](SD::ServiceDiscoveryEventType, const SD::ServiceDescriptor&) {}); + + EXPECT_NE(capturedContext, nullptr); + EXPECT_NE(capturedHandler, nullptr); +} + +// The C trampoline installed by the wrapper must translate the borrowed C descriptor into an owned +// C++ ServiceDescriptor and dispatch it to the user's std::function. +TEST_F(Test_HourglassServiceDiscovery, service_discovery_handler_round_trip) +{ + ServiceDiscoveryWrapper serviceDiscovery{mockParticipant}; + + void* capturedContext{nullptr}; + SilKit_Experimental_ServiceDiscoveryHandler_t capturedHandler{nullptr}; + EXPECT_CALL(capi, SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(mockServiceDiscovery, _, _)) + .WillOnce(DoAll(SaveArg<1>(&capturedContext), SaveArg<2>(&capturedHandler), + Return(SilKit_ReturnCode_SUCCESS))); + + SD::ServiceDiscoveryEventType receivedEvent{SD::ServiceDiscoveryEventType::Invalid}; + SD::ServiceDescriptor received{}; + int callCount{0}; + serviceDiscovery.SetServiceDiscoveryHandler( + [&](SD::ServiceDiscoveryEventType eventType, const SD::ServiceDescriptor& descriptor) { + ++callCount; + receivedEvent = eventType; + received = descriptor; + }); + ASSERT_NE(capturedHandler, nullptr); + + // Case 1: a data publisher with media type and two labels. + SilKit_Label cLabels[2]; + cLabels[0].key = "kA"; + cLabels[0].value = "vA"; + cLabels[0].kind = SilKit_LabelKind_Mandatory; + cLabels[1].key = "kB"; + cLabels[1].value = "vB"; + cLabels[1].kind = SilKit_LabelKind_Optional; + + SilKit_Experimental_ServiceDescriptor cDescriptor; + SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, cDescriptor); + cDescriptor.participantName = "ParticipantA"; + cDescriptor.serviceName = "MyPublisher"; + cDescriptor.serviceKind = SilKit_Experimental_ServiceKind_DataPublisher; + cDescriptor.primaryIdentifier = "TopicA"; + cDescriptor.mediaType = "application/json"; + cDescriptor.labelList.numLabels = 2; + cDescriptor.labelList.labels = cLabels; + + capturedHandler(capturedContext, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, &cDescriptor); + + EXPECT_EQ(callCount, 1); + EXPECT_EQ(receivedEvent, SD::ServiceDiscoveryEventType::ServiceCreated); + EXPECT_EQ(received.participantName, "ParticipantA"); + EXPECT_EQ(received.serviceName, "MyPublisher"); + EXPECT_EQ(received.serviceKind, SD::ServiceKind::DataPublisher); + EXPECT_EQ(received.primaryIdentifier, "TopicA"); + EXPECT_EQ(received.mediaType, "application/json"); + ASSERT_EQ(received.labels.size(), 2u); + EXPECT_EQ(received.labels[0].key, "kA"); + EXPECT_EQ(received.labels[0].value, "vA"); + EXPECT_EQ(received.labels[0].kind, SilKit::Services::MatchingLabel::Kind::Mandatory); + EXPECT_EQ(received.labels[1].key, "kB"); + EXPECT_EQ(received.labels[1].kind, SilKit::Services::MatchingLabel::Kind::Optional); + + // Case 2: a bus controller removal without labels/media type. + SilKit_Experimental_ServiceDescriptor cBusDescriptor; + SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, cBusDescriptor); + cBusDescriptor.participantName = "ParticipantB"; + cBusDescriptor.serviceName = "Can1"; + cBusDescriptor.serviceKind = SilKit_Experimental_ServiceKind_CanController; + cBusDescriptor.primaryIdentifier = "CAN1"; + cBusDescriptor.mediaType = ""; + cBusDescriptor.labelList.numLabels = 0; + cBusDescriptor.labelList.labels = nullptr; + + capturedHandler(capturedContext, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, &cBusDescriptor); + + EXPECT_EQ(callCount, 2); + EXPECT_EQ(receivedEvent, SD::ServiceDiscoveryEventType::ServiceRemoved); + EXPECT_EQ(received.serviceKind, SD::ServiceKind::CanController); + EXPECT_EQ(received.primaryIdentifier, "CAN1"); + EXPECT_TRUE(received.mediaType.empty()); + EXPECT_TRUE(received.labels.empty()); +} + +// Setting the handler twice must register the C trampoline exactly once (no duplicate delivery) and +// replace the user handler in place without dangling the previously stored std::function. +TEST_F(Test_HourglassServiceDiscovery, set_handler_twice_replaces_and_registers_once) +{ + ServiceDiscoveryWrapper serviceDiscovery{mockParticipant}; + + void* capturedContext{nullptr}; + SilKit_Experimental_ServiceDiscoveryHandler_t capturedHandler{nullptr}; + EXPECT_CALL(capi, SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(mockServiceDiscovery, _, _)) + .Times(1) + .WillOnce(DoAll(SaveArg<1>(&capturedContext), SaveArg<2>(&capturedHandler), + Return(SilKit_ReturnCode_SUCCESS))); + + int firstCalls{0}; + int secondCalls{0}; + serviceDiscovery.SetServiceDiscoveryHandler( + [&](SD::ServiceDiscoveryEventType, const SD::ServiceDescriptor&) { ++firstCalls; }); + serviceDiscovery.SetServiceDiscoveryHandler( + [&](SD::ServiceDiscoveryEventType, const SD::ServiceDescriptor&) { ++secondCalls; }); + + ASSERT_NE(capturedHandler, nullptr); + + SilKit_Experimental_ServiceDescriptor cDescriptor; + SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, cDescriptor); + cDescriptor.participantName = "P"; + cDescriptor.serviceName = "S"; + cDescriptor.serviceKind = SilKit_Experimental_ServiceKind_CanController; + cDescriptor.primaryIdentifier = "CAN1"; + cDescriptor.mediaType = ""; + cDescriptor.labelList.numLabels = 0; + cDescriptor.labelList.labels = nullptr; + + capturedHandler(capturedContext, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, &cDescriptor); + + // Only the second (current) handler is invoked; the first was replaced, not left dangling. + EXPECT_EQ(firstCalls, 0); + EXPECT_EQ(secondCalls, 1); +} + +TEST_F(Test_HourglassServiceDiscovery, to_string_maps_enums) +{ + EXPECT_EQ(SD::to_string(SD::ServiceKind::DataSubscriber), "DataSubscriber"); + EXPECT_EQ(SD::to_string(SD::ServiceKind::RpcServer), "RpcServer"); + EXPECT_EQ(SD::to_string(SD::ServiceKind::Link), "Link"); + EXPECT_EQ(SD::to_string(SD::ServiceDiscoveryEventType::ServiceCreated), "ServiceCreated"); + EXPECT_EQ(SD::to_string(SD::ServiceDiscoveryEventType::ServiceRemoved), "ServiceRemoved"); +} + +} // namespace diff --git a/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp new file mode 100644 index 000000000..39731bcee --- /dev/null +++ b/SilKit/IntegrationTests/ITest_CapiServiceDiscovery.cpp @@ -0,0 +1,471 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +// Integration test for the experimental service-discovery C API +// (SilKit_Experimental_ServiceDiscovery_*), driven against real participants over an +// in-process registry. +// +// Design: bus controllers, publishers/subscribers and RPC clients/servers are reported as their own +// service kinds on ServiceCreated/ServiceRemoved. A confirmed pub/sub or RPC match is reported as a +// SilKit_Experimental_ServiceKind_Link ServiceCreated event whose participantName/serviceName name +// the receiving side (subscriber/server) and whose connectedParticipantName/connectedServiceName +// name the peer (publisher/client). A match teardown emits no Link removal: it is inferred from the +// ServiceRemoved of one of the endpoints. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "silkit/SilKit.hpp" +#include "silkit/capi/SilKit.h" +#include "silkit/config/IParticipantConfiguration.hpp" +#include "silkit/services/pubsub/all.hpp" +#include "silkit/services/rpc/all.hpp" +#include "silkit/vendor/CreateSilKitRegistry.hpp" + +#include "gtest/gtest.h" + +namespace { + +using namespace std::chrono_literals; +using SilKit::Services::PubSub::PubSubSpec; +using SilKit::Services::PubSub::DataMessageEvent; +using SilKit::Services::PubSub::IDataSubscriber; +using SilKit::Services::Rpc::RpcSpec; +using SilKit::Services::Rpc::RpcCallHandler; +using SilKit::Services::Rpc::RpcCallResultHandler; + +constexpr auto kWaitTimeout = 10s; + +// A single service-discovery event as observed through the C API (all borrowed C data copied +// out, since the descriptor and its pointers are only valid for the duration of the callback). +struct ObservedLabel +{ + std::string key; + std::string value; + SilKit_LabelKind kind; +}; + +struct ObservedEvent +{ + SilKit_Experimental_ServiceDiscoveryEvent_Type eventType; + SilKit_Experimental_ServiceKind serviceKind; + std::string participantName; + std::string serviceName; + std::string primaryIdentifier; + std::string mediaType; + std::vector labels; + std::string connectedParticipantName; + std::string connectedServiceName; +}; + +// Shared state between the C callback thread and the test thread. +struct ObserverContext +{ + std::mutex mutex; + std::condition_variable cv; + std::vector events; +}; + +// The C trampoline registered with SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler. +// Copies the descriptor into the context and wakes any waiter. +void SilKitCALL OnServiceDiscovery(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type eventType, + const SilKit_Experimental_ServiceDescriptor* descriptor) +{ + auto* ctx = static_cast(context); + + ObservedEvent event{}; + event.eventType = eventType; + event.serviceKind = descriptor->serviceKind; + event.participantName = descriptor->participantName; + event.serviceName = descriptor->serviceName; + event.primaryIdentifier = descriptor->primaryIdentifier; + event.mediaType = descriptor->mediaType; + for (size_t i = 0; i < descriptor->labelList.numLabels; ++i) + { + const auto& label = descriptor->labelList.labels[i]; + event.labels.push_back({label.key, label.value, label.kind}); + } + event.connectedParticipantName = + descriptor->connectedParticipantName ? descriptor->connectedParticipantName : ""; + event.connectedServiceName = descriptor->connectedServiceName ? descriptor->connectedServiceName : ""; + + { + std::lock_guard lock{ctx->mutex}; + ctx->events.push_back(std::move(event)); + } + ctx->cv.notify_all(); +} + +class ITest_CapiServiceDiscovery : public testing::Test +{ +protected: + void SetUp() override + { + _registry = SilKit::Vendor::Vector::CreateSilKitRegistry( + SilKit::Config::ParticipantConfigurationFromString("")); + _registryUri = _registry->StartListening("silkit://127.0.0.1:0"); + + // The observer is created through the C API so we get a genuine SilKit_Participant* to + // pass to the experimental discovery API (no casting between the C and C++ worlds). + SilKit_ParticipantConfiguration* config{nullptr}; + ASSERT_EQ(SilKit_ParticipantConfiguration_FromString(&config, ""), SilKit_ReturnCode_SUCCESS); + ASSERT_EQ(SilKit_Participant_Create(&_observer, config, "Observer", _registryUri.c_str()), + SilKit_ReturnCode_SUCCESS); + SilKit_ParticipantConfiguration_Destroy(config); + + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&_serviceDiscovery, _observer), + SilKit_ReturnCode_SUCCESS); + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(_serviceDiscovery, &_ctx, + &OnServiceDiscovery), + SilKit_ReturnCode_SUCCESS); + } + + void TearDown() override + { + // The service discovery observer is owned by the participant; destroying the participant + // tears it down. Participants created by the individual tests are destroyed at the end of + // the test body (before this runs). _ctx outlives all of them. + if (_observer != nullptr) + { + SilKit_Participant_Destroy(_observer); + _observer = nullptr; + } + _registry.reset(); + } + + auto CreateParticipant(const std::string& name) -> std::unique_ptr + { + return SilKit::CreateParticipant(SilKit::Config::ParticipantConfigurationFromString(""), name, + _registryUri); + } + + // Counts observed events matching kind + type + primaryIdentifier. Caller must hold _ctx.mutex. + auto CountUnlocked(SilKit_Experimental_ServiceKind kind, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const std::string& primaryIdentifier) const -> size_t + { + return static_cast(std::count_if(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == kind && e.eventType == type && e.primaryIdentifier == primaryIdentifier; + })); + } + + auto Count(SilKit_Experimental_ServiceKind kind, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const std::string& primaryIdentifier) -> size_t + { + std::lock_guard lock{_ctx.mutex}; + return CountUnlocked(kind, type, primaryIdentifier); + } + + template + auto WaitFor(Predicate predicate, std::chrono::milliseconds timeout = kWaitTimeout) -> bool + { + std::unique_lock lock{_ctx.mutex}; + return _ctx.cv.wait_for(lock, timeout, [&] { return predicate(); }); + } + + // Returns a copy of the first observed event matching kind + type + primaryIdentifier. + auto FindEvent(SilKit_Experimental_ServiceKind kind, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const std::string& primaryIdentifier) -> ObservedEvent + { + std::lock_guard lock{_ctx.mutex}; + const auto it = std::find_if(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == kind && e.eventType == type && e.primaryIdentifier == primaryIdentifier; + }); + return it == _ctx.events.end() ? ObservedEvent{} : *it; + } + + // Returns a copy of the first observed event matching the predicate (caller must NOT hold lock). + template + auto FindEventWhere(Predicate pred) -> ObservedEvent + { + std::lock_guard lock{_ctx.mutex}; + const auto it = std::find_if(_ctx.events.begin(), _ctx.events.end(), pred); + return it == _ctx.events.end() ? ObservedEvent{} : *it; + } + + std::unique_ptr _registry; + std::string _registryUri; + SilKit_Participant* _observer{nullptr}; + SilKit_Experimental_ServiceDiscovery* _serviceDiscovery{nullptr}; + ObserverContext _ctx; +}; + +// 1 publisher + 2 subscribers on the same topic. The observer must see the publisher and both +// subscribers with the right kind, primaryIdentifier (== topic), mediaType and labels. +TEST_F(ITest_CapiServiceDiscovery, observer_sees_publisher_and_both_subscribers) +{ + const std::string topic{"T"}; + const std::string mediaType{"M"}; + + PubSubSpec spec{topic, mediaType}; + spec.AddLabel("K", "V", SilKit::Services::MatchingLabel::Kind::Mandatory); + + auto publisher = CreateParticipant("Pub"); + auto subscriber1 = CreateParticipant("Sub1"); + auto subscriber2 = CreateParticipant("Sub2"); + + publisher->CreateDataPublisher("PubCtrl", spec, 1); + subscriber1->CreateDataSubscriber("SubCtrl1", spec, [](IDataSubscriber*, const DataMessageEvent&) {}); + subscriber2->CreateDataSubscriber("SubCtrl2", spec, [](IDataSubscriber*, const DataMessageEvent&) {}); + + ASSERT_TRUE(WaitFor([&] { + return CountUnlocked(SilKit_Experimental_ServiceKind_DataPublisher, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic) + >= 1 + && CountUnlocked(SilKit_Experimental_ServiceKind_DataSubscriber, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic) + >= 2; + })) << "observer did not see the publisher and both subscribers within the timeout"; + + EXPECT_EQ(Count(SilKit_Experimental_ServiceKind_DataPublisher, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic), + 1u); + EXPECT_EQ(Count(SilKit_Experimental_ServiceKind_DataSubscriber, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic), + 2u); + + // The publisher descriptor must carry the topic as primaryIdentifier, plus mediaType + labels. + const auto publisherEvent = FindEvent(SilKit_Experimental_ServiceKind_DataPublisher, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic); + EXPECT_EQ(publisherEvent.primaryIdentifier, topic); + EXPECT_EQ(publisherEvent.mediaType, mediaType); + ASSERT_EQ(publisherEvent.labels.size(), 1u); + EXPECT_EQ(publisherEvent.labels[0].key, "K"); + EXPECT_EQ(publisherEvent.labels[0].value, "V"); + EXPECT_EQ(publisherEvent.labels[0].kind, SilKit_LabelKind_Mandatory); +} + +// Ground truth (independent of the C API): the two subscribers really connect to the publisher. +// The publisher uses history=1 and publishes once, so each subscriber receives the sample upon +// connecting, regardless of ordering. +TEST_F(ITest_CapiServiceDiscovery, both_subscribers_receive_published_data) +{ + const std::string topic{"T"}; + const std::string mediaType{"M"}; + PubSubSpec spec{topic, mediaType}; + + struct ReceiveLatch + { + std::mutex mutex; + std::condition_variable cv; + int count{0}; + void Hit() + { + { + std::lock_guard lock{mutex}; + ++count; + } + cv.notify_all(); + } + auto Wait(int expected, std::chrono::milliseconds timeout) -> bool + { + std::unique_lock lock{mutex}; + return cv.wait_for(lock, timeout, [&] { return count >= expected; }); + } + } latch; + + auto publisher = CreateParticipant("Pub"); + auto* dataPublisher = publisher->CreateDataPublisher("PubCtrl", spec, 1); + dataPublisher->Publish(std::vector{42}); + + auto subscriber1 = CreateParticipant("Sub1"); + auto subscriber2 = CreateParticipant("Sub2"); + subscriber1->CreateDataSubscriber("SubCtrl1", spec, [&](IDataSubscriber*, const DataMessageEvent&) { latch.Hit(); }); + subscriber2->CreateDataSubscriber("SubCtrl2", spec, [&](IDataSubscriber*, const DataMessageEvent&) { latch.Hit(); }); + + EXPECT_TRUE(latch.Wait(2, kWaitTimeout)) << "both subscribers should receive the published sample"; +} + +// A subscriber with no matching publisher is reported immediately on creation (as its own kind, with +// no link). The sentinel ensures in-order delivery (see below). +// +// The barrier is deterministic: the unmatched subscriber and a sentinel publisher are created, in +// that order, on the SAME participant. All announcements from one participant reach the observer +// over a single in-order connection, so the subscriber's announcement is delivered no later than +// the sentinel's. Once the observer has seen the sentinel publisher, it has necessarily already +// processed the subscriber's announcement. +TEST_F(ITest_CapiServiceDiscovery, subscriber_visible_immediately_without_matching_publisher) +{ + const std::string lonelyTopic{"LonelyTopic"}; + const std::string sentinelTopic{"SentinelTopic"}; + const std::string mediaType{"M"}; + + auto participant = CreateParticipant("LonelyParticipant"); + + // Unmatched subscriber first, then the sentinel publisher (unrelated topic, so it never matches + // the subscriber) -- both on the same participant to get in-order delivery to the observer. + participant->CreateDataSubscriber("SubCtrl", PubSubSpec{lonelyTopic, mediaType}, + [](IDataSubscriber*, const DataMessageEvent&) {}); + participant->CreateDataPublisher("SentinelPub", PubSubSpec{sentinelTopic, mediaType}, 0); + + ASSERT_TRUE(WaitFor([&] { + return CountUnlocked(SilKit_Experimental_ServiceKind_DataPublisher, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, sentinelTopic) + >= 1; + })) << "observer never saw the sentinel publisher"; + + // Subscriber was announced before the sentinel; the observer must have seen it by now. + EXPECT_EQ(Count(SilKit_Experimental_ServiceKind_DataSubscriber, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, lonelyTopic), + 1u) + << "a subscriber must be reported immediately on creation, even without a matching publisher"; + + // And no link exists for that topic (no publisher matched). + EXPECT_EQ(Count(SilKit_Experimental_ServiceKind_Link, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, lonelyTopic), + 0u); +} + +// When a publisher matches a subscriber, a Link ServiceCreated event fires. When the publisher then +// leaves, no Link removal is emitted; instead the publisher's own ServiceRemoved is observed, from +// which the teardown of the link is inferred. +TEST_F(ITest_CapiServiceDiscovery, link_created_and_publisher_removal_observed) +{ + const std::string topic{"T"}; + const std::string mediaType{"M"}; + PubSubSpec spec{topic, mediaType}; + + auto publisher = CreateParticipant("Pub"); + auto subscriber = CreateParticipant("Sub"); + publisher->CreateDataPublisher("PubCtrl", spec, 1); + subscriber->CreateDataSubscriber("SubCtrl", spec, [](IDataSubscriber*, const DataMessageEvent&) {}); + + // Wait for the match to be reported as a Link before destroying the publisher. + ASSERT_TRUE(WaitFor([&] { + return CountUnlocked(SilKit_Experimental_ServiceKind_Link, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic) + >= 1; + })) << "observer never saw the pub/sub match reported as a Link"; + + publisher.reset(); + + ASSERT_TRUE(WaitFor([&] { + return CountUnlocked(SilKit_Experimental_ServiceKind_DataPublisher, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, topic) + >= 1; + })) << "observer should see the publisher's ServiceRemoved when it leaves"; +} + +// A pub/sub match Link must name both endpoints: participantName/serviceName the subscriber, and +// connectedParticipantName/connectedServiceName the publisher. This lets the observer build a +// confirmed connection graph from service discovery data alone. +TEST_F(ITest_CapiServiceDiscovery, pubsub_match_link_carries_peer_identity) +{ + const std::string topic{"Sensor"}; + const std::string mediaType{"application/octet-stream"}; + + auto publisher = CreateParticipant("PublisherParticipant"); + auto subscriber = CreateParticipant("SubscriberParticipant"); + publisher->CreateDataPublisher("PublisherController", PubSubSpec{topic, mediaType}, 0); + subscriber->CreateDataSubscriber("SubscriberController", PubSubSpec{topic, mediaType}, + [](IDataSubscriber*, const DataMessageEvent&) {}); + + ASSERT_TRUE(WaitFor([&] { + return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == topic; + }); + })) << "the pub/sub match Link never arrived"; + + const auto ev = FindEventWhere([&](const ObservedEvent& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == topic; + }); + EXPECT_EQ(ev.participantName, "SubscriberParticipant") << "Link must name the subscriber as the receiving side"; + EXPECT_EQ(ev.serviceName, "SubscriberController"); + EXPECT_EQ(ev.connectedParticipantName, "PublisherParticipant") << "Link must identify the publisher's participant"; + EXPECT_EQ(ev.connectedServiceName, "PublisherController") << "Link must identify the publisher's controller name"; +} + +// An RPC match Link must carry the matching client's participant name and controller name, letting +// the observer resolve concrete RPC call edges. +TEST_F(ITest_CapiServiceDiscovery, rpc_match_link_carries_peer_identity) +{ + const std::string functionName{"RemoteProc"}; + const std::string mediaType{"application/octet-stream"}; + RpcSpec spec{functionName, mediaType}; + + auto server = CreateParticipant("ServerParticipant"); + auto client = CreateParticipant("ClientParticipant"); + server->CreateRpcServer("ServerController", spec, RpcCallHandler{[](auto*, auto) {}}); + client->CreateRpcClient("ClientController", spec, RpcCallResultHandler{[](auto*, auto) {}}); + + ASSERT_TRUE(WaitFor([&] { + return std::any_of(_ctx.events.begin(), _ctx.events.end(), [&](const auto& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == functionName; + }); + })) << "the RPC match Link never arrived"; + + const auto ev = FindEventWhere([&](const ObservedEvent& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == functionName; + }); + EXPECT_EQ(ev.participantName, "ServerParticipant") << "Link must name the server as the receiving side"; + EXPECT_EQ(ev.serviceName, "ServerController"); + EXPECT_EQ(ev.connectedParticipantName, "ClientParticipant") << "Link must identify the RPC client's participant"; + EXPECT_EQ(ev.connectedServiceName, "ClientController") << "Link must identify the RPC client's controller name"; +} + +// Attaching an observer to an already-running simulation must recover the match, including the peer +// identity, of connections that existed before the observer registered. This is the replay-ordering +// case: the DataSubscriberInternal may be replayed before its parent/publisher. +TEST_F(ITest_CapiServiceDiscovery, late_observer_recovers_preexisting_link) +{ + const std::string topic{"LateTopic"}; + const std::string mediaType{"M"}; + PubSubSpec spec{topic, mediaType}; + + auto publisher = CreateParticipant("LatePub"); + auto subscriber = CreateParticipant("LateSub"); + publisher->CreateDataPublisher("LatePubCtrl", spec, 0); + subscriber->CreateDataSubscriber("LateSubCtrl", spec, [](IDataSubscriber*, const DataMessageEvent&) {}); + + // Ensure the match is established (observed via the SetUp observer) before attaching a new one. + ASSERT_TRUE(WaitFor([&] { + return CountUnlocked(SilKit_Experimental_ServiceKind_Link, + SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, topic) + >= 1; + })) << "the pub/sub link was never established"; + + // Attach a brand-new observer that must replay the already-running simulation. + SilKit_ParticipantConfiguration* config{nullptr}; + ASSERT_EQ(SilKit_ParticipantConfiguration_FromString(&config, ""), SilKit_ReturnCode_SUCCESS); + SilKit_Participant* lateObserver{nullptr}; + ASSERT_EQ(SilKit_Participant_Create(&lateObserver, config, "LateObserver", _registryUri.c_str()), + SilKit_ReturnCode_SUCCESS); + SilKit_ParticipantConfiguration_Destroy(config); + + ObserverContext lateCtx; + SilKit_Experimental_ServiceDiscovery* lateSd{nullptr}; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&lateSd, lateObserver), SilKit_ReturnCode_SUCCESS); + ASSERT_EQ( + SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(lateSd, &lateCtx, &OnServiceDiscovery), + SilKit_ReturnCode_SUCCESS); + + std::unique_lock lock{lateCtx.mutex}; + const bool recovered = lateCtx.cv.wait_for(lock, kWaitTimeout, [&] { + return std::any_of(lateCtx.events.begin(), lateCtx.events.end(), [&](const auto& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == topic && e.connectedParticipantName == "LatePub" + && e.connectedServiceName == "LatePubCtrl"; + }); + }); + lock.unlock(); + EXPECT_TRUE(recovered) << "late observer must recover the pre-existing link and its peer identity"; + + SilKit_Participant_Destroy(lateObserver); +} + +} // namespace diff --git a/SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp b/SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp new file mode 100644 index 000000000..c5fe2502a --- /dev/null +++ b/SilKit/IntegrationTests/ITest_NetSimServiceDiscovery.cpp @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +// Integration test: verify that the service-discovery C API reports a network simulator as a Link +// service (SilKit_Experimental_ServiceKind_Link) whose primaryIdentifier is the simulated network +// name and whose participantName is the simulating participant. A consumer joins that network name +// against a bus controller's primaryIdentifier to learn the controller is simulated -- without any +// dedicated cross-referencing being done inside the API. +// +// The test sets up the minimal scenario: one network simulator participant that takes over one CAN +// network ("CAN1"), one regular participant with a CAN controller on that network, and one +// autonomous service-discovery observer. + +#include +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" + +#include "ITestFixture.hpp" + +#include "silkit/SilKit.hpp" +#include "silkit/capi/SilKit.h" +#include "silkit/experimental/netsim/all.hpp" +#include "silkit/services/can/all.hpp" + +namespace { + +using namespace std::chrono_literals; +using namespace SilKit::Tests; +using namespace SilKit::Experimental::NetworkSimulation; +using namespace SilKit::Experimental::NetworkSimulation::Can; + +// --------------------------------------------------------------------------- +// Minimal simulated CAN network — no message routing needed for this test. +// --------------------------------------------------------------------------- + +class MinimalSimulatedCanController : public ISimulatedCanController +{ +public: + void OnSetControllerMode(const CanControllerMode&) override {} + void OnSetBaudrate(const CanConfigureBaudrate&) override {} + void OnFrameRequest(const CanFrameRequest&) override {} +}; + +class MinimalSimulatedCanNetwork : public ISimulatedNetwork +{ + std::unique_ptr _producer; + std::vector> _controllers; + +public: + void SetEventProducer(std::unique_ptr producer) override + { + _producer = std::move(producer); + } + + auto ProvideSimulatedController(ControllerDescriptor) -> ISimulatedController* override + { + _controllers.push_back(std::make_unique()); + return _controllers.back().get(); + } + + void SimulatedControllerRemoved(ControllerDescriptor) override {} +}; + +// --------------------------------------------------------------------------- +// Observer state — accumulated service-discovery events from the C API. +// --------------------------------------------------------------------------- + +struct DiscoveryEvent +{ + SilKit_Experimental_ServiceKind serviceKind{}; + SilKit_Experimental_ServiceDiscoveryEvent_Type eventType{}; + std::string participantName; + std::string primaryIdentifier; // networkName for bus controllers and network-simulator links +}; + +struct DiscoveryCtx +{ + std::mutex mutex; + std::condition_variable cv; + std::vector events; +}; + +void SilKitCALL OnNetSimDiscovery(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type eventType, + const SilKit_Experimental_ServiceDescriptor* d) +{ + auto* ctx = static_cast(context); + + DiscoveryEvent ev{}; + ev.serviceKind = d->serviceKind; + ev.eventType = eventType; + ev.participantName = d->participantName ? d->participantName : ""; + ev.primaryIdentifier = d->primaryIdentifier ? d->primaryIdentifier : ""; + + { + std::lock_guard lk{ctx->mutex}; + ctx->events.push_back(std::move(ev)); + } + ctx->cv.notify_all(); +} + +// --------------------------------------------------------------------------- +// Test fixture +// --------------------------------------------------------------------------- + +class ITest_NetSimServiceDiscovery : public ITest_SimTestHarness +{ +}; + +// A network simulator takes over the "CAN1" network. A regular participant creates a CAN controller +// on that network. The service-discovery observer must see a Link service whose primaryIdentifier is +// "CAN1" and whose participantName is the network simulator's participant name — demonstrating that +// the API surfaces a network simulator as a Link, joinable to bus controllers by network name. +TEST_F(ITest_NetSimServiceDiscovery, netsim_reported_as_link_service) +{ + const std::string netSimName = "NetworkSimulator"; + const std::string canPartName = "CanParticipant"; + const std::string networkName = "CAN1"; + + SetupFromParticipantList({netSimName, canPartName}); + + // Service-discovery observer: autonomous participant, NOT part of the sync group. + // Creates a service discovery handle and registers a handler before the simulation starts, + // so it sees all service announcements as participants join. + DiscoveryCtx ctx; + SilKit_ParticipantConfiguration* cfg{nullptr}; + ASSERT_EQ(SilKit_ParticipantConfiguration_FromString(&cfg, ""), SilKit_ReturnCode_SUCCESS); + SilKit_Participant* observer{nullptr}; + ASSERT_EQ(SilKit_Participant_Create(&observer, cfg, "Observer", _registryUri.c_str()), + SilKit_ReturnCode_SUCCESS); + SilKit_ParticipantConfiguration_Destroy(cfg); + + SilKit_Experimental_ServiceDiscovery* sd{nullptr}; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&sd, observer), SilKit_ReturnCode_SUCCESS); + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(sd, &ctx, &OnNetSimDiscovery), + SilKit_ReturnCode_SUCCESS); + + // Network simulator participant: claims the CAN network and stops the simulation after 5 ms. + { + auto* p = _simTestHarness->GetParticipant(netSimName); + auto* lc = p->GetOrCreateLifecycleService(); + auto* ts = p->GetOrCreateTimeSyncService(); + auto* netsim = p->GetOrCreateNetworkSimulator(); + + netsim->SimulateNetwork(networkName, SimulatedNetworkType::CAN, + std::make_unique()); + netsim->Start(); + + ts->SetSimulationStepHandler( + [lc](auto now, auto) { + if (now >= 5ms) + lc->Stop("done"); + }, 1ms); + } + + // Regular participant: creates one CAN controller on the simulated network. + { + auto* p = _simTestHarness->GetParticipant(canPartName); + auto* ts = p->GetOrCreateTimeSyncService(); + p->Participant()->CreateCanController("CanCtrl", networkName); + ts->SetSimulationStepHandler([](auto, auto) {}, 1ms); + } + + auto ok = _simTestHarness->Run(10s); + ASSERT_TRUE(ok) << "simulation should complete without timeout"; + + // The Link service for the network simulator fires during the simulation. It is accumulated in + // ctx.events by the observer's IO thread. Allow a brief settle window after Run() returns for + // any in-flight IO delivery. + { + std::unique_lock lk{ctx.mutex}; + const bool found = ctx.cv.wait_for(lk, 5s, [&] { + return std::any_of(ctx.events.begin(), ctx.events.end(), [&](const DiscoveryEvent& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_Link + && e.eventType == SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated + && e.primaryIdentifier == networkName && e.participantName == netSimName; + }); + }); + ASSERT_TRUE(found) + << "no Link service with the network name and the network simulator participant was observed"; + + // The CAN controller is reported as its own kind on the same network; joining on + // primaryIdentifier lets a consumer conclude the controller is simulated. + const bool sawController = std::any_of(ctx.events.begin(), ctx.events.end(), [&](const DiscoveryEvent& e) { + return e.serviceKind == SilKit_Experimental_ServiceKind_CanController + && e.primaryIdentifier == networkName; + }); + EXPECT_TRUE(sawController) << "the CAN controller on the simulated network was not observed"; + } + + SilKit_Participant_Destroy(observer); +} + +} // namespace diff --git a/SilKit/include/silkit/capi/Experimental.h b/SilKit/include/silkit/capi/Experimental.h new file mode 100644 index 000000000..b00cbf4f0 --- /dev/null +++ b/SilKit/include/silkit/capi/Experimental.h @@ -0,0 +1,171 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once +#include +#include "silkit/capi/SilKitMacros.h" +#include "silkit/capi/Types.h" +#include "silkit/capi/InterfaceIdentifiers.h" + +#pragma pack(push) +#pragma pack(8) + +SILKIT_BEGIN_DECLS + +// ============================================================================ +// Experimental service discovery +// +// Allows a participant to passively observe the user-facing services (bus +// controllers, publishers/subscribers, RPC clients/servers) created by all +// other participants in the simulation. This builds on the internal service +// discovery used throughout the SIL Kit and requires no configuration of the +// observed participants. +// +// \warning The functions and types declared in this header are not part of the +// stable API and ABI of the SIL Kit. They may be removed or changed at +// any time without prior notice. +// ============================================================================ + +/*! \brief The kind of change reported for a discovered service. */ +typedef uint32_t SilKit_Experimental_ServiceDiscoveryEvent_Type; +/*! \brief An invalid / unknown service discovery event. */ +#define SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid ((SilKit_Experimental_ServiceDiscoveryEvent_Type)0) +/*! \brief A service has been created (or was already present on registration). */ +#define SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated \ + ((SilKit_Experimental_ServiceDiscoveryEvent_Type)1) +/*! \brief A service has been removed. */ +#define SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved \ + ((SilKit_Experimental_ServiceDiscoveryEvent_Type)2) + +/*! \brief The kind of a discovered service. Only user-facing services are reported. */ +typedef uint32_t SilKit_Experimental_ServiceKind; +#define SilKit_Experimental_ServiceKind_Undefined ((SilKit_Experimental_ServiceKind)0) +#define SilKit_Experimental_ServiceKind_CanController ((SilKit_Experimental_ServiceKind)1) +#define SilKit_Experimental_ServiceKind_EthernetController ((SilKit_Experimental_ServiceKind)2) +#define SilKit_Experimental_ServiceKind_FlexrayController ((SilKit_Experimental_ServiceKind)3) +#define SilKit_Experimental_ServiceKind_LinController ((SilKit_Experimental_ServiceKind)4) +#define SilKit_Experimental_ServiceKind_DataPublisher ((SilKit_Experimental_ServiceKind)5) +#define SilKit_Experimental_ServiceKind_DataSubscriber ((SilKit_Experimental_ServiceKind)6) +#define SilKit_Experimental_ServiceKind_RpcClient ((SilKit_Experimental_ServiceKind)7) +#define SilKit_Experimental_ServiceKind_RpcServer ((SilKit_Experimental_ServiceKind)8) +/*! \brief A link between two services: a pub/sub or RPC match, or a network-simulator link. */ +#define SilKit_Experimental_ServiceKind_Link ((SilKit_Experimental_ServiceKind)9) + +/*! \brief Describes a single discovered service, passed by value to a service discovery handler. + * + * All pointer members are borrowed and only valid for the duration of the handler invocation. Copy + * any data that must outlive the call. + * + * A \ref SilKit_Experimental_ServiceKind_Link describes a link between two services: a pub/sub or + * RPC match, or a network-simulator link. For a pub/sub or RPC match \p participantName / + * \p serviceName name the receiving side (the DataSubscriber or RpcServer) and + * \p connectedParticipantName / \p connectedServiceName name the peer (the DataPublisher or + * RpcClient); \p primaryIdentifier is the topic or function name. For a network-simulator link + * \p participantName is the simulating participant, \p primaryIdentifier is the simulated network + * name (which matches the \p primaryIdentifier of the affected bus controllers), and the + * \p connected... fields are empty. + * + * Links are reported as \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated. + * Network-simulator links are additionally reported as + * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved when the simulator detaches. + * A pub/sub or RPC match link does not emit a removal event: its teardown always coincides with a + * \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved of one of its endpoints, from + * which the disappearance of the link can be inferred. + * + * The \p connectedParticipantName and \p connectedServiceName fields are empty strings for all + * kinds other than pub/sub and RPC \p SilKit_Experimental_ServiceKind_Link events. + */ +typedef struct +{ + SilKit_StructHeader structHeader; + //! Name of the participant providing the service. For a Link this is the receiving side + //! (subscriber/server) or, for a network-simulator link, the simulating participant. + const char* participantName; + //! Name of the service (the controller / publisher / subscriber / client / server name). + const char* serviceName; + //! The kind of service. + SilKit_Experimental_ServiceKind serviceKind; + //! The primary, user-facing identifier of the service: the network name for bus controllers and + //! network-simulator links, the topic for pub/sub, and the function name for RPC. Suitable as a + //! display / join key for visualization and tooling. + const char* primaryIdentifier; + //! Media type for pub/sub and RPC services; empty string when not applicable. + const char* mediaType; + //! Decoded matching labels for pub/sub and RPC services; empty for bus controllers. + SilKit_LabelList labelList; + //! Reserved for future system-level simulation detection. Currently always an empty string. + const char* simulationName; + //! Name of the peer participant; populated only in pub/sub and RPC Link events. + const char* connectedParticipantName; + //! Name of the peer service; populated only in pub/sub and RPC Link events. + const char* connectedServiceName; +} SilKit_Experimental_ServiceDescriptor; + +/*! \brief Handler invoked when a user-facing service is created, updated, or removed in the simulation. + * + * The \p serviceDescriptor and all of its pointer members are only valid for the duration of the + * handler invocation. Copy the data if it must outlive the call. + * + * \note Threading: this handler may be invoked on an internal SIL Kit worker thread or on an + * application thread that creates or destroys a service; the invoking thread is unspecified. + * Invocations are serialized (the handler is never called concurrently with itself). The + * handler must not block and must not call back into the participant that owns the observer, + * as doing so may deadlock. + * + * \param context The user context pointer passed to \ref SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler. + * \param eventType Whether the service was created, updated, or removed. + * \param serviceDescriptor The affected service. + */ +typedef void(SilKitFPTR* SilKit_Experimental_ServiceDiscoveryHandler_t)( + void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type eventType, + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor); + +/*! \brief Obtain the experimental service discovery observer of a participant. + * + * The returned object is owned by the participant and must not be destroyed by the caller; there is + * no corresponding destroy function. It refers to the participant's single service discovery, so + * repeated calls for the same participant yield the same observer handle. + * + * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without + * prior notice. + * + * \param outServiceDiscovery Pointer through which the service discovery observer is returned (out parameter). + * \param participant The participant instance for which the observer is obtained. + */ +SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_Create( + SilKit_Experimental_ServiceDiscovery** outServiceDiscovery, SilKit_Participant* participant); + +typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDiscovery_Create_t)( + SilKit_Experimental_ServiceDiscovery** outServiceDiscovery, SilKit_Participant* participant); + +/*! \brief Register a handler that is called for every user-facing service in the simulation. + * + * Upon registration the handler is immediately invoked once for every service that is already known, + * each reported as \ref SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated. It is + * subsequently invoked for every user-facing service created or removed. Infrastructure/internal + * services are not reported. Pub/sub and RPC matches as well as network-simulator links are reported + * as \ref SilKit_Experimental_ServiceKind_Link services (see \ref SilKit_Experimental_ServiceDescriptor). + * + * \note Each call registers an additional, independent handler; handlers cannot be removed and remain + * registered for the lifetime of the participant. To observe with a single handler, call this + * function once. See the handler typedef for the threading contract. + * + * \warning This function is not part of the stable API and ABI of the SIL Kit. It may be removed at any time without + * prior notice. + * + * \param serviceDiscovery The observer obtained via \ref SilKit_Experimental_ServiceDiscovery_Create. + * \param context The user context pointer made available to the handler. + * \param handler The handler to be called on service creation, update, and removal. + */ +SilKitAPI SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler( + SilKit_Experimental_ServiceDiscovery* serviceDiscovery, void* context, + SilKit_Experimental_ServiceDiscoveryHandler_t handler); + +typedef SilKit_ReturnCode(SilKitFPTR* SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler_t)( + SilKit_Experimental_ServiceDiscovery* serviceDiscovery, void* context, + SilKit_Experimental_ServiceDiscoveryHandler_t handler); + +SILKIT_END_DECLS + +#pragma pack(pop) diff --git a/SilKit/include/silkit/capi/InterfaceIdentifiers.h b/SilKit/include/silkit/capi/InterfaceIdentifiers.h index 7490bd666..ed62243fe 100644 --- a/SilKit/include/silkit/capi/InterfaceIdentifiers.h +++ b/SilKit/include/silkit/capi/InterfaceIdentifiers.h @@ -240,12 +240,14 @@ typedef struct #define SilKit_LifecycleConfiguration_DATATYPE_ID 2 #define SilKit_WorkflowConfiguration_DATATYPE_ID 3 #define SilKit_ParticipantConnectionInformation_DATATYPE_ID 4 +#define SilKit_Experimental_ServiceDescriptor_DATATYPE_ID 5 // Participant data type Versions #define SilKit_ParticipantStatus_VERSION 1 #define SilKit_LifecycleConfiguration_VERSION 1 #define SilKit_WorkflowConfiguration_VERSION 3 #define SilKit_ParticipantConnectionInformation_VERSION 1 +#define SilKit_Experimental_ServiceDescriptor_VERSION 1 // Participant public API IDs #define SilKit_ParticipantStatus_STRUCT_VERSION SK_ID_MAKE(Participant, SilKit_ParticipantStatus) @@ -253,6 +255,8 @@ typedef struct #define SilKit_WorkflowConfiguration_STRUCT_VERSION SK_ID_MAKE(Participant, SilKit_WorkflowConfiguration) #define SilKit_ParticipantConnectionInformation_STRUCT_VERSION \ SK_ID_MAKE(Participant, SilKit_ParticipantConnectionInformation) +#define SilKit_Experimental_ServiceDescriptor_STRUCT_VERSION \ + SK_ID_MAKE(Participant, SilKit_Experimental_ServiceDescriptor) // NetworkSimulator diff --git a/SilKit/include/silkit/capi/SilKit.h b/SilKit/include/silkit/capi/SilKit.h index cfafcfb34..00d82d442 100644 --- a/SilKit/include/silkit/capi/SilKit.h +++ b/SilKit/include/silkit/capi/SilKit.h @@ -20,6 +20,7 @@ #include "silkit/capi/Version.h" #include "silkit/capi/NetworkSimulator.h" #include "silkit/capi/EventProducer.h" +#include "silkit/capi/Experimental.h" SILKIT_BEGIN_DECLS diff --git a/SilKit/include/silkit/capi/Types.h b/SilKit/include/silkit/capi/Types.h index a3f50f78a..9afc391e0 100644 --- a/SilKit/include/silkit/capi/Types.h +++ b/SilKit/include/silkit/capi/Types.h @@ -24,6 +24,12 @@ typedef struct SilKit_Vendor_Vector_SilKitRegistry SilKit_Vendor_Vector_SilKitRe */ typedef struct SilKit_Experimental_SystemController SilKit_Experimental_SystemController; +/*! \brief Opaque type. Used in functions prefixed with SilKit_Experimental_ServiceDiscovery_.... + * + * \warning This type is not part of the stable API of the SIL Kit. It may be removed at any time without prior notice. + */ +typedef struct SilKit_Experimental_ServiceDiscovery SilKit_Experimental_ServiceDiscovery; + typedef int32_t SilKit_ReturnCode; #define SilKit_ReturnCode_SUCCESS ((SilKit_ReturnCode)0) diff --git a/SilKit/include/silkit/detail/impl/experimental/participant/ParticipantExtensions.ipp b/SilKit/include/silkit/detail/impl/experimental/participant/ParticipantExtensions.ipp index 60394257b..fb181aa17 100644 --- a/SilKit/include/silkit/detail/impl/experimental/participant/ParticipantExtensions.ipp +++ b/SilKit/include/silkit/detail/impl/experimental/participant/ParticipantExtensions.ipp @@ -8,9 +8,11 @@ #include "silkit/participant/IParticipant.hpp" #include "silkit/experimental/services/orchestration/ISystemController.hpp" #include "silkit/experimental/netsim/INetworkSimulator.hpp" +#include "silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp" #include "silkit/detail/impl/participant/Participant.hpp" #include "silkit/detail/impl/experimental/services/orchestration/SystemController.hpp" +#include "silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp" namespace SilKit { @@ -34,6 +36,14 @@ auto CreateNetworkSimulator(SilKit::IParticipant* cppIParticipant) return cppParticipant.ExperimentalCreateNetworkSimulator(); } +auto CreateServiceDiscovery(SilKit::IParticipant* cppIParticipant) + -> SilKit::Experimental::ServiceDiscovery::IServiceDiscovery* +{ + auto& cppParticipant = dynamic_cast(*cppIParticipant); + + return cppParticipant.ExperimentalCreateServiceDiscovery(); +} + } // namespace Participant } // namespace Experimental DETAIL_SILKIT_DETAIL_VN_NAMESPACE_CLOSE @@ -45,6 +55,7 @@ namespace Experimental { namespace Participant { using SilKit::DETAIL_SILKIT_DETAIL_NAMESPACE_NAME::Experimental::Participant::CreateSystemController; using SilKit::DETAIL_SILKIT_DETAIL_NAMESPACE_NAME::Experimental::Participant::CreateNetworkSimulator; +using SilKit::DETAIL_SILKIT_DETAIL_NAMESPACE_NAME::Experimental::Participant::CreateServiceDiscovery; } // namespace Participant } // namespace Experimental } // namespace SilKit diff --git a/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp b/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp new file mode 100644 index 000000000..65db933eb --- /dev/null +++ b/SilKit/include/silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +#include "silkit/capi/SilKit.h" + +#include "silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp" + +#include "silkit/detail/impl/ThrowOnError.hpp" +#include "silkit/detail/macros.hpp" + + +namespace SilKit { +DETAIL_SILKIT_DETAIL_VN_NAMESPACE_BEGIN +namespace Impl { +namespace Experimental { +namespace ServiceDiscovery { + +class ServiceDiscovery : public SilKit::Experimental::ServiceDiscovery::IServiceDiscovery +{ +public: + inline explicit ServiceDiscovery(SilKit_Participant* participant); + + inline ~ServiceDiscovery() override = default; + + inline void SetServiceDiscoveryHandler( + SilKit::Experimental::ServiceDiscovery::ServiceDiscoveryHandler handler) override; + +public: + inline auto Get() const -> SilKit_Experimental_ServiceDiscovery*; + +private: + SilKit_Experimental_ServiceDiscovery* _serviceDiscovery{nullptr}; + // The handler is stored in a slot whose address is passed to the C API as the callback context and + // must therefore remain stable for the lifetime of this object. Repeated calls to + // SetServiceDiscoveryHandler replace the stored std::function in place (never freeing the slot) and + // register the C trampoline exactly once, so the context can never dangle and events are not + // delivered twice. See SetServiceDiscoveryHandler below. + std::shared_ptr _handler; + bool _registered{false}; +}; + +} // namespace ServiceDiscovery +} // namespace Experimental +} // namespace Impl +DETAIL_SILKIT_DETAIL_VN_NAMESPACE_CLOSE +} // namespace SilKit + + +// ================================================================================ +// Inline Implementations +// ================================================================================ + +namespace SilKit { +DETAIL_SILKIT_DETAIL_VN_NAMESPACE_BEGIN +namespace Impl { +namespace Experimental { +namespace ServiceDiscovery { + +ServiceDiscovery::ServiceDiscovery(SilKit_Participant* participant) +{ + const auto returnCode = SilKit_Experimental_ServiceDiscovery_Create(&_serviceDiscovery, participant); + ThrowOnError(returnCode); +} + +void ServiceDiscovery::SetServiceDiscoveryHandler( + SilKit::Experimental::ServiceDiscovery::ServiceDiscoveryHandler handler) +{ + // Store the handler in a slot with a stable address (used as the C callback context). On repeated + // calls we swap the std::function in place instead of freeing and re-registering, which would leave + // the previously registered C trampoline pointing at a destroyed std::function (use-after-free). + if (_handler == nullptr) + { + _handler = std::make_shared(); + } + *_handler = std::move(handler); + + if (_registered) + { + // The trampoline reads the current value of *_handler on every invocation, so the replacement + // above already takes effect; registering again would duplicate event delivery. + return; + } + + const auto cHandler = [](void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type eventType, + const SilKit_Experimental_ServiceDescriptor* cServiceDescriptor) { + namespace SD = SilKit::Experimental::ServiceDiscovery; + + const auto orEmpty = [](const char* s) -> const char* { return s != nullptr ? s : ""; }; + + SD::ServiceDescriptor serviceDescriptor{}; + serviceDescriptor.participantName = orEmpty(cServiceDescriptor->participantName); + serviceDescriptor.serviceName = orEmpty(cServiceDescriptor->serviceName); + serviceDescriptor.serviceKind = static_cast(cServiceDescriptor->serviceKind); + serviceDescriptor.primaryIdentifier = orEmpty(cServiceDescriptor->primaryIdentifier); + serviceDescriptor.mediaType = orEmpty(cServiceDescriptor->mediaType); + serviceDescriptor.labels.reserve(cServiceDescriptor->labelList.numLabels); + for (size_t i = 0; i < cServiceDescriptor->labelList.numLabels; ++i) + { + const auto& cLabel = cServiceDescriptor->labelList.labels[i]; + SilKit::Services::MatchingLabel label; + label.key = orEmpty(cLabel.key); + label.value = orEmpty(cLabel.value); + label.kind = static_cast(cLabel.kind); + serviceDescriptor.labels.emplace_back(std::move(label)); + } + serviceDescriptor.simulationName = orEmpty(cServiceDescriptor->simulationName); + serviceDescriptor.connectedParticipantName = orEmpty(cServiceDescriptor->connectedParticipantName); + serviceDescriptor.connectedServiceName = orEmpty(cServiceDescriptor->connectedServiceName); + + auto& userHandler = *static_cast(context); + if (userHandler) + { + userHandler(static_cast(eventType), serviceDescriptor); + } + }; + + const auto returnCode = + SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(_serviceDiscovery, _handler.get(), cHandler); + ThrowOnError(returnCode); + + _registered = true; +} + +auto ServiceDiscovery::Get() const -> SilKit_Experimental_ServiceDiscovery* +{ + return _serviceDiscovery; +} + +} // namespace ServiceDiscovery +} // namespace Experimental +} // namespace Impl +DETAIL_SILKIT_DETAIL_VN_NAMESPACE_CLOSE +} // namespace SilKit diff --git a/SilKit/include/silkit/detail/impl/participant/Participant.hpp b/SilKit/include/silkit/detail/impl/participant/Participant.hpp index a3ebada3c..8bea2b6af 100644 --- a/SilKit/include/silkit/detail/impl/participant/Participant.hpp +++ b/SilKit/include/silkit/detail/impl/participant/Participant.hpp @@ -33,6 +33,7 @@ #include "silkit/detail/impl/services/rpc/RpcServer.hpp" #include "silkit/detail/impl/experimental/services/orchestration/SystemController.hpp" +#include "silkit/detail/impl/experimental/serviceDiscovery/ServiceDiscovery.hpp" #include "silkit/detail/impl/netsim/NetworkSimulator.hpp" @@ -88,6 +89,9 @@ class Participant : public SilKit::IParticipant inline auto ExperimentalCreateSystemController() -> SilKit::Experimental::Services::Orchestration::ISystemController*; + inline auto ExperimentalCreateServiceDiscovery() + -> SilKit::Experimental::ServiceDiscovery::IServiceDiscovery*; + public: inline auto Get() const -> SilKit_Participant*; @@ -116,6 +120,8 @@ class Participant : public SilKit::IParticipant ControllerStorage _rpcClients; ControllerStorage _rpcServers; + ControllerStorage _serviceDiscoveries; + std::unique_ptr _lifecycleService; std::unique_ptr _systemMonitor; @@ -239,6 +245,11 @@ auto Participant::ExperimentalCreateNetworkSimulator() -> SilKit::Experimental:: return _networkSimulator.get(); } +auto Participant::ExperimentalCreateServiceDiscovery() -> SilKit::Experimental::ServiceDiscovery::IServiceDiscovery* +{ + return _serviceDiscoveries.Create(_participant); +} + auto Participant::Get() const -> SilKit_Participant* { return _participant; diff --git a/SilKit/include/silkit/experimental/participant/ParticipantExtensions.hpp b/SilKit/include/silkit/experimental/participant/ParticipantExtensions.hpp index f048eadab..fd695df32 100644 --- a/SilKit/include/silkit/experimental/participant/ParticipantExtensions.hpp +++ b/SilKit/include/silkit/experimental/participant/ParticipantExtensions.hpp @@ -7,6 +7,7 @@ #include "silkit/SilKitMacros.hpp" #include "silkit/participant/IParticipant.hpp" #include "silkit/experimental/services/orchestration/ISystemController.hpp" +#include "silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp" #include "silkit/detail/macros.hpp" @@ -28,6 +29,15 @@ DETAIL_SILKIT_CPP_API auto CreateSystemController(SilKit::IParticipant* particip DETAIL_SILKIT_CPP_API auto CreateNetworkSimulator(SilKit::IParticipant* participant) -> SilKit::Experimental::NetworkSimulation::INetworkSimulator*; +/*! \brief Return the experimental service-discovery observer at a given SIL Kit participant. +* +* \param participant The participant instance for which the observer is created. +* +* \throw SilKit::SilKitError The participant is invalid. +*/ +DETAIL_SILKIT_CPP_API auto CreateServiceDiscovery(SilKit::IParticipant* participant) + -> SilKit::Experimental::ServiceDiscovery::IServiceDiscovery*; + } // namespace Participant } // namespace Experimental diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp new file mode 100644 index 000000000..d08563f71 --- /dev/null +++ b/SilKit/include/silkit/experimental/serviceDiscovery/IServiceDiscovery.hpp @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp" + +namespace SilKit { +namespace Experimental { +namespace ServiceDiscovery { + +/*! \brief Passively observes the user-facing services (bus controllers, publishers/subscribers, + * RPC clients/servers, network links) created by all participants in the simulation. + * + * \warning This interface is experimental and not part of the stable API and ABI of the SIL Kit. It + * may be removed or changed at any time without prior notice. + */ +class IServiceDiscovery +{ +public: + virtual ~IServiceDiscovery() = default; + + /*! \brief Register the handler that is called for every user-facing service in the simulation. + * + * Upon registration the handler is immediately invoked once for every service that is already + * known, each reported as \ref ServiceDiscoveryEventType::ServiceCreated. It is subsequently + * invoked for every user-facing service created, updated, or removed. Internal / infrastructure + * services are not reported. + * + * Calling this method again replaces the handler; the previously set handler is no longer + * invoked. The replacement does not re-deliver the initial snapshot of already-known services. + * + * \note Threading: the handler may be invoked on an internal SIL Kit worker thread or on an + * application thread that creates or destroys a service; the invoking thread is + * unspecified. Invocations are serialized (never concurrent). The handler must not block + * and must not call back into the participant (doing so may deadlock). Copy any data that + * must outlive the call. + * + * \param handler The handler to be called on service creation, update, and removal. + */ + virtual void SetServiceDiscoveryHandler(ServiceDiscoveryHandler handler) = 0; +}; + +} // namespace ServiceDiscovery +} // namespace Experimental +} // namespace SilKit diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp new file mode 100644 index 000000000..2469382f9 --- /dev/null +++ b/SilKit/include/silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +#include "silkit/capi/Experimental.h" +#include "silkit/services/datatypes.hpp" + +namespace SilKit { +namespace Experimental { +namespace ServiceDiscovery { + +//! \brief The kind of change reported for a discovered service. +enum class ServiceDiscoveryEventType : SilKit_Experimental_ServiceDiscoveryEvent_Type +{ + //! An invalid / unknown service discovery event. + Invalid = SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid, + //! A service has been created (or was already present on registration). + ServiceCreated = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, + //! A service has been removed. + ServiceRemoved = SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved, +}; + +//! \brief The kind of a discovered service. Only user-facing services are reported. +enum class ServiceKind : SilKit_Experimental_ServiceKind +{ + Undefined = SilKit_Experimental_ServiceKind_Undefined, + CanController = SilKit_Experimental_ServiceKind_CanController, + EthernetController = SilKit_Experimental_ServiceKind_EthernetController, + FlexrayController = SilKit_Experimental_ServiceKind_FlexrayController, + LinController = SilKit_Experimental_ServiceKind_LinController, + DataPublisher = SilKit_Experimental_ServiceKind_DataPublisher, + DataSubscriber = SilKit_Experimental_ServiceKind_DataSubscriber, + RpcClient = SilKit_Experimental_ServiceKind_RpcClient, + RpcServer = SilKit_Experimental_ServiceKind_RpcServer, + //! A link between two services: a pub/sub or RPC match, or a network-simulator link. + Link = SilKit_Experimental_ServiceKind_Link, +}; + +//! \brief Describes a single discovered service, passed to a \ref ServiceDiscoveryHandler. +//! +//! A \ref ServiceKind::Link describes a link between two services: a pub/sub or RPC match, or a +//! network-simulator link. For a pub/sub or RPC match \p participantName / \p serviceName name the +//! receiving side (the DataSubscriber or RpcServer) and \p connectedParticipantName / +//! \p connectedServiceName name the peer (the DataPublisher or RpcClient). For a network-simulator +//! link \p participantName is the simulating participant, \p primaryIdentifier is the simulated +//! network name (matching the affected bus controllers' \p primaryIdentifier), and the +//! \p connected... fields are empty. +struct ServiceDescriptor +{ + //! Name of the participant providing the service. + std::string participantName; + //! Name of the service (the controller / publisher / subscriber / client / server name). + std::string serviceName; + //! The kind of service. + ServiceKind serviceKind{ServiceKind::Undefined}; + //! The primary, user-facing identifier of the service: the network name for bus controllers and + //! network-simulator links, the topic for pub/sub, and the function name for RPC. + std::string primaryIdentifier; + //! Media type for pub/sub and RPC services; empty string when not applicable. + std::string mediaType; + //! Decoded matching labels for pub/sub and RPC services; empty for bus controllers. + std::vector labels; + //! The simulation name this service belongs to. Empty string when not available. + std::string simulationName; + //! Name of the peer participant; populated only in pub/sub and RPC Link events. + std::string connectedParticipantName; + //! Name of the peer service; populated only in pub/sub and RPC Link events. + std::string connectedServiceName; +}; + +/*! \brief Handler invoked when a user-facing service is created or removed in the simulation. + * + * \param eventType Whether the service was created or removed. + * \param serviceDescriptor The affected service. + */ +using ServiceDiscoveryHandler = + std::function; + +} // namespace ServiceDiscovery +} // namespace Experimental +} // namespace SilKit diff --git a/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp b/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp new file mode 100644 index 000000000..2bf66ba58 --- /dev/null +++ b/SilKit/include/silkit/experimental/serviceDiscovery/string_utils.hpp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +#include "silkit/experimental/serviceDiscovery/ServiceDiscoveryDatatypes.hpp" + +namespace SilKit { +namespace Experimental { +namespace ServiceDiscovery { + +inline std::string to_string(ServiceKind serviceKind); +inline std::string to_string(ServiceDiscoveryEventType eventType); + +inline std::ostream& operator<<(std::ostream& out, ServiceKind serviceKind); +inline std::ostream& operator<<(std::ostream& out, ServiceDiscoveryEventType eventType); + +// ================================================================================ +// Inline Implementations +// ================================================================================ + +std::string to_string(ServiceKind serviceKind) +{ + switch (serviceKind) + { + case ServiceKind::Undefined: + return "Undefined"; + case ServiceKind::CanController: + return "CanController"; + case ServiceKind::EthernetController: + return "EthernetController"; + case ServiceKind::FlexrayController: + return "FlexrayController"; + case ServiceKind::LinController: + return "LinController"; + case ServiceKind::DataPublisher: + return "DataPublisher"; + case ServiceKind::DataSubscriber: + return "DataSubscriber"; + case ServiceKind::RpcClient: + return "RpcClient"; + case ServiceKind::RpcServer: + return "RpcServer"; + case ServiceKind::Link: + return "Link"; + } + + std::stringstream out; + out << "ServiceKind(" << static_cast(serviceKind) << ")"; + return out.str(); +} + +std::string to_string(ServiceDiscoveryEventType eventType) +{ + switch (eventType) + { + case ServiceDiscoveryEventType::Invalid: + return "Invalid"; + case ServiceDiscoveryEventType::ServiceCreated: + return "ServiceCreated"; + case ServiceDiscoveryEventType::ServiceRemoved: + return "ServiceRemoved"; + } + + std::stringstream out; + out << "ServiceDiscoveryEventType(" << static_cast(eventType) << ")"; + return out.str(); +} + +std::ostream& operator<<(std::ostream& out, ServiceKind serviceKind) +{ + return out << to_string(serviceKind); +} + +std::ostream& operator<<(std::ostream& out, ServiceDiscoveryEventType eventType) +{ + return out << to_string(eventType); +} + +} // namespace ServiceDiscovery +} // namespace Experimental +} // namespace SilKit diff --git a/SilKit/source/capi/CMakeLists.txt b/SilKit/source/capi/CMakeLists.txt index 59e74758d..8daf12ed6 100644 --- a/SilKit/source/capi/CMakeLists.txt +++ b/SilKit/source/capi/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(O_SilKit_Capi OBJECT CapiDataPubSub.cpp CapiEthernet.cpp CapiExceptions.hpp + CapiExperimental.cpp CapiFlexray.cpp CapiImpl.hpp CapiLin.cpp @@ -18,6 +19,8 @@ add_library(O_SilKit_Capi OBJECT CapiVendor.cpp CapiVersion.cpp CapiNetworkSimulator.cpp + ServiceObserver.cpp + ServiceObserver.hpp TypeConversion.hpp ) @@ -30,6 +33,8 @@ add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiCan.cpp LIBS O_Si add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiEthernet.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiFlexray.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiData.cpp LIBS S_SilKitImpl I_SilKit) +add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiServiceDiscovery.cpp LIBS S_SilKitImpl I_SilKit) +add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_ServiceObserver.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiRpc.cpp LIBS S_SilKitImpl I_SilKit) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiLogger.cpp LIBS S_SilKitImpl) add_silkit_test_to_executable(SilKitUnitTests SOURCES Test_CapiVersion.cpp LIBS S_SilKitImpl) diff --git a/SilKit/source/capi/CapiExperimental.cpp b/SilKit/source/capi/CapiExperimental.cpp new file mode 100644 index 000000000..f44c3a2ee --- /dev/null +++ b/SilKit/source/capi/CapiExperimental.cpp @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "silkit/capi/SilKit.h" +#include "silkit/SilKit.hpp" +#include "silkit/participant/exception.hpp" + +#include "capi/CapiImpl.hpp" +#include "capi/ServiceObserver.hpp" + +#include "core/internal/IParticipantInternal.hpp" +#include "core/service/IServiceDiscovery.hpp" +#include "core/internal/ServiceDescriptor.hpp" + +#include + +namespace { + +namespace Discovery = SilKit::Core::Discovery; + +auto GetServiceDiscovery(SilKit_Participant* participant) -> Discovery::IServiceDiscovery* +{ + auto* cppParticipant = reinterpret_cast(participant); + auto* participantInternal = dynamic_cast(cppParticipant); + if (participantInternal == nullptr) + { + throw SilKit::SilKitError{"participant is not a valid SilKit::IParticipant*"}; + } + return participantInternal->GetServiceDiscovery(); +} + +} // namespace + + +SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_Create( + SilKit_Experimental_ServiceDiscovery** outServiceDiscovery, SilKit_Participant* participant) +try +{ + ASSERT_VALID_OUT_PARAMETER(outServiceDiscovery); + ASSERT_VALID_POINTER_PARAMETER(participant); + + auto* serviceDiscovery = GetServiceDiscovery(participant); + *outServiceDiscovery = reinterpret_cast(serviceDiscovery); + + return SilKit_ReturnCode_SUCCESS; +} +CAPI_CATCH_EXCEPTIONS + + +SilKit_ReturnCode SilKitCALL SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler( + SilKit_Experimental_ServiceDiscovery* serviceDiscovery, void* context, + SilKit_Experimental_ServiceDiscoveryHandler_t handler) +try +{ + ASSERT_VALID_POINTER_PARAMETER(serviceDiscovery); + ASSERT_VALID_HANDLER_PARAMETER(handler); + + auto* cppServiceDiscovery = reinterpret_cast(serviceDiscovery); + + auto observer = std::make_shared(handler, context); + + cppServiceDiscovery->RegisterServiceDiscoveryHandler( + [observer](Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& serviceDescriptor) { + // Invoked by the internal service discovery with its lock held, serialized, on an unspecified + // thread (an IO worker for remote events, or the caller's thread for locally created/removed + // services). Exceptions must never propagate into SIL Kit internals. + try + { + observer->HandleEvent(type, serviceDescriptor); + } + catch (...) + { + } + }); + + return SilKit_ReturnCode_SUCCESS; +} +CAPI_CATCH_EXCEPTIONS diff --git a/SilKit/source/capi/ServiceObserver.cpp b/SilKit/source/capi/ServiceObserver.cpp new file mode 100644 index 000000000..90a8d9cd7 --- /dev/null +++ b/SilKit/source/capi/ServiceObserver.cpp @@ -0,0 +1,424 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include "capi/ServiceObserver.hpp" + +#include "silkit/services/datatypes.hpp" + +#include "core/internal/ServiceConfigKeys.hpp" + +#include "config/YamlParser.hpp" + +#include + +namespace { + +namespace Discovery = SilKit::Core::Discovery; + +auto ToC(Discovery::ServiceDiscoveryEvent::Type type) -> SilKit_Experimental_ServiceDiscoveryEvent_Type +{ + using Type = Discovery::ServiceDiscoveryEvent::Type; + switch (type) + { + case Type::ServiceCreated: + return SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated; + case Type::ServiceRemoved: + return SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved; + default: + return SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid; + } +} + +// Owns the backing storage for the label list so that the borrowed c-string pointers in the +// SilKit_Experimental_ServiceDescriptor remain valid for the duration of the handler invocation. +struct LabelStorage +{ + std::vector labels; + std::vector cLabels; +}; + +// Maps the internal ServiceDescriptor to the public struct. Returns false if the service is not user-facing +// (infrastructure / internal endpoints), in which case the handler must not be invoked. The connectedParticipantName +// and connectedServiceName fields are initialised to empty strings here; the link path sets them (and overrides +// serviceKind to Link) after this function returns. +auto ClassifyAndFill(const SilKit::Core::ServiceDescriptor& serviceDescriptor, + SilKit_Experimental_ServiceDescriptor& out, LabelStorage& storage) -> bool +{ + const auto& supplementalData = serviceDescriptor.GetSupplementalDataRef(); + + const auto findValue = [&supplementalData](const std::string& key) -> const char* { + const auto it = supplementalData.find(key); + return it == supplementalData.end() ? nullptr : it->second.c_str(); + }; + + SilKit_Struct_Init(SilKit_Experimental_ServiceDescriptor, out); + out.participantName = serviceDescriptor.GetParticipantName().c_str(); + out.serviceName = serviceDescriptor.GetServiceName().c_str(); + out.primaryIdentifier = serviceDescriptor.GetNetworkName().c_str(); + out.mediaType = ""; + out.simulationName = serviceDescriptor.GetSimulationName().c_str(); + out.connectedParticipantName = ""; + out.connectedServiceName = ""; + + // Network-simulator links are user-facing: they are reported as a Link service whose primaryIdentifier is + // the simulated network name (matching the affected bus controllers' primaryIdentifier). + if (serviceDescriptor.GetServiceType() == SilKit::Core::ServiceType::Link) + { + out.serviceKind = SilKit_Experimental_ServiceKind_Link; + return true; + } + + std::string controllerType; + if (!serviceDescriptor.GetSupplementalDataItem(Discovery::controllerType, controllerType)) + { + return false; + } + + // Decode the YAML-encoded matching labels into the public label list. A malformed value must not propagate + // an exception: the service is still reported, just without labels. + const auto decodeLabels = [&](const std::string& labelsKey) { + const char* labelsStr = findValue(labelsKey); + if (labelsStr == nullptr || *labelsStr == '\0') + { + return; + } + try + { + storage.labels = SilKit::Config::Deserialize>(labelsStr); + } + catch (...) + { + return; + } + storage.cLabels.reserve(storage.labels.size()); + for (const auto& label : storage.labels) + { + SilKit_Label cLabel; + cLabel.key = label.key.c_str(); + cLabel.value = label.value.c_str(); + cLabel.kind = static_cast(label.kind); + storage.cLabels.push_back(cLabel); + } + out.labelList.numLabels = storage.cLabels.size(); + out.labelList.labels = storage.cLabels.data(); + }; + + if (controllerType == Discovery::controllerTypeCan) + { + out.serviceKind = SilKit_Experimental_ServiceKind_CanController; + } + else if (controllerType == Discovery::controllerTypeEthernet) + { + out.serviceKind = SilKit_Experimental_ServiceKind_EthernetController; + } + else if (controllerType == Discovery::controllerTypeFlexray) + { + out.serviceKind = SilKit_Experimental_ServiceKind_FlexrayController; + } + else if (controllerType == Discovery::controllerTypeLin) + { + out.serviceKind = SilKit_Experimental_ServiceKind_LinController; + } + else if (controllerType == Discovery::controllerTypeDataPublisher) + { + out.serviceKind = SilKit_Experimental_ServiceKind_DataPublisher; + if (const char* topic = findValue(Discovery::supplKeyDataPublisherTopic)) + { + out.primaryIdentifier = topic; + } + if (const char* mediaType = findValue(Discovery::supplKeyDataPublisherMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyDataPublisherPubLabels); + } + else if (controllerType == Discovery::controllerTypeDataSubscriber) + { + out.serviceKind = SilKit_Experimental_ServiceKind_DataSubscriber; + if (const char* topic = findValue(Discovery::supplKeyDataSubscriberTopic)) + { + out.primaryIdentifier = topic; + } + if (const char* mediaType = findValue(Discovery::supplKeyDataSubscriberMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyDataSubscriberSubLabels); + } + else if (controllerType == Discovery::controllerTypeRpcClient) + { + out.serviceKind = SilKit_Experimental_ServiceKind_RpcClient; + if (const char* functionName = findValue(Discovery::supplKeyRpcClientFunctionName)) + { + out.primaryIdentifier = functionName; + } + if (const char* mediaType = findValue(Discovery::supplKeyRpcClientMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyRpcClientLabels); + } + else if (controllerType == Discovery::controllerTypeRpcServer) + { + out.serviceKind = SilKit_Experimental_ServiceKind_RpcServer; + if (const char* functionName = findValue(Discovery::supplKeyRpcServerFunctionName)) + { + out.primaryIdentifier = functionName; + } + if (const char* mediaType = findValue(Discovery::supplKeyRpcServerMediaType)) + { + out.mediaType = mediaType; + } + decodeLabels(Discovery::supplKeyRpcServerLabels); + } + else + { + // Infrastructure / internal controllers (ServiceDiscovery, SystemMonitor, lifecycle, metrics, + // DataSubscriberInternal, RpcServerInternal, ...) are not user-facing and are not reported. + return false; + } + + return true; +} + +auto TryParseEndpointId(const std::string& text, SilKit::Core::EndpointId& out) -> bool +{ + try + { + out = static_cast(std::stoull(text)); + return true; + } + catch (...) + { + return false; + } +} + +} // namespace + + +namespace VSilKit { + +ServiceObserver::ServiceObserver(SilKit_Experimental_ServiceDiscoveryHandler_t handler, void* context) + : _handler{handler} + , _context{context} +{ +} + +auto ServiceObserver::MakeLink(const SilKit::Core::ServiceDescriptor& parent, + const SilKit::Core::ServiceDescriptor& peer) -> LinkEmission +{ + return LinkEmission{parent, peer.GetParticipantName(), peer.GetServiceName()}; +} + +void ServiceObserver::EmitService(SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const SilKit::Core::ServiceDescriptor& descriptor) +{ + SilKit_Experimental_ServiceDescriptor out{}; + LabelStorage storage; + if (!ClassifyAndFill(descriptor, out, storage)) + { + return; + } + _handler(_context, type, &out); +} + +void ServiceObserver::EmitLink(const LinkEmission& emission) +{ + SilKit_Experimental_ServiceDescriptor out{}; + LabelStorage storage; + if (!ClassifyAndFill(emission.parentDescriptor, out, storage)) + { + return; + } + out.serviceKind = SilKit_Experimental_ServiceKind_Link; + out.connectedParticipantName = emission.connectedParticipantName.c_str(); + out.connectedServiceName = emission.connectedServiceName.c_str(); + _handler(_context, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated, &out); +} + +void ServiceObserver::DrainResolvablePending(std::vector& emissions) +{ + auto it = _pending.begin(); + while (it != _pending.end()) + { + const auto parentIt = _parents.find(it->parentKey); + const auto peerIt = _peersByUuid.find(it->peerUuid); + if (parentIt != _parents.end() && peerIt != _peersByUuid.end()) + { + emissions.push_back(MakeLink(parentIt->second, peerIt->second)); + it = _pending.erase(it); + } + else + { + ++it; + } + } +} + +// An internal-match endpoint (DataSubscriberInternal / RpcServerInternal) appeared or disappeared. On creation it is +// turned into a Link event once its parent (subscriber/server) and peer (publisher/client) are both known; if either +// is still missing it is remembered in the pending list. No Link removal event is emitted: a match teardown always +// coincides with a ServiceRemoved of one of its endpoints, from which the link's disappearance is inferred. +void ServiceObserver::HandleInternalMatch(Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor, + const std::string& parentIdKey, const std::string& peerUuid) +{ + using EventType = Discovery::ServiceDiscoveryEvent::Type; + + std::string parentIdStr; + if (!descriptor.GetSupplementalDataItem(parentIdKey, parentIdStr)) + { + return; + } + SilKit::Core::EndpointId parentServiceId{0}; + if (!TryParseEndpointId(parentIdStr, parentServiceId)) + { + return; + } + const ServiceKey parentKey{descriptor.GetParticipantName(), parentServiceId}; + + std::vector emissions; + { + std::lock_guard lock{_mutex}; + if (type == EventType::ServiceCreated) + { + const auto parentIt = _parents.find(parentKey); + const auto peerIt = _peersByUuid.find(peerUuid); + if (parentIt != _parents.end() && peerIt != _peersByUuid.end()) + { + emissions.push_back(MakeLink(parentIt->second, peerIt->second)); + } + else + { + _pending.push_back(PendingMatch{parentKey, peerUuid}); + } + } + else if (type == EventType::ServiceRemoved) + { + _pending.erase(std::remove_if(_pending.begin(), _pending.end(), + [&](const PendingMatch& p) { + return p.parentKey == parentKey && p.peerUuid == peerUuid; + }), + _pending.end()); + } + } + for (const auto& e : emissions) + { + EmitLink(e); + } +} + +// A peer (DataPublisher / RpcClient) appeared or disappeared. Its identity (keyed by its networkName / UUID) enables +// resolving matches. The peer's own ServiceCreated/ServiceRemoved is always emitted. +void ServiceObserver::HandlePeer(Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor) +{ + using EventType = Discovery::ServiceDiscoveryEvent::Type; + + const std::string peerUuid = descriptor.GetNetworkName(); + std::vector emissions; + { + std::lock_guard lock{_mutex}; + if (type == EventType::ServiceCreated) + { + _peersByUuid[peerUuid] = descriptor; + DrainResolvablePending(emissions); + } + else if (type == EventType::ServiceRemoved) + { + _peersByUuid.erase(peerUuid); + _pending.erase(std::remove_if(_pending.begin(), _pending.end(), + [&](const PendingMatch& p) { return p.peerUuid == peerUuid; }), + _pending.end()); + } + } + EmitService(ToC(type), descriptor); + for (const auto& e : emissions) + { + EmitLink(e); + } +} + +// A parent (DataSubscriber / RpcServer) appeared or disappeared. Its identity (keyed by participant + serviceId) +// enables resolving matches. The parent's own ServiceCreated/ServiceRemoved is always emitted. +void ServiceObserver::HandleParent(Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor) +{ + using EventType = Discovery::ServiceDiscoveryEvent::Type; + + const ServiceKey key{descriptor.GetParticipantName(), descriptor.GetServiceId()}; + std::vector emissions; + { + std::lock_guard lock{_mutex}; + if (type == EventType::ServiceCreated) + { + _parents[key] = descriptor; + DrainResolvablePending(emissions); + } + else if (type == EventType::ServiceRemoved) + { + _parents.erase(key); + _pending.erase(std::remove_if(_pending.begin(), _pending.end(), + [&](const PendingMatch& p) { return p.parentKey == key; }), + _pending.end()); + } + } + EmitService(ToC(type), descriptor); + for (const auto& e : emissions) + { + EmitLink(e); + } +} + +void ServiceObserver::HandleEvent(Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor) +{ + // Network-simulator links are reported directly as a Link service (created and removed: a detaching simulator + // leaves its controllers alive, so its removal is not otherwise observable). + if (descriptor.GetServiceType() == SilKit::Core::ServiceType::Link) + { + EmitService(ToC(type), descriptor); + return; + } + + std::string controllerType; + descriptor.GetSupplementalDataItem(Discovery::controllerType, controllerType); + + // Internal-match endpoints: a confirmed pub/sub or RPC match, surfaced as a Link event. + if (controllerType == Discovery::controllerTypeDataSubscriberInternal) + { + HandleInternalMatch(type, descriptor, Discovery::supplKeyDataSubscriberInternalParentServiceID, + descriptor.GetNetworkName()); + return; + } + if (controllerType == Discovery::controllerTypeRpcServerInternal) + { + std::string clientUuid; + descriptor.GetSupplementalDataItem(Discovery::supplKeyRpcServerInternalClientUUID, clientUuid); + HandleInternalMatch(type, descriptor, Discovery::supplKeyRpcServerInternalParentServiceID, clientUuid); + return; + } + + // Peers: DataPublisher / RpcClient (the connecting side of a match). + if (controllerType == Discovery::controllerTypeDataPublisher + || controllerType == Discovery::controllerTypeRpcClient) + { + HandlePeer(type, descriptor); + return; + } + + // Parents: DataSubscriber / RpcServer (the receiving side of a match). + if (controllerType == Discovery::controllerTypeDataSubscriber + || controllerType == Discovery::controllerTypeRpcServer) + { + HandleParent(type, descriptor); + return; + } + + // Bus controllers are emitted as-is; infrastructure / internal services are suppressed by ClassifyAndFill. + EmitService(ToC(type), descriptor); +} + +} // namespace VSilKit diff --git a/SilKit/source/capi/ServiceObserver.hpp b/SilKit/source/capi/ServiceObserver.hpp new file mode 100644 index 000000000..3db76e1d6 --- /dev/null +++ b/SilKit/source/capi/ServiceObserver.hpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "silkit/capi/Experimental.h" + +#include "core/internal/ServiceDescriptor.hpp" +#include "core/service/ServiceDatatypes.hpp" + +#include +#include +#include +#include +#include + +namespace VSilKit { + +// Translates the internal service-discovery events of a participant into the events of the +// experimental public service-discovery C API. It is driven from a single internal service-discovery +// handler (see CapiExperimental.cpp) and owns the small amount of state needed to correlate the raw +// internal announcements into fully-named public events. +// +// Reported events: +// - Bus controllers, publishers/subscribers and RPC clients/servers are forwarded as their own +// service kind on ServiceCreated / ServiceRemoved. +// - A confirmed pub/sub or RPC match (announced internally as a DataSubscriberInternal / +// RpcServerInternal endpoint) is surfaced as a SilKit_Experimental_ServiceKind_Link ServiceCreated +// event once both endpoints are known; no removal event is emitted for it (its teardown is implied +// by the ServiceRemoved of one of its endpoints). +// - A network-simulator link is surfaced as a SilKit_Experimental_ServiceKind_Link on both +// ServiceCreated and ServiceRemoved. +// - Infrastructure / internal services are suppressed. +class ServiceObserver +{ +public: + ServiceObserver(SilKit_Experimental_ServiceDiscoveryHandler_t handler, void* context); + + // Handle a single internal discovery event, emitting zero or more public events through the + // handler. Invocations are expected to be serialized (never concurrent) but may originate from + // different threads; the internal state is guarded by a mutex and the handler is always invoked + // outside that lock. + void HandleEvent(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor); + +private: + // Identifies a service on a participant: (participant name, service id). + using ServiceKey = std::pair; + + // An internal-match endpoint whose parent (subscriber/server) or peer (publisher/client) is not + // yet known. Resolved into a Link event once both descriptors have been discovered. + struct PendingMatch + { + ServiceKey parentKey; + std::string peerUuid; + }; + + // A resolved link ready to be emitted (holds the backing storage for the emitted struct). The + // parent descriptor is the receiving side (DataSubscriber / RpcServer); the connected... fields + // name the peer (DataPublisher / RpcClient). + struct LinkEmission + { + SilKit::Core::ServiceDescriptor parentDescriptor; + std::string connectedParticipantName; + std::string connectedServiceName; + }; + + void HandleInternalMatch(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor, const std::string& parentIdKey, + const std::string& peerUuid); + void HandlePeer(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor); + void HandleParent(SilKit::Core::Discovery::ServiceDiscoveryEvent::Type type, + const SilKit::Core::ServiceDescriptor& descriptor); + + // Emits any pending match whose parent and peer are now both known. Called with _mutex held. + void DrainResolvablePending(std::vector& emissions); + + void EmitService(SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const SilKit::Core::ServiceDescriptor& descriptor); + void EmitLink(const LinkEmission& emission); + + static LinkEmission MakeLink(const SilKit::Core::ServiceDescriptor& parent, + const SilKit::Core::ServiceDescriptor& peer); + + SilKit_Experimental_ServiceDiscoveryHandler_t _handler{}; + void* _context{nullptr}; + + std::mutex _mutex; + // DataPublisher / RpcClient UUID (= their networkName) -> their descriptor. The UUID is globally + // unique, so publishers and clients share this map without collision. + std::map _peersByUuid; + // (participant, serviceId) -> DataSubscriber / RpcServer descriptor (the receiving side). + std::map _parents; + // Internal-match endpoints awaiting resolution of their parent and/or peer. + std::vector _pending; +}; + +} // namespace VSilKit diff --git a/SilKit/source/capi/Test_CapiInterfaces.cpp b/SilKit/source/capi/Test_CapiInterfaces.cpp index 360657ec7..1527aa351 100644 --- a/SilKit/source/capi/Test_CapiInterfaces.cpp +++ b/SilKit/source/capi/Test_CapiInterfaces.cpp @@ -64,6 +64,7 @@ constexpr uint64_t allSilkidIds[] = { SilKit_LifecycleConfiguration_STRUCT_VERSION, SilKit_WorkflowConfiguration_STRUCT_VERSION, SilKit_ParticipantConnectionInformation_STRUCT_VERSION, + SilKit_Experimental_ServiceDescriptor_STRUCT_VERSION, SilKit_Experimental_EventReceivers_STRUCT_VERSION, SilKit_Experimental_SimulatedNetworkFunctions_STRUCT_VERSION, SilKit_Experimental_SimulatedCanControllerFunctions_STRUCT_VERSION, diff --git a/SilKit/source/capi/Test_CapiServiceDiscovery.cpp b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp new file mode 100644 index 000000000..dfebd12f7 --- /dev/null +++ b/SilKit/source/capi/Test_CapiServiceDiscovery.cpp @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +// Tests for the thin CAPI wrapper around the service-discovery observer. The event-translation logic +// itself is covered by Test_ServiceObserver.cpp, which drives the VSilKit::ServiceObserver directly. +// Here we only verify parameter validation and that the CAPI wires the participant's internal service +// discovery through to the user handler. + +#include + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "silkit/capi/SilKit.h" + +#include "core/mock/participant/MockParticipant.hpp" +#include "core/internal/ServiceDescriptor.hpp" +#include "core/internal/ServiceConfigKeys.hpp" + +namespace { + +using SilKit::Core::Tests::DummyParticipant; +using SilKit::Core::ServiceDescriptor; +using SilKit::Core::ServiceType; +using ServiceDiscoveryEvent = SilKit::Core::Discovery::ServiceDiscoveryEvent; +namespace Discovery = SilKit::Core::Discovery; + +struct CallbackData +{ + int callCount{0}; + SilKit_Experimental_ServiceDiscoveryEvent_Type lastType{ + SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid}; + SilKit_Experimental_ServiceKind serviceKind{SilKit_Experimental_ServiceKind_Undefined}; + std::string participantName; +}; + +void SilKitCALL CapturingHandler(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor) +{ + auto* data = static_cast(context); + data->callCount += 1; + data->lastType = type; + data->serviceKind = serviceDescriptor->serviceKind; + data->participantName = serviceDescriptor->participantName; +} + +void SilKitCALL NoopHandler(void* /*context*/, SilKit_Experimental_ServiceDiscoveryEvent_Type /*type*/, + const SilKit_Experimental_ServiceDescriptor* /*serviceDescriptor*/) +{ +} + +class Test_CapiServiceDiscovery : public testing::Test +{ +public: + DummyParticipant mockParticipant; +}; + +TEST_F(Test_CapiServiceDiscovery, create_returns_service_discovery) +{ + SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), + SilKit_ReturnCode_SUCCESS); + EXPECT_NE(serviceDiscovery, nullptr); +} + +TEST_F(Test_CapiServiceDiscovery, create_bad_parameters) +{ + SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(nullptr, (SilKit_Participant*)&mockParticipant), + SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, nullptr), SilKit_ReturnCode_BADPARAMETER); +} + +TEST_F(Test_CapiServiceDiscovery, set_handler_bad_parameters) +{ + SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), + SilKit_ReturnCode_SUCCESS); + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(nullptr, nullptr, &NoopHandler), + SilKit_ReturnCode_BADPARAMETER); + EXPECT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, nullptr, nullptr), + SilKit_ReturnCode_BADPARAMETER); +} + +// The CAPI must register a handler on the participant's internal service discovery and route the +// events it delivers through to the user's C handler (via a ServiceObserver). +TEST_F(Test_CapiServiceDiscovery, wires_internal_events_to_user_handler) +{ + SilKit::Core::Discovery::ServiceDiscoveryHandler internalHandler; + EXPECT_CALL(mockParticipant.mockServiceDiscovery, RegisterServiceDiscoveryHandler(testing::_)) + .WillOnce(testing::SaveArg<0>(&internalHandler)); + + SilKit_Experimental_ServiceDiscovery* serviceDiscovery = nullptr; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_Create(&serviceDiscovery, (SilKit_Participant*)&mockParticipant), + SilKit_ReturnCode_SUCCESS); + + CallbackData data; + ASSERT_EQ(SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(serviceDiscovery, &data, + &CapturingHandler), + SilKit_ReturnCode_SUCCESS); + ASSERT_TRUE(static_cast(internalHandler)); + + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ParticipantA"); + descriptor.SetServiceName("Can1"); + descriptor.SetNetworkName("CAN1"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeCan); + + internalHandler(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); + + EXPECT_EQ(data.callCount, 1); + EXPECT_EQ(data.lastType, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.serviceKind, SilKit_Experimental_ServiceKind_CanController); + EXPECT_EQ(data.participantName, "ParticipantA"); +} + +} // namespace diff --git a/SilKit/source/capi/Test_CapiSymbols.cpp b/SilKit/source/capi/Test_CapiSymbols.cpp index 8f0612407..de06fafe1 100644 --- a/SilKit/source/capi/Test_CapiSymbols.cpp +++ b/SilKit/source/capi/Test_CapiSymbols.cpp @@ -129,6 +129,8 @@ TEST(Test_CapiSymbols, DISABLED_link_all_public_symbols) (void)SilKit_Experimental_NetworkSimulator_Start(nullptr); (void)SilKit_Experimental_NetworkSimulator_SimulateNetwork(nullptr, "", SilKit_NetworkType_Undefined, nullptr, nullptr); + (void)SilKit_Experimental_ServiceDiscovery_Create(nullptr, nullptr); + (void)SilKit_Experimental_ServiceDiscovery_SetServiceDiscoveryHandler(nullptr, nullptr, nullptr); } } // namespace diff --git a/SilKit/source/capi/Test_ServiceObserver.cpp b/SilKit/source/capi/Test_ServiceObserver.cpp new file mode 100644 index 000000000..f44edf58b --- /dev/null +++ b/SilKit/source/capi/Test_ServiceObserver.cpp @@ -0,0 +1,561 @@ +// SPDX-FileCopyrightText: 2026 Vector Informatik GmbH +// +// SPDX-License-Identifier: MIT + +#include +#include + +#include "gtest/gtest.h" + +#include "silkit/capi/SilKit.h" +#include "silkit/services/datatypes.hpp" + +#include "capi/ServiceObserver.hpp" + +#include "core/internal/ServiceDescriptor.hpp" +#include "core/internal/ServiceConfigKeys.hpp" +#include "config/YamlParser.hpp" + +namespace { + +using SilKit::Core::ServiceDescriptor; +using SilKit::Core::ServiceType; +using ServiceDiscoveryEvent = SilKit::Core::Discovery::ServiceDiscoveryEvent; +namespace Discovery = SilKit::Core::Discovery; + +struct CapturedLabel +{ + std::string key; + std::string value; + SilKit_LabelKind kind; +}; + +// A single captured event. Copying into owned storage also verifies that the struct and all its +// borrowed pointers are valid for the duration of the callback. +struct CapturedEvent +{ + SilKit_Experimental_ServiceDiscoveryEvent_Type type{SilKit_Experimental_ServiceDiscoveryEvent_Type_Invalid}; + SilKit_Experimental_ServiceKind serviceKind{SilKit_Experimental_ServiceKind_Undefined}; + std::string participantName; + std::string serviceName; + std::string primaryIdentifier; + std::string mediaType; + std::vector labels; + std::string simulationName; + std::string connectedParticipantName; + std::string connectedServiceName; +}; + +// Captures every event the public handler receives, plus the latest one for convenience. +struct CallbackData +{ + int callCount{0}; + std::vector events; + + const CapturedEvent& last() const + { + return events.back(); + } +}; + +void SilKitCALL CapturingHandler(void* context, SilKit_Experimental_ServiceDiscoveryEvent_Type type, + const SilKit_Experimental_ServiceDescriptor* serviceDescriptor) +{ + auto* data = static_cast(context); + data->callCount += 1; + + CapturedEvent event; + event.type = type; + event.serviceKind = serviceDescriptor->serviceKind; + event.participantName = serviceDescriptor->participantName; + event.serviceName = serviceDescriptor->serviceName; + event.primaryIdentifier = serviceDescriptor->primaryIdentifier; + event.mediaType = serviceDescriptor->mediaType; + for (size_t i = 0; i < serviceDescriptor->labelList.numLabels; ++i) + { + const auto& label = serviceDescriptor->labelList.labels[i]; + event.labels.push_back({label.key, label.value, label.kind}); + } + event.simulationName = serviceDescriptor->simulationName; + event.connectedParticipantName = serviceDescriptor->connectedParticipantName; + event.connectedServiceName = serviceDescriptor->connectedServiceName; + + data->events.push_back(std::move(event)); +} + +auto SerializeLabels(const std::vector& labels) -> std::string +{ + return SilKit::Config::Serialize(labels); +} + +auto MakeController(const std::string& controllerType, const std::string& networkName, + const std::string& serviceName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ParticipantA"); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(networkName); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetSupplementalDataItem(Discovery::controllerType, controllerType); + return descriptor; +} + +auto MakeUserSubscriber(SilKit::Core::EndpointId serviceId, const std::string& topic) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("SubParticipant"); + descriptor.SetServiceName("Sub"); + descriptor.SetNetworkName("default"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataSubscriber); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberTopic, topic); + return descriptor; +} + +// DataSubscriberInternal: a confirmed match between a publisher (identified by its UUID = networkName) +// and the parent DataSubscriber. +auto MakeInternalConnection(SilKit::Core::EndpointId serviceId, SilKit::Core::EndpointId parentServiceId, + const std::string& publisherUuid = "pub-uuid") -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("SubParticipant"); + descriptor.SetServiceName("SubInternal"); + descriptor.SetNetworkName(publisherUuid); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataSubscriberInternal); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberInternalParentServiceID, + std::to_string(parentServiceId)); + return descriptor; +} + +auto MakeDataPublisher(const std::string& publisherUuid, const std::string& participantName, + const std::string& serviceName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(participantName); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(publisherUuid); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeDataPublisher); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "T"); + return descriptor; +} + +auto MakeRpcServer(SilKit::Core::EndpointId serviceId, const std::string& functionName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ServerParticipant"); + descriptor.SetServiceName("RpcSrv"); + descriptor.SetNetworkName("srv-net"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcServer); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerFunctionName, functionName); + return descriptor; +} + +auto MakeRpcClient(const std::string& clientUuid, const std::string& participantName, + const std::string& serviceName) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(participantName); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(clientUuid); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcClient); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientFunctionName, "Add"); + return descriptor; +} + +auto MakeRpcServerInternal(SilKit::Core::EndpointId serviceId, SilKit::Core::EndpointId parentServiceId, + const std::string& clientUuid) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId("ServerParticipant"); + descriptor.SetServiceName("RpcSrvInternal"); + descriptor.SetNetworkName("srv-internal-net"); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, Discovery::controllerTypeRpcServerInternal); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerInternalParentServiceID, + std::to_string(parentServiceId)); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcServerInternalClientUUID, clientUuid); + return descriptor; +} + +auto MakeBusController(const std::string& ctrlType, const std::string& networkName, const std::string& serviceName, + SilKit::Core::EndpointId serviceId, + const std::string& participantName = "CtrlParticipant") -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(participantName); + descriptor.SetServiceName(serviceName); + descriptor.SetNetworkName(networkName); + descriptor.SetServiceType(ServiceType::Controller); + descriptor.SetServiceId(serviceId); + descriptor.SetSupplementalDataItem(Discovery::controllerType, ctrlType); + return descriptor; +} + +auto MakeNetworkLink(const std::string& networkName, const std::string& simulatorParticipant) -> ServiceDescriptor +{ + ServiceDescriptor descriptor; + descriptor.SetParticipantNameAndComputeId(simulatorParticipant); + descriptor.SetServiceName(networkName + "_link"); + descriptor.SetNetworkName(networkName); + descriptor.SetServiceType(ServiceType::Link); + return descriptor; +} + +// Drives the ServiceObserver directly: no CAPI wrapper and no mock participant. The observer emits +// through the C handler into `data`. +class Test_ServiceObserver : public testing::Test +{ +public: + CallbackData data; + VSilKit::ServiceObserver observer{&CapturingHandler, &data}; +}; + +// -------------------------------------------------------------------------------------------------- +// Basic classification. +// -------------------------------------------------------------------------------------------------- + +TEST_F(Test_ServiceObserver, reports_can_controller) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeCan, "CAN1", "Can1")); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_CanController); + EXPECT_EQ(data.last().participantName, "ParticipantA"); + EXPECT_EQ(data.last().serviceName, "Can1"); + EXPECT_EQ(data.last().primaryIdentifier, "CAN1"); // bus controller: identifier is the network name + EXPECT_EQ(data.last().mediaType, ""); + EXPECT_TRUE(data.last().labels.empty()); + EXPECT_EQ(data.last().connectedParticipantName, ""); + EXPECT_EQ(data.last().connectedServiceName, ""); +} + +TEST_F(Test_ServiceObserver, reports_data_publisher_with_decoded_metadata) +{ + auto descriptor = MakeController(Discovery::controllerTypeDataPublisher, "pub-uuid-1234", "MyPublisher"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "TopicA"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherMediaType, "application/json"); + descriptor.SetSupplementalDataItem( + Discovery::supplKeyDataPublisherPubLabels, + SerializeLabels({{"kA", "vA", SilKit::Services::MatchingLabel::Kind::Mandatory}, + {"kB", "vB", SilKit::Services::MatchingLabel::Kind::Optional}})); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); + EXPECT_EQ(data.last().primaryIdentifier, "TopicA"); // pub/sub: identifier is the topic + EXPECT_EQ(data.last().mediaType, "application/json"); + ASSERT_EQ(data.last().labels.size(), 2u); + EXPECT_EQ(data.last().labels[0].key, "kA"); + EXPECT_EQ(data.last().labels[0].value, "vA"); + EXPECT_EQ(data.last().labels[0].kind, SilKit_LabelKind_Mandatory); + EXPECT_EQ(data.last().labels[1].key, "kB"); + EXPECT_EQ(data.last().labels[1].kind, SilKit_LabelKind_Optional); +} + +TEST_F(Test_ServiceObserver, reports_rpc_client_with_function_name) +{ + auto descriptor = MakeController(Discovery::controllerTypeRpcClient, "rpc-uuid-9", "MyClient"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientFunctionName, "Add"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyRpcClientMediaType, "application/octet-stream"); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_RpcClient); + EXPECT_EQ(data.last().primaryIdentifier, "Add"); // rpc: identifier is the function name + EXPECT_EQ(data.last().mediaType, "application/octet-stream"); +} + +TEST_F(Test_ServiceObserver, reports_removal) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceRemoved, + MakeController(Discovery::controllerTypeLin, "LIN1", "Lin1")); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_LinController); +} + +TEST_F(Test_ServiceObserver, filters_infrastructure_and_internal_services) +{ + // Infrastructure controller (SystemMonitor) is not user-facing. + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeSystemMonitor, "default", "SystemMonitor")); + // DataSubscriberInternal with no parent id is silently dropped (no match to report). + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeDataSubscriberInternal, "sub-uuid", "SubInternal")); + + // A controller without a controller.type supplemental entry is skipped, too. + ServiceDescriptor noControllerType; + noControllerType.SetParticipantNameAndComputeId("ParticipantA"); + noControllerType.SetServiceName("Mystery"); + noControllerType.SetServiceType(ServiceType::Controller); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, noControllerType); + + EXPECT_EQ(data.callCount, 0); +} + +TEST_F(Test_ServiceObserver, malformed_labels_are_swallowed) +{ + auto descriptor = MakeController(Discovery::controllerTypeDataPublisher, "pub-uuid", "MyPublisher"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherTopic, "TopicA"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataPublisherPubLabels, "{ this is : not [ valid"); + + // Must not throw across the FFI boundary; the service is still reported. + EXPECT_NO_THROW(observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor)); + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); + EXPECT_EQ(data.last().primaryIdentifier, "TopicA"); +} + +TEST_F(Test_ServiceObserver, reports_data_subscriber_with_decoded_labels) +{ + auto descriptor = MakeController(Discovery::controllerTypeDataSubscriber, "default", "MySubscriber"); + descriptor.SetSupplementalDataItem(Discovery::supplKeyDataSubscriberTopic, "TopicS"); + descriptor.SetSupplementalDataItem( + Discovery::supplKeyDataSubscriberSubLabels, + SerializeLabels({{"sk", "sv", SilKit::Services::MatchingLabel::Kind::Mandatory}})); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, descriptor); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + EXPECT_EQ(data.last().primaryIdentifier, "TopicS"); + ASSERT_EQ(data.last().labels.size(), 1u); + EXPECT_EQ(data.last().labels[0].key, "sk"); + EXPECT_EQ(data.last().labels[0].value, "sv"); + EXPECT_EQ(data.last().labels[0].kind, SilKit_LabelKind_Mandatory); +} + +TEST_F(Test_ServiceObserver, reports_ethernet_and_flexray_controllers) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeEthernet, "ETH1", "Eth1")); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_EthernetController); + EXPECT_EQ(data.last().primaryIdentifier, "ETH1"); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeController(Discovery::controllerTypeFlexray, "FR1", "Fr1")); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_FlexrayController); + EXPECT_EQ(data.last().primaryIdentifier, "FR1"); +} + +// -------------------------------------------------------------------------------------------------- +// Pub/sub matches: reported as a Link ServiceCreated event once both endpoints are known. +// -------------------------------------------------------------------------------------------------- + +TEST_F(Test_ServiceObserver, subscriber_reported_immediately_on_creation) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); + EXPECT_EQ(data.last().primaryIdentifier, "T"); + EXPECT_EQ(data.last().connectedParticipantName, ""); + EXPECT_EQ(data.last().connectedServiceName, ""); +} + +TEST_F(Test_ServiceObserver, pubsub_match_reported_as_link) +{ + const std::string uuid = "pub-uuid-abc"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + + ASSERT_EQ(data.callCount, 3); // publisher + subscriber + link + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().participantName, "SubParticipant"); // receiving side + EXPECT_EQ(data.last().serviceName, "Sub"); + EXPECT_EQ(data.last().primaryIdentifier, "T"); + EXPECT_EQ(data.last().connectedParticipantName, "PubParticipant"); // peer + EXPECT_EQ(data.last().connectedServiceName, "MyPub"); +} + +TEST_F(Test_ServiceObserver, pubsub_match_deferred_until_publisher_arrives) +{ + const std::string uuid = "pub-uuid-late"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + EXPECT_EQ(data.callCount, 1); // only the subscriber so far; the match is pending + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + + ASSERT_EQ(data.callCount, 3); // publisher event + the now-resolvable link + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().connectedParticipantName, "PubParticipant"); + EXPECT_EQ(data.last().connectedServiceName, "MyPub"); +} + +TEST_F(Test_ServiceObserver, pubsub_match_deferred_until_subscriber_arrives) +{ + const std::string uuid = "pub-uuid-replay"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + EXPECT_EQ(data.callCount, 0); // nothing user-facing yet + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + EXPECT_EQ(data.callCount, 1); // publisher, but subscriber still unknown + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + + ASSERT_EQ(data.callCount, 3); // subscriber event + the now-resolvable link + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().connectedParticipantName, "PubParticipant"); + EXPECT_EQ(data.last().connectedServiceName, "MyPub"); +} + +TEST_F(Test_ServiceObserver, pubsub_match_no_link_removed_event) +{ + const std::string uuid = "pub-uuid-teardown"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, uuid)); + ASSERT_EQ(data.callCount, 3); + + // Removing the internal match endpoint produces no event. + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeInternalConnection(200, 100, uuid)); + EXPECT_EQ(data.callCount, 3); + + // Removing the publisher is observable, and lets the consumer infer the link is gone. + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceRemoved, + MakeDataPublisher(uuid, "PubParticipant", "MyPub")); + ASSERT_EQ(data.callCount, 4); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataPublisher); +} + +TEST_F(Test_ServiceObserver, subscriber_service_removed_on_removal) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + ASSERT_EQ(data.callCount, 1); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeUserSubscriber(100, "T")); + + ASSERT_EQ(data.callCount, 2); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_DataSubscriber); +} + +TEST_F(Test_ServiceObserver, pubsub_multiple_matches_each_reported_as_link) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeUserSubscriber(100, "T")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeDataPublisher("uuid-1", "PubA", "PubCtrlA")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeDataPublisher("uuid-2", "PubB", "PubCtrlB")); + const int before = data.callCount; + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(200, 100, "uuid-1")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeInternalConnection(201, 100, "uuid-2")); + + ASSERT_EQ(data.callCount, before + 2); + EXPECT_EQ(data.events[before].serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.events[before].connectedParticipantName, "PubA"); + EXPECT_EQ(data.events[before + 1].serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.events[before + 1].connectedParticipantName, "PubB"); +} + +// -------------------------------------------------------------------------------------------------- +// RPC matches: reported as a Link ServiceCreated event once both endpoints are known. +// -------------------------------------------------------------------------------------------------- + +TEST_F(Test_ServiceObserver, rpc_server_reported_immediately_on_creation) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_RpcServer); + EXPECT_EQ(data.last().primaryIdentifier, "Add"); +} + +TEST_F(Test_ServiceObserver, rpc_match_reported_as_link) +{ + const std::string clientUuid = "client-uuid-xyz"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeRpcClient(clientUuid, "ClientParticipant", "MyClient")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, clientUuid)); + + ASSERT_EQ(data.callCount, 3); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().participantName, "ServerParticipant"); // receiving side + EXPECT_EQ(data.last().serviceName, "RpcSrv"); + EXPECT_EQ(data.last().primaryIdentifier, "Add"); + EXPECT_EQ(data.last().connectedParticipantName, "ClientParticipant"); // peer + EXPECT_EQ(data.last().connectedServiceName, "MyClient"); +} + +TEST_F(Test_ServiceObserver, rpc_match_deferred_until_client_arrives) +{ + const std::string clientUuid = "client-uuid-late"; + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServer(50, "Add")); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeRpcServerInternal(60, 50, clientUuid)); + EXPECT_EQ(data.callCount, 1); // only the server; the match is pending + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeRpcClient(clientUuid, "ClientParticipant", "MyClient")); + + ASSERT_EQ(data.callCount, 3); // client event + the now-resolvable link + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().connectedParticipantName, "ClientParticipant"); + EXPECT_EQ(data.last().connectedServiceName, "MyClient"); +} + +// -------------------------------------------------------------------------------------------------- +// Network-simulator links: reported as a Link service (created and removed). +// -------------------------------------------------------------------------------------------------- + +TEST_F(Test_ServiceObserver, network_link_reported_as_link_service) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); + + ASSERT_EQ(data.callCount, 1); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceCreated); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().participantName, "SimParticipant"); + EXPECT_EQ(data.last().primaryIdentifier, "CAN1"); // network name, joins to bus controllers' identifier +} + +TEST_F(Test_ServiceObserver, network_link_removal_reported) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); + ASSERT_EQ(data.callCount, 1); + + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceRemoved, MakeNetworkLink("CAN1", "SimParticipant")); + + ASSERT_EQ(data.callCount, 2); + EXPECT_EQ(data.last().type, SilKit_Experimental_ServiceDiscoveryEvent_Type_ServiceRemoved); + EXPECT_EQ(data.last().serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.last().primaryIdentifier, "CAN1"); +} + +TEST_F(Test_ServiceObserver, bus_controller_reported_independently_of_link) +{ + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, + MakeBusController(Discovery::controllerTypeCan, "CAN1", "Can1", 1)); + observer.HandleEvent(ServiceDiscoveryEvent::Type::ServiceCreated, MakeNetworkLink("CAN1", "SimParticipant")); + + ASSERT_EQ(data.callCount, 2); + EXPECT_EQ(data.events[0].serviceKind, SilKit_Experimental_ServiceKind_CanController); + EXPECT_EQ(data.events[0].primaryIdentifier, "CAN1"); + EXPECT_EQ(data.events[1].serviceKind, SilKit_Experimental_ServiceKind_Link); + EXPECT_EQ(data.events[1].primaryIdentifier, "CAN1"); // same network -> the controller is simulated +} + +} // namespace diff --git a/SilKit/source/core/internal/ServiceDescriptor.hpp b/SilKit/source/core/internal/ServiceDescriptor.hpp index ececc8ebd..65b4d4b74 100644 --- a/SilKit/source/core/internal/ServiceDescriptor.hpp +++ b/SilKit/source/core/internal/ServiceDescriptor.hpp @@ -81,6 +81,9 @@ class ServiceDescriptor inline void SetServiceId(SilKit::Core::EndpointId val); inline auto GetSupplementalData() const -> SupplementalData; + //! \brief Direct access to the supplemental data without copying. The reference is valid for the lifetime of this + //! ServiceDescriptor. + inline auto GetSupplementalDataRef() const -> const SupplementalData&; inline void SetSupplementalData(SupplementalData val); inline bool GetSupplementalDataItem(const std::string& key, std::string& value) const; @@ -226,6 +229,11 @@ auto ServiceDescriptor::GetSupplementalData() const -> SupplementalData return _supplementalData; } +auto ServiceDescriptor::GetSupplementalDataRef() const -> const SupplementalData& +{ + return _supplementalData; +} + void ServiceDescriptor::SetSupplementalData(SupplementalData val) { _supplementalData = std::move(val);