Skip to content

v0.9.11 — Plays well with others

Pre-release
Pre-release

Choose a tag to compare

@jamesgober jamesgober released this 14 May 13:01
· 2 commits to main since this release

Release Notes for v0.9.11 - Plays well with others

Version 0.9.11 - 2026-05-14

smol works now. The async feature is runtime-agnostic since 0.9.11 — drop tokio from your tree and drive update_region_async / flush_async straight from smol, async-std, or any custom executor. See "Smol support, in detail" below for the implementation story; jump to the "Runtime-agnostic async" highlight for the quick version.

Patch release responding to two issues filed against 0.9.10. bbqsrc in #6 flagged that the as_slice signature change in 0.9.7 was a semver violation against Cargo's pre-1.0 compatibility rules (^0.9.6 resolves to <0.10.0, so 0.9.7 should not have shipped a breaking change). ararog asked for smol runtime support, which the tokio-locked async feature did not provide. Both fixed here, plus an opportunistic ecosystem polish pass: bytes::Bytes integration, io::Read + io::Seek cursor, and AsFd / AsRawFd / AsHandle / AsRawHandle trait impls. Everything additive; no API breaks.

Highlights

  • Runtime-agnostic async (smol unlocked). update_region_async, flush_async, flush_range_async, and the manager::async::* helpers now route through blocking::unblock instead of tokio::task::spawn_blocking. Existing tokio users see no API change. The futures drive to completion on any executor (tokio, smol, async-std, embassy on hosted, custom executors). The tokio direct dep is gone under --features async; the much smaller blocking crate replaces it. The transitive dep tree shrinks notably. smol-based projects can finally use the async surface without dragging tokio in.
  • Compat shims for the 0.9.7 semver violation.
    • MemoryMappedFile::as_slice_bytes(offset, len) -> Result<&[u8]> mirrors the 0.9.6 as_slice signature exactly: returns &[u8] directly on ReadOnly and CopyOnWrite mappings, returns MmapIoError::InvalidMode on ReadWrite matching the 0.9.6 behavior. Code that was broken by the 0.9.7 return-type change recovers with a one-method-name rename.
    • ChunkIteratorMut::for_each_mut_legacy<F, E>(F) -> Result<Result<(), E>> mirrors the 0.9.6 nested-Result signature. Internally uses the same single-held-write-guard loop as the flattened for_each_mut, so the H2 perf win from 0.9.7 is preserved.
    • chunks_owned() (already shipped in 0.9.7) covers the iterator-item migration path.
  • bytes::Bytes integration (feature = "bytes"). New MemoryMappedFile::read_bytes(offset, len) -> Result<bytes::Bytes> plus From<MappedSlice<'_>> / From<&MappedSlice<'_>> for bytes::Bytes. One allocation + memcpy at the conversion boundary; the resulting Bytes is mapping-lifetime-independent and travels freely through hyper / tower / tonic / axum / reqwest. Opt-in feature, zero cost when disabled.
  • io::Read + io::Seek cursor. mmap.reader() returns an MmapReader<'_> that plugs the mapping into every parser / decoder that takes a generic R: Read: serde_json::from_reader, flate2::read::GzDecoder, tar::Archive::new, image::ImageReader::new, the standard library's BufReader, etc. Bounds-checked under the hood. position() and set_position() for direct cursor control.
  • AsFd + AsRawFd (Unix) and AsHandle + AsRawHandle (Windows) trait impls on MemoryMappedFile. The std-blessed way to hand the underlying OS handle to FFI code, nix, rustix, polling, etc., without going through unmap.

The semver story

The 0.9.7 release changed as_slice from Result<&[u8]> to Result<MappedSlice<'_>>, changed iterator Item from Result<Vec<u8>> to MappedSlice<'a>, and flattened for_each_mut's closure return shape. Those were real wins (the iterator change alone is 13-475x faster than the old allocating form per the 0.9.10 measurements). The mistake was the version number: 0.9.6 → 0.9.7 is treated as a minor / patch bump by Cargo's resolver (Rust pre-1.0 semver rule: the leftmost non-zero digit acts as "major"), so ^0.9.6 users were silently upgraded into broken code.

The break carried for four releases (0.9.7 through 0.9.10) without acknowledgement. The cargo-semver-checks workflow added in 0.9.10 would have caught this exact case at PR time but did not exist yet in 0.9.7. The compat shims in this release give downstream callers a one-line recovery path. Apologies to bbqsrc and to anyone else whose 0.9.6 code stopped compiling on 0.9.7. The CHANGELOG and the README's new "Migrating from 0.9.6" section document the breaks and the recovery path explicitly so the issue is visible in the public record.

Smol support, in detail

The async feature implementation through 0.9.10 wrapped synchronous calls in tokio::task::spawn_blocking and required a tokio runtime to drive the returned future. smol users calling mmap.update_region_async(...).await from a smol-based application got a future that never executed because spawn_blocking depends on tokio's runtime being installed.

Since 0.9.11 we use blocking::unblock instead. The blocking crate is the runtime-agnostic primitive used by smol, async-fs, async-net, async-process, and other "small async" crates. tokio's own runtime drives futures from blocking just fine because they're standard Futures. The cost of the swap is one extra dep (blocking plus its small set of transitive deps), more than offset by dropping tokio's much larger tree.

The integration test async_surface_runs_under_a_non_tokio_executor proves the case directly: it constructs a block_on from std::thread::park (the same primitive smol and pollster use), drives the async methods through it, and verifies the bytes round-trip. No tokio runtime, no panic, full async functionality.

Tests

  • 140 tests pass under --all-features (up from 127 in 0.9.10), 1 ignored (the unrelated hugepages-fallback test), 0 failed.
  • 13 new tests in tests/v0_9_11_additions.rs cover every new method and the smol-compat validation.
  • cargo build (default / no-default / all-features), cargo +1.75 build --all-features (MSRV), cargo fmt --check, cargo clippy --all-targets --all-features -D warnings, cargo doc --no-deps --all-features, and cargo audit --deny warnings all clean.
  • 124 transitive crates in the lockfile (was ~130 with tokio in [dependencies]); the dep tree is smaller despite the new additions.
  • Banned-words scan zero hits.

Internals

  • tokio removed from [dependencies]; added to [dev-dependencies] purely so the existing #[tokio::test] test suite continues to drive the runtime-agnostic async surface. Downstream consumers no longer pull tokio via --features async.
  • blocking 1.6 added under --features async.
  • bytes 1 added under --features bytes.

Notes

  • MSRV unchanged at Rust 1.75.
  • All 0.9.7-introduced API surface (MappedSlice, the new iterator items, the flattened for_each_mut) remains the recommended path. The compat shims are explicitly migration aids; the rustdoc on each shim points back at the modern equivalent.
  • bytes::Bytes::from(MappedSlice<'_>) is one allocation + memcpy. For zero-copy networking, callers should use as_slice directly and pass the borrowed &[u8] through the chain; the Bytes conversion is the right call when ownership has to cross a thread or process boundary.

Full Changelog: v0.9.10...v0.9.11