Phase20g vsomeip docker setup#110
Conversation
Adds a `tools/size_probe/` workspace member that mirrors halo PR
#4429's `rust_simple_someip` C-callable FFI surface (header
encode/decode + E2E Profile 4/5 round-trips) and builds as a
`staticlib` for `thumbv7em-none-eabihf`. Used during phase
20-pre to estimate simple-someip's flash footprint.
Build + measure:
cargo build -p size_probe --release --target thumbv7em-none-eabihf
llvm-size target/thumbv7em-none-eabihf/release/libsize_probe.a
(rustup toolchain ships `llvm-size` under
`~/.rustup/toolchains/.../bin/`).
Why a probe instead of measuring simple-someip's rlib directly:
rlibs include compiler metadata that bloats them ~60×. A
staticlib with `extern "C"` entry points lets post-link
dead-code elimination strip everything an actual FFI consumer
wouldn't reach, giving a closer-to-real-world flash number.
First measurement (default release profile, no `opt-level=z`,
no LTO at the probe level): ~12 KB of simple-someip-specific
text + 14 KB of transitive dep code (heapless, thiserror,
tracing). Compiler-rt builtins and `core::fmt` chains aren't
simple-someip-unique — they're amortized firmware-wide — and
were excluded from the per-component breakdown.
NOT a production crate. Pure measurement tool. Includes a
panic-on-alloc stub `GlobalAlloc` to satisfy the link-target
requirement on builds where some transitive dep pulls
`extern crate alloc` even though the codec FFI surface itself
is alloc-free.
Why thumbv7em-none-eabihf and not the actual TC4D target:
halo's TriCore build pipeline uses an in-house LLVM-IR-to-
TriCore proxy + a private Docker image we don't have local
access to. cortex-m4f is the closest upstream-Rust-supported
target with similar code-density characteristics; gives a
defensible bracket for the real TC4D flash cost (likely within
±50% on the proxy toolchain).
Future use: when the Option-A stateful FFI surface lands,
re-add equivalent `extern "C"` shims for the new entry points
(`rust_handle_udp_rx`, `rust_tick`, etc.) and re-measure.
Lets us track the flash-cost delta from codec-only → full
state machines as that work progresses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Makes phase 20f's `tests/vsomeip_sd_compat.rs` actually
runnable. Adds `tests/data/vsomeip-offerer/`:
- `Dockerfile` — multi-stage Ubuntu 22.04 base. Stage 1 builds
vsomeip 3.4.10 (the LumPDK / EnVision pinned version per
`LumPDK/packages/thirdparty/vsomeip/vsomeip.MODULE.bazel`)
from upstream tarball, plus our minimal C++ offerer. Stage 2
is a slim runtime image with just libvsomeip3 + the offerer
binary + entrypoint. ~463 MB final image.
- `offerer.cpp` — ~85 LOC. Calls `application->offer_service(0x1234,
0x0001, 1, 0)` and idles while vsomeip's SD subsystem emits
cyclic OfferService broadcasts.
- `offerer.json` — vsomeip configuration. Standard SD multicast
`224.0.23.0:30490` per spec defaults; cyclic_offer_delay=1000ms;
ttl=5s. `unicast` is templated at container start (see below).
- `entrypoint.sh` — substitutes `VSOMEIP_UNICAST` env var into the
JSON before exec'ing the offerer. Bails loudly if the env var
isn't set. The substitution exists because `unicast: 127.0.0.1`
doesn't work on Linux — `lo` lacks the `MULTICAST` flag by
default, so SD multicast never actually leaves the host. Caller
must pick a real interface IP via `ip route get 224.0.23.0`.
- `CMakeLists.txt` — builds offerer against `find_package(vsomeip3)`.
- `README.md` — full build + run + test invocation flow with the
multicast-on-lo gotcha documented.
Test file (`tests/vsomeip_sd_compat.rs`) module docs updated to
match the new harness shape. The `#[ignore]`'d test itself is
unchanged from 20f.
Verified end-to-end on 2026-04-29:
docker build --network=host -t vsomeip-offerer tests/data/vsomeip-offerer/
docker run --rm -d --name vsomeip-offerer --network host \
-e VSOMEIP_UNICAST=172.20.21.206 vsomeip-offerer
SIMPLE_SOMEIP_TEST_INTERFACE=172.20.21.206 \
cargo test --features client-tokio,server-tokio \
--test vsomeip_sd_compat -- --ignored --nocapture
# client_sees_vsomeip_offer_service ... ok in 0.59s
This is the FIRST wire-level conformance signal in the project.
Every prior test ran simple-someip on both sides of the wire and
couldn't catch protocol non-compliance against an external
reference. Today: simple-someip's Client successfully decoded a
real vsomeip-emitted SD `OfferService` entry — service ID,
instance ID, TTL, major/minor version, source address all
matched the spec.
What this proves:
- vsomeip 3.4.10 builds + runs from upstream source in our docker
- simple-someip's SD-receive code path is wire-conformant against
vsomeip's SD-emit path for OfferService entries (one rung)
What this does NOT prove (worth being explicit about):
- Anything on TC4D — all of this is x86_64 Linux + native upstream
Rust + tokio. No proxy LLVM-IR-TriCore exercise.
- Bidirectional wire compatibility — we only tested vsomeip ->
simple-someip. The reverse (simple-someip emits SD that vsomeip
parses) is the next test (phase 20h).
- Other SD entry types — FindService, SubscribeEventGroup,
SubscribeAck, SubscribeNack are all separate code paths.
- Anything stateful — request/response correlation, subscription
state, event publishing, E2E protect/check on real payloads.
- The lwip transport story — vsomeip uses its own UDP socket; nothing
about Halo's planned lwip integration was tested.
- The Option-A FFI shape — doesn't exist yet. This test went
through simple-someip's existing tokio/`Client` API, which
Halo won't use in production.
CI integration deferred. The test stays `#[ignore]`'d by default;
flipping it on `cargo test` would fail until a CI runner has
docker + the harness available. That's the next phase (20i?)
once we have the full conformance test set built out.
What this leaves:
- 20h: bidirectional SD test (simple-someip emits, vsomeip
subscribes; proves TX wire format).
- 20i+: SubscribeEventGroup roundtrip, request/response, E2E
conformance.
- Eventual CI: TestContainers-rs (or equivalent) to bring up
this docker on every PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
09c9e00 to
886e2fe
Compare
There was a problem hiding this comment.
Pull request overview
Adds a runnable local harness for the ignored vsomeip_sd_compat integration test by introducing a vsomeip-based Docker offerer container, and updates the test’s module docs accordingly. Also introduces a new tools/size_probe workspace crate intended for no-std flash-size probing via an exported C ABI surface.
Changes:
- Add
tests/data/vsomeip-offerer/Docker image (vsomeip 3.4.10 build + minimal C++ offerer + entrypoint templating) to emit SOME/IP-SD OfferService broadcasts. - Update
tests/vsomeip_sd_compat.rs“Running locally” documentation to use the new container flow and document the multicast/loopback gotcha. - Add a new
tools/size_probeno-stdstaticlibcrate to measure post-link size of an FFI-like surface, and register it in the workspace.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| Cargo.toml | Adds tools/size_probe to workspace members. |
| Cargo.lock | Records the new size_probe package in the lockfile. |
| tools/size_probe/Cargo.toml | Defines a no-std staticlib crate configured for size-optimized builds. |
| tools/size_probe/src/lib.rs | Implements exported C ABI shims for SOME/IP header encode + E2E profile round-trips. |
| tests/vsomeip_sd_compat.rs | Updates local-run documentation to match the new dockerized vsomeip offerer harness. |
| tests/data/vsomeip-offerer/Dockerfile | Multi-stage build: compiles vsomeip 3.4.10 + offerer; runtime image packages libs + binary + entrypoint. |
| tests/data/vsomeip-offerer/offerer.cpp | Minimal vsomeip app that offers a fixed service/instance and stays alive to emit SD offers. |
| tests/data/vsomeip-offerer/offerer.json | vsomeip configuration template with SD multicast defaults and a templated unicast address. |
| tests/data/vsomeip-offerer/entrypoint.sh | Templates VSOMEIP_UNICAST into config then runs offerer; errors if env var missing. |
| tests/data/vsomeip-offerer/CMakeLists.txt | Builds the offerer against vsomeip3. |
| tests/data/vsomeip-offerer/README.md | Step-by-step build/run/test instructions for the harness. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let Ok(msg_type_raw) = MessageType::try_from(h.message_type & 0xBF) else { | ||
| return 0; | ||
| }; | ||
| let msg_type = MessageTypeField::new(msg_type_raw, (h.message_type & 0x20) != 0); |
There was a problem hiding this comment.
MessageType::try_from(h.message_type & 0xBF) masks off bit 0x40 before validation. That can turn otherwise-invalid on-wire message_type values (with reserved bits set) into valid ones (e.g., 0x40 becomes Request), which is incorrect for a header encode shim. Prefer validating the raw byte and constructing the field from it (e.g., MessageTypeField::try_from(h.message_type)), rather than clearing bits first.
| let Ok(msg_type_raw) = MessageType::try_from(h.message_type & 0xBF) else { | |
| return 0; | |
| }; | |
| let msg_type = MessageTypeField::new(msg_type_raw, (h.message_type & 0x20) != 0); | |
| let Ok(msg_type) = MessageTypeField::try_from(h.message_type) else { | |
| return 0; | |
| }; |
| pub length: u32, | ||
| pub client_id: u16, | ||
| pub session_id: u16, | ||
| pub protocol_version: u8, | ||
| pub interface_version: u8, | ||
| pub message_type: u8, | ||
| pub return_code: u8, | ||
| } | ||
|
|
||
| /// # Safety | ||
| /// Caller must ensure `header` points to a valid `CSomeIpHeader` and | ||
| /// `buf` points to at least `buf_len` writable bytes. | ||
| #[unsafe(no_mangle)] | ||
| pub unsafe extern "C" fn someip_header_encode( | ||
| header: *const CSomeIpHeader, | ||
| buf: *mut u8, | ||
| buf_len: usize, | ||
| ) -> usize { | ||
| if header.is_null() || buf.is_null() || buf_len < 16 { | ||
| return 0; | ||
| } | ||
| let h = unsafe { &*header }; | ||
| let message_id = MessageId::new_from_service_and_method(h.service_id, h.method_id); | ||
| let request_id = (u32::from(h.client_id) << 16) | u32::from(h.session_id); | ||
| let Ok(msg_type_raw) = MessageType::try_from(h.message_type & 0xBF) else { | ||
| return 0; | ||
| }; | ||
| let msg_type = MessageTypeField::new(msg_type_raw, (h.message_type & 0x20) != 0); | ||
| let Ok(ret_code) = ReturnCode::try_from(h.return_code) else { | ||
| return 0; | ||
| }; | ||
| let header = Header::new( | ||
| message_id, | ||
| request_id, | ||
| h.protocol_version, | ||
| h.interface_version, | ||
| msg_type, | ||
| ret_code, | ||
| 0, | ||
| ); |
There was a problem hiding this comment.
CSomeIpHeader includes a length field, but someip_header_encode ignores it and always constructs the Header with payload_len = 0, forcing the encoded length to 8. If the intent is to encode an arbitrary on-wire header (as the FFI shape suggests), use Header::from_fields(..., h.length, ...) or otherwise derive the payload length from h.length and pass that through so the encoded header matches the caller-provided value.
| /// Stub allocator. Some 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 | ||
| /// this stub returning null on alloc is sound for the probe; if any | ||
| /// path it fronts ever does allocate, that's an explicit FFI-design | ||
| /// bug surfaced at link time, not silent corruption at runtime. | ||
| struct PanicAllocator; | ||
|
|
||
| unsafe impl GlobalAlloc for PanicAllocator { | ||
| unsafe fn alloc(&self, _: Layout) -> *mut u8 { | ||
| ptr::null_mut() | ||
| } | ||
| unsafe fn dealloc(&self, _: *mut u8, _: Layout) {} | ||
| } |
There was a problem hiding this comment.
The allocator docs say this stub returning null will surface allocation attempts "at link time", but a null-returning GlobalAlloc::alloc would instead be a runtime OOM path (and depending on how allocation is triggered, could end up in an abort or an alloc error handler). Also, the type name PanicAllocator is misleading since it never panics. Consider updating the comment to describe the actual behavior and/or renaming the allocator, or making alloc explicitly abort/panic if you want allocation attempts to fail loudly.
| @@ -0,0 +1,34 @@ | |||
| { | |||
| "_comment": "vsomeip configuration for the phase-20f host-side conformance test offerer. Defaults follow the SOME/IP-SD spec (multicast 224.0.23.0:30490) so simple-someip's test-side Client picks up the broadcast without any non-default routing. The 'unicast' field is the vsomeip-side IP — 127.0.0.1 works on Linux Docker host-network mode because both sides share the loopback. For real-NIC testing, set unicast to the host interface IP and adjust the test's SIMPLE_SOMEIP_TEST_INTERFACE env var to match.", | |||
There was a problem hiding this comment.
The _comment text claims unicast: 127.0.0.1 works in host-network mode, but the rest of this harness (entrypoint.sh + test docs) explicitly says loopback doesn't work due to lo lacking MULTICAST. This is confusing for users; update/remove the 127.0.0.1 claim so the JSON docs match the required VSOMEIP_UNICAST behavior.
| "_comment": "vsomeip configuration for the phase-20f host-side conformance test offerer. Defaults follow the SOME/IP-SD spec (multicast 224.0.23.0:30490) so simple-someip's test-side Client picks up the broadcast without any non-default routing. The 'unicast' field is the vsomeip-side IP — 127.0.0.1 works on Linux Docker host-network mode because both sides share the loopback. For real-NIC testing, set unicast to the host interface IP and adjust the test's SIMPLE_SOMEIP_TEST_INTERFACE env var to match.", | |
| "_comment": "vsomeip configuration for the phase-20f host-side conformance test offerer. Defaults follow the SOME/IP-SD spec (multicast 224.0.23.0:30490) so simple-someip's test-side Client picks up the broadcast without any non-default routing. The 'unicast' field is the vsomeip-side IP and must be set to a non-loopback host interface IP with MULTICAST support. Set unicast via VSOMEIP_UNICAST and adjust the test's SIMPLE_SOMEIP_TEST_INTERFACE env var to match.", |
| # Run (host network mode so SD multicast 224.0.23.0:30490 reaches the | ||
| # host's listener — required for the cargo test to receive the | ||
| # OfferService broadcast): | ||
| # docker run --rm -d --name vsomeip-offerer --network host \ |
There was a problem hiding this comment.
The Dockerfile's header comment shows a docker run ... vsomeip-offerer invocation without -e VSOMEIP_UNICAST=..., but the image ENTRYPOINT requires VSOMEIP_UNICAST and exits with an error if it's missing. Update the example run command in the comment block to include -e VSOMEIP_UNICAST=<iface-ip> so the documented usage matches actual behavior.
| # docker run --rm -d --name vsomeip-offerer --network host \ | |
| # docker run --rm -d --name vsomeip-offerer --network host \ | |
| # -e VSOMEIP_UNICAST=<iface-ip> \ |
| "examples/embassy_net_client", | ||
| "simple-someip-embassy-net", | ||
| "tools/size_probe", |
There was a problem hiding this comment.
PR description focuses on making tests/vsomeip_sd_compat.rs runnable via the new vsomeip Docker harness, but this PR also adds a new workspace member tools/size_probe (and lockfile entries) that isn't mentioned. If size_probe is intentional here, please call it out in the PR description; otherwise consider splitting it into a separate PR to keep scope focused.
Makes phase 20f's
tests/vsomeip_sd_compat.rsactuallyrunnable. Adds
tests/data/vsomeip-offerer/:Dockerfile— multi-stage Ubuntu 22.04 base. Stage 1 buildsvsomeip 3.4.10 (the LumPDK / EnVision pinned version per
LumPDK/packages/thirdparty/vsomeip/vsomeip.MODULE.bazel)from upstream tarball, plus our minimal C++ offerer. Stage 2
is a slim runtime image with just libvsomeip3 + the offerer
binary + entrypoint. ~463 MB final image.
offerer.cpp— ~85 LOC. Callsapplication->offer_service(0x1234, 0x0001, 1, 0)and idles while vsomeip's SD subsystem emitscyclic OfferService broadcasts.
offerer.json— vsomeip configuration. Standard SD multicast224.0.23.0:30490per spec defaults; cyclic_offer_delay=1000ms;ttl=5s.
unicastis templated at container start (see below).entrypoint.sh— substitutesVSOMEIP_UNICASTenv var into theJSON before exec'ing the offerer. Bails loudly if the env var
isn't set. The substitution exists because
unicast: 127.0.0.1doesn't work on Linux —
lolacks theMULTICASTflag bydefault, so SD multicast never actually leaves the host. Caller
must pick a real interface IP via
ip route get 224.0.23.0.CMakeLists.txt— builds offerer againstfind_package(vsomeip3).README.md— full build + run + test invocation flow with themulticast-on-lo gotcha documented.
Test file (
tests/vsomeip_sd_compat.rs) module docs updated tomatch the new harness shape. The
#[ignore]'d test itself isunchanged from 20f.
Verified end-to-end on 2026-04-29:
This is the FIRST wire-level conformance signal in the project.
Every prior test ran simple-someip on both sides of the wire and
couldn't catch protocol non-compliance against an external
reference. Today: simple-someip's Client successfully decoded a
real vsomeip-emitted SD
OfferServiceentry — service ID,instance ID, TTL, major/minor version, source address all
matched the spec.
What this proves:
vsomeip's SD-emit path for OfferService entries (one rung)
What this does NOT prove (worth being explicit about):
Rust + tokio. No proxy LLVM-IR-TriCore exercise.
simple-someip. The reverse (simple-someip emits SD that vsomeip
parses) is the next test (phase 20h).
SubscribeAck, SubscribeNack are all separate code paths.
state, event publishing, E2E protect/check on real payloads.
about Halo's planned lwip integration was tested.
through simple-someip's existing tokio/
ClientAPI, whichHalo won't use in production.
CI integration deferred. The test stays
#[ignore]'d by default;flipping it on
cargo testwould fail until a CI runner hasdocker + the harness available. That's the next phase (20i?)
once we have the full conformance test set built out.
What this leaves:
subscribes; proves TX wire format).
conformance.
this docker on every PR.