Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,24 @@ tracing-subscriber = "0.3"
[features]
default = ["std"]
std = ["embedded-io/std", "thiserror/std", "tracing/std"]
client = ["std", "dep:tokio", "dep:socket2", "dep:futures"]
# Phase 13 split: `client` exposes the protocol/trait-surface client
# (no tokio, no socket2); `client-tokio` layers the tokio + socket2
# convenience defaults on top. Consumers of the bare-metal trait surface
# enable `client` only (and supply their own `Spawner` / `Timer` /
# `ChannelFactory` / `TransportFactory` impls). Consumers who want the
# `Client::new` shortcut (defaulting to `TokioSpawner` / `TokioTimer` /
# `TokioChannels` / `TokioTransport`) enable `client-tokio`.
#
# `server` is **not** split in phase 13 — `src/server/sd_state.rs`,
# `src/server/subscription_manager.rs`, and `src/server/mod.rs` still
# reference `tokio::net::UdpSocket`, `tokio::sync::RwLock`, and
# `socket2::Socket` directly in production code. Phase 14 (server
# parallel) is the phase that retargets the server to the trait
# surface; once that lands, `server` will gain the same split into
# `server` + `server-tokio`. Until then, enabling `server` continues
# to pull tokio + socket2.
client = ["std", "dep:futures"]
client-tokio = ["client", "dep:tokio", "dep:socket2"]
Comment on lines +69 to +70

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that client is std + futures and client-tokio layers tokio/socket2 on top, the bare-metal feature docs later in this section (notably the statement that EmbassySyncChannels is available without tokio) don’t match the current module gating (EmbassySyncChannels still lives under tokio_transport). Please reconcile this: either move/re-export the embassy channel backend for bare_metal builds, or update the feature docs to reflect its actual availability.

Copilot uses AI. Check for mistakes.
server = ["std", "dep:tokio", "dep:socket2", "dep:futures"]
# Marks a build as intended for bare-metal / no_std consumption.
# Currently a pure marker — enables no crate code on its own. Reserved
Expand All @@ -74,4 +91,4 @@ bare_metal = ["dep:embassy-sync"]

[[test]]
name = "client_server"
required-features = ["client", "server"]
required-features = ["client-tokio", "server"]
23 changes: 15 additions & 8 deletions examples/bare_metal/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,20 @@
//! - Phase 12: `TransportSocket` GATs — `SendFuture` / `RecvFuture`
//! express `Send` bounds without RTN; `Socket = TokioSocket` pin
//! removed from `bind_*` functions
//! - Phase 13 (partial): client-side feature-flag split. `client` no
//! longer pulls tokio + socket2; the tokio convenience defaults
//! (`Client::new`, `TokioSpawner`, etc.) live behind a new
//! `client-tokio` feature.
//!
//! **Remaining gaps:**
//! 1. **Feature-flag split** (Phase 13): `client` / `server` still
//! pull in tokio + socket2. A future split (`client` vs
//! `client-tokio`) will make the core types `no_std`-compatible.
//!
//! Until those are closed, `feature = "client"` / `feature = "server"`
//! pull in `std + tokio + socket2`.
//! 1. **Server-side feature-flag split** (Phase 13 server half,
//! deferred to Phase 14): `feature = "server"` still pulls in
//! tokio + socket2 because `server::sd_state` and
//! `server::subscription_manager` reference `tokio::net::UdpSocket`
//! / `tokio::sync::RwLock` / `socket2::Socket` directly. Phase 14
//! (server parallel) is the phase that retargets the server to the
//! trait surface; once that lands, `server` will gain the same
//! `server` + `server-tokio` split.
//!
//! # Recommendation for `no_alloc` consumers today
//!
Expand Down Expand Up @@ -407,7 +413,8 @@ fn main() {
println!(
"note: trait layer (TransportSocket + TransportFactory + Timer + \
Spawner + ChannelFactory) exercised end-to-end. Phases 9-12 \
complete. Remaining gap: client/server feature flags still pull \
in tokio + socket2 (Phase 13). See top-of-file docblock."
complete; phase 13 (client half) complete. Remaining: phase 14 \
server-trait retargeting + server-side `server-tokio` split. \
See top-of-file docblock."
);
}
2 changes: 1 addition & 1 deletion examples/client_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ edition = "2024"
publish = false

[dependencies]
simple-someip = { path = "../..", features = ["client", "server"] }
simple-someip = { path = "../..", features = ["client-tokio", "server"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
tracing = "0.1"
tracing-subscriber = "0.3"
3 changes: 2 additions & 1 deletion examples/client_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

// ── Create the client (handles discovery, subscriptions, SD socket) ──

let (client, mut updates, run_fut) = simple_someip::Client::<Payload>::new(interface);
let (client, mut updates, run_fut) =
simple_someip::Client::<Payload, _, _, _>::new(interface);
let _run_handle = tokio::spawn(run_fut);
client.bind_discovery().await?;
info!("Client discovery bound");
Expand Down
2 changes: 1 addition & 1 deletion examples/discovery_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ publish = false

[dependencies]
embedded-io = "0.7"
simple-someip = { path = "../..", features = ["client"] }
simple-someip = { path = "../..", features = ["client-tokio"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tracing = "0.1"
tracing-subscriber = "0.3"
3 changes: 2 additions & 1 deletion examples/discovery_client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,8 @@ async fn main() -> Result<(), Error> {

info!("Starting discovery client on interface {interface}");

let (client, mut updates, run_fut) = simple_someip::Client::<Payload>::new(interface);
let (client, mut updates, run_fut) =
simple_someip::Client::<Payload, _, _, _>::new(interface);
let _run_handle = tokio::spawn(run_fut);
client.bind_discovery().await.unwrap();

Expand Down
15 changes: 8 additions & 7 deletions src/client/inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ use crate::{
},
e2e::E2ERegistry,
protocol::{self, Message},
tokio_transport::{TokioChannels, TokioSpawner, TokioTimer, TokioTransport},
traits::PayloadWireFormat,
transport::{
ChannelFactory, E2ERegistryHandle, MpscRecv, OneshotSend, Spawner,
UnboundedSend,
},
};
#[cfg(feature = "client-tokio")]
use crate::tokio_transport::{TokioChannels, TokioSpawner, TokioTimer, TokioTransport};

use super::error::Error;

Expand All @@ -42,7 +43,7 @@ const PENDING_RESPONSES_CAP: usize = 64;
/// two.
const UNICAST_SOCKETS_CAP: usize = 8;

pub(super) enum ControlMessage<P: PayloadWireFormat + 'static, C: ChannelFactory = TokioChannels> {
pub(super) enum ControlMessage<P: PayloadWireFormat + 'static, C: ChannelFactory> {
SetInterface(Ipv4Addr, C::OneshotSender<Result<(), Error>>),
BindDiscovery(C::OneshotSender<Result<(), Error>>),
UnbindDiscovery(C::OneshotSender<Result<(), Error>>),
Expand Down Expand Up @@ -308,9 +309,9 @@ pub(super) struct Inner<
/// Target interface for sockets
interface: Ipv4Addr,
/// Socket manager for service discovery if bound
discovery_socket: Option<SocketManager<PayloadDefinitions>>,
discovery_socket: Option<SocketManager<PayloadDefinitions, C>>,
/// Socket managers for unicast messages, keyed by local port
unicast_sockets: FnvIndexMap<u16, SocketManager<PayloadDefinitions>, UNICAST_SOCKETS_CAP>,
unicast_sockets: FnvIndexMap<u16, SocketManager<PayloadDefinitions, C>, UNICAST_SOCKETS_CAP>,
/// Per-sender SD session state for reboot detection
session_tracker: SessionTracker,
/// Registry of known service endpoints (auto-populated from SD + manual)
Expand Down Expand Up @@ -528,7 +529,7 @@ where
}

async fn receive_discovery(
socket_manager: &mut Option<SocketManager<PayloadDefinitions>>,
socket_manager: &mut Option<SocketManager<PayloadDefinitions, C>>,
) -> Result<
(
SocketAddr,
Expand Down Expand Up @@ -563,7 +564,7 @@ where
async fn receive_any_unicast(
unicast_sockets: &mut FnvIndexMap<
u16,
SocketManager<PayloadDefinitions>,
SocketManager<PayloadDefinitions, C>,
UNICAST_SOCKETS_CAP,
>,
) -> Result<ReceivedMessage<PayloadDefinitions>, Error> {
Expand Down Expand Up @@ -1135,7 +1136,7 @@ mod tests {
use tokio::sync::{mpsc, oneshot};
use tokio::sync::mpsc::Sender;

type TestControl = ControlMessage<TestPayload>;
type TestControl = ControlMessage<TestPayload, TokioChannels>;

#[test]
fn test_control_message_constructors() {
Expand Down
72 changes: 52 additions & 20 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,42 @@
//! (either a `static` or a heap allocator); the capacity constants plus
//! [`crate::UDP_BUFFER_SIZE`] are the knobs for trimming this footprint.
mod error;
#[cfg(feature = "client-tokio")]
mod inner;
#[cfg(feature = "client-tokio")]
mod service_registry;
#[cfg(feature = "client-tokio")]
mod session;
#[cfg(feature = "client-tokio")]
mod socket_manager;

pub use error::Error;

#[cfg(feature = "client-tokio")]
use crate::Timer;
use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile, E2ERegistry};
use crate::e2e::E2ECheckStatus;
#[cfg(feature = "client-tokio")]
use crate::e2e::{E2EKey, E2EProfile, E2ERegistry};
#[cfg(feature = "client-tokio")]
use crate::tokio_transport::{TokioChannels, TokioSpawner, TokioTimer};
use crate::transport::{
ChannelFactory, E2ERegistryHandle, InterfaceHandle, MpscSend, OneshotRecv, Spawner,
UnboundedRecv,
};
use crate::transport::{ChannelFactory, OneshotRecv, UnboundedRecv};
#[cfg(feature = "client-tokio")]
use crate::transport::{E2ERegistryHandle, InterfaceHandle, MpscSend, Spawner};
use crate::{protocol, protocol::Message, traits::PayloadWireFormat};
#[cfg(feature = "client-tokio")]
use inner::{ControlMessage, Inner};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::net::SocketAddr;
#[cfg(feature = "client-tokio")]
use std::net::{Ipv4Addr, SocketAddrV4};
#[cfg(feature = "client-tokio")]
use std::sync::{Arc, Mutex, RwLock};
#[cfg(feature = "client-tokio")]
use tracing::info;

/// Handle to a pending SOME/IP request-response transaction.
/// Resolves when the inner loop receives a matching unicast reply.
/// Does not borrow `Client`.
pub struct PendingResponse<P: Send + 'static, C: ChannelFactory = TokioChannels> {
pub struct PendingResponse<P: Send + 'static, C: ChannelFactory> {
receiver: C::OneshotReceiver<Result<P, Error>>,
}

Expand Down Expand Up @@ -144,7 +156,7 @@ impl<P: PayloadWireFormat> std::fmt::Debug for ClientUpdate<P> {
///
/// Returned by [`Client::new`]. Call [`recv`](Self::recv) to receive
/// discovery, unicast, and error updates.
pub struct ClientUpdates<MessageDefinitions: PayloadWireFormat + 'static, C: ChannelFactory = TokioChannels> {
pub struct ClientUpdates<MessageDefinitions: PayloadWireFormat + 'static, C: ChannelFactory> {
update_receiver: C::UnboundedReceiver<ClientUpdate<MessageDefinitions>>,
}

Expand Down Expand Up @@ -178,18 +190,20 @@ impl<MessageDefinitions: PayloadWireFormat + 'static, C: ChannelFactory> ClientU
/// (`Arc<Mutex<E2ERegistry>>` and `Arc<RwLock<Ipv4Addr>>`) are used by the
/// standard constructors [`Self::new`] / [`Self::new_with_loopback`] /
/// [`Self::new_with_spawner_and_loopback`].
#[cfg(feature = "client-tokio")]
#[derive(Clone)]
pub struct Client<
MessageDefinitions: PayloadWireFormat + Send + 'static,
R: E2ERegistryHandle = Arc<Mutex<E2ERegistry>>,
I: InterfaceHandle = Arc<RwLock<Ipv4Addr>>,
C: ChannelFactory = TokioChannels,
R: E2ERegistryHandle,
I: InterfaceHandle,
C: ChannelFactory,
> {
interface: I,
control_sender: C::BoundedSender<inner::ControlMessage<MessageDefinitions, C>>,
e2e_registry: R,
}

#[cfg(feature = "client-tokio")]
impl<MessageDefinitions, R, I, C> std::fmt::Debug for Client<MessageDefinitions, R, I, C>
where
MessageDefinitions: PayloadWireFormat + Send + 'static,
Expand All @@ -204,7 +218,13 @@ where
}
}

/// Constructors that create the default `Arc`-backed handles for `std + tokio`.
/// Convenience constructors that default to `Arc<Mutex<_>>` / `Arc<RwLock<_>>`
/// handles, the `TokioChannels` channel factory, and the `TokioSpawner` task
/// submitter. Available under the `client-tokio` feature, which pulls in
/// `tokio` + `socket2`. Bare-metal callers use
/// [`Self::new_with_spawner_and_loopback`] (always available under `client`)
/// and supply their own channel factory + spawner.
Comment on lines +224 to +226

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc comment suggests bare-metal callers can use Client::new_with_spawner_and_loopback “always available under client”, but the Client type itself is #[cfg(feature = "client-tokio")] (and the impl block here is also client-tokio). Please adjust the wording to avoid implying Client is usable under client/bare-metal today (e.g., point bare-metal users at the transport trait surface instead).

Suggested change
/// `tokio` + `socket2`. Bare-metal callers use
/// [`Self::new_with_spawner_and_loopback`] (always available under `client`)
/// and supply their own channel factory + spawner.
/// `tokio` + `socket2`.
///
/// Bare-metal / non-`tokio` integrations should use the `transport` trait
/// surface directly today; this `Client` type and its constructors are gated
/// on `client-tokio`.

Copilot uses AI. Check for mistakes.
#[cfg(feature = "client-tokio")]
impl<MessageDefinitions>
Client<MessageDefinitions, Arc<Mutex<E2ERegistry>>, Arc<RwLock<Ipv4Addr>>, TokioChannels>
where
Expand All @@ -228,7 +248,7 @@ where
/// # use simple_someip::{Client, RawPayload};
/// # use std::net::Ipv4Addr;
/// # async fn demo() {
/// let (client, mut updates, run) = Client::<RawPayload>::new(Ipv4Addr::LOCALHOST);
/// let (client, mut updates, run) = Client::<RawPayload, _, _, _>::new(Ipv4Addr::LOCALHOST);
/// let _run_task = tokio::spawn(run);
/// // ...interact with `client` and `updates`...
/// # let _ = (client, updates);
Expand All @@ -239,7 +259,7 @@ where
interface: Ipv4Addr,
) -> (
Self,
ClientUpdates<MessageDefinitions>,
ClientUpdates<MessageDefinitions, TokioChannels>,
impl core::future::Future<Output = ()> + Send + 'static,
) {
Self::new_with_loopback(interface, false)
Expand Down Expand Up @@ -274,7 +294,7 @@ where
multicast_loopback: bool,
) -> (
Self,
ClientUpdates<MessageDefinitions>,
ClientUpdates<MessageDefinitions, TokioChannels>,
impl core::future::Future<Output = ()> + Send + 'static,
) {
Self::new_with_spawner_and_loopback(interface, multicast_loopback, TokioSpawner)
Expand All @@ -293,7 +313,7 @@ where
/// # fn spawn(&self, _: impl core::future::Future<Output = ()> + Send + 'static) {}
/// # }
/// let (client, mut updates, run) =
/// Client::<RawPayload>::new_with_spawner_and_loopback(
/// Client::<RawPayload, _, _, _>::new_with_spawner_and_loopback(
/// Ipv4Addr::LOCALHOST,
/// false,
/// MySpawner,
Expand Down Expand Up @@ -345,6 +365,7 @@ where
}

/// Methods available on all `Client<M, R, I, C>` regardless of handle types.
#[cfg(feature = "client-tokio")]
impl<MessageDefinitions, R, I, C> Client<MessageDefinitions, R, I, C>
where
MessageDefinitions: PayloadWireFormat + Clone + std::fmt::Debug + 'static,
Expand Down Expand Up @@ -737,6 +758,7 @@ where
/// because it requires `tokio::sync::mpsc::Sender::downgrade()` for the
/// weak-sender shutdown pattern. A bare-metal alternative would need a
/// different lifecycle mechanism (phase-future).
#[cfg(feature = "client-tokio")]
impl<MessageDefinitions, R, I> Client<MessageDefinitions, R, I, TokioChannels>
where
MessageDefinitions: PayloadWireFormat + Clone + std::fmt::Debug + 'static,
Expand Down Expand Up @@ -769,9 +791,18 @@ where
/// (via `shut_down()` or going out of scope).
///
/// ```no_run
/// # use simple_someip::{Client, RawPayload, VecSdHeader};
/// # use simple_someip::{Client, RawPayload, TokioChannels, VecSdHeader};
/// # use simple_someip::protocol::sd::{self, RebootFlag, Flags};
/// # async fn demo(client: Client<RawPayload>) {
/// # use std::sync::{Arc, Mutex, RwLock};
/// # use std::net::Ipv4Addr;
/// # async fn demo(
/// # client: Client<
/// # RawPayload,
/// # Arc<Mutex<simple_someip::e2e::E2ERegistry>>,
/// # Arc<RwLock<Ipv4Addr>>,
/// # TokioChannels,
/// # >,
/// # ) {
/// let header = VecSdHeader {
/// flags: Flags::new_sd(RebootFlag::RecentlyRebooted),
/// entries: vec![],
Expand Down Expand Up @@ -877,14 +908,15 @@ where
}
}

#[cfg(test)]
#[cfg(all(test, feature = "client-tokio"))]
mod tests {
use super::*;
use crate::protocol::sd::test_support::{TestPayload, empty_sd_header};
use crate::traits::WireFormat;
use std::format;

type TestClient = Client<TestPayload, Arc<Mutex<E2ERegistry>>, Arc<RwLock<Ipv4Addr>>>;
type TestClient =
Client<TestPayload, Arc<Mutex<E2ERegistry>>, Arc<RwLock<Ipv4Addr>>, TokioChannels>;

#[tokio::test]
async fn test_client_new_and_interface() {
Expand Down
Loading