Gateway IPC Binding on mw::com#
This document describes an alternative implementation of the Gateway IPC Binding that replaces the
score::message_passing control channel and the hand-rolled shared-memory transport with mw::com
(LoLa).
It is developed side by side with the existing implementation described in Gateway IPC Binding:
it implements the same public interfaces
Gateway_ipc_binding_clientandGateway_ipc_binding_serverit is constructed through new factory functions, not through the existing
create()methods
Because mw::com already provides service discovery, subscription management, shared-memory transport and
slot lifetime management, this implementation has no IPC protocol of its own. There are no message ids, no
wire format and no shared-memory slot manager. The entire design question is therefore: how are SOME/IP
services mapped onto ``mw::com`` features?
The one thing mw::com does not give away for free is peer liveness, because per-service availability
cannot distinguish “the peer is down” from “the peer is up but the service is not there”. That is solved with
a dedicated, typed SomeipdService, see SomeipdService and peer liveness.
Goals and non-goals#
Goals:
transport SOCom event updates between
gatewaydandsomeipdovermw::comkeep the SOME/IP header contiguously in front of every event payload so that E2E can be computed over header plus payload without an additional copy
keep the public interface of the component unchanged, including the meaning of
is_connected(), so that the daemons can be switched over by exchanging a single factory calldelete, not reimplement, everything that
mw::comalready provides
Non-goals:
method calls.
GenericProxyandGenericSkeletonare event-only, so methods stay unimplemented, as they are today.dynamic, runtime-driven service discovery between the two daemons.
mw::cominteroperability for thegatewaydto application boundary. That boundary already usesmw::comand is not touched by this design.
Design decisions taken up front#
These decisions are inputs to this design, not conclusions of it. They are restated here because the rest of the document only makes sense with them in mind.
D1: GenericProxy and GenericSkeleton represent a SOME/IP service#
GenericProxy and GenericSkeleton treat event payloads as opaque binary data and are configured at
runtime from deployment information instead of from generated IDL code. That is exactly what a gateway
needs: the gateway never interprets the payload of a bridged service, it only moves bytes. One bridged
SOME/IP service instance therefore becomes one mw::com service instance.
D2: every event sample carries the full SOME/IP message#
E2E protection is computed over the SOME/IP header and the payload together and requires contiguous memory.
The event samples exchanged between gatewayd and someipd therefore carry the full SOME/IP message,
header included. score::socom::Payload::data() is a read-only view of it; the sender writes it through
Writable_payload::wdata(). See Event sample layout.
Mapping SOME/IP services to mw::com features#
Concept mapping#
Gateway concept |
Today (message_passing + shm) |
|
|---|---|---|
Bridged service instance |
|
one |
Service interface identity and version |
|
|
Event identity |
|
named |
Service becomes available |
|
|
Service goes away |
|
|
Event subscription propagation |
|
|
Payload buffer allocation |
|
|
Event transmission |
|
|
Event reception |
open peer segment read-only, build payload from slot index |
|
Payload lifetime |
|
|
Slot count and slot size |
|
|
Peer liveness |
|
dedicated typed |
Method calls |
declared, never dispatched |
not representable, out of scope |
Everything in the middle column that has a counterpart in the right column is deleted, not ported.
Role assignment#
A bridged service always has a data source and a data sink. The binding peer on the source side owns the
GenericSkeleton, the binding peer on the sink side owns the GenericProxy.
Service kind |
Data direction |
|
|
Binding SOCom role on the provider side |
|---|---|---|---|---|
local service instance (application provides, network consumes) |
application to network |
provider, |
consumer, |
|
remote service instance (network provides, application consumes) |
network to application |
consumer, |
provider, |
|
Read as a rule:
provider role: the binding is a SOCom
Client_connectorof the locally offered SOCom service and pushes what it consumes into aGenericSkeleton.consumer role: the binding is a SOCom
Server_connectorfor the same service and publishes what it receives from aGenericProxy.
Fig. 3 Role assignment for both data directions#
Because both daemons hold both roles at the same time, for different services, the bridged-service part of the
implementation is a single symmetric class. The only asymmetry left is the SomeipdService described next,
and it is what distinguishes the client flavour of the public interface from the server flavour.
SomeipdService and peer liveness#
The bridged services alone cannot tell gatewayd whether someipd is running. A remote service instance
only becomes available once the SOME/IP service behind it is available on the network, so its absence is
ambiguous: it means either “someipd is down” or “someipd is up but the network service is not there”.
The Connect / Connect_reply handshake resolves exactly this ambiguity today and must be preserved.
It is replaced by SomeipdService, a dedicated mw::com service that represents the someipd daemon
itself rather than any SOME/IP service:
someipdprovides oneSomeipdServiceinstance. Its presence is the liveness signal.gatewaydconsumes it withStartFindServiceand creates aSomeipdServiceProxyfor it.Gateway_ipc_binding_client::is_connected()returns true once that proxy has been created andsomeipdacceptedAddServiceConfigurationon it. It returns to false when the find-service handler reports the instance gone.gatewaydcallsAddServiceConfigurationwith its services on every new proxy, so a restartedsomeipdgets them again. See Registering gatewayd services with someipd.
Properties of this design:
it is a genuine peer-liveness signal, not a local self-check, so
gatewayd’s existing startup wait loop keeps working unchanged, with the same meaning and the same point in the sequence as todayit recovers automatically. If
someipdrestarts, the handler fires with an empty container and then again with a handle, sois_connected()goes false and true again. The bridged services re-establish themselves independently and need no ordering guarantee relative to it.the instance specifier plays the role that the
message_passingchannel name plays today: it must be unique persomeipdon a host, and it is the one piece of configurationsomeipdand all itsgatewaydinstances must agree on.every
gatewaydof asomeipdMUST have its ownmw::comapplicationID, or its own UID if it configures none. LoLa treats a secondgatewaydwith the same one as a restart of the first.
Offering order#
someipd offers SomeipdService first. It has no bridged services until a gatewayd calls
AddServiceConfiguration. is_connected() turns true only once someipd accepted the call, so
is_connected() == true means “the peer’s binding is up, accepting, and has the services of this
gatewayd”, not “the peer has finished setting up its services”. Bridged services arrive asynchronously
afterwards, each signalled through the normal SOCom service state.
Event sample layout#
A mw::com event sample is a fixed-size, type-erased buffer described by DataTypeMetaInfo{size, alignment}.
SOME/IP messages are variable length and, per D2: every event sample carries the full SOME/IP message, travel whole. The binding therefore defines
the following layout for every event sample it exchanges:
+----------------------+-----------------------------+
| binding prefix (8 B) | payload bytes |
| u32 payload_length | up to max_payload_size, |
| u32 reserved | e.g. a SOME/IP message |
+----------------------+-----------------------------+
^ ^
sample base Payload::data()
sizeof theDataTypeMetaInfois8 + max_payload_sizerounded up to a multiple of 8,alignmentis 8. The 8-byte prefix keeps the payload 8-byte aligned; the rounding is required becausemw::comrejects a size that is not an integer multiple of its alignment. The padding at the end is never addressed, soPayload::data()is exactlymax_payload_sizelong.the binding builds
socom::Payloadandsocom::Writable_payloadover the bytes after the prefix, so the producing and consuming applications see the whole message indata(), and E2E can run over it.payload_lengthis the only piece of binding metadata on the link. It is required because LoLa samples are fixed size while SOME/IP payloads are not. The producer shrinks itsWritable_payload; the binding writespayload.data().size()into the prefix just beforeSend(); the consumer side reads it back and sizes thesocom::Payloadaccordingly.
Two alternatives were considered and rejected:
deriving the length from the SOME/IP header length field. That would make
gateway_ipc_bindingparse SOME/IP, which it deliberately does not do today, and the field is currently not filled in bygatewayd.one
mw::comevent per length class. That multiplies the deployment configuration and gains nothing.
Event sizes#
Only gatewayd knows the event sizes (serialized payload plus SOME/IP header), from its generated
serializers. someipd is type-agnostic and configures max_payload_size as 0. In mw::com, the
Role::provider side allocates the shared-memory sample slots when it creates the GenericSkeleton, so
the provider needs the size up front. How someipd gets the size depends on which daemon provides the
bridged service:
gatewayd provides, someipd consumes:
gatewaydcreates theGenericSkeletonwith its configured size. Whensomeipd’sConsumer_service_bindingattaches to the peer, it reads the sample size fromGenericProxyEvent::GetDataTypeSizeInfo().Size()and subtractskSample_prefix_size. On any consumer, if the peer’s sample size is smaller than the configuredmax_payload_size, the binding logs an error and leaves the service unavailable.someipd provides, gatewayd consumes:
someipdmust size itsGenericSkeletonbeforegatewaydattaches to it.gatewaydtherefore sends{service_id, event_id, max_payload_size}for every event of itsRole::consumerservices inAddServiceConfiguration(at most 1024 events, in any order). Those services areRole::providerservices insomeipd. The call MUST supply one size for every provider event it adds tosomeipd, with no missing, unknown, or duplicate events. Otherwisesomeipdrejects the call and logs an error.
Registering gatewayd services with someipd#
someipd starts with no service configuration of its own. Instead, each gatewayd registers its services
at runtime via AddServiceConfiguration:
Payload:
gatewaydsends the raw FlatBuffers bytes of itsmw_someip_config(up to 64 KiB) together with its consumer event sizes in a single fixed-size, trivially copyableAddServiceConfigurationcall.gatewaydfails at startup if its configuration exceeds 64 KiB.Setup in someipd: the IPC binding treats the configuration bytes as opaque and passes them to
someipd’s handler.someipdvalidates the buffer with the FlatBuffersVerifier, builds the IPC bridged services viabridged_service_configs(), and creates its SOME/IP network services from the same buffer, keeping the buffer alive for as long as the services exist.All-or-nothing rollback: if any step fails,
someipddestroys the service bindings created during the call and returnsfalse.gatewaydlogs the failure and stays disconnected. Anymw::comdeployment already registered withruntime::AddConfigurationstays insomeipduntilsomeipdrestarts, becausemw::comcannot remove a configuration.Restarts and ownership:
someipdkeeps agatewayd’s services alive when thatgatewaydstops. When a restartedgatewaydreconnects and sends the exact same configuration bytes and event sizes,someipdaccepts the call immediately without recreating the services. Any different call that reuses aservice_idfrom an already-accepted call is rejected: each service belongs to onegatewaydand cannot change whilesomeipdruns.Starting service discovery for consumer services: every
Role::consumerservice (Consumer_service_binding) callsGenericProxy::StartFindServiceto discover when the peer daemon’sGenericSkeletonoffers that service. Ingatewayd,create_client()callsStartFindServicedirectly on the startup thread. Insomeipd, services are created inside theAddServiceConfigurationhandler on anmw::commessage-passing thread, which must not callStartFindService.Server_adaptertherefore postsStartFindServiceforsomeipd’sRole::consumerservices (services flowing fromgatewaydtosomeipd) to a dedicated worker thread once the call is accepted. If that fails, the binding logs the error and leaves the service unavailable.
Availability propagation#
Request_service, Offer_service, Connect_service and Connect_service_reply are replaced by
mw::com service discovery:
provider side: the
GenericSkeletonis created eagerly at startup but only offered while the local SOCom service is available.Client_connector::on_service_state_change(available)triggersOfferService(),not_availabletriggersStopOfferService().consumer side:
GenericProxy::StartFindServiceis started at startup. A non-empty handle container triggersGenericProxy::Createand enabling of the SOComDisabled_server_connector; an empty container disables it again.
The GenericSkeleton object is kept alive across StopOfferService, matching the guidance in the
LoLa gateway documentation: destroying it would zero the shared-memory subscription control block underneath
consumers that still hold a subscription.
Subscription propagation#
Subscribe_event is replaced by a symmetric pair of mw::com callbacks, so that data is only produced
while somebody actually wants it:
consumer side: SOCom
on_event_subscription_change(event_id, subscribed)maps toGenericProxyEvent::Subscribe(max_sample_count)plusSetReceiveHandler, and toUnsetReceiveHandlerplusUnsubscribeonnot_subscribed.provider side:
GenericSkeletonEvent::SetReceiveHandlerRegistrationChangedHandlerfires when the first remote receive handler is registered or the last one is removed. It maps toClient_connector::subscribe_event(event_id, Event_mode::update)andunsubscribe_event.
This callback exists on GenericSkeletonEvent only, and was added upstream for exactly this gateway use
case.
The two events can race: the remote receive handler may be registered before the local SOCom service is
available. The provider side therefore keeps a per-event desired-subscription flag and reconciles it whenever
either input changes, rather than calling SOCom directly from the mw::com callback.
Payload lifetime and flow control#
Payload_consumed and the manual reference counting disappear. LoLa owns the slots:
provider side:
GenericSkeletonEvent::Allocate()returns aSampleAllocateePtr<void>. SOCom only hands the binding aWritable_payload, so the binding keeps a small per-event table of in-flight allocations, keyed by theslot_handleit mints and stores in the payload. Onon_event_update(event_id, payload)the binding extracts the entry, writes the length prefix and callsSend(std::move(sample)). If the producer drops the payload without sending it, the payload destructor removes the entry and theSampleAllocateePtrreleases the slot.consumer side:
GetNewSamplesyields aSamplePtr<void>per sample. The binding moves it into the destruction callback of thesocom::Payloadit builds. When the consuming application drops the payload, the slot is returned to LoLa.
Consequences:
back pressure is expressed as
Allocate()failing when all slots are held. The binding logs and drops the event update, as it does today when the slot pool is exhausted.numberOfSampleSlotsmust cover the samples in flight plus the samples the peer holds. The peer holds at mostmax_sample_countper event, which the binding passes toSubscribe.all
SampleAllocateePtrandSamplePtrinstances must be destroyed before the owning skeleton or proxy event. The binding tears down in the order: unsubscribe, drain in-flight tables, destroy proxies and skeletons.
Architecture#
Fig. 4 Gateway IPC Binding on mw::com, component view#
Client_adapterandServer_adapterimplement the two public interfaces. Each owns oneService_bindingper configured service instance plus oneSomeipd_service_binding. Only theSomeipd_service_bindinghalf and the point in time at which the setup runs differ between them, so they share the factorymake_service_binding()instead of a common aggregate class.Someipd_service_bindingimplements one of the two halves of SomeipdService and peer liveness: the provider half owns theSomeipdServiceSkeleton, the consumer half owns theFindServiceHandle, theSomeipdServiceProxyand thestd::atomic<bool>behindis_connected(). It is the only place in the binding that touches a typed proxy or skeleton.Provider_service_bindingholds theGenericSkeleton, its per-event allocation table, and the SOComClient_connector.Consumer_service_bindingholds theFindServiceHandle, theGenericProxyonce found, and the SOComDisabled_server_connector/Enabled_server_connector.Setting a bridged service up is all-or-nothing: if any entry of
Service_configsfails,create_client()returnsnullptrinstead of leaving a link that silently carries only part of the configured services. On thesomeipdsideAddServiceConfigurationreturns false, see Registering gatewayd services with someipd.
What is gone compared to Gateway IPC Binding:
impl/ipc_messages.hpp,impl/gateway_ipc_binding_util.hpp,impl/reply_channel.hpp,impl/connections.hpp,impl/pending_connects.hpp,impl/connection_metadata.hpp— no protocolimpl/shared_memory_slot_manager.cpp,impl/shared_memory_managers.hpp— no shared-memory bookkeepingRuntime::register_service_bridge— with static roles the connectors are created eagerly at startup, so there is no on-demandrequest_servicepath to hook intothe client/server asymmetry for bridged services. What remains of it is
SomeipdService: the client flavour consumes it, the server flavour provides it.
Public interface#
A new public header gateway_ipc_binding_mw_com.hpp adds the configuration types and two free factory
functions. The existing headers are not modified.
Interface semantics under mw::com#
The public interfaces are unchanged. Their behaviour is defined as follows and must be documented at the factory functions.
Member |
Behaviour |
|---|---|
|
True while the |
|
Offers |
is_connected() is read from a different thread than the find-service handler that maintains it, so it is
backed by an std::atomic<bool>.
Behavioural views#
Startup#
Fig. 5 someipd and gatewayd startup: SomeipdService before bridged services#
Event flow#
The flow for one bridged service is split into availability, subscription lifecycle and event delivery. Availability precedes subscription; event delivery takes place between subscription and unsubscription.
Fig. 6 Availability for one bridged service#
Fig. 7 Subscription lifecycle for one bridged service#
Fig. 8 Event delivery for one bridged service#
Teardown#
Ordering matters because LoLa terminates the process if a SampleAllocateePtr or SamplePtr outlives its
event:
consumer side:
UnsetReceiveHandler,Unsubscribe, thenStopFindServicedestroy the SOCom connectors, which guarantees no further SOCom callback can run
drop all payloads still held in the in-flight allocation tables
destroy the
GenericProxyandGenericSkeletonobjects
Threading and concurrency#
SOCom callbacks and
mw::comreceive handlers run on different threads. EveryService_bindingguards its state with a mutex, plus one outer mutex per blocking transition it has to serialize.Both stacks call the binding back while holding an internal lock of their own, and both take that same internal lock from the API the binding calls into. A binding mutex that a callback takes may therefore never be held across such a call, or the two lock orders form a cycle and the daemon deadlocks against a stack-owned thread. Concretely:
mw::comholds its service discovery lock across the find-service handler, and creating or destroying aGenericProxytakes that same lock. Proxies are therefore constructed before, and destroyed after, every binding mutex is released.mw::comholds its event notification registry lock across the receive-handler-registration handler, andOfferService()/StopOfferService()take that same lock. The provider offers underm_offer_mutex, which that handler does not take.SOCom holds connector locks across the subscription callbacks, and
enable()/disable()take those same locks. The consumer enables and disables underm_lifecycle_mutex, which no callback takes.
The outer mutexes exist purely to keep those transitions serialized against each other while the inner ones are released.
Neither a SOCom callback nor a
mw::comhandler may block. The binding does no I/O and no allocation beyond the in-flight table inside them. TheAddServiceConfigurationhandler is the exception: it sets up services, but never waits for a reply, see Registering gatewayd services with someipd.GetNewSamplesmust not run concurrently for the sameGenericProxyEvent. Because the only caller is the receive handler registered for that event, andmw::comserialises those, this holds without extra locking; the mutex is still taken for the SOCom-visible state.The SOCom deadlock detector terminates the process if a connector is destroyed from inside one of its own callbacks. Teardown therefore never runs from a callback; the
mw::comfind-service handler schedules the disable and destroys the connector outside the handler, exactly as the current implementation does forConnect_service_reply.
Configuration#
Both daemons need a mw_com_config.json. someipd needs one for the first time, and has to call
InitializeRuntime. Of the gatewayd / someipd link it holds only the entry for
SomeipdService and peer liveness. Both files must spell it identically. It replaces the shared ipc_channel_name
command line option:
{
"serviceTypes": [
{
"serviceTypeName": "/score/someip/SomeipdService",
"version": { "major": 1, "minor": 0 },
"bindings": [ {
"binding": "SHM",
"serviceId": 6400,
"methods": [ { "methodName": "AddServiceConfiguration", "methodId": 1 } ]
} ]
}
],
"serviceInstances": [
{
"instanceSpecifier": "someipd/daemon",
"serviceTypeName": "/score/someip/SomeipdService",
"version": { "major": 1, "minor": 0 },
"instances": [ {
"instanceId": 1,
"asil-level": "QM",
"binding": "SHM",
"methods": [ { "methodName": "AddServiceConfiguration", "queueSize": 1 } ]
} ]
}
]
}
Bridged deployment#
Bridged services are not listed in mw_com_config.json. Instead, each daemon builds its Service_configs
from mw_someip_config using bridged_service_configs() in score/bridged_services: gatewayd
builds them from its own file, and someipd builds them from the bytes each gatewayd sends, see
Registering gatewayd services with someipd. The binding then synthesizes their serviceTypes and serviceInstances entries
and registers them at runtime with mw::com::runtime::AddConfiguration:
Naming: both
serviceTypeNameandinstanceSpecifierarebridged_ipc/<service_type_name>(with version1.0), so they cannot collide with thegatewayd-to-application instances.IDs:
serviceIdandeventIdare taken directly from the SOME/IPservice_idandevent_idinmw_someip_config.Instance ID:
instanceIdisipc_instance_idfrommw_someip_config, which defaults to0xFFFF(the SOME/IP “any instance” ID) to avoid colliding with local application instance IDs. Allgatewaydinstances of the samesomeipdMUST use the sameipc_instance_id; a host running more than onesomeipdneeds a distinct value persomeipd.Sample slots and subscribers:
numberOfSampleSlotsiskNumber_of_sample_slots(20, see Event sample layout) andmaxSubscribersis1, because only the single peer daemon subscribes.ASIL level:
asil-levelis"QM", becausesomeipdis a QM process andmw::comrequires both ends of a shared-memory instance to use"QM"when either peer is QM.Validation and existing entries:
mw::com::runtime::AddConfigurationterminates the process on a duplicate or malformed entry.add_to_mw_com_runtime()therefore validates everyService_configfirst, checksruntime::ResolveInstanceIDs, keeps anyinstanceSpecifieralready present inmw_com_config.json, and adds only the missing ones.
Event sample sizes are not part of the JSON deployment; they come from Service_config (see
Event sizes) and are passed to GenericSkeleton::Create. On the provider side, the binding checks
that GenericSkeletonEvent::GetSizeInfo().size matches sample_size(event) and fails setup on mismatch.
Known gaps#
method calls on bridged services are not representable, because
GenericProxyandGenericSkeletonare event-only. They are declared but never dispatched today, so this is not a regression, but it does close the door on the incremental path that the currentConnectshared-memory configuration was designed for. Note that this limitation applies to bridged services only;SomeipdServiceis typed and can carry methods, see SomeipdService and peer liveness.requested event updates: SOCom
on_event_update_requesthas no LoLa counterpart. LoLa has no pull API on the proxy side, so field-style initial values cannot be served on demand. The request is logged and ignored.re-offering a provider service: if the local SOCom service of a provider-role bridge goes away and comes back while a peer still holds an event subscription,
Provider_service_bindingkeeps theGenericSkeletonalive acrossStopOfferService()/OfferService(), as the LoLa gateway documentation demands, because destroying it would zero the shared-memory subscription control block underneath that peer. Re-offering the same skeleton in that case still aborts inside LoLa (PrepareOffer()callsRemoveStaleArtefacts()on a region the skeleton itself still owns), so that case has to be fixed upstream rather than worked around here. When no peer holds a subscription, the binding instead fully destroys and recreates theGenericSkeletonon the next offer, which LoLa’s own stale-shared-memory detection (an flock on a per-instance usage marker file) supports. A restart of the providing application while a peer is actively subscribed is therefore still not covered.changing an event size: requires restarting both daemons.
someipdrejects a modified configuration from a restartedgatewayd, and LoLa reuses a provider’s existing shared-memory segment while a consumer still holds it open.
Open points#
Whether
someipdshould keep themessage_passingimplementation available behind a switch until the upstream re-offer problem described under Known gaps is solved.