Context
The 0.8.0 API exposes the SOMEIP state machine as a run-future (driven by an executor — see #121) with two side-channel APIs for application-level interaction:
- Outbound:
EventPublisher::publish_event(...) returns an impl Future<Output = Result<usize, Error>> that the caller awaits to send.
- Inbound: subscribers learn of subscribe/unsubscribe via the run-future's internal
handle_sd_message path; received unicast/SD events are surfaced through Client::recv (which is also async).
For the TC4 C-host integration (see #121 for the executor side), the integration model agreed in the bring-up meeting is queue-shaped:
"I want to send a message" / "I want to update a heartbeat" — push into a queue, the executor's tick drains it.
Receive direction is symmetric — Rust pushes parsed inbound records to a queue, C drains on its own tick.
— meeting transcript
The current async-call-and-go shape works fine inside Rust async code. From C, calling an async fn requires either entering an async context (which the C super-loop doesn't have) or wrapping it. The queue model side-steps this entirely: the FFI surface is plain blocking-or-error functions, the run-future inside the executor sees the queue and does the actual work asynchronously.
Goal
Define a queue-shaped C-FFI surface that bridges the synchronous-superloop world to the async run-future world without adding async-FFI complexity.
Sketch
// Outbound — C calls these to enqueue work.
//
// Returns 0 on enqueue, non-zero on full-queue / invalid-args.
int32_t simple_someip_publish(
uint16_t service_id,
uint16_t instance_id,
uint16_t event_group_id,
const uint8_t* payload,
size_t payload_len
);
// Inbound — C calls these to drain.
//
// Returns 0 if a record was written into *out, non-zero if queue empty.
int32_t simple_someip_poll_inbound_event(
SimpleSomeipInboundEvent* out
);
// Rust side: SPSC queues (heapless::spsc) bridge FFI to the run-future.
//
// Outbound: simple_someip_publish() pushes (svc, inst, eg, payload-copy) into OUTBOUND.
// Inside the run-future, an additional select arm drains OUTBOUND and calls
// EventPublisher::publish_event() for each entry.
//
// Inbound: handle_sd_message and friends push parsed records into INBOUND.
// simple_someip_poll_inbound_event() pops a record into the C-side struct.
static OUTBOUND: heapless::spsc::Queue<OutboundEvent, OUTBOUND_CAP> = ...;
static INBOUND: heapless::spsc::Queue<InboundRecord, INBOUND_CAP> = ...;
Open questions
- Queue cardinality: one queue per direction across all services, or per-service / per-event-group? Single queue is simpler; per-service avoids head-of-line blocking on a slow consumer.
- Backpressure policy: full-queue behavior — return error to caller (caller retries), drop-oldest (silently lose data), drop-newest (silently lose new). E2E sequence-number gaps surface dropped events correctly only for ASIL traffic; QM events would just disappear.
- Payload ownership: pointer-to-static (zero-copy but caller owns lifetime), copy-into-static-arena (Rust owns, fixed cap),
&[u8]-with-callback (Rust borrows during dispatch). Probably copy-into-arena to avoid lifetime headaches across the FFI boundary.
- Multi-producer: SPSC works if C is single-threaded. If safety tasks (interrupt-driven) and QM tasks both publish, we need MPSC — the embedded MPSC primitives are messier, so prefer architectural single-producer if at all possible.
- Granularity of inbound events: just-received-bytes, or pre-parsed protocol events (Subscribe/Unsubscribe, EventReceived, etc.)? Pre-parsed is friendlier to C consumers but couples the FFI struct to internal protocol types — bumps every time we add an SD entry type.
- Synchronous publish path: is there ever a need for "publish and wait for confirmation"? Probably not at the SOMEIP layer (UDP fire-and-forget); but UDS-on-IP might have request/response semantics that need this.
Blocking dependency
TC4 bring-up. The exact shape depends on what the integrating firmware actually needs — premature design risks getting cardinality / backpressure / payload ownership wrong. Defer concrete implementation until #121's executor is polling a real run-future and the next concrete need is "how does C push a heartbeat in."
Non-goals
- This is not a replacement for the async
EventPublisher::publish_event API. The async API stays as the primary Rust-side interface; the queue is purely the C-FFI bridge.
- This does not involve adding async-FFI (
async extern "C" fn-style) — that's not stable and not what the integration needs.
Pairing
Companion to #121 (Rust executor for the C super-loop). The queue is what gets drained on each tick().
Context
The 0.8.0 API exposes the SOMEIP state machine as a run-future (driven by an executor — see #121) with two side-channel APIs for application-level interaction:
EventPublisher::publish_event(...)returns animpl Future<Output = Result<usize, Error>>that the caller awaits to send.handle_sd_messagepath; received unicast/SD events are surfaced throughClient::recv(which is also async).For the TC4 C-host integration (see #121 for the executor side), the integration model agreed in the bring-up meeting is queue-shaped:
— meeting transcript
The current async-call-and-go shape works fine inside Rust async code. From C, calling an
async fnrequires either entering an async context (which the C super-loop doesn't have) or wrapping it. The queue model side-steps this entirely: the FFI surface is plain blocking-or-error functions, the run-future inside the executor sees the queue and does the actual work asynchronously.Goal
Define a queue-shaped C-FFI surface that bridges the synchronous-superloop world to the async run-future world without adding async-FFI complexity.
Sketch
Open questions
&[u8]-with-callback (Rust borrows during dispatch). Probably copy-into-arena to avoid lifetime headaches across the FFI boundary.Blocking dependency
TC4 bring-up. The exact shape depends on what the integrating firmware actually needs — premature design risks getting cardinality / backpressure / payload ownership wrong. Defer concrete implementation until #121's executor is polling a real run-future and the next concrete need is "how does C push a heartbeat in."
Non-goals
EventPublisher::publish_eventAPI. The async API stays as the primary Rust-side interface; the queue is purely the C-FFI bridge.async extern "C" fn-style) — that's not stable and not what the integration needs.Pairing
Companion to #121 (Rust executor for the C super-loop). The queue is what gets drained on each
tick().