The safety half of the rust-safety programme's §8.4: `std::env::set_var`/`remove_var` are
`unsafe fn` in edition 2024, converting the class of bug the programme found the hard way
(the 972af299 environ data race lived in a file with ZERO occurrences of the word
`unsafe`) from invisible to counted and compiler-enforced.
Manifests: [workspace.package] edition 2021→2024, rust-version 1.82→1.85 (the pinned
toolchain is 1.96.0, so no toolchain bump — only the declared floor rises); the 13 crates
pinning `edition = "2021"` literally now inherit it (Trap 1: the root bump alone reaches
only `edition.workspace = true` crates and would have left pf-encode/pf-capture/pf-inject
et al. on 2021 while reading as complete); pf-driver-proto's stale rust-version 1.82 pin
now inherits; pf-vkhdr-layer (a separate workspace, inherits nothing) bumped to 2024. The
four vendored crates (fec-rs, cros-codecs, usbip-sim, the patched ndk) stay on 2021
deliberately — upstream code stays pristine. The excluded usbip-poc standalone PoC is
untouched.
Mechanical, done textually across ALL cfg branches so no platform's half is left behind
(Trap 3 — 44% of the host's unsafe is Windows-only and a one-platform `cargo fix` misses
it): 148 `#[no_mangle]` → `#[unsafe(no_mangle)]` (83 in abi.rs); 12 bare extern blocks →
`unsafe extern`; `gen` is a reserved keyword, so pf-vdisplay's generation stamps
(registry.rs, windows/manager.rs) and the WinUI shell's animation counters rename
gen → generation (internal identifiers only, no serde/wire surface); two
match-ergonomics patterns take the compiler's suggested reference form.
env mutation: every `set_var`/`remove_var` site (20 files) now sits in an `unsafe` block
whose SAFETY comment states the real serialization argument (pf-vdisplay's ENV_LOCK,
CONFIG_DIR_TEST_LOCK, ART_ROOTS_LOCK, vkdecode's gpu_lock, the `--test-threads=1`
contracts of the hardware spikes, or single-threaded startup). Two genuine hazards
surfaced en route — exactly the WP3b-class finds this migration exists to make visible —
and are fixed here:
- windows/service.rs spawned the network-profile warner thread BEFORE `load_host_env()`,
so a child-spawning thread (child spawn snapshots the env block) was live while
`set_var` ran in a loop; the load now precedes the spawn.
- pf-console-ui's `fake_home()` re-set HOME outside its OnceLock on EVERY call, so two
parallel tests could race the write; the set now happens exactly once inside
`get_or_init`.
cbindgen (Trap 2): 0.29.4 parses `#[unsafe(no_mangle)]` — verified empirically; the
header regenerates byte-identical. The ci.yml drift check could never catch "failed to
regenerate" (build.rs demotes a cbindgen failure to a warning and writes nothing, leaving
the checked-in header untouched and the diff clean), so the step now first asserts the
"punktfunk-core: wrote" line and the absence of "cbindgen failed" (sh -e safe: no `!`
pipeline, no tee-masked exit).
rustfmt: style_edition pinned to 2021 at the root — edition 2024 would otherwise flip the
style edition and reformat ~370 untouched files inside this same commit, burying the
migration diff. The drivers workspace pins its already-current 2024 style. Adopting the
2024 style tree-wide is its own future one-line-plus-reformat commit.
Census: the primary metric moves UP BY DESIGN — 2435 → 2453 operations, unsafe blocks
1534 → 1577, and env_set_var is now a counted category (45 ops). The newly counted env
sites are a truer number, not a regression; baseline snapshot saved as punktfunk-planning
design/rust-safety-census-baseline-2026-08-12-edition-2024.txt. Gate C's env ratchet is
now compiler-enforced (the hygiene-script header says so); the two shrunk file counts
(nvenc_cuda 49→2 via the test helpers, shell/tests 2→1) are lowered in the same commit
per the gate's own rule.
Drop order (the semantic change most likely to bite this codebase): the migration lint
`-W tail-expr-drop-order` reports zero findings on the macOS-visible halves of
pf-encode / pf-zerocopy / pf-capture / pf-frame; the Linux and Windows halves run the
same lint on the gate boxes. The four #[ignore]d alloc/drop-cycle tests on the hardware
boxes remain owed, as before this change.
89 lines
3.7 KiB
Rust
89 lines
3.7 KiB
Rust
//! The bandwidth speed test over an established session.
|
|
//!
|
|
//! The host bursts filler over the real data plane — the same path a stream uses, so the answer is
|
|
//! about the link the stream will actually take rather than about some generic throughput. It is
|
|
//! deliberately *measure-only*: which layer a measured bitrate belongs in (the global default, or a
|
|
//! host's bound profile) is a decision the UI makes with the user
|
|
//! (design/client-settings-profiles.md §5.3), never one this shim makes for them.
|
|
//!
|
|
//! Two calls: start, then poll. Both are cheap and non-blocking, so Kotlin can drive them from a
|
|
//! coroutine on the main thread the way it polls the stats HUD.
|
|
|
|
use super::{jni_guard, SessionHandle};
|
|
use jni::objects::JObject;
|
|
use jni::sys::{jboolean, jdoubleArray, jint, jlong};
|
|
use jni::JNIEnv;
|
|
|
|
/// The `DoubleArray` [`Java_io_unom_punktfunk_kit_NativeBridge_nativeProbeResult`] returns. Kept in
|
|
/// one place because Kotlin indexes it positionally; see the Kotlin doc for the field order.
|
|
const PROBE_RESULT_LEN: usize = 6;
|
|
|
|
/// `NativeBridge.nativeSpeedTest(handle, targetKbps, durationMs): Boolean` — ask the host to burst
|
|
/// filler at `targetKbps` of goodput for `durationMs` (each clamped host-side to ≤ 3 Gbps / ≤ 5 s),
|
|
/// **briefly pausing video**. Non-blocking: poll
|
|
/// [`Java_io_unom_punktfunk_kit_NativeBridge_nativeProbeResult`] until its `done` element is 1.
|
|
/// Starting a probe resets any prior measurement. `false` on a `0` handle or a closed session.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSpeedTest(
|
|
_env: JNIEnv,
|
|
_this: JObject,
|
|
handle: jlong,
|
|
target_kbps: jint,
|
|
duration_ms: jint,
|
|
) -> jboolean {
|
|
jni_guard(0, || {
|
|
if handle == 0 {
|
|
return 0;
|
|
}
|
|
// SAFETY: live handle per the nativeConnect/nativeClose contract.
|
|
let h = unsafe { &*(handle as *const SessionHandle) };
|
|
let target = target_kbps.clamp(0, i32::MAX) as u32;
|
|
let duration = duration_ms.clamp(0, i32::MAX) as u32;
|
|
match h.client.request_probe(target, duration) {
|
|
Ok(()) => 1,
|
|
Err(e) => {
|
|
log::warn!("speed test: could not ask the host to probe: {e:?}");
|
|
0
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// `NativeBridge.nativeProbeResult(handle): DoubleArray?` — the current measurement, partial until
|
|
/// `[0] == 1`. Safe to poll; before any probe it reports zeros. `null` on a `0` handle.
|
|
///
|
|
/// Layout (doubles so one array carries both the counts and the percentages):
|
|
/// `[done, throughputKbps, lossPct, hostDropPct, elapsedMs, recvBytes]`.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeProbeResult<'local>(
|
|
env: JNIEnv<'local>,
|
|
_this: JObject<'local>,
|
|
handle: jlong,
|
|
) -> jdoubleArray {
|
|
jni_guard(JObject::null().into_raw(), || {
|
|
if handle == 0 {
|
|
return JObject::null().into_raw();
|
|
}
|
|
// SAFETY: live handle per the nativeConnect/nativeClose contract.
|
|
let h = unsafe { &*(handle as *const SessionHandle) };
|
|
let r = h.client.probe_result();
|
|
let values: [f64; PROBE_RESULT_LEN] = [
|
|
f64::from(u8::from(r.done)),
|
|
f64::from(r.throughput_kbps),
|
|
f64::from(r.loss_pct),
|
|
f64::from(r.host_drop_pct),
|
|
f64::from(r.elapsed_ms),
|
|
r.recv_bytes as f64,
|
|
];
|
|
match env.new_double_array(PROBE_RESULT_LEN as i32) {
|
|
Ok(arr) => {
|
|
if env.set_double_array_region(&arr, 0, &values).is_err() {
|
|
return JObject::null().into_raw();
|
|
}
|
|
arr.into_raw()
|
|
}
|
|
Err(_) => JObject::null().into_raw(),
|
|
}
|
|
})
|
|
}
|