Skip to content

ffi: queue-shaped C boundary for inbound/outbound SOMEIP messages #122

Description

@JustinKovacich

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().

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsimple_someipIssue related to the simple_someip crate

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions