Repository navigation
simple-someip-v0.8.0
Breaking — bare-metal server buffer extraction (PR #125 / PR 3)
These changes are free before the 0.8.0 release — no published version
exposes the pre-refactor API.
-
Server::run_with_bufferstakes two additional send-scratch buffers —
the signature now requiresrecv_send_buf: &mut [u8]and
announce_send_buf: &mut [u8]after the existingunicast_buf/sd_buf
receive buffers. Callers that used the pre-refactor four-buffer form must
add two morestatic [u8; N]arguments (or equivalent heap slices). The
_allocconvenienceServer::runis unchanged. -
New
Server::announce_only_with_buffer— bare-metal supplementary
servers that need only the SDOfferServiceannouncement loop (no recv)
now call this method instead ofannounce_only_future. It accepts a
caller-owned&mut [u8]scratch so the future does NOT park a
[u8; UDP_BUFFER_SIZE](≈ 1500 B) in its own state.
announce_only_future(alloc-only) now delegates to this method internally
and carries a#[cfg(feature = "_alloc")]gate. -
EventPublisher::publish_event_with_buffers/
publish_raw_event_with_bufferstake caller scratch — the two methods
now accept explicitmsg_buf: &mut [u8]/protected_buf: &mut [u8]
slices. The future no longer parks two[u8; UDP_BUFFER_SIZE]arrays;
bare-metal callers supplystaticbuffers. The_allocwrappers
publish_event/publish_raw_eventare unchanged.
Client/Server API symmetry & ergonomics
The 0.8.0 ergonomics pass aligning the public Client and Server surfaces, removing the tokio-only Server::new cliff, and improving discoverability for bare-metal adopters. Six bundled changes: generic-parameter alignment, tokio-defaulted Deps builders, channel-types rustdoc, ServerConfig fluent builder, Server::new constructor reshape (the one breaking change in this set), and SubscriptionHandle GAT promotion. Migration shapes below are written against the previous published version (0.7.0); cargo build will surface every remaining call-site.
Breaking — SubscriptionHandle::SubscribeFuture / UnsubscribeFuture are now generic associated types
The subscribe and unsubscribe methods on SubscriptionHandle previously returned impl Future<…> + '_ (return-position impl Trait). They are now named GATs:
pub trait SubscriptionHandle: Clone + 'static {
type SubscribeFuture<'a>: Future<Output = Result<(), SubscribeError>> + 'a where Self: 'a;
type UnsubscribeFuture<'a>: Future<Output = ()> + 'a where Self: 'a;
fn subscribe(...) -> Self::SubscribeFuture<'_>;
fn unsubscribe(...) -> Self::UnsubscribeFuture<'_>;
// for_each_subscriber stayed RPIT — no Server::run-side bound needs it.
}Implementors must now spell their concrete return type (typically Pin<Box<dyn Future<Output = …> + Send + 'a>>). All four in-tree implementations (Arc<RwLock<SubscriptionManager>>, StaticSubscriptionHandle, the example InMemorySubscriptions, three test MockSubscriptions variants) were converted; downstream implementors will hit a compile error that names the missing associated types verbatim.
Why this is worth a breaking change: it lets [Server::run]'s where clause spell for<'a> Sub::SubscribeFuture<'a>: Send, which in turn lets the function declare + Send on its return type instead of relying on auto-trait inference. Compile errors for tokio::spawn-vs-!Send-handle mismatches now surface at the library boundary instead of deep inside tokio::spawn's bound check.
Migration
Before:
impl SubscriptionHandle for MyHandle {
fn subscribe(&self, ...) -> impl Future<Output = Result<(), SubscribeError>> + '_ {
async move { /* … */ }
}
fn unsubscribe(&self, ...) -> impl Future<Output = ()> + '_ {
async move { /* … */ }
}
}After:
impl SubscriptionHandle for MyHandle {
type SubscribeFuture<'a> =
Pin<Box<dyn Future<Output = Result<(), SubscribeError>> + Send + 'a>>;
type UnsubscribeFuture<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
fn subscribe(&self, ...) -> Self::SubscribeFuture<'_> {
Box::pin(async move { /* … */ })
}
fn unsubscribe(&self, ...) -> Self::UnsubscribeFuture<'_> {
Box::pin(async move { /* … */ })
}
}Drop + Send from the type aliases on !Send handles (rare). The Box::pin allocation happens at SD-rate (typically ≤ 1 Hz subscribes during steady-state SD churn), small cost relative to the wire activity it gates.
Breaking — Server::new now returns a (Server, ServerHandles, run-future) tuple
Server::new (and the new_with_loopback, new_passive, new_with_deps, new_passive_with_deps variants) now mirrors Client::new's shape: the constructor returns a three-tuple of (Server, ServerHandles, impl Future + 'static) instead of just Server. The single returned run-future drives the receive loop and the SD OfferService announcement loop concurrently, so callers no longer have to remember to spawn announcement_loop separately. The runtime check that used to live on Server::announcement_loop ("called on passive server" → Err) is now structurally impossible because the announcement loop has no separate entry point. The dispatcher topology where a co-located Client drives SD announcements is now opted into via [ServerConfig::with_announce(false)] instead of "just don't call this method".
Server::new_with_handles and Server::new_passive_with_handles are unchanged — bare-metal callers still get back Self and call server.run_with_buffers(unicast, sd) directly with their own static buffers. The combined receive + announce select runs in run_with_buffers too.
The previous ServerHandles type (the no-alloc deps bundle accepted by new_with_handles) was renamed to ServerStorage to free the ServerHandles name for the new post-construction accessor struct returned from Server::new. The eight fields are unchanged.
Migration
Before:
let mut server = Server::new(config).await?;
let publisher = server.publisher();
tokio::spawn(server.announcement_loop()?);
tokio::spawn(async move {
if let Err(e) = server.run().await { /* … */ }
});After:
let (_server, handles, run) = Server::new(config).await?;
let publisher = handles.publisher;
tokio::spawn(async move {
if let Err(e) = run.await { /* … */ }
});Bare-metal-no-alloc, before:
let server = Server::new_with_handles(deps, config)?;
let announce = server.announcement_loop_local()?;
spawn_local(announce);
spawn_local(server.run_with_buffers(unicast_buf, sd_buf));After:
let server = Server::new_with_handles(deps, config)?;
spawn_local(server.run_with_buffers(unicast_buf, sd_buf));(run_with_buffers is now the combined receive + announce future.)
Dispatcher topology (was: implicit "just don't call announcement_loop") becomes explicit:
let config = ServerConfig::new(svc, inst).with_announce(false);
let (_server, _handles, run) = Server::new(config).await?;
tokio::spawn(run); // receive only; co-located Client drives SDBreaking — NonSdRequestCallback is now a parsed, ctx-carrying callback
// before
pub type NonSdRequestCallback = fn(data: &[u8], source: SocketAddrV4);
// after
pub type NonSdRequestCallback = fn(
ctx: usize,
source: core::net::SocketAddrV4,
service_id: u16,
method_id: u16,
payload: &[u8],
e2e_status: u8,
);The observer is now registered as a (callback, ctx) pair
(ServerDeps::non_sd_observer: Option<(NonSdRequestCallback, usize)>),
and ctx is passed back verbatim on every invocation. recv_loop
parses the SOME/IP header so consumers receive decoded
(service_id, method_id, payload) and never hand-roll parsing — the
parse stays in the audited crate per MISRA/ASIL rather than being
replicated in N C consumers. payload is the bytes after the 16-byte
header. e2e_status is 0 (unchecked) — server-side request E2E is
not applied today. usize (rather than a stored *mut c_void) keeps
Server: Send — and therefore Server::run's declared + Send bound
— with no unsafe in this crate; the pointer cast lives in the
consumer's callback body. FFI consumers stash their state pointer as
usize; pure-Rust callers pass 0.
Migration
non_sd_observer: Some(my_cb) → non_sd_observer: Some((my_cb, 0)),
and replace the leading ctx: usize parameter plus manual header parsing
with the decoded arguments (ctx, source, service_id, method_id, payload, e2e_status). Registration becomes Some((my_cb, 0)).
Removed
Server::announcement_loop/Server::announcement_loop_local— folded into the combined run-future. Theannouncement_loop_started: AtomicBoollatch that protected against two simultaneously-driven announcement futures is gone with them (single entry point makes the failure mode structurally impossible).Server::set_local_port— vestigial. The bind-time back-fill onnew_with_deps/new_with_handlesalready records the kernel-assigned port intoconfig.local_portbeforeServer::newreturns, and mutating it post-construction would lie to peers about an endpoint the unicast socket isn't actually bound to (the same failure modenew_with_handlesrejects withlocal_port_mismatch). Both in-tree call sites were no-ops.
Added (additive — no migration)
ServerHandles<Hep>— post-construction accessor bundle returned alongsideServerfromServer::new. Single public field today:publisher. Reserved for future fields (e.g. aBindCompletedoneshot if a real adopter asks).ServerConfig::announce: bool+with_announce(bool)fluent setter. Defaults totrue. Set tofalsefor the dispatcher topology.ServerStorage<F, Tm, R, Sub, H, Hsd, Hep>(replaces the oldServerHandlesdeps bundle name).- 21a — generic-parameter alignment across
Client,Server,ClientDeps,ServerDeps. Letter-collisions between Spawner (Son Client) and SubscriptionHandle (Son Server) resolved (SpandSubrespectively). - 21c — tokio-defaulted
Depsbuilders.ClientDeps::tokio()andServerDeps::tokio()produce a fully-defaulted bundle; chainwith_factory/with_timer/with_e2e_registry/with_subscriptions/with_spawner/with_local_spawnerto override individual fields without spelling the rest out. - 21d — discoverable channel types. New
ClientChannelTypestrait alias surfaces the set of channel types thatdefine_static_channels!must populate;client::channelsre-exports the relevant types with rustdoc that names each as "channel type fordefine_static_channels!." - 21e —
ServerConfigfluent builder. Existing struct-literal route stays open;ServerConfig::new(svc, inst).with_interface(…).with_local_port(…).with_ttl(…).with_event_group(…).with_announce(…)is the recommended path in docs.
Added — correctness & no_std cleanup
simple-someip-embassy-net::LINK_MTU—pub const usize = 1500shared by the loopback driver and example consumers for sizingSocketPoolRX/TX buffers andCapabilities::max_transmission_unit. Distinct fromsimple_someip::UDP_BUFFER_SIZE(an application-payload cap) — they coincide at 1500 today but are conceptually orthogonal.- Per-package pedantic clippy CI gates for
simple-someipunderclient+bare_metal,server+bare_metal, andclient+server+bare_metal. The pre-existing--workspace --all-featuresgate is feature-unified and could mask feature-set regressions; per-package gates surface a regression against its responsible feature flag.
Changed — correctness & no_std cleanup
SocketOptionsdocs — explicit Linux-side guidance that the SD socket needs bothSO_REUSEADDRandSO_REUSEPORT(Linux ties multicast-group membership to the REUSEPORT group).SdStateManager::with_initialandnext_session_id_with_reboot_flaglifted frompub(super)topubso external test harnesses can pre-seed counter state and validate wrap-around behaviour without a full Server lifecycle. The remaining racy accessors staypub(super) + cfg(test).Server::new_with_handles/new_passive_with_handles— back-fillconfig.local_portonly when the caller passed0; returnError::InvalidUsageon a non-zero mismatch with the unicast socket's bound port. Matchesnew_with_deps's back-fill-only-on-zero discipline.SubscriptionManager::get_subscribers— cfg widened fromfeature = "std"to internalfeature = "_alloc"and return type fromstd::vec::Vectoalloc::vec::Vec. Reachable inembassy_channelsand pure-server,bare_metalbuilds where alloc is in scope.OfferedEndpoint— re-exported unconditionally (previouslycfg(feature = "std")). The trait methodPayloadWireFormat::for_each_offered_endpointthat surfaces it is unconditional.tokio_transport::TokioSpawner::spawn— single tokio task per spawn (was 2: work future + JoinHandle watcher). Panic logging now lives insidePanicLoggingFutviastd::panic::catch_unwind.Server::run_with_buffersdoc example — replaced unsound&mut UNICAST_BUFonstatic mut(hard error in Rust 2024) with astatic UnsafeCell<[u8; …]>+unsafe impl Syncpattern.- Three event-loop sites (
client/inner.rs,client/socket_manager.rs,server/mod.rs) — comments referencedselect!while the code usedselect_biased!. Server and socket_manager's 2-arm selects now flip arm priority each iteration to approximate the fairnessselect!would give without pullingstd. Comments rewritten to match.
Fixed — correctness & no_std cleanup
tools/size_probe::someip_header_encode—MessageType::try_from(byte & 0xBF)masked off bit 6 before validation (0x40silently coerced toRequest); switched toMessageTypeField::try_from(byte). The encoder also ignored the caller'slengthfield and hardcodedpayload_len = 0; now derivespayload_len = length - 8withchecked_sub.simple-someip-embassy-net::EmbassyNetFactory— dropped a bogus'poollifetime parameter and an identity-onlymem::transmute<&SocketPool, &'static>. Factory now takes&'static SocketPooldirectly. Marked!Send + !SyncviaPhantomData<*const ()>because embassy-net'sStackinteriorRefCellis not safe to drivebind()on from multiple threads.simple-someip-embassy-net::EmbassyNetSocket::local_addr/EmbassyNetFactory::bind—bind()now honoursaddr.ip()(previously ignored) and reads the actual bound port back fromsocket.endpoint()post-bind, so port-0 ephemeral binds report the real port instead of:0.tools/size_probe— excluded from[workspace], given its own empty[workspace]table.cargo clippy --workspace --all-featuresno longer tripsE0152against the probe's#[panic_handler]/#[global_allocator].extern crate alloccfg — tied to a single internal_allocfeature implied byserver,embassy_channels, andstd. The previouscfg(any(feature = "embassy_channels", feature = "server"))was right by accident and silently omitted std-only flavours.- CI alloc-symbol audit — pinned the rlib path (was nondeterministic via
find | head); replacedrm -fof the rlib (which doesn't invalidate cargo's fingerprint cache) withcargo clean -p simple-someip --target …; droppednm 2>/dev/nullso a tool failure stops surfacing as0alloc references. - vsomeip TX-conformance test — captures TWO consecutive announcements; asserts exact TTL (3 s default), session-ID monotonicity,
RebootFlag::RecentlyRebootedon both announcements (the flag staysRecentlyRebooteduntil the session counter wraps0xFFFF→0x0001, which two announcements don't reach — the wrap transition itself is covered by theSdStateManagerunit tests), exactly one SD entry / one SD option /(first_options, second_options) == (1, 0)per OfferService entry. Previously asserted onlyttl > 0. - vsomeip RX-conformance test — verifies vsomeip's OfferService carries an IPv4 endpoint option with the expected
(port=30509, UDP). A parser regression dropping options would have passed the old entry-only check. tests/data/vsomeip-offerer/subscriber.json—clients[].unreliablewas 30509, mirroring offerer.json instead of matching the simple-someip Server'sADVERTISED_PORT = 30500that subscriber.json is paired with. Fixed to 30500.- vsomeip module docs — referred to multicast group
224.0.23.0(vsomeip spec default) while simple-someip and offerer.json both use239.255.0.255. Updated. SocketAddrV4IP wildcard handling in embassy-net adapter —socket.bind(addr.port())was passing only the port, ignoring caller's IP. Now passes a fullIpListenEndpointwithaddr: Nonefor0.0.0.0(smoltcp's wildcard) or the explicit IPv4 otherwise.RecvError::Truncated→Err(Io(Other))mapping — documented at the call site why this is a deliberate adapter choice (embassy-net 0.4 doesn't deliver bytes on truncation and doesn't surface the original datagram length, so we can't honour the trait'struncated: truecontract truthfully).- Doc-link rot —
Self::reboot_flag(cfg(test)-only),Self::for_each_subscriber(lives on the trait),EventPublisherHandle(collapsed intoSharedHandlein 19f / 20e),E2ERegistryFull(needscrate::e2e::prefix),futures::future::BoxFuture(futures crate not a direct dep). Allcargo doc --no-depspartial-feature gates clean. - Embassy-net loopback test rename pretext —
client_send_request_server_runloop_stablewas vacuous (passive server'srun()returnsErr(InvalidUsage)immediately). Removed the no-op spawn and rewrote the doc to honestly describe what the test verifies (the client's send path). - Adversarial-pass micro-issues:
payload_len + 12/payload_len + 432-bit wrap in size_probe (nowchecked_add);PanicAllocator→NullAllocator(it returns null, doesn't panic);EmbassyNetBindFuture::pollpanicked on second poll (now wrapscore::future::Readyfor stdlib panic message + standard semantics);EventPublisher'sPhantomData<T>→PhantomData<fn() -> T>(no redundantSend + Syncre-imposition).
Added — 0.7.0 → 0.8.0 surface baseline
client::Error::Capacity(&'static str)— new variant returned when a fixed-capacity internal structure is full. Current tags:"unicast_sockets","udp_buffer","pending_responses","request_queue". Becauseclient::Erroris not#[non_exhaustive], this is a breaking change for downstream crates that match the enum exhaustively.client::Error::Transport(crate::transport::TransportError)— new variant surfacing failures from the pluggable transport backend (#[from]-converted, displays transparently). Same exhaustive-match caveat as above.client::Error::Shutdown— new variant returned by everyClientmethod when the control channel is closed (run-loop future was dropped, cancelled, or exited). Replaces the previous.unwrap()-on-closed-channel panic path.server::SubscribeError— new public enum (SubscribersPerGroupFull,EventGroupsFull) returned bySubscriptionManager::subscribeandEventPublisher::register_subscriberwhen a bounded capacity rejects a subscription. Re-exported fromserver::mod.Client::new_with_loopback(interface, multicast_loopback)— constructor that exposes the previously-internalmulticast_loopbackknob for same-host integration tests.Client::new_with_spawner_and_loopback(interface, multicast_loopback, spawner)— executor-agnostic constructor that accepts a caller-suppliedSpawnerimpl. Bare-metal callers swapTokioSpawnerfor their own task pool.Client::new_with_deps_local— constructor for single-threaded /!Sendexecutors. Accepts aLocalSpawnerinstead ofSpawnerand relaxes theSendbound on the transport socket.transport::Spawnertrait (re-exported assimple_someip::Spawner) — executor-agnostic task-spawn abstraction.tokio_transport::TokioSpawneris the defaultstd + tokioimpl.transport::LocalSpawnertrait — single-threaded task-spawn abstraction for!Sendfutures. Enables use on runtimes liketokio::LocalSetor embassy's single-threaded executor.transport::TransportSocket/TransportFactory/Timertraits — executor-agnostic UDP transport abstraction. Defaulttokio_transport::TokioTransport/TokioSocket/TokioTimerimpls available behind theclient-tokio/server-tokiofeatures.bare_metalcargo feature — activates embassy-sync as the channel backend and enables thestatic_channelsmodule,AtomicInterfaceHandle,StaticE2EHandle, andStaticSubscriptionHandletypes. All four are pureno_std(no allocator required). The heap-backedEmbassySyncChannelsfactory is separately gated by theembassy_channelsfeature (which impliesbare_metal). Seeexamples/bare_metal_client/andexamples/bare_metal_server/for runnable integration examples. Validate withcargo build -p bare_metal_client/cargo build -p bare_metal_server, NOTcargo build --workspace(workspace builds may unify features and mask regressions).SubscriptionManager::subscribereturning aResult— see "Changed" below; the regression test list now exercises the major-version mismatch path explicitly.StaticSubscriptionHandle+StaticSubscriptionStorage— no-allocSubscriptionHandleimpl backed by&'static BlockingMutex<CriticalSectionRawMutex, RefCell<SubscriptionManager>>. The bare-metal counterpart toArc<RwLock<SubscriptionManager>>.SubscriptionManager::new()is nowconst, so the storage can live in a plainstatic(noBox::leak). Gated onfeature = "bare_metal", re-exported fromserver::*.server::Error::InvalidUsage(&'static str)— new variant forServerAPI misuse paths. Currently emitted with the tags"passive_server_announcement_loop","announcement_loop_already_started", and"passive_server_run". Replaces the previousError::Io(std::io::Error::new(InvalidInput, ..))paths so these errors are reachable on no_std builds.E2ERegistryFull— new typed error returned byE2ERegistry::register(and propagated throughE2ERegistryHandle::register/Client::register_e2e/Server::register_e2e) when the fixed-capacity registry is at itsE2E_REGISTRY_CAPlimit. Replacing an already-registered key still always succeeds.PayloadWireFormat::for_each_offered_endpoint/for_each_service_instance— visitor-pattern methods replacing the previousVec-returningoffered_endpoints/service_instances. Lets theClientrun loop iterate SD entries without per-message heap allocation, which was the last bare-metal blocker on the receive path. TheVec-returning forms are preserved ascfg(feature = "std")convenience wrappers that delegate to the visitors, so std consumers keep the original ergonomic shape.
Changed — 0.7.0 → 0.8.0 surface baseline
- Breaking:
Client::new(interface)return shape — previously returned(Client, ClientUpdates); now returns(Client, ClientUpdates, impl Future<Output = ()> + Send + 'static). The third element is the run-loop future, which the caller MUST drive (typically viatokio::spawn) for anyClientmethod to make progress. Migration: change destructuring to a 3-tuple and spawn or otherwise actively poll the future. - Breaking:
Server::start_announcingremoved →Server::announcement_loop— the new method returnsResult<impl Future<Output = ()> + Send + 'static, Error>(annotated#[must_use]). Spawn the returned future to fire announcements; calling and dropping the future is a silent no-op. - Breaking:
Client::start_sd_announcementsrenamed toClient::sd_announcements_loop— same semantic shift asannouncement_loop: returns animpl Futureinstead of spawning internally, so the caller drives execution. - Breaking:
Client::reboot_flag(&self)now returnsResult<protocol::sd::RebootFlag, Error>— previously returned the bare flag and could panic if the run-loop had exited. All other publicClientmethods migrated to the sameErr(Error::Shutdown)policy in this release;reboot_flagis now consistent. - Breaking:
server::SubscriptionManager::subscribesignature change — now returnsResult<(), server::SubscribeError>instead of(). Previously, capacity rejections were silently dropped with only awarn!log, which let the server emit aSubscribeAckfor a subscription that had not been recorded. Callers must now handle theErrpath (the server's own SD loop emitsSubscribeNackonErr). - Breaking:
server::EventPublisher::register_subscribersignature change — now returnsResult<(), server::SubscribeError>instead of(), surfacing the same capacity-rejection signal to externally managed subscription dispatchers. - Breaking:
Server::unicast_local_addrreturn type changed — previously returnedResult<std::net::SocketAddr, std::io::Error>; now returnsResult<std::net::SocketAddr, server::Error>. Callers that pattern-matched onstd::io::Errormust update toserver::Error::Transport(e)and access the innerTransportErrorfrom there. - Breaking: default features changed
default = []→default = ["std"]— previouslyembedded-io/std,thiserror/std, andtracing/stdwere always-on; they are now gated behind the newstdfeature. Downstream consumers building withdefault-features = falsewho relied on the implicitstdpropagation must addfeatures = ["std"](or one ofclient/server, which both implystd). - Breaking:
Client::newtype signature nowClient::<M, R, I, C>::new— theClientstruct gained three additional type parameters for the executor traits (R: TransportFactory,I: InterfaceHandle,C: ChannelFactory). The tokio-default convenience constructor is now gated behind theclient-tokiofeature (wasclient). Migration: addfeatures = ["client-tokio"]to continue usingClient::new; trait-surface consumers useClient::new_with_deps. - Breaking:
Server::newtype signature nowServer::<R, S, F, Tm>::new— theServerstruct gained type parameters for the pluggable backends. The tokio-default convenience constructor is now gated behind theserver-tokiofeature (wasserver). Migration: addfeatures = ["server-tokio"]to continue usingServer::new; trait-surface consumers useServer::new_with_deps. - Breaking:
SubscriptionHandletrait redesigned — the previousget_subscribers(&self, …) -> impl Future<Output = Vec<Subscriber>>method has been replaced withfor_each_subscriber(&self, …, f: FnMut)visitor pattern. This allowsEventPublisher::publish_eventto copy subscriber addresses into a stack buffer (heapless::Vec<_, 16>) instead of allocating per-event. Implementors of customSubscriptionHandlemust migrate. - Breaking:
SubscriptionHandleRPITIT futures no longer+ Send— thesubscribe,unsubscribe, andfor_each_subscribermethods now returnimpl Future<…>without a+ Sendbound. This enables single-threaded lock-free implementations on bare-metal targets, but meansSubscriptionHandletrait objects cannot be held across.awaitpoints in multi-threaded executors. Direct usage with the defaultArc<RwLock<SubscriptionManager>>is unaffected. - Breaking:
clientandserverfeatures no longer implystd— previouslyclient = ["std", "dep:futures"]andserver = ["std", "dep:futures"]; nowclient = ["dep:futures-util"]andserver = ["dep:futures-util"]. Thestdfeature moved toclient-tokio/server-tokio, which is where it belongs (the tokio backends genuinely require std). Bare-metal trait-surface consumers (features = ["client", "bare_metal"]) compile in pure no_std now.serverstill pullsextern crate allocbecauseServerholdsArc<EventPublisher>andEventPublisherholdsArc<F::Socket>— documented inlib.rs; refactor to&'staticborrows is tracked in #115. - Breaking: optional dep
futuresreplaced withfutures-util— direct dependency onfutures-utilwith features["async-await", "async-await-macro"]. Thefuturesumbrella crate'sselect!macro re-export is gated on itsstdfeature, which transitively pullsslab/memchr/futures-ioand breaks no_std cross-compiles.futures-utilprovidesselect_biased!,pin_mut!, andFutureExtunder justasync-await(-macro). - Breaking: internal
select!→select_biased!—Inner::run_future,socket_loop_future, andserver::runnow poll their select arms top-first instead of pseudo-randomly. For these workloads the bias gives slightly better behavior (control messages, sends, and unicast recvs get priority over their lower-priority siblings) and there is no genuine starvation path because the higher-priority arms are sporadic. The change is observable only under contrived workloads where every arm is permanently ready simultaneously. - Breaking:
PayloadWireFormat::offered_endpoints/service_instancesreplaced by visitor-pattern methods — seefor_each_offered_endpoint/for_each_service_instancein "Added" above. Implementors of customPayloadWireFormattypes must override the visitors instead of theVec-returning forms. TheVec-returning forms remain as default-implementedcfg(feature = "std")convenience wrappers, so std callers' code keeps compiling unchanged. - Breaking:
PayloadWireFormat::new_subscription_sd_headerparameter type —client_ipis nowcore::net::Ipv4Addr(wasstd::net::Ipv4Addr). The two are the same underlying type; the change unblocks no_std builds. Dropping the#[cfg(feature = "std")]gate on the method itself makes it reachable in pure no_std. - Breaking:
PayloadWireFormat::set_reboot_flagno longercfg(feature = "std")— the method is now always available on the trait. Its default impl is still a no-op; downstream payload types that participate in SD reboot tracking must override it. - Breaking:
OfferedEndpointno longercfg(feature = "std")— type is always available; itsaddrfield isOption<core::net::SocketAddrV4>(wasOption<std::net::SocketAddrV4>). Same underlying type; allows no_std consumers to receive offered-endpoint visits. - Breaking:
server::Error::Io(std::io::Error)nowcfg(feature = "std")— the variant is gated onfeature = "std"becausestd::io::Erroris itself std-only. No-std consumers receive transport failures viaError::Transport(TransportError)which carries the portableIoErrorKind. - Breaking: misuse paths on
Server::announcement_loop/Server::runreturnError::InvalidUsage(...)— previously these returnedError::Io(std::io::Error::new(InvalidInput, ..))with a formatted message. The new variant is no_std-friendly and carries a machine-readable&'static strtag ("passive_server_announcement_loop","announcement_loop_already_started","passive_server_run"); the diagnostic moves totracing::warn!. - Breaking:
server::SubscriptionManager::get_subscribersnowcfg(feature = "std")— convenience accessor returning a heapVec<Subscriber>. Production code paths usefor_each_subscriber(visitor) since 0.8.0; this accessor remains for std consumers' tests and ad-hoc tooling. No_std consumers must usefor_each_subscriber. - Breaking:
server::ServiceInfo/server::EventGroupInfonowcfg(feature = "std")— both types'pubfields holdVec<...>. Bare-metal consumers don't construct these types today; if the use case emerges, a port toheapless::Vecis tracked in #116.Subscriberis unaffected and stays no_std. - Breaking:
E2ERegistryAPI change — backing storage migrated fromstd::collections::HashMaptoheapless::index_map::FnvIndexMap(cap =E2E_REGISTRY_CAP = 32, exposed).E2ERegistry::registernow returnsResult<(), E2ERegistryFull>; replacing an already-registered key always succeeds, adding a new key past the cap returnsErr.E2ERegistry::new()is nowconst. The module is no longercfg(feature = "std")—E2ERegistryworks in pure no_std. - Breaking:
E2ERegistryHandle::registertrait method now returnsResult<(), E2ERegistryFull>— propagates the new typed overflow fromE2ERegistry::registerthrough every handle impl. Callers (Client::register_e2e,Server::register_e2e) lift theResultthrough to their public surface. client::Error::Transportadopts#[error(transparent)]Display delegation (the previous wrapping with{:?}debug-formatted the innerTransportError); user-facing error strings are now stable.- Subscribe-NACK reason strings normalized to
snake_casefor log consistency:wrong_service_id,wrong_instance_id,wrong_major_version,no_endpoint_in_options,subscribers_per_group_full,event_groups_full. Wire format is unchanged (NACK is signalled byTTL=0).
Fixed — 0.7.0 → 0.8.0 surface baseline
server::EventPublisher::publish_eventno longer silently sends UNPROTECTED payloads on E2E protect failure — counter exhaustion / key-lookup races etc. now surface asErr(Error::E2e(_))rather than logging and falling through (which had been emitting an unprotected message claiming an E2E-protected channel).- SD
Subscribewith mismatchedmajor_versionis now NACKed — previously an Ack would be returned and the subscription registered, leaving the application stack to silently mis-decode incompatible-version traffic. SocketManager::sendno longer panics on a dropped response oneshot — user-suppliedSpawnermade this path reachable; failures now returnErr(Error::SocketClosedUnexpectedly).client::Innerrequest-queue overflow no longer drops control messages silently — full queue now invokesreject_with_capacity("request_queue")on the rejected message, so callers see a typedErr(Error::Capacity("request_queue"))instead of aRecvErrormapped toError::Shutdown.- Per-socket recv-error hot loop bounded —
SocketManager's socket loop now closes afterMAX_CONSECUTIVE_RECV_ERRORS = 16consecutiverecv_fromfailures rather than spinning indefinitely on a permanently broken fd. Client::sendfails fast on oversize messages — pre-encode size check returnsErr(Error::Capacity("udp_buffer"))for messages whoserequired_size()exceedsUDP_BUFFER_SIZE. Mirrors the existingEventPublisher::publish_eventcapacity guard.
Notes
- Crate version bumped to 0.8.0 — reflects the breaking changes above. Downstream
Cargo.tomlsnippets inREADME.mdwere updated accordingly. - Bare-metal compile gate is now literal.
cargo build --target thumbv7em-none-eabihf --no-default-features --features client,server,bare_metalsucceeds;client + bare_metalis verified alloc-free (zero__rust_allocreferences in the resulting rlib). CI runs this matrix on every PR. The cortex-m4f target is the closest no_std proxy mainline Rust supports — the project's actual production target (Infineon AURIX TriCore) requires HighTec's commercial Rust distribution because mainline Rust + LLVM don't have a TriCore backend; a TriCore CI runner is tracked in #117. server + bare_metalis now alloc-free (PR #124 closed the former known limitation): theserverfeature no longer pullsextern crate alloc;Arcdefaults apply only under std/tokio configurations. CI audits the rlib for allocator symbols in bothclient,bare_metalandserver,bare_metal.
Test runner
tests/client_server.rsintegration tests share the SD multicast port (30490) viaSO_REUSEPORTand rely on Linux's reuseport hashing for traffic delivery. Under cargo's default parallel test runner cross-test Subscribe deliveries flake. The crate's.config/nextest.tomlserializesclient_servervia theserial-sd-porttest-group, socargo nextest run(used by CI) gives stable results. For the legacy harness, pass--test-threads=1:cargo test --test client_server -- --test-threads=1.
Internal / Infrastructure
- Future-size witness tests (host-arch proxies with +25% budgets) for the
client run/socket-loop futures and server run future, in tokio and
static-channel configurations (issue #125 measurement baseline, PR 0). tools/capture_type_sizes.sh: host + thumbv7em-Zprint-type-sizes
capture;tools/size_probenow instantiates the client futures no_std.transport::probe: public zero-behaviorNull*dependency stubs
(promoted from test-private; layout probing + trait-conformance only).- CI:
-Zbuild-std=corethumbv7em gate (halo's no-alloc-sysroot
certification) for client/server/combined bare_metal;nmalloc-symbol
audit extended toserver,bare_metal.