ワンクリックで
rust-panic-safety
Rust panic safety, JNI boundaries, Java exceptions, typed errors, unwrap audits, and catch_unwind.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Rust panic safety, JNI boundaries, Java exceptions, typed errors, unwrap audits, and catch_unwind.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Use this skill to generate well-branded interfaces and assets for RIPDPI (an Android-native, Compose-first VPN and DPI-bypass app), either for production or throwaway prototypes/mocks/etc. Contains essential design guidelines, colors, type, fonts, brand-ship icons, and an interactive UI kit of every public composable from the live ui/components tree.
Use when managing the Rust workspace, adding/removing crates, editing workspace dependencies, running cargo nextest/audit/deny, configuring Cargo profiles for Android cross-compilation, debugging Cargo.lock churn, migrating crate edition, or wiring Gradle to cargo via the ripdpi.android.rust-native plugin.
Use when modifying the diagnostics scan pipeline, ScanRequest/ScanReport types, ProbeTask families, ripdpi-monitor-engine / ripdpi-diagnostics-* crates, strategy-probe candidates, the diagnostics catalog (packs/profiles), wire-schema contracts between Rust and Kotlin, DIAGNOSTICS_ENGINE_SCHEMA_VERSION, golden contract tests, or adding a new probe type / profile. Triggers on diagnostics scans, strategy probes, automatic audit, dpi-detector profiles, or anything in core/diagnostics or native/rust/crates/ripdpi-monitor-*.
Android-specific Rust build, verification, and packaging — per-target 16 KiB page alignment, size-optimized release profile, ELF symbol allowlist, .so size budgets, NDK 29 specifics. Use when modifying .cargo/config.toml for Android targets, the workspace [profile.release] / [profile.android-jni] block, or when verifying a built .so before release.
Telemetry and observability discipline for the RIPDPI Android Rust stack — control-plane vs data-plane logging, bounded flume event queues, atomic counters, snapshot polling, readiness callbacks, and deterministic JSON contracts. Use when authoring or modifying telemetry emission code, the bounded event ring, the Kotlin-side telemetry consumer, or any per-packet logging.
Custom detekt rules, DI/privacy/suppression guardrails, detekt.yml configuration, and false-positive triage.
| name | rust-panic-safety |
| description | Rust panic safety, JNI boundaries, Java exceptions, typed errors, unwrap audits, and catch_unwind. |
A Rust panic that unwinds across an extern "C" / extern "system" boundary is undefined behaviour. On Android this manifests as a SIGABRT with a corrupted ART stack — the JVM crashes without a usable backtrace. Every FFI entry point must intercept the panic, convert it to a typed error, and either throw a Java exception or return a sentinel value.
This skill formalizes the pattern RIPDPI already uses across its 4 JNI adapter crates and defines the rules for extending it.
JNI_OnLoad patternAll four JNI adapter crates wrap JNI_OnLoad identically. New adapters MUST match this shape:
#[unsafe(no_mangle)]
#[allow(improper_ctypes_definitions)]
pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *mut std::ffi::c_void) -> jint {
// Store the VM handle BEFORE catch_unwind so it survives a panic in init.
let _ = JVM.set(vm);
match std::panic::catch_unwind(|| {
android_support::ignore_sigpipe();
init_android_logging("ripdpi-<crate>-native");
android_support::install_panic_hook();
JNI_VERSION
}) {
Ok(version) => version,
Err(_) => jni::sys::JNI_ERR,
}
}
Locate JNI_OnLoad by symbol in ripdpi-android, ripdpi-tunnel-android, ripdpi-relay-android, ripdpi-warp-android, and ripdpi-amneziawg-android; do not retain line-number anchors. The entire loader initialization must remain inside catch_unwind so a panic returns JNI_ERR instead of crossing the FFI boundary. A panic that happens before install_panic_hook() runs is still contained by catch_unwind, but it cannot be reported by the custom hook; do not claim otherwise.
For all other extern "system" fn Java_* methods, use EnvUnowned::with_env + into_outcome. The Outcome tri-state (Ok / Err / Panic) surfaces panics distinctly from errors:
pub extern "system" fn Java_com_poyka_ripdpi_core_FooNativeBindings_jniCreate(
mut env: EnvUnowned<'_>,
_thiz: JObject,
config_json: JString,
) -> jlong {
match env.with_env(move |env| -> jni::errors::Result<jlong> {
Ok(create_session(env, config_json))
}).into_outcome() {
Outcome::Ok(handle) => handle,
Outcome::Err(err) => {
let _ = env.throw_new("java/lang/RuntimeException", err.to_string());
0
}
Outcome::Panic(payload) => {
let msg = payload.downcast_ref::<&str>().copied().unwrap_or("panic at JNI boundary");
let _ = env.throw_new("java/lang/RuntimeException", msg);
0
}
}
}
Rule: Never write a bare extern "system" fn body without with_env + into_outcome. Bare bodies propagate panics as UB.
.unwrap() / .expect() policyAs of 2026-04-17, the Rust workspace contains 1,727 .unwrap() / .expect() calls in production code (excluding target/, tests/, benches/). The pre-existing count is a latent risk: any of these can fire in a JNI-reachable context and corrupt the JVM.
.expect() on a Mutex/RwLock .lock() result — standard practice; a poisoned lock is a fatal invariant violation..unwrap() on a compile-time-known infallible conversion (u32::try_from(42_u16))..unwrap() inside a catch_unwind-wrapped closure that is itself the FFI entry point — the panic is contained.#[cfg(test)] gated blocks, integration tests, benchmarks, fuzz harnesses..unwrap() / .expect() on Result<_, E> where E: std::error::Error — propagate with ?..unwrap() on Option<T> where the None case is a downstream input (JSON parse, DNS reply, user config). Convert to a domain error..expect("should never happen") — if it really can't happen, write a safety-comment proof and use unreachable_unchecked() with a SAFETY block, OR model the invariant in the type system..unwrap()If you must add one, include a line comment directly above explaining the infallibility proof:
// Infallible: `buf.len() <= MAX_U32` checked above.
let len: u32 = buf.len().try_into().unwrap();
A bare .unwrap() with no proof fails pr-reviewer in CI.
anyhow vs thiserror| Context | Use | Why |
|---|---|---|
| Library crate (anything a JNI adapter depends on) | thiserror | Callers need to match on error variants. Typed errors enforce exhaustive handling. |
Application crate (ripdpi-cli, integration tests, warp-core orchestration layer) | anyhow | Top-level error propagation; human-readable report at the boundary. |
| JNI adapter crates | thiserror internally, convert to string at the throw site | The JNI boundary flattens to a string anyway; variants matter during propagation, not at the exit. |
Rule: a library crate src/lib.rs that imports anyhow::Result in its public API is a smell. The rust-api-auditor agent flags this.
Current state (2026-04-17): 2 crates use anyhow (ripdpi-warp-core, ripdpi-dns-resolver); 6 use thiserror (ripdpi-hysteria2, ripdpi-ipfrag, ripdpi-tls-profiles, ripdpi-tun-driver, ripdpi-tunnel-config, ripdpi-xhttp). Most crates lack either — they propagate via Result<_, String> or bare unwraps, which is the drift this skill exists to reverse.
Prefer RuntimeException at the JNI throw site unless there is a specific typed Java exception the caller expects. Rationale: the Kotlin side already has a single catch handler for JNI failures; adding new Java exception types for each Rust panic class fragments that handling.
The panic message that reaches Java should be the panic payload's string form. android_support::install_panic_hook() also emits the full payload + backtrace to android_logger / logcat so the hand-off doesn't lose diagnostic information.
extern "system" fn JNI_OnLoad matches the uniform pattern above.with_env + into_outcome..unwrap() / .expect() in non-test code carries an infallibility comment.thiserror errors for its public Result types.anyhow::Result.env.throw_new(...) with the panic message.rust-unsafe — overlapping ground on extern "system" + raw pointer safety.rust-async-internals — panic propagation in tokio task bodies (tokio converts panics into JoinError::Panic; the JNI-side spawn sites must handle this).rust-io-loop — the NoopWaker-based manual poll pattern does NOT wrap its poll calls in catch_unwind; that's accepted because it runs inside an already-wrapped async task.