diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5306d17c..323cf02d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,11 +73,13 @@ jobs: # is a separate `cargo build` so a failure surfaces the specific # feature combo that regressed. # - # `client + bare_metal` is verified alloc-free (no `__rust_alloc` - # symbols in the rlib); `server + bare_metal` and the combined - # build pull `extern crate alloc` for `Arc` / - # `Arc` and so do reference allocator symbols — that's - # documented in `lib.rs` and tracked for a future refactor. + # Builds every bare-metal feature combo against the prebuilt + # thumbv7em sysroot, then audits the rlibs for allocator symbols. + # Since PR #124 BOTH `client + bare_metal` and `server + + # bare_metal` must be alloc-free; the build_std_core job below is + # the stronger sysroot-level certification, this audit is the + # cheap stable-toolchain tripwire that names the offending symbol + # when something regresses. name: no_std target build (thumbv7em-none-eabihf) needs: check runs-on: ubuntu-latest @@ -93,9 +95,10 @@ jobs: run: cargo build --target thumbv7em-none-eabihf --no-default-features --features server,bare_metal - name: client + server + bare_metal run: cargo build --target thumbv7em-none-eabihf --no-default-features --features client,server,bare_metal - # `client + bare_metal` runs LAST so the rlib in - # target/thumbv7em-none-eabihf/debug/ comes from this exact - # feature set when the alloc-symbol audit reads it. + # Each audited combo below is a `cargo clean -p` + build + # immediately followed by its own alloc-symbol audit, so the + # rlib in target/thumbv7em-none-eabihf/debug/ is guaranteed to + # come from exactly that feature set when the audit reads it. - name: client + bare_metal run: | # Invalidate the cargo fingerprint for any prior `simple-someip` @@ -111,9 +114,9 @@ jobs: # If `client + bare_metal` ever starts pulling `__rust_alloc`, # something inside the client engine has regressed onto an # allocator-bound primitive. Fail loudly so it gets caught in - # the PR rather than discovered downstream. (`server` and - # `client+server` builds DO reference alloc symbols via - # `Arc` — documented; not gated here.) + # the PR rather than discovered downstream. (`server + + # bare_metal` gets the same audit below; the combined + # `client+server` build is covered by the build_std_core job.) run: | # Pin to the exact rlib path. `find ... | head -1` was # nondeterministic and silently picked up stale debug-script @@ -137,6 +140,56 @@ jobs: nm -A "$rlib" | grep -E '__rust_alloc|__rg_alloc' || true exit 1 fi + - name: server + bare_metal rebuild for audit + run: | + # Same fingerprint-invalidation rationale as the client + # audit above: `cargo clean -p` guarantees the rlib below + # was built under exactly `server,bare_metal`. + cargo clean -p simple-someip --target thumbv7em-none-eabihf + cargo build --target thumbv7em-none-eabihf --no-default-features --features server,bare_metal + - name: alloc-symbol audit (server + bare_metal must be alloc-free) + # Alloc-free since PR #124 (phase 22). A regression here means + # something in the server engine reacquired an allocator-bound + # primitive — fail loudly and name the symbols. + run: | + rlib="target/thumbv7em-none-eabihf/debug/libsimple_someip.rlib" + if [ ! -f "$rlib" ]; then + echo "::error::expected rlib not found at $rlib" + ls -la target/thumbv7em-none-eabihf/debug/ || true + exit 1 + fi + set -o pipefail + alloc_refs=$(nm -A "$rlib" | grep -c -E '__rust_alloc|__rg_alloc' || true) + echo "server+bare_metal alloc-symbol references: $alloc_refs" + if [ "$alloc_refs" -ne 0 ]; then + echo "::error::server+bare_metal must be alloc-free; found $alloc_refs alloc references." + nm -A "$rlib" | grep -E '__rust_alloc|__rg_alloc' || true + exit 1 + fi + + build_std_core: + # Halo's TC4 proxy compiles with `-Zbuild-std=core`: `alloc` is + # absent from the sysroot entirely. The prebuilt-sysroot thumb job + # above SHIPS alloc, so an `extern crate alloc` regression passes + # there and still breaks halo. This job is the halo-certification + # gate (phase 22 / measurement PR 0 — see + # docs/simple_someip/plans/2026-06-09-phase22-125-memory-reduction-design.md). + name: build-std core gate (no alloc in sysroot) + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + targets: thumbv7em-none-eabihf + - uses: Swatinem/rust-cache@v2 + - name: client + bare_metal + run: cargo +nightly build --no-default-features --features client,bare_metal -Zbuild-std=core --target thumbv7em-none-eabihf + - name: server + bare_metal + run: cargo +nightly build --no-default-features --features server,bare_metal -Zbuild-std=core --target thumbv7em-none-eabihf + - name: client + server + bare_metal + run: cargo +nightly build --no-default-features --features client,server,bare_metal -Zbuild-std=core --target thumbv7em-none-eabihf test: name: Build, Test & Coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e135008..7818775f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -231,12 +231,25 @@ tokio::spawn(run); // receive only; co-located Client drives SD - **Crate version bumped to 0.8.0** — reflects the breaking changes above. Downstream `Cargo.toml` snippets in `README.md` were updated accordingly. - **Bare-metal compile gate is now literal.** `cargo build --target thumbv7em-none-eabihf --no-default-features --features client,server,bare_metal` succeeds; `client + bare_metal` is verified alloc-free (zero `__rust_alloc` references 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. -- **Known limitation: `server` feature pulls `extern crate alloc`.** `Server` holds `Arc` and `EventPublisher` holds `Arc`; both require an allocator. Pure no_std-without-allocator consumers can use the `client` feature alone (alloc-free) but will need a global allocator for the server side. A refactor to `&'static` borrows is tracked in #115. +- **`server + bare_metal` is now alloc-free** (PR #124 closed the former known limitation): the `server` feature no longer pulls `extern crate alloc`; `Arc` defaults apply only under std/tokio configurations. CI audits the rlib for allocator symbols in both `client,bare_metal` and `server,bare_metal`. ### Test runner - `tests/client_server.rs` integration tests share the SD multicast port (30490) via `SO_REUSEPORT` and 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.toml` serializes `client_server` via the `serial-sd-port` test-group, so `cargo 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_probe` now instantiates the client futures no_std. +- `transport::probe`: public zero-behavior `Null*` dependency stubs + (promoted from test-private; layout probing + trait-conformance only). +- CI: `-Zbuild-std=core` thumbv7em gate (halo's no-alloc-sysroot + certification) for client/server/combined bare_metal; `nm` alloc-symbol + audit extended to `server,bare_metal`. + ## [0.6.0](https://github.com/luminartech/simple_someip/compare/v0.5.3...v0.6.0) - 2026-04-20 diff --git a/Cargo.toml b/Cargo.toml index cb2e77c7..43053c7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,8 +96,8 @@ client = ["dep:futures-util"] client-tokio = ["client", "std", "dep:tokio", "dep:socket2"] # Internal marker: features that need `extern crate alloc`. Pulls in # `alloc::sync::Arc` for `SharedHandle` and `Arc`. -# Not part of the public surface — implied by `server` / -# `embassy_channels` / `std` and tied to the `extern crate alloc` +# Not part of the public surface — implied by `std` / +# `embassy_channels` and tied to the `extern crate alloc` # declaration in `lib.rs` so both sides of "alloc is available" # move in lockstep. Naming: `_`-prefix flags it as private. _alloc = [] diff --git a/docs/simple_someip/plans/2026-06-09-phase22-125-memory-reduction-design.md b/docs/simple_someip/plans/2026-06-09-phase22-125-memory-reduction-design.md new file mode 100644 index 00000000..e336f18b --- /dev/null +++ b/docs/simple_someip/plans/2026-06-09-phase22-125-memory-reduction-design.md @@ -0,0 +1,274 @@ +# Memory-footprint reduction: issue #125 + Phase 22 close-out + +**Date:** 2026-06-09 (rev 2 — post adversarial review, same day) +**Base:** `feature/phase21_api_symmetry` (PR #114), after PR #124 merges into it +**Closes:** issue #125; completes the Phase 22 server alloc-elimination plan + +## Problem + +Issue #125 reports Embassy arena exhaustion in the consuming firmware +build (halo / TC4): `TaskStorage` entries for simple_someip tasks +dominate the arena, and static pool symbols are oversized. Two root +causes: + +1. **Async state-machine bloat.** `Inner::run_future` is a single + `select_biased!` loop that inlines the entire + `handle_control_message` call tree (~370 lines, itself awaiting + `bind_unicast` → socket spawn → send → oneshot recv). Rustc reserves + layout for the sum of all nested awaited futures along the deepest + path. The same pattern exists in `socket_loop_future` and the + server's `recv_loop` / `announce_loop`. +2. **Buffers and pools held by value.** + - `socket_manager.rs:569` holds a `[u8; UDP_BUFFER_SIZE]` (1500 B) + live across the whole socket loop, and `socket_manager.rs:632` + adds a second 1500 B buffer during E2E sends. With + `UNICAST_SOCKETS_CAP = 8` (`inner.rs:40`), that is **~12 KiB + always-live and ~24 KiB worst case** during concurrent E2E sends + — all of it inside future state, i.e. inside the Embassy arena. + - Static channel pools holding `SendMessage` / `ReceivedMessage` + elements that embed full `Message

` payloads by value. The + `define_static_channels!` macro already lets consumers tune + **pool sizes** per type; what is hardcoded in crate code is the + bounded **slot caps** (`C::bounded::()` call sites), the + control queue `Deque<_, 32>`, and the pending-responses map (64). + +Separately, the Phase 22 plan (make `server,bare_metal` build under +halo's `-Zbuild-std=core`, i.e. no alloc in the sysroot) gates any +on-target measurement of the server loops. PR #124 (Feliciano) has +since implemented most of Phase 22 — **verified 2026-06-09**: on +#124's branch, both `--features server,bare_metal` and +`--features client,server,bare_metal` compile clean under +`cargo +nightly build -Zbuild-std=core --target thumbv7em-none-eabihf`. + +### Framing: arena vs. `.bss` + +Moving buffers out of futures does **not** reduce total RAM — it moves +bytes from the Embassy arena (TaskStorage) into consumer-declared +statics (`.bss`), and shrinks them where the consumer right-sizes the +declarations. That is the correct fix for #125's failure mode: the +arena is the thing that exhausts, and `.bss` is sized explicitly and +predictably. Reviewers of the before/after tables should expect +TaskStorage entries to shrink while some static symbols grow or move +to the consumer's crate; the headline win is arena predictability +plus whatever the consumer saves by right-sizing. + +## Decisions already made + +- **Measurement:** in-repo harness for development and regression + gating; the locally available TC4 build is the final acceptance + check once hooked up. On-target "before" numbers remain capturable + later by building the pre-optimization commit — baselines do not + block on TC4 bring-up. +- **Scope:** everything in #125 (client, server, pools), with Phase 22 + folded into the same stack. +- **Client restructuring aggressiveness:** moderate — buffer + extraction plus handler-tree flattening, staying in ordinary async + Rust. No hand-written poll state machines (the polled module from + PR #126 already serves users who need exact layouts). +- **Buffer sizing (rev 2):** buffers become **caller-sized**. Once + extracted from the futures, socket loops take `&'static mut [u8]` + slices, so the buffer count and length are chosen by the consumer's + static declaration at runtime-slice granularity — no const-generic + threading through `Client`'s parameters. Halo can declare e.g. + 2 × 512 B = 1 KiB instead of 12 KiB. The tokio path provisions + 8 × 1500 internally (API and behavior unchanged). + + *512 B rationale (sized against Iris generic interface 0.11):* the + largest defined payload is `SoftwareApplicationInfo` at 256 B + (≈284 B on-wire with SOME/IP + E2E P04 headers); `ScanCmd` is 88 B. + ScanCmd's command list is the growth risk (`uint16` length field), + but the crate's hard ceiling is already one UDP datagram + (`UDP_BUFFER_SIZE = 1500`, no SOME/IP-TP), so nothing larger was + ever sendable; outgrowing 512 B is a logged drop fixed by a + one-line bump in the consumer's declaration. Any halo-side traffic + beyond the generic interface (e.g. HWP1 method requests) needs the + same size check before the declaration is locked. +- **Pool capacities:** narrowed from "promote everything to + const-generic knobs". Pool sizes are already consumer-tunable via + `define_static_channels!`. The remaining hardcoded numbers (slot + caps 16, `Deque<_, 32>`, pending map 64) can only become knobs on + stable Rust as **literal const parameters threaded through + `Client`/`Inner`'s public types** (associated-const capacities at + the call sites would require unstable `generic_const_exprs`). That + churn is taken only where PR 0/TC4 measurement shows the win + justifies it; otherwise the numbers stay hardcoded and the decision + is recorded. +- **PR #124:** merges as-is; our review findings are fixed by us in + this stack rather than requested from the author. +- **Stack hygiene:** all 37 stale phase PRs beneath #114 were closed + without merging on 2026-06-09 (branches retained). + +## PR #124 coverage of Phase 22 (reviewed 2026-06-09) + +| Phase 22 item | Status in #124 | +|---|---| +| Item 4 — `_alloc`-gate `Server::run` | Done (`run` + `run_inner`) | +| Item 5 — remove `Pin>` GATs | Done via `core::future::Ready` (simpler than the planned hand-written futures; supersedes the saved pre-flight patch) | +| Item 2 — started latch without `Arc` | Done via cfg-switched `StartedLatch` alias instead of an `Hstart` generic | +| Item 3 — Arc type-param defaults | Done via cfg-switched `Default*Handle` aliases instead of dropping defaults | +| Items 1+10 — import reshape, `server` feature drops `_alloc` | Done | +| CI gate `server,bare_metal -Zbuild-std=core` | **Missing** (gap-filled in PR 0; verified locally that it passes) | + +**Why the existing CI doesn't already cover this:** phase21's CI does +build `server,bare_metal` for thumbv7em-none-eabihf — but against the +**prebuilt sysroot, which ships `alloc`**, so an `extern crate alloc` +regression would never E0463 there. Halo's proxy builds with +`-Zbuild-std=core`, where `alloc` is absent from the sysroot entirely. +PR 0's build-std job is the only configuration that certifies halo's +actual constraint. Relatedly, the existing `nm` alloc-symbol audit +covers only the `client,bare_metal` rlib; PR 0 extends it to +`server,bare_metal` (stable-toolchain, nearly free). + +**Accepted trade-off:** the cfg-switched aliases violate strict feature +additivity (enabling `_alloc` changes type identities). This is +documented as a hazard rather than redesigned — halo builds with a +fixed feature set, and explicit `new_with_handles` callers spell their +types. The generic-parameter design remains available if a real +unification break ever appears. + +**Pre-flight note:** the Phase 22 plan's open risk — whether a concrete +(non-boxed) future satisfies `Server::run`'s phase-21F +`for<'a> Sub::SubscribeFuture<'a>: Send` HRTB — was verified resolved +on 2026-06-09 against phase21 tip `892cb5b`. `core::future::Ready` +satisfies the same bounds. + +## The stack + +Four PRs off `feature/phase21_api_symmetry`, post-#124. + +### PR 0 — Measurement harness + CI gates + +- **Future-size regression tests:** `size_of_val`-based assertions on + client `run_future`, `socket_loop_future`, and the server run + future, in both tokio and bare-metal-deps configurations (modeled on + the existing `client_new_run_future_is_send_static` witness, which + already returns the run future by value). These are **host-arch + proxies**: x86_64 layouts differ from thumbv7 (pointer width, + alignment), so budgets are generous regression tripwires, not + targets. Budgets start at current size (recording the baseline) and + tighten as PRs 2–3 land. +- **`-Z print-type-sizes` capture script** in `tools/`, producing the + TaskStorage-style table for a thumbv7em build. This is the + **authoritative** size number. Baseline committed. +- **New CI jobs:** `--no-default-features --features server,bare_metal` + and `client,server,bare_metal` under `-Zbuild-std=core` for + thumbv7em (nightly + rust-src — new CI infrastructure; the current + workflows are stable-only). Verified passing locally on #124's + branch. Plus the `nm` alloc-symbol audit extended to the server + rlib. + +### PR 1 — #124 follow-ups (small, lands the breaking change early) + +The #124 review findings, fixed by us: + +- (a) Document (or deliberately change) the eager-vs-lazy semantics of + the `Ready`-based `subscribe`/`unsubscribe` — the locked mutation now + happens at future construction, not first poll. +- (b) `NonSdRequestCallback` gains a context argument. **Design note:** + a stored `*mut c_void` would make `Server` `!Send` and break + `Server::run`'s declared `+ Send` bound. The shape is decided in the + implementation plan from: `ctx: usize` (caller casts), a newtype + with a documented `unsafe impl Send`, or a generic observer + parameter. Breaking now is free (nothing published); breaking later + is not. Flag to Feliciano before this lands so no further FFI builds + on the bare `fn` shape. +- (c) Record the shared-socket-topology rationale for + `announce_only_future` (it partially reintroduces the split-future + shape phase 21 removed). The originally-planned MSRV check is moot: + the crate is edition 2024 (requires Rust ≥ 1.85); `use<>` precise + capture needs only 1.82. +- (d) Strengthen the non-SD-observer negative test (currently cannot + fail — the witness callback is never registered). + +### PR 2 — #125 client async-state reduction + +- **Buffer extraction via a claim/release buffer pool.** Socket loops + are spawned dynamically per bind/unbind (up to + `UNICAST_SOCKETS_CAP` live), so buffers need checkout/return + semantics: a buffer pool in the consumer's static storage (same + shape as `OneshotPool` — claim on bind, release on unbind, no + `&'static mut` aliasing). Loops take `&'static mut [u8]` slices; + count and length are the consumer's choice (halo: ~1 KiB total). + The tokio path provisions 8 × 1500 internally — API unchanged. + Defined behavior changes: an inbound datagram larger than the + claimed buffer is dropped with a log; the existing oversize-send + rejection (`socket_manager.rs:447`) checks `buf.len()` instead of + `UDP_BUFFER_SIZE`. The E2E `protected` buffer gets the same pool + treatment, or is restructured to not be live across the + `protect().await` point — whichever measures better. +- **Handler-tree flattening:** `handle_control_message` splits into a + synchronous "decode + decide" section returning a small action + value; the actual awaits are hoisted to shallow helpers at + `run_future`'s top level. **Expectation setting:** the awaited + futures remain part of `run_future`'s layout — the wins are locals + no longer held across awaits, avoided per-nesting-level argument + duplication, and better variant overlap. The buffers are expected to + be the dominant win; flattening is secondary and is kept only where + PR 0's numbers move. +- Doc debt: rewrite `src/client/mod.rs:12-30` (describes the old + buffer-in-future architecture and the 12 KiB math). +- Every change is validated against PR 0's numbers; changes that don't + move the measurement are dropped, not merged on faith. +- **Deferred follow-up (recorded, not scheduled):** a readiness-split + receive (`await readiness, then synchronous copy-out`) would let one + shared buffer serve all socket loops on a single-threaded executor + (~1.5 KiB total regardless of socket count). It requires a + `TransportSocket` trait change; only worth it if caller-sizing + proves insufficient. + +### PR 3 — #125 server + pools, final numbers + +- Same flatten/extract treatment for `recv_loop`, `announce_loop`, + `send_subscribe_nack_from_view`, on the post-#124 code. +- Pool-capacity knobs per the narrowed decision above (literal const + params only where measurement justifies the churn; otherwise record + and keep). +- Final before/after tables (TaskStorage sizes + `llvm-nm` pool + symbols) in the PR description — issue #125's acceptance criteria. + +### Scope cuts from issue #125 (recorded) + +- **SD encode monomorphization** (`Header::encode`, + `ServiceEntry::encode`, `EventGroupEntry::encode`, generic over + `embedded_io::Write`): code-size pressure, not arena/RAM pressure. + Out of scope for the arena-exhaustion failure mode. If flash size + becomes the constraint, the cheap fix is an inner non-generic + `&mut dyn embedded_io::Write` function — separate issue. +- **`unbind_discovery`:** addressed only implicitly via the + `run_future` flattening in PR 2 (it is one arm of the same control + path); no dedicated work item unless PR 0's table shows it as an + independent hotspot. + +## Invariants + +- No behavioral changes except the agreed `NonSdRequestCallback` + signature change and the two defined buffer-size behaviors in PR 2. +- Existing suite (~543 tests as of #114, plus #124's additions) green + on every PR; embassy-net loopback live-wire test guards the announce + path. +- No nightly-only features in the crate itself (halo's consumer is + nightly; the crate stays stable; CI may use nightly for measurement + and build-std jobs). +- Wire format untouched. + +## Risks / coordination + +- **#126 (polled module)** is Feliciano's, has an outstanding + hold-merge punch list, and also bases on phase21. Merge order + relative to this stack is decided between Justin and Feliciano; the + polled module is a parallel surface, so PRs 2–3 should rebase + trivially either way. +- **#124 is force-pushed actively.** Our stack starts only after it + merges into phase21, to avoid chasing a moving base. +- **Future-size assertions can be brittle across rustc versions** and + are host-arch proxies. Budgets use generous headroom (e.g. +25%) + over the post-optimization baseline; the thumbv7em + `print-type-sizes` harness is the authoritative number. +- **The buffer pool's claim/release lifecycle** is new unsafe-adjacent + surface (handing out `&'static mut [u8]`); the implementation plan + includes loom-style or witness tests for double-claim and + release-on-unbind. +- The whole stack still sits on the unmerged #114 tower + (134+ commits ahead of main); the eventual consolidation rebase is a + known cost of the established workflow, accepted to keep reviewable + PR boundaries. diff --git a/docs/simple_someip/plans/baselines/pr0-size-baseline.md b/docs/simple_someip/plans/baselines/pr0-size-baseline.md new file mode 100644 index 00000000..9a721573 --- /dev/null +++ b/docs/simple_someip/plans/baselines/pr0-size-baseline.md @@ -0,0 +1,137 @@ +# PR 0 size baseline (pre-optimization) + +Captured 2026-06-10 on x86_64-unknown-linux-gnu, before any #125 +optimization work. The "after" tables in PRs 2–3 diff against these +numbers. Regenerate with `tools/capture_type_sizes.sh` (needs nightly +with `rust-src` and `rustup target add thumbv7em-none-eabihf +--toolchain nightly`) and the witness tests: + +```sh +cargo test --features client-tokio,server-tokio,bare_metal future_size_witness -- --nocapture +``` + +(The witnesses are feature-gated; a bare `cargo test future_size_witness` +finds zero tests and exits green.) + +Scope note: the thumb table covers client futures only; the server's +no-alloc probe lands with PR 3 (needs new_with_handles static +plumbing). Server sizes below are host-proxy numbers. + +Payload note: the thumb probe instantiates the client over a +probe-local `ProbePayload` (heapless, fixed-capacity — `RawPayload` +is std-gated), while the host witnesses use `RawPayload`. Thumb and +host rows are therefore NOT comparable layout-for-layout; compare +thumb-to-thumb across captures. Thumb is authoritative for TC4. + +## Toolchains + +- host witnesses: `rustc 1.96.0 (ac68faa20 2026-05-25)` +- thumb capture (`-Zprint-type-sizes`): `rustc 1.96.0-nightly (562dee482 2026-03-21)` + +## Host witness numbers (FUTURE_SIZE lines) + +``` +FUTURE_SIZE tokio_client_run_future 106152 +FUTURE_SIZE tokio_client_socket_loop 6968 +FUTURE_SIZE tokio_server_run_future 7744 +FUTURE_SIZE bm_client_run_future 27208 +FUTURE_SIZE bm_client_socket_loop 2224 +FUTURE_SIZE bm_server_run_future 7696 +``` + +## Capture summary (`target/type-sizes/summary.md`) + +Note: the host-proxy table below comes from compiling only the +`bare_metal_e2e` test target, so it correlates with the `bm_*` +FUTURE_SIZE lines above; the `tokio_*` lines come from lib unit tests +the capture doesn't cover. + +### Type-size capture — rustc 1.96.0-nightly (562dee482 2026-03-21) + +### thumbv7em (authoritative) +| bytes | future | +|---|---| +| 103056 | {async fn body of simple_someip::client::inner::Inner>::run_future()} | +| 12092 | core::mem::MaybeUninit<{async fn body of simple_someip::client::inner::Inner>::handle_control_message()}> | +| 12092 | core::mem::MaybeDangling<{async fn body of simple_someip::client::inner::Inner>::handle_control_message()}> | +| 12092 | core::mem::ManuallyDrop<{async fn body of simple_someip::client::inner::Inner>::handle_control_message()}> | +| 12092 | {async fn body of simple_someip::client::inner::Inner>::handle_control_message()} | +| 5316 | {async fn body of simple_someip::client::socket_manager::SocketManager::socket_loop_future()} | +| 4840 | core::mem::MaybeUninit<{async fn body of simple_someip::client::socket_manager::SocketManager::send()}> | +| 4840 | core::mem::MaybeDangling<{async fn body of simple_someip::client::socket_manager::SocketManager::send()}> | +| 4840 | core::mem::ManuallyDrop<{async fn body of simple_someip::client::socket_manager::SocketManager::send()}> | +| 4840 | {async fn body of simple_someip::client::socket_manager::SocketManager::send()} | +| 2464 | core::mem::MaybeUninit<{async fn body of , simple_someip::client::Error>, 16> as simple_someip::MpscSend, simple_someip::client::Error>>>::send()}> | +| 2464 | core::mem::MaybeDangling<{async fn body of , simple_someip::client::Error>, 16> as simple_someip::MpscSend, simple_someip::client::Error>>>::send()}> | +| 2464 | core::mem::ManuallyDrop<{async fn body of , simple_someip::client::Error>, 16> as simple_someip::MpscSend, simple_someip::client::Error>>>::send()}> | +| 2464 | {async fn body of , simple_someip::client::Error>, 16> as simple_someip::MpscSend, simple_someip::client::Error>>>::send()} | +| 2440 | core::mem::MaybeUninit<{async fn body of , 16> as simple_someip::MpscSend>>::send()}> | +| 2440 | core::mem::MaybeDangling<{async fn body of , 16> as simple_someip::MpscSend>>::send()}> | +| 2440 | core::mem::ManuallyDrop<{async fn body of , 16> as simple_someip::MpscSend>>::send()}> | +| 2440 | {async fn body of , 16> as simple_someip::MpscSend>>::send()} | +| 140 | core::mem::MaybeUninit<{async fn body of simple_someip::client::inner::Inner>::unbind_discovery()}> | +| 140 | core::mem::MaybeDangling<{async fn body of simple_someip::client::inner::Inner>::unbind_discovery()}> | +| 140 | core::mem::ManuallyDrop<{async fn body of simple_someip::client::inner::Inner>::unbind_discovery()}> | +| 140 | {async fn body of simple_someip::client::inner::Inner>::unbind_discovery()} | +| 104 | core::mem::MaybeUninit<{async fn body of simple_someip::client::inner::Inner>::bind_discovery()}> | +| 104 | core::mem::MaybeDangling<{async fn body of simple_someip::client::inner::Inner>::bind_discovery()}> | +| 104 | core::mem::ManuallyDrop<{async fn body of simple_someip::client::inner::Inner>::bind_discovery()}> | +| 104 | {async fn body of simple_someip::client::inner::Inner>::bind_discovery()} | +| 96 | core::mem::MaybeUninit<{async fn body of simple_someip::client::inner::Inner>::bind_unicast()}> | +| 96 | core::mem::MaybeDangling<{async fn body of simple_someip::client::inner::Inner>::bind_unicast()}> | +| 96 | core::mem::ManuallyDrop<{async fn body of simple_someip::client::inner::Inner>::bind_unicast()}> | +| 96 | {async fn body of simple_someip::client::inner::Inner>::bind_unicast()} | +| 92 | core::mem::MaybeUninit<{async fn body of simple_someip::client::socket_manager::SocketManager::bind_discovery_seeded_with_transport()}> | +| 92 | core::mem::MaybeDangling<{async fn body of simple_someip::client::socket_manager::SocketManager::bind_discovery_seeded_with_transport()}> | +| 92 | core::mem::ManuallyDrop<{async fn body of simple_someip::client::socket_manager::SocketManager::bind_discovery_seeded_with_transport()}> | +| 92 | {async fn body of simple_someip::client::socket_manager::SocketManager::bind_discovery_seeded_with_transport()} | +| 80 | core::mem::MaybeUninit<{async fn body of simple_someip::client::socket_manager::SocketManager::bind_with_transport()}> | +| 80 | core::mem::MaybeDangling<{async fn body of simple_someip::client::socket_manager::SocketManager::bind_with_transport()}> | +| 80 | core::mem::ManuallyDrop<{async fn body of simple_someip::client::socket_manager::SocketManager::bind_with_transport()}> | +| 80 | {async fn body of simple_someip::client::socket_manager::SocketManager::bind_with_transport()} | +| 68 | core::mem::MaybeUninit<{async fn body of simple_someip::client::socket_manager::SocketManager::shut_down()}> | +| 68 | core::mem::MaybeDangling<{async fn body of simple_someip::client::socket_manager::SocketManager::shut_down()}> | + +### host x86_64 (proxy) +| bytes | future | +|---|---| +| 35632 | {async block@tests/bare_metal_e2e.rs:604:1: 604:15} | +| 27392 | tokio::runtime::task::core::Cell<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc> | +| 27392 | tokio::runtime::task::core::Cell<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc> | +| 27392 | tokio::runtime::task::core::Cell<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc> | +| 27392 | tokio::runtime::task::core::Cell<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc> | +| 27232 | tokio::runtime::task::core::Core<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc> | +| 27232 | tokio::runtime::task::core::Core<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc> | +| 27224 | {closure@tokio::task::spawn::spawn_inner<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}>::{closure#0}} | +| 27224 | {closure@tokio::runtime::context::current::with_current<{closure@tokio::task::spawn::spawn_inner<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}>::{closure#0}}, tokio::task::JoinHandle<()>>::{closure#0}} | +| 27216 | tokio::runtime::task::core::Stage<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}> | +| 27216 | tokio::runtime::task::core::CoreStage<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}> | +| 27216 | tokio::runtime::task::core::Core<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc> | +| 27216 | tokio::runtime::task::core::Core<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc> | +| 27216 | tokio::loom::std::unsafe_cell::UnsafeCell>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}>> | +| 27216 | std::cell::UnsafeCell>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}>> | +| 27216 | {closure@tokio::runtime::task::core::Core<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc>::set_stage::{closure#0}} | +| 27216 | {closure@tokio::runtime::task::core::Core<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc>::set_stage::{closure#0}} | +| 27208 | std::mem::MaybeUninit<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}> | +| 27208 | std::mem::MaybeDangling<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}> | +| 27208 | std::mem::ManuallyDrop<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}> | +| 27208 | {closure@tokio::task::spawn::spawn_inner<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}>::{closure#0}} | +| 27208 | {closure@tokio::runtime::context::current::with_current<{closure@tokio::task::spawn::spawn_inner<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}>::{closure#0}}, tokio::task::JoinHandle<()>>::{closure#0}} | +| 27208 | {async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()} | +| 27200 | tokio::runtime::task::core::Stage<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}> | +| 27200 | tokio::runtime::task::core::CoreStage<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}> | +| 27200 | tokio::loom::std::unsafe_cell::UnsafeCell>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}>> | +| 27200 | std::cell::UnsafeCell>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}>> | +| 27200 | {closure@tokio::runtime::task::core::Core<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc>::set_stage::{closure#0}} | +| 27200 | {closure@tokio::runtime::task::core::Core<{async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()}, std::sync::Arc>::set_stage::{closure#0}} | +| 27192 | {async fn body of simple_someip::client::inner::Inner>, E2ETestChannels, simple_someip::client::bind_dispatch::SpawnerDispatch>::run_future()} | +| 15616 | tokio::runtime::task::core::Cell<{async block@tests/bare_metal_e2e.rs:487:35: 487:45}, std::sync::Arc> | +| 15616 | tokio::runtime::task::core::Cell<{async block@tests/bare_metal_e2e.rs:487:35: 487:45}, std::sync::Arc> | +| 15424 | tokio::runtime::task::core::Core<{async block@tests/bare_metal_e2e.rs:487:35: 487:45}, std::sync::Arc> | +| 15424 | tokio::runtime::task::core::Core<{async block@tests/bare_metal_e2e.rs:487:35: 487:45}, std::sync::Arc> | +| 15416 | {closure@tokio::task::spawn::spawn_inner<{async block@tests/bare_metal_e2e.rs:487:35: 487:45}>::{closure#0}} | +| 15416 | {closure@tokio::runtime::context::current::with_current<{closure@tokio::task::spawn::spawn_inner<{async block@tests/bare_metal_e2e.rs:487:35: 487:45}>::{closure#0}}, tokio::task::JoinHandle<()>>::{closure#0}} | +| 15408 | tokio::runtime::task::core::Stage<{async block@tests/bare_metal_e2e.rs:487:35: 487:45}> | +| 15408 | tokio::runtime::task::core::CoreStage<{async block@tests/bare_metal_e2e.rs:487:35: 487:45}> | +| 15408 | tokio::loom::std::unsafe_cell::UnsafeCell> | +| 15408 | std::cell::UnsafeCell> | diff --git a/examples/embassy_net_client/src/main.rs b/examples/embassy_net_client/src/main.rs index 9f00c67a..61024c38 100644 --- a/examples/embassy_net_client/src/main.rs +++ b/examples/embassy_net_client/src/main.rs @@ -342,8 +342,11 @@ async fn main() { let stack_b = build_stack(drv_b, IP_B, SEED_B); let local = tokio::task::LocalSet::new(); + // Box::pin: the combined setup future is ~16 KiB + // (clippy::large_futures); park it on the heap instead of main's + // stack frame. local - .run_until(async move { + .run_until(Box::pin(async move { tokio::task::spawn_local(async move { stack_a.run().await }); tokio::task::spawn_local(async move { stack_b.run().await }); @@ -367,7 +370,9 @@ async fn main() { Box::leak(Box::new(SocketPool::new())); let server_factory = EmbassyNetFactory::new(stack_a, server_pool); let server_e2e: Arc> = Arc::new(Mutex::new(E2ERegistry::new())); - let server_config = ServerConfig::new(SERVICE_ID, INSTANCE_ID).with_interface(IP_A).with_local_port(30500); + let server_config = ServerConfig::new(SERVICE_ID, INSTANCE_ID) + .with_interface(IP_A) + .with_local_port(30500); let server_deps = ServerDeps { factory: server_factory, @@ -385,17 +390,14 @@ async fn main() { // the `run`-future `!Send`; ignoring it and re-building // via `run_with_buffers` keeps us on the `spawn_local` // path. - let (server, _handles, _run): ( - Server<_, _, _, _, Arc>, - _, - _, - ) = Server::new_with_deps(server_deps, server_config, false) - .await - .expect("server construction over embassy-net"); + let (server, _handles, _run): (Server<_, _, _, _, Arc>, _, _) = + Server::new_with_deps(server_deps, server_config, false) + .await + .expect("server construction over embassy-net"); tokio::task::spawn_local(server.run_with_buffers( - Box::leak(Box::new([0u8; 65535])), - Box::leak(Box::new([0u8; 65535])), + Box::leak(vec![0u8; 65535].into_boxed_slice()), + Box::leak(vec![0u8; 65535].into_boxed_slice()), )); println!( "[server] run loop spawned, emitting OfferService(0x{SERVICE_ID:04X}) every 1s" @@ -449,6 +451,6 @@ async fn main() { Ok(false) => println!("[example] update stream closed before SD arrived"), Err(_) => println!("[example] TIMEOUT — no SD message in 5s"), } - }) + })) .await; } diff --git a/simple-someip-embassy-net/tests/loopback.rs b/simple-someip-embassy-net/tests/loopback.rs index f9fb2eec..bb27c108 100644 --- a/simple-someip-embassy-net/tests/loopback.rs +++ b/simple-someip-embassy-net/tests/loopback.rs @@ -471,11 +471,9 @@ impl SubscriptionHandle for MockSubscriptions { // Boxed `!Send` futures — the `spawn_local` paths that exercise // this loopback don't need `Send` and the `Mutex` is only used // synchronously inside. - type SubscribeFuture<'a> = core::pin::Pin< - Box> + 'a>, - >; - type UnsubscribeFuture<'a> = - core::pin::Pin + 'a>>; + type SubscribeFuture<'a> = + core::pin::Pin> + 'a>>; + type UnsubscribeFuture<'a> = core::pin::Pin + 'a>>; fn subscribe( &self, diff --git a/src/client/inner.rs b/src/client/inner.rs index f4b32d76..38d3a47a 100644 --- a/src/client/inner.rs +++ b/src/client/inner.rs @@ -1,3 +1,4 @@ +use crate::log::{debug, error, info, trace, warn}; use core::future; use core::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use core::task::Poll; @@ -5,7 +6,6 @@ use futures_util::{FutureExt, pin_mut, select_biased}; use heapless::{Deque, index_map::FnvIndexMap}; #[cfg(all(test, feature = "client-tokio"))] use std::sync::{Arc, Mutex}; -use crate::log::{debug, error, info, trace, warn}; #[cfg(all(test, feature = "client-tokio"))] use crate::e2e::E2ERegistry; diff --git a/src/client/mod.rs b/src/client/mod.rs index 71815258..6724d43b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -51,6 +51,7 @@ use crate::Timer; #[cfg(feature = "client-tokio")] use crate::e2e::E2ERegistry; use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile}; +use crate::log::info; #[cfg(feature = "client-tokio")] use crate::tokio_transport::{TokioChannels, TokioSpawner, TokioTimer}; use crate::transport::{ @@ -62,7 +63,6 @@ use core::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use inner::Inner; #[cfg(feature = "client-tokio")] use std::sync::{Arc, Mutex, RwLock}; -use crate::log::info; /// Marker trait declaring the channel-pool entries a [`ChannelFactory`] /// must declare for [`Client`] to compile against it. End users do not @@ -2154,4 +2154,68 @@ mod tests { client.shut_down(); } + + /// Host-arch PROXY budgets for the client's two dominant futures. + /// thumbv7em layouts differ (pointer width/alignment) — the + /// authoritative numbers come from `tools/capture_type_sizes.sh`. + /// Values are observed-at-capture × 1.25 rounded up to a multiple + /// of 64 (see docs/simple_someip/plans/baselines/pr0-size-baseline.md). + /// If this trips: run the capture script and compare against the + /// baseline before raising the budget — a layout regression in a PR + /// is exactly what this witness exists to catch. + const TOKIO_CLIENT_RUN_FUTURE_BUDGET: usize = 132736; // = ceil64(106152 × 1.25) + /// See [`TOKIO_CLIENT_RUN_FUTURE_BUDGET`] — same proxy-budget rules. + const TOKIO_CLIENT_SOCKET_LOOP_BUDGET: usize = 8768; // = ceil64(6968 × 1.25) + + #[tokio::test] + async fn future_size_witness_tokio_client() { + use core::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Arc; + + /// Records the size of every future it is asked to spawn (the + /// per-socket I/O loops), then delegates to tokio so the client + /// still works. + #[derive(Clone)] + struct SizeRecordingSpawner { + max_spawned: Arc, + } + + impl Spawner for SizeRecordingSpawner { + fn spawn(&self, future: impl core::future::Future + Send + 'static) { + self.max_spawned + .fetch_max(core::mem::size_of_val(&future), Ordering::SeqCst); + let _run_handle = tokio::spawn(future); + } + } + + let max_spawned = Arc::new(AtomicUsize::new(0)); + let spawner = SizeRecordingSpawner { + max_spawned: Arc::clone(&max_spawned), + }; + + let (client, _updates, run_fut) = + TestClient::new_with_spawner_and_loopback(Ipv4Addr::LOCALHOST, false, spawner); + + // Measure BEFORE tokio::spawn moves it. + let run_size = core::mem::size_of_val(&run_fut); + let _run_handle = tokio::spawn(run_fut); + + // Binding the discovery socket forces one socket-loop spawn. + client.bind_discovery().await.expect("bind_discovery"); + let loop_size = max_spawned.load(Ordering::SeqCst); + + std::println!("FUTURE_SIZE tokio_client_run_future {run_size}"); + std::println!("FUTURE_SIZE tokio_client_socket_loop {loop_size}"); + + assert!(loop_size > 0, "spawner never received the socket loop"); + assert!( + run_size <= TOKIO_CLIENT_RUN_FUTURE_BUDGET, + "Inner::run_future grew: {run_size} B > budget {TOKIO_CLIENT_RUN_FUTURE_BUDGET} B" + ); + assert!( + loop_size <= TOKIO_CLIENT_SOCKET_LOOP_BUDGET, + "socket loop future grew: {loop_size} B > budget {TOKIO_CLIENT_SOCKET_LOOP_BUDGET} B" + ); + client.shut_down(); + } } diff --git a/src/client/socket_manager.rs b/src/client/socket_manager.rs index ef567c64..72fc1b12 100644 --- a/src/client/socket_manager.rs +++ b/src/client/socket_manager.rs @@ -52,12 +52,12 @@ use crate::{ }; use super::error::Error; +use crate::log::{debug, error, info, trace, warn}; use core::{ net::{Ipv4Addr, SocketAddr, SocketAddrV4}, task::{Context, Poll}, }; use futures_util::{FutureExt, pin_mut, select_biased}; -use crate::log::{debug, error, info, trace, warn}; /// A received message together with the source address it came from. /// diff --git a/src/lib.rs b/src/lib.rs index cfef6f3a..d49a654a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,7 @@ //! | `std` | yes | Enables std-dependent helpers (`RawPayload`, `VecSdHeader`) and the `Arc>` / `Arc>` default lock-handle impls used by the tokio backends. | //! | `client` | no | Trait-surface client. Pure `no_std`-clean (does not pull `extern crate alloc`). Caller supplies `Spawner` / `Timer` / `ChannelFactory` / `TransportFactory` / `E2ERegistryHandle` / `InterfaceHandle` impls. | //! | `client-tokio` | no | Adds the `Client::new` / `TokioSpawner` / `TokioTransport` convenience defaults; implies `client` + std + tokio + socket2. | -//! | `server` | no | Trait-surface server. Pulls `extern crate alloc` (for `Arc` / `Arc`); on `no_std`, downstream consumers must provide a `#[global_allocator]`. | +//! | `server` | no | Trait-surface server. Alloc-free since PR #124: the no-alloc path is `Server::new_with_handles` + `run_with_buffers` with static handles. The `Arc`-backed conveniences (`new_with_deps`, `run`) are gated behind the internal `_alloc` feature (pulled in by `std` / `embassy_channels`). | //! | `server-tokio` | no | Adds the `Server::new` / `TokioTransport` / `TokioTimer` convenience defaults; implies `server` + std + tokio + socket2. | //! | `bare_metal` | no | Activates embassy-sync, the `static_channels` module (no-alloc `ChannelFactory`), `AtomicInterfaceHandle`, `StaticE2EHandle`, and `StaticSubscriptionHandle`. All five are pure `no_std` (no allocator required). See `examples/bare_metal_client/` and `examples/bare_metal_server/` for runnable bare-metal integration examples. | //! | `embassy_channels` | no | Heap-backed `EmbassySyncChannels` `ChannelFactory`. Implies `bare_metal` and pulls `extern crate alloc;` into the crate; **on `no_std`, downstream consumers must provide a `#[global_allocator]`**. Useful for tests / early prototypes before sizing static pools. | @@ -112,11 +112,10 @@ extern crate std; // `alloc` is required by: // - `embassy_channels` — `EmbassySyncChannels` heap-allocates an // `Arc>` per oneshot/bounded/unbounded. -// - `server` — `EventPublisher` and the `Server` struct hold -// `Arc>` / `Arc` for sharing -// between the run loop and external publishing tasks. A -// the `&'static`-borrow refactor tracked in #115 would let -// server compile in pure no_std without an allocator. +// - the allocator-backed server conveniences (`new_with_deps` / +// `new_passive_with_deps`, `run`/`run_inner`'s owned buffers, the +// `Arc` `StartedLatch`). The core `server` engine is alloc-free +// since PR #124 (`new_with_handles` + `run_with_buffers`). // // The `static_channels` module (under `bare_metal` alone) does // NOT need alloc — users wanting `client` + `bare_metal` without @@ -124,7 +123,7 @@ extern crate std; // macro. Pure `bare_metal` without `client` / `server` / // `embassy_channels` also stays alloc-free. // Pulls `alloc` into scope. Gated on the internal `_alloc` feature -// (implied by `server`, `embassy_channels`, and `std`). The +// (implied by `std` and `embassy_channels`). The // `Arc: SharedHandle` impl in `transport.rs` shares the same // gate so they move in lockstep. #[cfg(feature = "_alloc")] @@ -160,9 +159,9 @@ pub const UDP_BUFFER_SIZE: usize = 1500; /// SOME/IP client for discovering services and exchanging messages. #[cfg(feature = "client")] pub mod client; -mod log; /// End-to-end (E2E) protection utilities for SOME/IP payloads. pub mod e2e; +mod log; /// SOME/IP protocol primitives: headers, messages, return codes, and service discovery. pub mod protocol; /// A general-purpose, heap-allocated [`PayloadWireFormat`] implementation. diff --git a/src/log.rs b/src/log.rs index f1d2ec3d..25393a5c 100644 --- a/src/log.rs +++ b/src/log.rs @@ -31,13 +31,22 @@ macro_rules! noop { }; } +// `unused_imports` for the same macro-table reason as the `tracing` +// branch above, plus: with `--no-default-features` (no client/server) +// every call site is compiled out, so all five aliases count as +// unused. #[cfg(not(feature = "tracing"))] +#[allow(unused_imports)] pub(crate) use noop as debug; #[cfg(not(feature = "tracing"))] +#[allow(unused_imports)] pub(crate) use noop as error; #[cfg(not(feature = "tracing"))] +#[allow(unused_imports)] pub(crate) use noop as info; #[cfg(not(feature = "tracing"))] +#[allow(unused_imports)] pub(crate) use noop as trace; #[cfg(not(feature = "tracing"))] +#[allow(unused_imports)] pub(crate) use noop as warn; diff --git a/src/protocol/sd/test_support.rs b/src/protocol/sd/test_support.rs index 9deb3f7e..557b06e1 100644 --- a/src/protocol/sd/test_support.rs +++ b/src/protocol/sd/test_support.rs @@ -20,6 +20,10 @@ impl WireFormat for TestSdHeader { } } +// NOTE: `tools/size_probe`'s `ProbePayload` mirrors this type +// field-for-field for thumbv7em layout capture (it can't reach this +// `pub(crate)` item). If you change `TestPayload`/`TestSdHeader`, +// update the probe or its measured layouts silently drift. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct TestPayload { pub header: TestSdHeader, diff --git a/src/server/mod.rs b/src/server/mod.rs index 5264a1d7..2859a028 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -32,13 +32,11 @@ use crate::e2e::{E2EKey, E2EProfile}; use crate::protocol::sd; #[cfg(test)] use crate::protocol::sd::{Entry, Flags, ServiceEntry}; -use crate::transport::{ - E2ERegistryHandle, SharedHandle, TransportFactory, TransportSocket, -}; #[cfg(feature = "_alloc")] use crate::transport::SocketOptions; #[cfg(feature = "_alloc")] use crate::transport::WrappableSharedHandle; +use crate::transport::{E2ERegistryHandle, SharedHandle, TransportFactory, TransportSocket}; #[cfg(feature = "_alloc")] use alloc::sync::Arc; use core::net::Ipv4Addr; @@ -1235,9 +1233,7 @@ where &self, unicast_buf: &'a mut [u8], sd_buf: &'a mut [u8], - ) -> impl core::future::Future> - + 'a - + use<'a, F, Tm, R, Sub, H, Hsd, Hep> + ) -> impl core::future::Future> + 'a + use<'a, F, Tm, R, Sub, H, Hsd, Hep> where Tm: 'a, Sub: 'a, @@ -1369,9 +1365,8 @@ where #[cfg(feature = "_alloc")] fn run_inner( &self, - ) -> impl core::future::Future> - + 'static - + use { + ) -> impl core::future::Future> + 'static + use + { let config = self.config.clone(); let unicast_socket = self.unicast_socket.clone(); let sd_socket = self.sd_socket.clone(); @@ -1497,7 +1492,10 @@ mod tests { ); let suppressed = default_cfg.clone().with_announce(false); - assert!(!suppressed.announce, "with_announce(false) must clear the field"); + assert!( + !suppressed.announce, + "with_announce(false) must clear the field" + ); let restored = suppressed.with_announce(true); assert!( @@ -1700,8 +1698,7 @@ mod tests { let config = ServerConfig::new(0xFE15, 1) .with_interface(Ipv4Addr::LOCALHOST) .with_local_port(0); - let server = - TestServer::new_passive_with_handles(handles, config).expect("passive ctor"); + let server = TestServer::new_passive_with_handles(handles, config).expect("passive ctor"); let mut unicast_buf = vec![0u8; 1500]; let mut sd_buf = vec![0u8; 1500]; let result = server.run_with_buffers(&mut unicast_buf, &mut sd_buf).await; @@ -1838,13 +1835,10 @@ mod tests { .with_local_port(0); // Explicit `Arc` H so the compiler doesn't have // to invent it across the deps-bundle indirection. - let (server, _handles, _run): ( - Server<_, _, _, _, Arc>, - _, - _, - ) = Server::new_with_deps(deps, config, false) - .await - .expect("create failing-socket server"); + let (server, _handles, _run): (Server<_, _, _, _, Arc>, _, _) = + Server::new_with_deps(deps, config, false) + .await + .expect("create failing-socket server"); // Build a valid Subscribe; our service id/instance/major // match the config's defaults, so the only failure point @@ -1865,7 +1859,15 @@ mod tests { // The H3 fix: handle_sd_message must NOT bubble the ACK send // failure as Err — it logs and continues. - let result = runtime::handle_sd_message(&server.config, server.sd_socket.get(), server.sd_state.get(), &server.subscriptions, &sd_view, sender).await; + let result = runtime::handle_sd_message( + &server.config, + server.sd_socket.get(), + server.sd_state.get(), + &server.subscriptions, + &sd_view, + sender, + ) + .await; assert!( result.is_ok(), "handle_sd_message must not propagate transient SD-socket I/O errors; got {result:?}" @@ -2027,7 +2029,16 @@ mod tests { let data = &buf[..len]; let view = MessageView::parse(data).unwrap(); let sd_view = view.sd_header().unwrap(); - runtime::handle_sd_message(&server.config, server.sd_socket.get(), server.sd_state.get(), &server.subscriptions, &sd_view, addr).await.unwrap(); + runtime::handle_sd_message( + &server.config, + server.sd_socket.get(), + server.sd_state.get(), + &server.subscriptions, + &sd_view, + addr, + ) + .await + .unwrap(); // Check subscription was added let subs = server.subscriptions.read().await; @@ -2081,7 +2092,16 @@ mod tests { let data = &buf[..len]; let view = MessageView::parse(data).unwrap(); let sd_view = view.sd_header().unwrap(); - runtime::handle_sd_message(&server.config, server.sd_socket.get(), server.sd_state.get(), &server.subscriptions, &sd_view, addr).await.unwrap(); + runtime::handle_sd_message( + &server.config, + server.sd_socket.get(), + server.sd_state.get(), + &server.subscriptions, + &sd_view, + addr, + ) + .await + .unwrap(); // No subscription should have been added let subs = server.subscriptions.read().await; @@ -2132,7 +2152,16 @@ mod tests { let data = &buf[..len]; let view = MessageView::parse(data).unwrap(); let sd_view = view.sd_header().unwrap(); - runtime::handle_sd_message(&server.config, server.sd_socket.get(), server.sd_state.get(), &server.subscriptions, &sd_view, addr).await.unwrap(); + runtime::handle_sd_message( + &server.config, + server.sd_socket.get(), + server.sd_state.get(), + &server.subscriptions, + &sd_view, + addr, + ) + .await + .unwrap(); let subs = server.subscriptions.read().await; assert_eq!(subs.subscription_count(), 0); @@ -2181,7 +2210,16 @@ mod tests { let data = &buf[..len]; let view = MessageView::parse(data).unwrap(); let sd_view = view.sd_header().unwrap(); - runtime::handle_sd_message(&server.config, server.sd_socket.get(), server.sd_state.get(), &server.subscriptions, &sd_view, addr).await.unwrap(); + runtime::handle_sd_message( + &server.config, + server.sd_socket.get(), + server.sd_state.get(), + &server.subscriptions, + &sd_view, + addr, + ) + .await + .unwrap(); }); // Receive the unicast OfferService response @@ -2233,7 +2271,16 @@ mod tests { let data = &buf[..len]; let view = MessageView::parse(data).unwrap(); let sd_view = view.sd_header().unwrap(); - runtime::handle_sd_message(&server.config, server.sd_socket.get(), server.sd_state.get(), &server.subscriptions, &sd_view, addr).await.unwrap(); + runtime::handle_sd_message( + &server.config, + server.sd_socket.get(), + server.sd_state.get(), + &server.subscriptions, + &sd_view, + addr, + ) + .await + .unwrap(); }); let mut resp_buf = vec![0u8; 65535]; @@ -2282,7 +2329,16 @@ mod tests { let data = &buf[..len]; let view = MessageView::parse(data).unwrap(); let sd_view = view.sd_header().unwrap(); - runtime::handle_sd_message(&server.config, server.sd_socket.get(), server.sd_state.get(), &server.subscriptions, &sd_view, addr).await.unwrap(); + runtime::handle_sd_message( + &server.config, + server.sd_socket.get(), + server.sd_state.get(), + &server.subscriptions, + &sd_view, + addr, + ) + .await + .unwrap(); }); // Should NOT receive any response (short timeout) @@ -2324,7 +2380,16 @@ mod tests { let data = &buf[..len]; let view = MessageView::parse(data).unwrap(); let sd_view = view.sd_header().unwrap(); - runtime::handle_sd_message(&server.config, server.sd_socket.get(), server.sd_state.get(), &server.subscriptions, &sd_view, addr).await.unwrap(); + runtime::handle_sd_message( + &server.config, + server.sd_socket.get(), + server.sd_state.get(), + &server.subscriptions, + &sd_view, + addr, + ) + .await + .unwrap(); // No subscription should have been added let subs = server.subscriptions.read().await; @@ -2583,7 +2648,16 @@ mod tests { let data = &buf[..len]; let view = MessageView::parse(data).unwrap(); let sd_view = view.sd_header().unwrap(); - runtime::handle_sd_message(&server.config, server.sd_socket.get(), server.sd_state.get(), &server.subscriptions, &sd_view, addr).await.unwrap(); + runtime::handle_sd_message( + &server.config, + server.sd_socket.get(), + server.sd_state.get(), + &server.subscriptions, + &sd_view, + addr, + ) + .await + .unwrap(); // Subscription should have been added let subs = server.subscriptions.read().await; @@ -2666,7 +2740,10 @@ mod tests { #[test] fn extract_endpoint_zero_options_in_both_runs_returns_none() { let iter = sd::OptionIter::new(&[]); - assert_eq!(runtime::extract_subscriber_endpoint(&iter, 0, 0, 0, 0), None); + assert_eq!( + runtime::extract_subscriber_endpoint(&iter, 0, 0, 0, 0), + None + ); } #[test] @@ -2678,7 +2755,10 @@ mod tests { let total = fill_ipv4_endpoints(&mut buf, 2, 30100); let iter = sd::OptionIter::new(&buf[..total]); - assert_eq!(runtime::extract_subscriber_endpoint(&iter, 1, 0, 0, 0), None); + assert_eq!( + runtime::extract_subscriber_endpoint(&iter, 1, 0, 0, 0), + None + ); } #[test] @@ -2779,7 +2859,10 @@ mod tests { offset += write_load_balancing_option(&mut buf[offset..], 3, 4); let iter = sd::OptionIter::new(&buf[..offset]); - assert_eq!(runtime::extract_subscriber_endpoint(&iter, 0, 2, 0, 0), None); + assert_eq!( + runtime::extract_subscriber_endpoint(&iter, 0, 2, 0, 0), + None + ); } #[test] @@ -2880,7 +2963,16 @@ mod tests { let sender = core::net::SocketAddr::V4(datagram.source); let view = MessageView::parse(&buf[..len]).unwrap(); let sd_view = view.sd_header().unwrap(); - runtime::handle_sd_message(&server.config, server.sd_socket.get(), server.sd_state.get(), &server.subscriptions, &sd_view, sender).await.unwrap(); + runtime::handle_sd_message( + &server.config, + server.sd_socket.get(), + server.sd_state.get(), + &server.subscriptions, + &sd_view, + sender, + ) + .await + .unwrap(); // The server must have registered exactly one subscriber, and // its endpoint must be the SubscribeEventGroup entry's options[1] @@ -3368,7 +3460,10 @@ mod tests { with_default(subscriber, || { // 0 endpoints → warn! "No IPv4 endpoint" branch. let iter_empty = sd::OptionIter::new(&[]); - assert_eq!(runtime::extract_subscriber_endpoint(&iter_empty, 0, 0, 0, 0), None); + assert_eq!( + runtime::extract_subscriber_endpoint(&iter_empty, 0, 0, 0, 0), + None + ); // 1 endpoint → trace! "Found IPv4 endpoint" branch. let mut buf_one = [0u8; 32]; @@ -3471,4 +3566,25 @@ mod tests { announce_handle.abort(); let _ = announce_handle.await; } + + /// Host-arch PROXY budget — see the twin constant in + /// src/client/mod.rs for semantics and the update procedure. + const TOKIO_SERVER_RUN_FUTURE_BUDGET: usize = 9728; // = ceil64(7744 × 1.25) + + #[tokio::test] + async fn future_size_witness_tokio_server() { + // Port 0: kernel-assigned, back-filled by the constructor — + // avoids collisions with sibling tests running in parallel. + let config = ServerConfig::new(0x5B, 1) + .with_interface(Ipv4Addr::LOCALHOST) + .with_local_port(0); + let (_server, _handles, run) = TestServer::new(config).await.expect("Server::new"); + + let run_size = core::mem::size_of_val(&run); + std::println!("FUTURE_SIZE tokio_server_run_future {run_size}"); + assert!( + run_size <= TOKIO_SERVER_RUN_FUTURE_BUDGET, + "server run future grew: {run_size} B > budget {TOKIO_SERVER_RUN_FUTURE_BUDGET} B" + ); + } } diff --git a/src/server/runtime.rs b/src/server/runtime.rs index ed56df7d..33314c91 100644 --- a/src/server/runtime.rs +++ b/src/server/runtime.rs @@ -181,7 +181,7 @@ where Ok(()) } -/// Handle a Service Discovery message (Subscribe / FindService etc.). +/// Handle a Service Discovery message (Subscribe / `FindService` etc.). #[allow(clippy::too_many_lines)] pub(super) async fn handle_sd_message( config: &ServerConfig, @@ -373,9 +373,7 @@ where find_service_id, config.service_id ); - if let Err(e) = - send_unicast_offer(config, sd_socket, sd_state, sender).await - { + if let Err(e) = send_unicast_offer(config, sd_socket, sd_state, sender).await { crate::log::warn!("Unicast OfferService send failed: {e}"); } } else { @@ -547,7 +545,9 @@ where cb(data, src_v4); } } else { - crate::log::trace!("Non-SD unicast SOME/IP message, no observer registered — ignoring"); + crate::log::trace!( + "Non-SD unicast SOME/IP message, no observer registered — ignoring" + ); } } else { crate::log::trace!("Non-SD multicast SOME/IP message, ignoring"); @@ -609,7 +609,16 @@ where let sd = sd_socket.get(); let sd_state_ref = sd_state.get(); - let recv_fut = recv_loop(&config, unicast, sd, sd_state_ref, &subscriptions, unicast_buf, sd_buf, non_sd_observer); + let recv_fut = recv_loop( + &config, + unicast, + sd, + sd_state_ref, + &subscriptions, + unicast_buf, + sd_buf, + non_sd_observer, + ); if config.announce { let announce_fut = announce_loop(&config, sd, sd_state_ref, &timer); diff --git a/src/server/subscription_manager.rs b/src/server/subscription_manager.rs index 7c3a59ee..da395c8b 100644 --- a/src/server/subscription_manager.rs +++ b/src/server/subscription_manager.rs @@ -368,8 +368,9 @@ impl SubscriptionHandle for Arc> { /// satisfiable. The `Box::pin` allocation happens at SD-rate /// (~1 Hz subscribes during steady state), small cost relative to /// the wire-side activity it gates. - type SubscribeFuture<'a> = - core::pin::Pin> + Send + 'a>>; + type SubscribeFuture<'a> = core::pin::Pin< + alloc::boxed::Box> + Send + 'a>, + >; type UnsubscribeFuture<'a> = core::pin::Pin + Send + 'a>>; diff --git a/src/transport.rs b/src/transport.rs index b81eb24f..fbc49a77 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -1360,12 +1360,168 @@ pub trait UnboundedPooled: Send + Sized + 'static { fn unbounded_pair() -> (C::UnboundedSender, C::UnboundedReceiver); } +/// Zero-behavior implementations of the client- and server-side +/// dependency traits. Two uses: (1) compile-time proof the trait +/// signatures are implementable without async machinery, (2) +/// **layout probing** — `tools/size_probe` instantiates `Client` +/// with these on `thumbv7em-none-eabihf` so `-Zprint-type-sizes` +/// reports the real on-target future layouts (see +/// `docs/simple_someip/plans/2026-06-09-phase22-125-memory-reduction-design.md`). +/// +/// NOT for production use: sockets error, and the spawner panics +/// outright — probe code is compiled, never executed, and a loud +/// failure beats the silent deadlock a future-dropping spawner +/// would cause in a driven `Client`. +#[cfg(any(test, feature = "bare_metal"))] +pub mod probe { + use super::{ + E2ERegistryHandle, InterfaceHandle, ReceivedDatagram, SocketOptions, Spawner, Timer, + TransportError, TransportFactory, TransportSocket, + }; + use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile, Error as E2EError}; + use core::future::Future; + use core::net::{Ipv4Addr, SocketAddrV4}; + use core::time::Duration; + + /// Socket whose I/O futures resolve immediately with + /// `TransportError::Unsupported`. + pub struct NullSocket { + addr: SocketAddrV4, + } + + impl NullSocket { + #[must_use] + pub const fn new(addr: SocketAddrV4) -> Self { + Self { addr } + } + } + + impl TransportSocket for NullSocket { + type SendFuture<'a> = core::future::Ready>; + type RecvFuture<'a> = core::future::Ready>; + + fn send_to<'a>(&'a self, _buf: &'a [u8], _target: SocketAddrV4) -> Self::SendFuture<'a> { + core::future::ready(Err(TransportError::Unsupported)) + } + + fn recv_from<'a>(&'a self, _buf: &'a mut [u8]) -> Self::RecvFuture<'a> { + core::future::ready(Err(TransportError::Unsupported)) + } + + fn local_addr(&self) -> Result { + Ok(self.addr) + } + + fn join_multicast_v4( + &self, + _group: Ipv4Addr, + _iface: Ipv4Addr, + ) -> Result<(), TransportError> { + Err(TransportError::Unsupported) + } + + fn leave_multicast_v4( + &self, + _group: Ipv4Addr, + _iface: Ipv4Addr, + ) -> Result<(), TransportError> { + Err(TransportError::Unsupported) + } + } + + /// Factory that "binds" a [`NullSocket`] at the requested addr. + pub struct NullFactory; + + impl TransportFactory for NullFactory { + type Socket = NullSocket; + type BindFuture<'a> = core::future::Ready>; + + fn bind<'a>( + &'a self, + addr: SocketAddrV4, + _options: &'a SocketOptions, + ) -> Self::BindFuture<'a> { + core::future::ready(Ok(NullSocket::new(addr))) + } + } + + /// Timer whose sleeps resolve immediately. + pub struct NullTimer; + + impl Timer for NullTimer { + type SleepFuture<'a> = core::future::Ready<()>; + + fn sleep(&self, _duration: Duration) -> Self::SleepFuture<'_> { + core::future::ready(()) + } + } + + /// E2E registry handle that registers nothing and checks nothing. + #[derive(Clone)] + pub struct NullE2ERegistry; + + impl E2ERegistryHandle for NullE2ERegistry { + fn register( + &self, + _key: E2EKey, + _profile: E2EProfile, + ) -> Result<(), crate::e2e::E2ERegistryFull> { + Ok(()) + } + fn unregister(&self, _key: &E2EKey) {} + fn contains_key(&self, _key: &E2EKey) -> bool { + false + } + fn protect( + &self, + _key: E2EKey, + _payload: &[u8], + _upper_header: [u8; 8], + _output: &mut [u8], + ) -> Option> { + None + } + fn check<'a>( + &self, + _key: E2EKey, + _payload: &'a [u8], + _upper_header: [u8; 8], + ) -> Option<(E2ECheckStatus, &'a [u8])> { + None + } + } + + /// Interface handle pinned to a fixed address. + #[derive(Clone)] + pub struct NullInterface(pub Ipv4Addr); + + impl InterfaceHandle for NullInterface { + fn get(&self) -> Ipv4Addr { + self.0 + } + fn set(&self, _addr: Ipv4Addr) {} + } + + /// Spawner that PANICS if asked to spawn. Probe code only + /// constructs futures, never drives them — failing loudly beats + /// violating [`Spawner`]'s poll-to-completion contract by + /// silently dropping the future. + pub struct NullSpawner; + + impl Spawner for NullSpawner { + fn spawn(&self, _future: impl Future + Send + 'static) { + panic!("NullSpawner is layout-probe-only; it never polls"); + } + } +} + #[cfg(test)] mod tests { //! The traits are pure interfaces — these tests only verify that //! trivial mock implementations compile and that defaults behave as //! documented. + use super::probe::{NullE2ERegistry, NullFactory, NullInterface, NullSocket, NullTimer}; use super::*; /// `IoErrorKind::is_transient_recv` must classify the well-known @@ -1423,73 +1579,6 @@ mod tests { assert_eq!(a.multicast_loop_v4, b.multicast_loop_v4); } - // A minimal `TransportSocket` + `TransportFactory` + `Timer` - // implementation. Exists purely to prove the trait signatures are - // implementable with zero `async` machinery — the futures are produced - // by `core::future` primitives, no executor involved. If this module - // compiles, any tokio / embassy / smoltcp adapter will also compile. - struct NullSocket { - addr: SocketAddrV4, - } - - impl TransportSocket for NullSocket { - type SendFuture<'a> = core::future::Ready>; - type RecvFuture<'a> = core::future::Ready>; - - fn send_to<'a>(&'a self, _buf: &'a [u8], _target: SocketAddrV4) -> Self::SendFuture<'a> { - core::future::ready(Err(TransportError::Unsupported)) - } - - fn recv_from<'a>(&'a self, _buf: &'a mut [u8]) -> Self::RecvFuture<'a> { - core::future::ready(Err(TransportError::Unsupported)) - } - - fn local_addr(&self) -> Result { - Ok(self.addr) - } - - fn join_multicast_v4( - &self, - _group: Ipv4Addr, - _iface: Ipv4Addr, - ) -> Result<(), TransportError> { - Err(TransportError::Unsupported) - } - - fn leave_multicast_v4( - &self, - _group: Ipv4Addr, - _iface: Ipv4Addr, - ) -> Result<(), TransportError> { - Err(TransportError::Unsupported) - } - } - - struct NullFactory; - - impl TransportFactory for NullFactory { - type Socket = NullSocket; - type BindFuture<'a> = core::future::Ready>; - - fn bind<'a>( - &'a self, - addr: SocketAddrV4, - _options: &'a SocketOptions, - ) -> Self::BindFuture<'a> { - core::future::ready(Ok(NullSocket { addr })) - } - } - - struct NullTimer; - - impl Timer for NullTimer { - type SleepFuture<'a> = core::future::Ready<()>; - - fn sleep(&self, _duration: Duration) -> Self::SleepFuture<'_> { - core::future::ready(()) - } - } - #[test] fn null_factory_bind_resolves_with_addr() { let factory = NullFactory; @@ -1501,9 +1590,7 @@ mod tests { #[test] fn max_datagram_size_default_is_udp_buffer_size() { - let sock = NullSocket { - addr: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0), - }; + let sock = NullSocket::new(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)); assert_eq!(sock.max_datagram_size(), crate::UDP_BUFFER_SIZE); } @@ -1543,52 +1630,6 @@ mod tests { assert_ne!(e, TransportError::AddressInUse); } - // Minimal no-op implementations to verify that E2ERegistryHandle and - // InterfaceHandle are implementable without any executor machinery. - #[derive(Clone)] - struct NullE2ERegistry; - - impl E2ERegistryHandle for NullE2ERegistry { - fn register( - &self, - _key: E2EKey, - _profile: E2EProfile, - ) -> Result<(), crate::e2e::E2ERegistryFull> { - Ok(()) - } - fn unregister(&self, _key: &E2EKey) {} - fn contains_key(&self, _key: &E2EKey) -> bool { - false - } - fn protect( - &self, - _key: E2EKey, - _payload: &[u8], - _upper_header: [u8; 8], - _output: &mut [u8], - ) -> Option> { - None - } - fn check<'a>( - &self, - _key: E2EKey, - _payload: &'a [u8], - _upper_header: [u8; 8], - ) -> Option<(E2ECheckStatus, &'a [u8])> { - None - } - } - - #[derive(Clone)] - struct NullInterface(Ipv4Addr); - - impl InterfaceHandle for NullInterface { - fn get(&self) -> Ipv4Addr { - self.0 - } - fn set(&self, _addr: Ipv4Addr) {} - } - #[test] fn null_e2e_registry_compiles() { let r = NullE2ERegistry; diff --git a/tests/bare_metal_e2e.rs b/tests/bare_metal_e2e.rs index 12f10919..dedeb6f4 100644 --- a/tests/bare_metal_e2e.rs +++ b/tests/bare_metal_e2e.rs @@ -43,6 +43,18 @@ use simple_someip::transport::{ use simple_someip::{Client, ClientDeps, RawPayload, Server, ServerDeps}; // ── Static-pool channel factory ─────────────────────────────────────── +// +// Pool budget: each `Client::new_with_deps` claims one `ControlMessage` +// bounded slot and one `ClientUpdate` unbounded slot for the lifetime +// of the client. Both pools hold 4. A plain parallel `cargo test` runs +// every test in this file in ONE process, so concurrent tests share +// these pools — currently 3 client-constructing tests worst-case. If a +// new test pushes that past 4, grow the two pool counts below or the +// exhaustion panic will land in whichever test loses the race. +// +// NOTE: `tools/size_probe`'s `ProbeChannels` mirrors this entry list +// for thumbv7em layout capture. If you change the entries here, +// update the probe or its measured layouts silently drift. define_static_channels! { name: E2ETestChannels, @@ -575,3 +587,106 @@ async fn client_send_request_server_runloop_stable() { run_handle.abort(); client_run_handle.abort(); } + +/// Host-arch PROXY budgets for the bare-metal-channel configuration +/// (static pools + mock transport) — the closest host-side analog to +/// the TC4 build. Same semantics/update procedure as the constants in +/// src/client/mod.rs; authoritative numbers come from +/// `tools/capture_type_sizes.sh` (thumbv7em). +const BM_CLIENT_RUN_FUTURE_BUDGET: usize = 34048; // = ceil64(27208 × 1.25) +const BM_CLIENT_SOCKET_LOOP_BUDGET: usize = 2816; // = ceil64(2224 × 1.25) +const BM_SERVER_RUN_FUTURE_BUDGET: usize = 9664; // = ceil64(7696 × 1.25) + +#[tokio::test] +async fn future_size_witness_bare_metal_channels() { + use core::sync::atomic::{AtomicUsize, Ordering}; + + #[derive(Clone)] + struct SizeRecordingSpawner { + max_spawned: Arc, + } + + impl Spawner for SizeRecordingSpawner { + fn spawn(&self, future: impl core::future::Future + Send + 'static) { + self.max_spawned + .fetch_max(core::mem::size_of_val(&future), Ordering::SeqCst); + let _run_handle = tokio::spawn(future); + } + } + + let network = SharedNetwork::new(); + + // ── Server (mirror client_receives_server_sd_announcement) ── + let server_factory = MockFactory { + tx_pipe: Arc::clone(&network.server_to_client), + rx_pipe: Arc::clone(&network.client_to_server), + next_port: Arc::new(Mutex::new(0)), + }; + let server_e2e: Arc> = Arc::new(Mutex::new(E2ERegistry::new())); + let server_config = ServerConfig::new(0x1234, 1) + .with_interface(Ipv4Addr::LOCALHOST) + .with_local_port(30600); + let server_deps = ServerDeps { + factory: server_factory, + timer: MockTimer, + e2e_registry: server_e2e, + subscriptions: MockSubscriptions::default(), + non_sd_observer: None, + }; + let (_server, _handles, server_run): ( + Server>, MockSubscriptions>, + _, + _, + ) = Server::new_with_deps(server_deps, server_config, false) + .await + .expect("server creation"); + let server_run_size = core::mem::size_of_val(&server_run); + drop(server_run); // not driven; witness only + + // ── Client (static channel pools) ── + let client_factory = MockFactory { + tx_pipe: Arc::clone(&network.client_to_server), + rx_pipe: Arc::clone(&network.server_to_client), + next_port: Arc::new(Mutex::new(100)), + }; + let max_spawned = Arc::new(AtomicUsize::new(0)); + let client_deps = ClientDeps { + factory: client_factory, + spawner: SizeRecordingSpawner { + max_spawned: Arc::clone(&max_spawned), + }, + timer: MockTimer, + e2e_registry: Arc::new(Mutex::new(E2ERegistry::new())), + interface: Arc::new(RwLock::new(Ipv4Addr::LOCALHOST)), + }; + let (client, _updates, run_fut) = Client::< + RawPayload, + Arc>, + Arc>, + E2ETestChannels, + >::new_with_deps(client_deps, false); + + let run_size = core::mem::size_of_val(&run_fut); + let _run_handle = tokio::spawn(run_fut); + client.bind_discovery().await.expect("bind_discovery"); + let loop_size = max_spawned.load(Ordering::SeqCst); + + println!("FUTURE_SIZE bm_client_run_future {run_size}"); + println!("FUTURE_SIZE bm_client_socket_loop {loop_size}"); + println!("FUTURE_SIZE bm_server_run_future {server_run_size}"); + + assert!(loop_size > 0, "spawner never received the socket loop"); + assert!( + run_size <= BM_CLIENT_RUN_FUTURE_BUDGET, + "client run future grew: {run_size} B > budget {BM_CLIENT_RUN_FUTURE_BUDGET} B" + ); + assert!( + loop_size <= BM_CLIENT_SOCKET_LOOP_BUDGET, + "socket loop future grew: {loop_size} B > budget {BM_CLIENT_SOCKET_LOOP_BUDGET} B" + ); + assert!( + server_run_size <= BM_SERVER_RUN_FUTURE_BUDGET, + "server run future grew: {server_run_size} B > budget {BM_SERVER_RUN_FUTURE_BUDGET} B" + ); + client.shut_down(); +} diff --git a/tests/bare_metal_server.rs b/tests/bare_metal_server.rs index ba96752c..25952ac5 100644 --- a/tests/bare_metal_server.rs +++ b/tests/bare_metal_server.rs @@ -32,12 +32,12 @@ use std::sync::{Arc, Mutex}; use std::vec::Vec; use simple_someip::e2e::E2ERegistry; +use simple_someip::server::NonSdRequestCallback; use simple_someip::server::ServerConfig; use simple_someip::server::{SubscribeError, Subscriber, SubscriptionHandle}; use simple_someip::transport::{ ReceivedDatagram, SocketOptions, Timer, TransportError, TransportFactory, TransportSocket, }; -use simple_someip::server::NonSdRequestCallback; use simple_someip::{Server, ServerDeps}; // ── Mock transport ───────────────────────────────────────────────────── @@ -556,9 +556,7 @@ async fn non_sd_observer_none_preserves_ignore_behavior() { // arm cycles at least once. tokio::time::sleep(Duration::from_millis(10)).await; - let observed = OBSERVED_NONE - .get() - .and_then(|m| m.lock().unwrap().clone()); + let observed = OBSERVED_NONE.get().and_then(|m| m.lock().unwrap().clone()); assert!( observed.is_none(), "callback must NOT fire when non_sd_observer is None; got {:?}", diff --git a/tests/no_alloc_server_witness.rs b/tests/no_alloc_server_witness.rs index 7761493c..026983de 100644 --- a/tests/no_alloc_server_witness.rs +++ b/tests/no_alloc_server_witness.rs @@ -131,10 +131,10 @@ fn poll_once_to_ready(mut fut: Pin<&mut F>) -> F::Output { // `SubscriptionManager::new()` is `const`, so the backing storage can // live in a plain `static` — no `Box::leak` needed. -static SUBS: StaticSubscriptionStorage = - BlockingMutex::>::new(RefCell::new( - SubscriptionManager::new(), - )); +static SUBS: StaticSubscriptionStorage = BlockingMutex::< + CriticalSectionRawMutex, + RefCell, +>::new(RefCell::new(SubscriptionManager::new())); // ── Witnesses ───────────────────────────────────────────────────────────── diff --git a/tools/capture_type_sizes.sh b/tools/capture_type_sizes.sh new file mode 100755 index 00000000..33aff377 --- /dev/null +++ b/tools/capture_type_sizes.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Capture -Zprint-type-sizes async-future layouts (PR 0, issue #125). +# +# Host capture — compiles the bare_metal_e2e test (instantiates +# Client+Server with mock deps) on the host triple. +# Thumb capture — compiles tools/size_probe (instantiates the +# client futures no_std) for thumbv7em-none-eabihf. +# THESE are the authoritative numbers. +# +# Dedicated CARGO_TARGET_DIRs force fresh builds: rustc only emits +# type sizes for crates it actually (re)compiles. +# +# Usage: tools/capture_type_sizes.sh [out_dir] (default target/type-sizes) +set -euo pipefail +cd "$(dirname "$0")/.." +OUT="${1:-target/type-sizes}" +mkdir -p "$OUT" +# Resolve to an absolute path so the thumb capture's `cd` into +# tools/size_probe can't break a relative CARGO_TARGET_DIR/out path. +OUT="$(cd "$OUT" && pwd)" +# Wipe previous capture builds — a warm CARGO_TARGET_DIR turns the +# build into a no-op and rustc emits no type sizes on a no-op. +rm -rf "$OUT/host" "$OUT/thumb" + +echo "== host capture (bare_metal_e2e test) ==" +RUSTFLAGS="-Zprint-type-sizes" CARGO_TARGET_DIR="$OUT/host" \ + cargo +nightly test --no-run --features client,server,bare_metal \ + --test bare_metal_e2e >"$OUT/host_raw.txt" 2>&1 \ + || { echo "host capture FAILED; tail:"; tail -20 "$OUT/host_raw.txt"; exit 1; } + +echo "== thumb capture (size_probe) ==" +( cd tools/size_probe && \ + RUSTFLAGS="-Zprint-type-sizes" CARGO_TARGET_DIR="$OUT/thumb" \ + cargo +nightly build --release --target thumbv7em-none-eabihf \ + >"$OUT/thumb_raw.txt" 2>&1 ) \ + || { echo "thumb capture FAILED; tail:"; tail -20 "$OUT/thumb_raw.txt"; exit 1; } + +summarize() { + # `|| true` is load-bearing twice over: grep exits 1 on an empty + # capture (no async-future lines), and `head -40` SIGPIPEs `sort` + # (exit 141) whenever there are >40 rows — either would abort the + # whole script under `set -euo pipefail` mid-summary. + grep 'print-type-size type' "$1" \ + | grep -E 'async fn body|async block' \ + | sed -E 's/.*type: `([^`]+)`: ([0-9]+) bytes.*/\2 \1/' \ + | sort -rn | head -40 \ + | awk '{printf "| %s | %s |\n", $1, substr($0, length($1)+2)}' \ + || true +} + +{ + echo "# Type-size capture — $(rustc +nightly --version)" + echo + echo "## thumbv7em (authoritative)" + echo "| bytes | future |" + echo "|---|---|" + summarize "$OUT/thumb_raw.txt" + echo + echo "## host x86_64 (proxy)" + echo "| bytes | future |" + echo "|---|---|" + summarize "$OUT/host_raw.txt" +} >"$OUT/summary.md" + +echo "wrote $OUT/summary.md" diff --git a/tools/size_probe/Cargo.lock b/tools/size_probe/Cargo.lock index 85d3e6ff..d87e6df8 100644 --- a/tools/size_probe/Cargo.lock +++ b/tools/size_probe/Cargo.lock @@ -76,6 +76,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -95,6 +106,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", ] @@ -159,15 +171,17 @@ dependencies = [ "crc", "embassy-sync", "embedded-io 0.7.1", + "futures-util", "heapless 0.9.2", "thiserror", - "tracing", ] [[package]] name = "size_probe" version = "0.0.0" dependencies = [ + "embedded-io 0.7.1", + "heapless 0.9.2", "simple-someip", ] @@ -208,22 +222,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" - [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/tools/size_probe/Cargo.toml b/tools/size_probe/Cargo.toml index a16a6b67..26057c47 100644 --- a/tools/size_probe/Cargo.toml +++ b/tools/size_probe/Cargo.toml @@ -12,31 +12,52 @@ version = "0.0.0" edition = "2024" publish = false -# Phase-20-pre flash-size measurement probe. Builds a `staticlib` -# that exposes `extern "C"` shims around simple-someip's -# Option-A-relevant entry points, so post-link dead-code-elimination -# only keeps what an actual halo-style FFI consumer would call. +# Two probes share this crate: +# +# 1. Phase-20-pre flash-size probe: `extern "C"` shims around +# simple-someip's Option-A-relevant entry points, so post-link +# dead-code-elimination only keeps what an actual halo-style FFI +# consumer would call. +# 2. Client-future layout probe (PR 0, issue #125): instantiates the +# client run future so `-Zprint-type-sizes` reports real thumbv7em +# layouts. Driven by `tools/capture_type_sizes.sh`. # # Build: # cd tools/size_probe && cargo build --release --target thumbv7em-none-eabihf # -# Measure: +# Measure (flash floor): # llvm-size target/thumbv7em-none-eabihf/release/libsize_probe.a # -# NOT a real production crate — exists purely to give us a flash-size -# floor on the cortex-m4f target, since we don't have access to the -# actual proxy LLVM-IR-TriCore toolchain locally. +# CAVEAT: `llvm-size` on the staticlib does no DCE, so since the +# layout probe landed the raw number also includes the client +# run-future machinery + `ProbeChannels` pool `.bss` — it is NOT +# comparable to the phase-20-pre flash-floor baseline. For a codec-only +# flash floor, link only the three codec symbols and measure the +# linked output. +# +# NOT a real production crate — exists purely for measurement, since +# we don't have access to the actual proxy LLVM-IR-TriCore toolchain +# locally. [lib] name = "size_probe" crate-type = ["staticlib"] [dependencies] -# `bare_metal` only — no `server` (pulls `extern crate alloc` per -# the lib.rs feature table). Codec-only FFI doesn't need server's -# Server actor or Arc-shared state. `client` would be alloc-free -# but not needed here either. Matches halo PR #4429's surface. -simple-someip = { path = "../..", default-features = false, features = ["bare_metal"] } +# `client,bare_metal` — an audited alloc-free combo (`server` is +# alloc-free too since PR #124; the layout probe just targets the +# client futures in PR 0 — server probing lands with PR 3's static +# plumbing). `bare_metal` covers the codec-only FFI surface (matches halo PR +# #4429); `client` adds the run-future instantiation used by the +# `-Zprint-type-sizes` layout probe (PR 0, issue #125). The +# `NullAllocator` in lib.rs stays as the link target for the +# transitive `extern crate alloc`. +simple-someip = { path = "../..", default-features = false, features = ["bare_metal", "client"] } +# For the layout probe's `ProbePayload` (a no_std `PayloadWireFormat` +# impl — `RawPayload` is std-gated). Versions track the parent +# crate's own `heapless` / `embedded-io` dependencies. +heapless = "0.9" +embedded-io = { version = "0.7", default-features = false } [profile.release] opt-level = "z" # optimize for size diff --git a/tools/size_probe/src/lib.rs b/tools/size_probe/src/lib.rs index 7c41fb46..2ce0cf13 100644 --- a/tools/size_probe/src/lib.rs +++ b/tools/size_probe/src/lib.rs @@ -1,14 +1,20 @@ -//! Phase-20-pre flash-size measurement probe. +//! no_std measurement probes for `thumbv7em-none-eabihf`. Two live here: //! -//! Mirrors halo PR #4429's `rust_simple_someip` C-callable FFI -//! surface (header encode/decode + E2E protect/check round-trips) -//! to get a realistic post-link flash-size floor on -//! `thumbv7em-none-eabihf` for what a Halo TC4D `rust_simple_someip` -//! staticlib would cost. +//! 1. **Flash-size probe** (phase 20-pre): mirrors halo PR #4429's +//! `rust_simple_someip` C-callable FFI surface (header +//! encode/decode + E2E protect/check round-trips) to get a +//! realistic post-link flash-size floor for what a Halo TC4D +//! `rust_simple_someip` staticlib would cost. +//! 2. **Client-future layout probe** (PR 0, issue #125): instantiates +//! the client run future with zero-behavior deps so +//! `-Zprint-type-sizes` reports its real on-target layout — see +//! `client_future_probe` below and `tools/capture_type_sizes.sh`. //! //! NOT production code. Exposes `#[no_mangle] extern "C"` entry //! points only so post-link DCE keeps what an actual FFI consumer -//! would reach, and discards everything else. +//! would reach, and discards everything else. Flash measurements that +//! predate the layout probe only linked the codec symbols — see the +//! comparability caveat in this crate's `Cargo.toml`. #![no_std] @@ -21,8 +27,10 @@ use core::slice; /// transitive dep pulls `extern crate alloc` even with simple-someip's /// `default-features = false`, requiring a `#[global_allocator]` /// link target. The codec-only FFI surface (header encode + E2E -/// protect/check) never actually allocates, so a `null_mut()` return -/// is sound for the probe — if any code path ever does try to alloc, +/// protect/check) never actually allocates, and the client layout +/// probe rides the `client,bare_metal` combo certified alloc-free by +/// the TC4 audit (CI's `nm` alloc-symbol gate), so a `null_mut()` +/// return is sound for the probe — if any code path ever does try to alloc, /// the resulting null deref shows up at runtime as the FFI-design /// bug it is, rather than being papered over with hidden heap usage. /// (Named `NullAllocator` rather than `PanicAllocator` because it @@ -223,3 +231,185 @@ pub unsafe extern "C" fn e2e_profile5_round_trip( out.payload_match = i32::from(result.payload == Some(payload)); out } + +// ── Client-future layout probe (PR 0, issue #125) ──────────────────── +// +// Instantiates the client's run-future and (transitively) the +// per-socket loop with zero-behavior deps so `-Zprint-type-sizes` +// reports their REAL thumbv7em layouts during this crate's codegen. +// The entry point is `extern "C"` + `#[unsafe(no_mangle)]` purely so +// post-link DCE keeps the instantiation; nothing ever calls it on +// hardware. The server probe is deferred to PR 3 (needs the no-alloc +// `new_with_handles` static plumbing that PR 3 builds anyway). + +mod client_future_probe { + use simple_someip::client::Error as ClientError; + use simple_someip::client::{ClientUpdate, ControlMessage, ReceivedMessage, SendMessage}; + use simple_someip::protocol::sd::RebootFlag; + use simple_someip::protocol::{MessageId, sd}; + use simple_someip::transport::probe::{ + NullE2ERegistry, NullFactory, NullInterface, NullSpawner, NullTimer, + }; + use simple_someip::{Client, ClientDeps, PayloadWireFormat, WireFormat}; + + // `RawPayload` is std-gated (heap `Vec` SD storage), so the probe + // carries its own minimal no_std `PayloadWireFormat` impl — + // heapless 4-entry SD storage, mirroring the crate-internal + // `protocol::sd::test_support::TestPayload` (which is + // `pub(crate)` and unreachable from here). A real firmware build + // ships its own payload type the same way. + + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct ProbeSdHeader { + flags: sd::Flags, + entries: heapless::Vec, + options: heapless::Vec, + } + + impl WireFormat for ProbeSdHeader { + fn required_size(&self) -> usize { + sd::Header::new(self.flags, &self.entries, &self.options).required_size() + } + fn encode( + &self, + writer: &mut T, + ) -> Result { + sd::Header::new(self.flags, &self.entries, &self.options).encode(writer) + } + } + + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct ProbePayload { + header: ProbeSdHeader, + } + + impl PayloadWireFormat for ProbePayload { + type SdHeader = ProbeSdHeader; + fn message_id(&self) -> MessageId { + MessageId::SD + } + fn as_sd_header(&self) -> Option<&ProbeSdHeader> { + Some(&self.header) + } + fn from_payload_bytes( + message_id: MessageId, + payload: &[u8], + ) -> Result { + match message_id { + MessageId::SD => { + let view = sd::SdHeaderView::parse(payload)?; + let mut entries = heapless::Vec::new(); + for ev in view.entries() { + entries.push(ev.to_owned().unwrap()).ok(); + } + let mut options = heapless::Vec::new(); + for ov in view.options() { + options.push(ov.to_owned().unwrap()).ok(); + } + Ok(Self { + header: ProbeSdHeader { + flags: view.flags(), + entries, + options, + }, + }) + } + _ => Err(simple_someip::protocol::Error::UnsupportedMessageID( + message_id, + )), + } + } + fn new_sd_payload(header: &ProbeSdHeader) -> Self { + Self { + header: header.clone(), + } + } + fn sd_flags(&self) -> Option { + Some(self.header.flags) + } + fn required_size(&self) -> usize { + self.header.required_size() + } + fn encode( + &self, + writer: &mut T, + ) -> Result { + self.header.encode(writer) + } + fn new_subscription_sd_header( + service_id: u16, + instance_id: u16, + major_version: u8, + ttl: u32, + event_group_id: u16, + client_ip: core::net::Ipv4Addr, + protocol: sd::TransportProtocol, + client_port: u16, + reboot_flag: sd::RebootFlag, + ) -> ProbeSdHeader { + let entry = sd::Entry::SubscribeEventGroup(sd::EventGroupEntry::new( + service_id, + instance_id, + major_version, + ttl, + event_group_id, + )); + let endpoint = sd::Options::IpV4Endpoint { + ip: client_ip, + protocol, + port: client_port, + }; + let mut entries = heapless::Vec::new(); + entries.push(entry).unwrap(); + let mut options = heapless::Vec::new(); + options.push(endpoint).unwrap(); + ProbeSdHeader { + flags: sd::Flags::new_sd(reboot_flag), + entries, + options, + } + } + fn set_reboot_flag(header: &mut ProbeSdHeader, reboot: sd::RebootFlag) { + header.flags = sd::Flags::new(bool::from(reboot), header.flags.unicast()); + } + } + + // Entry list mirrors `tests/bare_metal_e2e.rs`'s `E2ETestChannels` + // (with `ProbePayload` standing in for the std-gated `RawPayload`) + // so the probed futures see the same channel shapes as the host + // capture. + simple_someip::define_static_channels! { + name: ProbeChannels, + oneshot: [ + (Result<(), ClientError>, 16), + (Result, 8), + (Result, 8), + ], + bounded: [ + ((ControlMessage, 4), 4), + ((SendMessage, 16), 8), + ((Result, ClientError>, 16), 8), + ], + unbounded: [ + (ClientUpdate, 4), + ], + } + + #[unsafe(no_mangle)] + pub extern "C" fn probe_client_run_future_size() -> usize { + let deps = ClientDeps { + factory: NullFactory, + spawner: NullSpawner, + timer: NullTimer, + e2e_registry: NullE2ERegistry, + interface: NullInterface(core::net::Ipv4Addr::LOCALHOST), + }; + let (_client, _updates, run_fut) = Client::< + ProbePayload, + NullE2ERegistry, + NullInterface, + ProbeChannels, + >::new_with_deps(deps, false); + core::mem::size_of_val(&run_fut) + } +}