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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

.claude/
CLAUDE.md
.DS_Store
Expand Down
27 changes: 15 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,24 @@ tracing-subscriber = "0.3"
[features]
default = ["std"]
std = ["embedded-io/std", "thiserror/std", "tracing/std"]
# Phase 13 split: `client` exposes the protocol/trait-surface client
# Phase 13a 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"]
server = ["std", "dep:tokio", "dep:socket2", "dep:futures"]
# Phase 14b split (matches phase 13a on the client side): `server`
# exposes the trait-surface server (no tokio, no socket2). The engine
# itself uses `futures::select!` so `dep:futures` lives here.
# `server-tokio` adds the tokio + socket2 convenience defaults
# (`Server::new`, `Server::new_with_loopback`, `Server::new_passive`),
# bringing `Arc<Mutex<E2ERegistry>>` / `Arc<RwLock<SubscriptionManager>>`
# / `TokioTransport` / `TokioTimer` defaults into scope.
server = ["std", "dep:futures"]
server-tokio = ["server", "dep:tokio", "dep:socket2"]
# Marks a build as intended for bare-metal / no_std consumption.
# Currently a pure marker — enables no crate code on its own. Reserved
# for future phases to gate no_std-specific helper types.
Expand All @@ -96,7 +95,7 @@ bare_metal = ["dep:embassy-sync"]

[[test]]
name = "client_server"
required-features = ["client-tokio", "server"]
required-features = ["client-tokio", "server-tokio"]

[[test]]
name = "bare_metal_client"
Expand All @@ -105,3 +104,7 @@ required-features = ["client", "bare_metal"]
[[test]]
name = "static_channels_alloc_witness"
required-features = ["client", "bare_metal"]

[[test]]
name = "bare_metal_server"
required-features = ["server", "bare_metal"]
45 changes: 30 additions & 15 deletions examples/bare_metal/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,30 @@
//! `EmbassySyncChannels` extracted from `tokio_transport` to
//! `crate::embassy_channels` so it is reachable from no-tokio builds.
//!
//! - Phase 14a (server feature-flag detangle): `server` is now a
//! topology marker; `server-tokio` carries the working tokio-backed
//! server. The strategic-goal feature combo
//! `default-features = false, features = ["bare_metal", "client", "server"]`
//! now compiles, though the `server` half is empty until 14b
//! retargets the engine.
//! - Phase 14b: `Server` is now constructible without
//! `server-tokio`. The engine carries `F: TransportFactory`,
//! `Tm: Timer`, `R: E2ERegistryHandle`, and `S: SubscriptionHandle`
//! generics, and the new `Server::new_with_deps` /
//! `Server::new_passive_with_deps` constructors take everything
//! explicitly via a `ServerDeps` bundle. The tokio convenience
//! constructors (`Server::new`, `Server::new_with_loopback`,
//! `Server::new_passive`) live behind the `server-tokio` feature
//! and delegate to `new_with_deps`. Witness:
//! `tests/bare_metal_server.rs` (gated on `server + bare_metal`).
//!
//! **Remaining gaps:**
//! 1. **Server-side feature-flag split** (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 retargets the server to
//! the trait surface; once that lands, `server` will gain the same
//! `server` + `server-tokio` split.
//! 2. **No-alloc Client**: `Client` / `Inner` still depend on
//! `alloc` (heapless internals are fine, but `EmbassySyncChannels`
//! uses `Arc`, and `e2e_registry` uses `Arc<Mutex<_>>`). Phase 16
//! is the verification phase that lights up an alloc-panicking
//! harness; the no-alloc port itself is its own follow-on phase.
//! 1. **No-alloc Client/Server**: `Client` / `Server` engines still
//! depend on `alloc` (heapless internals are fine, but
//! `EmbassySyncChannels` uses `Arc`, and `e2e_registry` uses
//! `Arc<Mutex<_>>`). Phase 13.6 (static-pool ChannelFactory) is
//! the engine fix; phase 16 is the CI verification that lights up
//! an alloc-panicking harness.
//!
//! # Recommendation for `no_alloc` consumers today
//!
Expand Down Expand Up @@ -432,8 +443,12 @@ fn main() {
"note: trait layer (TransportSocket + TransportFactory + Timer + \
Spawner + ChannelFactory) exercised end-to-end. Phases 9-12 \
complete; phases 13a + 13.5 (client + Client engine generic) \
complete. Remaining: phase 14 server-trait retargeting + \
server-side `server-tokio` split, then phase 16 no-alloc \
verification. See top-of-file docblock."
complete; phase 14a (server feature topology) complete; \
phase 14b (Server engine generic over TransportFactory + \
Timer + E2ERegistryHandle + SubscriptionHandle, reachable \
via Server::new_with_deps under just `server`) complete — see \
tests/bare_metal_server.rs for the witness. Remaining: \
phase 13.6 static-pool ChannelFactory + phase 16 no-alloc \
CI verification. 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-tokio", "server"] }
simple-someip = { path = "../..", features = ["client-tokio", "server-tokio"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
tracing = "0.1"
tracing-subscriber = "0.3"
9 changes: 7 additions & 2 deletions src/client/socket_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,14 @@
//! socket2 on top of `client`.
//!
//! **Remaining gaps:**
//! - **Server-side split** (deferred to Phase 14): `feature = "server"`
//! still pulls tokio + socket2 because `server::sd_state` /
//! - **Working server without tokio** (Phase 14b): the bare `server`
//! feature is currently a topology marker only (Phase 14a, commit
//! `b7fc30f`). The actual server engine still requires
//! `server-tokio` because `server::sd_state` /
//! `server::subscription_manager` reference tokio types directly.
//! Phase 14b retargets the engine to the trait surface (mirroring
//! phase 13.5 on the client) so a working server lives under just
//! `server`.
Comment on lines +43 to +50

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

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

The “Remaining gaps” bullet still states that the bare server feature is only a topology marker and that a working server still requires server-tokio. In this PR the server engine has been retargeted to the transport/timer/subscription trait surface and server is now functional without tokio, so this doc comment is now inaccurate. Update or remove this bullet to reflect the current feature split/state (e.g. describe server as trait-surface and server-tokio as convenience defaults).

Copilot uses AI. Check for mistakes.
//!
//! For `no_alloc` SOME/IP usage today, consume `protocol`, `e2e`, and
//! the `transport` trait layer directly — the `bare_metal` example
Expand Down
31 changes: 19 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
//! | `std` | yes | Enables std-dependent helpers (`RawPayload`, `VecSdHeader`, `OfferedEndpoint`) |
//! | `client` | no | Trait-surface client; implies `std` + futures (no tokio) |
//! | `client-tokio` | no | Adds the `Client::new` / `TokioSpawner` / `TokioTransport` convenience defaults; implies `client` + tokio + socket2 |
//! | `server` | no | Async tokio server; implies `std` + tokio + socket2 + futures (server-tokio split deferred to phase 14) |
//! | `server` | no | Trait-surface server; implies `std` + futures (no tokio) |
//! | `server-tokio` | no | Adds the `Server::new` / `TokioTransport` / `TokioTimer` convenience defaults; implies `server` + tokio + socket2 |
//! | `bare_metal` | no | Pure marker — does not enable any crate code. See `examples/bare_metal/` (the trait-surface canary) for the full bare-metal-readiness story. |
//!
//! The default feature set is `["std"]`, which links `std` and enables
Expand Down Expand Up @@ -151,14 +152,22 @@ pub mod protocol;
#[cfg(feature = "std")]
mod raw_payload;
/// SOME/IP server for offering services and handling incoming requests.
///
/// Phase 14b: the engine is generic over [`transport::TransportFactory`] +
/// [`transport::Timer`] + [`transport::E2ERegistryHandle`] +
/// [`server::SubscriptionHandle`], so the bare `server` feature exposes the
/// trait-surface server. The `server-tokio` feature additionally provides
/// the tokio convenience constructors ([`server::Server::new`],
/// [`server::Server::new_with_loopback`], [`server::Server::new_passive`])
/// that default the type parameters to
/// `Arc<Mutex<E2ERegistry>>` / `Arc<RwLock<SubscriptionManager>>` /
/// `TokioTransport` / `TokioTimer`.
#[cfg(feature = "server")]
pub mod server;
/// Tokio + `socket2` implementation of the [`transport`] traits. Provided
/// as the default `std` backend — available whenever `client-tokio` or
/// `server` is enabled. (Phase 13: `client` is now no-tokio; the tokio
/// backend lives behind `client-tokio`. `server` still pulls tokio
/// transitively until phase 14 retargets it to the trait surface.)
#[cfg(any(feature = "client-tokio", feature = "server"))]
/// `server-tokio` is enabled.
#[cfg(any(feature = "client-tokio", feature = "server-tokio"))]
pub mod tokio_transport;

/// `embassy-sync`-backed implementation of [`transport::ChannelFactory`].
Expand All @@ -176,9 +185,9 @@ mod traits;
/// Executor-agnostic UDP transport abstraction used by the client and
/// server modules. `no_std`-compatible; a default `std + tokio` backend
/// ships in `tokio_transport` (available under the `client-tokio` /
/// `server` features) — the link is rendered as a code literal because
/// the target module is feature-gated and would break default-feature
/// rustdoc builds.
/// `server-tokio` features) — the link is rendered as a code literal
/// because the target module is feature-gated and would break
/// default-feature rustdoc builds.
pub mod transport;
#[cfg(feature = "std")]
pub use raw_payload::{RawPayload, VecSdHeader};
Expand All @@ -192,10 +201,8 @@ pub use client::{
};
pub use e2e::{E2ECheckStatus, E2EKey, E2EProfile};
#[cfg(feature = "server")]
pub use server::Server;
#[cfg(feature = "server")]
pub use server::SubscriptionHandle;
#[cfg(any(feature = "client-tokio", feature = "server"))]
pub use server::{Server, ServerDeps, SubscriptionHandle};
#[cfg(any(feature = "client-tokio", feature = "server-tokio"))]
pub use tokio_transport::{TokioChannels, TokioSocket, TokioSpawner, TokioTimer, TokioTransport};
pub use transport::{
ChannelFactory, E2ERegistryHandle, InterfaceHandle, IoErrorKind, MpscRecv, MpscSend,
Expand Down
4 changes: 4 additions & 0 deletions src/server/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ pub enum Error {
/// An I/O error from the underlying network transport.
#[error(transparent)]
Io(#[from] std::io::Error),
/// A transport-layer error from a [`crate::transport::TransportFactory`]
/// or [`crate::transport::TransportSocket`] operation.
#[error("transport error: {0}")]
Transport(#[from] crate::transport::TransportError),
/// An E2E protection or checking error occurred.
#[error(transparent)]
E2e(#[from] crate::e2e::Error),
Expand Down
90 changes: 64 additions & 26 deletions src/server/event_publisher.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
//! Event publishing functionality

use super::Error;
use super::subscription_manager::{SubscriptionHandle, SubscriptionManager};
use super::subscription_manager::SubscriptionHandle;
use crate::UDP_BUFFER_SIZE;
use crate::e2e::{E2EKey, E2ERegistry};
use crate::e2e::E2EKey;
use crate::protocol::{Header, Message};
use crate::traits::{PayloadWireFormat, WireFormat};
use crate::transport::E2ERegistryHandle;
use std::sync::{Arc, Mutex};
use tokio::net::UdpSocket;
use tokio::sync::RwLock;

/// Publishes events to subscribers
pub struct EventPublisher<
R: E2ERegistryHandle = Arc<Mutex<E2ERegistry>>,
S: SubscriptionHandle = Arc<RwLock<SubscriptionManager>>,
> {
use crate::transport::{E2ERegistryHandle, TransportSocket};
use std::sync::Arc;

/// Publishes events to subscribers.
///
/// Generic over `T: TransportSocket` (the socket primitive — `TokioSocket`
/// in the std/tokio path, a bare-metal embassy / smoltcp wrapper on
/// firmware), `R: E2ERegistryHandle`, and `S: SubscriptionHandle`.
pub struct EventPublisher<R, S, T>
where
R: E2ERegistryHandle,
S: SubscriptionHandle,
T: TransportSocket + Send + Sync + 'static,
{
subscriptions: S,
socket: Arc<UdpSocket>,
socket: Arc<T>,
e2e_registry: R,
}

impl<R: E2ERegistryHandle, S: SubscriptionHandle> EventPublisher<R, S> {
impl<R, S, T> EventPublisher<R, S, T>
where
R: E2ERegistryHandle,
S: SubscriptionHandle,
T: TransportSocket + Send + Sync + 'static,
{
/// Create a new event publisher
pub fn new(subscriptions: S, socket: Arc<UdpSocket>, e2e_registry: R) -> Self {
pub fn new(subscriptions: S, socket: Arc<T>, e2e_registry: R) -> Self {
Self {
subscriptions,
socket,
Expand Down Expand Up @@ -144,7 +153,7 @@ impl<R: E2ERegistryHandle, S: SubscriptionHandle> EventPublisher<R, S> {
let mut sent_count = 0;
for subscriber in &subscribers {
match self.socket.send_to(datagram, subscriber.address).await {
Ok(_) => {
Ok(()) => {
sent_count += 1;
tracing::trace!(
"Sent event to subscriber {} ({} bytes)",
Expand Down Expand Up @@ -258,7 +267,7 @@ impl<R: E2ERegistryHandle, S: SubscriptionHandle> EventPublisher<R, S> {
let mut sent_count = 0;
for subscriber in &subscribers {
match self.socket.send_to(datagram, subscriber.address).await {
Ok(_) => {
Ok(()) => {
sent_count += 1;
}
Err(e) => {
Expand Down Expand Up @@ -385,22 +394,55 @@ impl<R: E2ERegistryHandle, S: SubscriptionHandle> EventPublisher<R, S> {
}
}

#[cfg(test)]
#[cfg(all(test, feature = "server-tokio"))]
mod tests {
use super::*;
use crate::e2e::E2ERegistry;
use crate::protocol::sd::test_support::{TestPayload, empty_sd_header};
use crate::server::SubscriptionManager;
use crate::tokio_transport::TokioSocket;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::sync::Mutex;
use std::vec;
use std::vec::Vec;
use tokio::net::UdpSocket;
use tokio::sync::RwLock;

/// Type alias bringing the tokio-flavor concrete type parameters back
/// into scope so tests can spell `TestEventPublisher` without
/// chasing the three-type-parameter signature on every call site.
type TestEventPublisher = EventPublisher<
Arc<Mutex<E2ERegistry>>,
Arc<RwLock<SubscriptionManager>>,
TokioSocket,
>;

fn test_registry() -> Arc<Mutex<E2ERegistry>> {
Arc::new(Mutex::new(E2ERegistry::new()))
}

/// Bind a `TokioSocket` for tests. The publisher path under
/// `server-tokio` already depends on `tokio_transport`, so we use it
/// directly rather than constructing a `tokio::net::UdpSocket` and
/// adapting it.
async fn bind_tokio_socket() -> Arc<TokioSocket> {
use crate::transport::{SocketOptions, TransportFactory};
let factory = crate::tokio_transport::TokioTransport;
Arc::new(
factory
.bind(
SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0),
&SocketOptions::new(),
)
.await
.expect("bind tokio socket for test"),
)
}

async fn make_publisher(
subscriptions: Arc<RwLock<SubscriptionManager>>,
) -> (EventPublisher, Arc<UdpSocket>) {
let socket = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
) -> (TestEventPublisher, Arc<TokioSocket>) {
let socket = bind_tokio_socket().await;
let publisher = EventPublisher::new(subscriptions, Arc::clone(&socket), test_registry());
(publisher, socket)
}
Expand All @@ -412,11 +454,7 @@ mod tests {
#[tokio::test]
async fn test_event_publisher_creation() {
let subscriptions = Arc::new(RwLock::new(SubscriptionManager::new()));
let socket = Arc::new(
UdpSocket::bind("127.0.0.1:0")
.await
.expect("Failed to bind socket"),
);
let socket = bind_tokio_socket().await;

let publisher = EventPublisher::new(subscriptions, socket, test_registry());
assert!(std::mem::size_of_val(&publisher) > 0);
Expand Down Expand Up @@ -579,7 +617,7 @@ mod tests {
.unwrap();
}

let socket = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
let socket = bind_tokio_socket().await;
let publisher = EventPublisher::new(subscriptions, socket, e2e_registry);

// Size the payload from `UDP_BUFFER_SIZE` and `PROFILE4_HEADER_SIZE`
Expand Down
Loading