test(qemu): run the no_std code on emulated Cortex-M0, not just cargo check - #21
Merged
Merged
Conversation
PR #20 added a CI matrix proving the crate compiles for 8 bare-metal targets. `cargo check` does not link and executes nothing, so the `critical-section` path on Cortex-M0 was still an unverified claim. New `qemu-test/` crate links a real `cortex-m-rt` binary and boots it under `qemu-system-arm`, on two machines: -cpu cortex-m3 thumbv7m — portable-atomic's spinlock fallback -machine microbit thumbv6m — Cortex-M0, no CAS instruction at all The second is the point. `MetaWord::on_access` runs an AtomicU64 CAS on every `get` hit, and on ARMv6-M that can only work through `critical-section`, so a passing `get` there is runtime proof. microbit (nRF51822) is the only emulated ARM machine that is actually thumbv6m. 12 assertions: capacity, empty state, hit/miss, insert past 4x capacity to force eviction then peek every key (absence is legal, a value the key was never stored with is not), eviction_count, TTL live-then-expired by insertion count, remove. Failures exit EXIT_FAILURE through semihosting, which reaches `cargo run` as a non-zero exit; confirmed by breaking one assertion on purpose. Also measured, and now documented: a map costs 128 bytes per bucket up front — 64 for the Bucket, 4x16 for its SlotTTL entries — not the 64 the cache-line framing suggests. A 64-bucket map exhausted an 8 KiB heap. riscv32imc stays compile-checked only: qemu-system-riscv32 -machine virt has the A extension, so it would test a target that doesn't need the feature.
This was referenced Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
PR #20 added a CI matrix proving
pulse_mapcompiles for 8 bare-metal targets, and found two that previously did not (thumbv6m-none-eabi,riscv32imc-unknown-none-elf). Butcargo checkdoes not link and never executes an instruction, so thecritical-sectionpath it introduced was still an unverified claim.This boots a real binary on emulated hardware.
New
qemu-test/crate —cortex-m-rt+memory.x+ a panic handler — run underqemu-system-armon two machines:-cpu cortex-m3 -machine lm3s6965evbthumbv7m-none-eabi-machine microbit(nRF51822)thumbv6m-none-eabicritical-section— no CAS instruction existsThe second row is the point.
MetaWord::on_accessperforms anAtomicU64CAS on everygethit, and ARMv6-M has noLDREX/STREX, so on Cortex-M0 that CAS can only work through thecritical-sectionfeature. A passinggetthere is the runtime proof.microbitis the only emulated ARM machine that is actuallythumbv6m—lm3s6965evb, the machine the Embedded Rust Book uses, is Cortex-M3 and already passed before PR #20, so it proves nothing new on its own and serves here as the control.Local results
What it checks
12 assertions: capacity, empty state, get hit/miss,
lenafter insert, insert 4× capacity to force eviction thenpeekevery key,eviction_count() > 0,lenwithin capacity, TTL live-then-expired by insertion count,removereports a hit, removed key gone.The eviction check is deliberately asymmetric: a missing key is legal — which of a bucket's four slots loses is not observable from outside — but a key reading back a value it was never stored with never is. That is the invariant worth asserting.
Failures are real failures. Checks report through semihosting and call
debug::exit(EXIT_FAILURE), which reachescargo runas a non-zero process exit and fails the job. Verified by breaking one assertion on purpose:Measured along the way: 128 bytes per bucket, not 64
The first run died with
memory allocation of 256 bytes failedon an 8 KiB heap.PulseMapRaw::newallocates two Vecs, not one:buckets— 64 B per bucket, the cache lineslots_ttl—4 × sizeof(SlotTTL)=4 × 16B per bucketSo a map costs 128 B per bucket, taken upfront regardless of occupancy. A 64-bucket map is 8 KiB — the entire heap I had budgeted. Not a crate bug; my test was sized against a wrong model of the cost. It now runs 16 buckets (2 KiB, 64 nominal slots) and prints its own heap usage into the CI log.
This is consistent with the README's measured 40.0 B/entry (16,384 buckets × 128 B ÷ ~52,800 resident entries ≈ 39.7), but on a 16 KiB part bucket count, not entry count, is the number to budget with — so the README now says so explicitly.
Scope, stated plainly
riscv32imc-unknown-none-elf(ESP32-C3) stays compile-checked only.qemu-system-riscv32 -machine virthas the A extension, so emulating it would exercise a target that does not need the feature — a green job there would be misleading.deallocis a no-op); real firmware wantsembedded-alloc. It is marked as such.Notes
qemu-test/is its own workspace with its own.cargo/config.tomlrunner — the same isolationfuzz/already uses — so it cannot affect a host build ofpulse_map. No changes tosrc/.--no-install-recommends: the default pullsqemu-efi-aarch64, 322 MB of aarch64 UEFI firmware that nothing here boots.## [Unreleased] — v0.6.5.