diff --git a/Cargo.lock b/Cargo.lock index 9829ec0..4197002 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "android-ffi" +version = "0.1.0" +dependencies = [ + "anyhow", + "common", + "jni 0.21.1", + "tokio", + "viewer-core", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -76,7 +87,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "thiserror", + "thiserror 2.0.21", ] [[package]] @@ -121,6 +132,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.5" @@ -387,6 +404,22 @@ dependencies = [ "web-sys", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -396,10 +429,10 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.21", "walkdir", "windows-link", ] @@ -417,6 +450,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + [[package]] name = "jni-sys" version = "0.4.1" @@ -764,7 +806,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.21", "tokio", "tracing", "web-time", @@ -788,7 +830,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier", "slab", - "thiserror", + "thiserror 2.0.21", "tinyvec", "tracing", "web-time", @@ -973,7 +1015,7 @@ checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" dependencies = [ "core-foundation", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -1231,13 +1273,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -1690,13 +1752,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1708,20 +1779,35 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1733,18 +1819,36 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1757,24 +1861,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index af65a74..e344cde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,8 +2,9 @@ resolver = "2" # agent-core:平台无关会话层 + 软件编码视频管线(当前仅 linux-agent 使用) # linux-agent:Linux 被控端(X11 + XTest) -# viewer-core:安卓主控的连接/协议/收帧层(桌面 headless 可验证;Android 经 UniFFI 复用) -members = ["common", "agent-core", "agent", "linux-agent", "viewer", "viewer-core"] +# viewer-core:安卓主控的连接/协议/收帧层(桌面 headless 可验证;Android 经 JNI 复用) +# android-ffi:viewer-core 的 JNI 薄层(cdylib,交叉编译成 libandroidffi.so) +members = ["common", "agent-core", "agent", "linux-agent", "viewer", "viewer-core", "android-ffi"] # 默认成员保持 Windows 三件套,`cargo build --release` 行为与改动前完全一致; # Linux 被控端显式构建:`cargo build --release -p linux-agent` default-members = ["common", "agent", "viewer"] diff --git a/android-ffi/Cargo.toml b/android-ffi/Cargo.toml new file mode 100644 index 0000000..4714978 --- /dev/null +++ b/android-ffi/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "android-ffi" +version = "0.1.0" +edition = "2021" + +# 安卓主控的 JNI 薄层:把 viewer-core 的异步 Session 包成 Kotlin 可同步调用的句柄。 +# 2026-10-29 裁决变更:原方案 A2 选 UniFFI,但其实训运行时不在 Maven Central / +# Mozilla maven(查证 404),需自编译发布,成本超过收益。改为裸 JNI——阶段 0 +# (D:\working\android\phase0)已验证该链路真机可跑。接口刻意保持窄(连接/取帧/ +# 发输入/销毁),将来若引入 UniFFI 也只是替换本层。 +[lib] +name = "androidffi" +crate-type = ["cdylib"] + +[dependencies] +viewer-core = { path = "../viewer-core" } +common = { path = "../common" } +anyhow.workspace = true +tokio.workspace = true +jni = "0.21" diff --git a/android-ffi/src/lib.rs b/android-ffi/src/lib.rs new file mode 100644 index 0000000..c1f9ee7 --- /dev/null +++ b/android-ffi/src/lib.rs @@ -0,0 +1,300 @@ +//! android-ffi:viewer-core 的 JNI 薄层(阶段 1)。 +//! +//! 线程模型(决定接口形态的原因): +//! - Kotlin 解码线程调用 `nativeNextFrame` 同步取帧(内部 block_on,一次一帧); +//! - Kotlin UI 线程调用 `nativeSend*` 发输入——tokio unbounded channel 入队不阻塞; +//! - 共用一个 current-thread runtime:`block_on(next_frame)` 期间 runtime 仍在驱动 +//! QUIC 收发与 Ping;输入入队只是 channel send,不需要 runtime 参与。 +//! +//! **锁纪律**:`nativeNextFrame` 在等帧期间**不得持有**会话锁——桌面静止时帧间隔 +//! 可达 500ms,持锁会让 UI 线程的输入调用同步卡住。做法:短暂持锁克隆 +//! `Arc` 与 `&'static Runtime` 引用后立即释放,再 block_on。 +//! 代价:block_on 期间 destroy 会 use-after-free——阶段 1 由 Kotlin 侧 +//! 「先停解码线程、后 destroy」的顺序纪律保证(登记在方案风险清单)。 +//! +//! 句柄约定:`nativeConnect` 返回 Box 裸指针(永不为 0)作为 jlong; +//! `nativeDestroy` 释放。句柄释放后不得再使用,Kotlin 侧用置 0 保护。 +//! +//! 帧返回约定:byte[],布局 = [1 标志][8 seq LE][8 ts LE][4 w LE][4 h LE][payload]。 +//! 定长头 25 字节;Kotlin 读 buf[0] 判断 keyframe(标志位复用为:0=P 帧, 1=IDR 帧, +//! 会话结束则整个返回 null)。 + +use common::InputEvent; +use jni::objects::{JClass, JString}; +use jni::sys::{jbyteArray, jint, jlong, jstring}; +use jni::JNIEnv; +use std::sync::{Arc, Mutex}; +use viewer_core::{ConnectConfig, Session}; + +/// 帧头长度:keyframe 标志(1) + seq(8) + ts(8) + w(4) + h(4) +const FRAME_HEADER_LEN: usize = 25; + +struct SessionBox { + session: Arc, + runtime: tokio::runtime::Runtime, +} + +/// 连接被控端。成功返回句柄(>0),失败返回 0;错误消息经 `nativeLastError()` 取。 +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeConnect<'local>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + host: JString<'local>, + port: jint, + code: JString<'local>, +) -> jlong { + let host_s = match env.get_string(&host) { + Ok(s) => s.to_string_lossy().into_owned(), + Err(_) => { + set_last_error("host 参数不是合法字符串"); + return 0; + } + }; + let code_s = match env.get_string(&code) { + Ok(s) => s.to_string_lossy().into_owned(), + Err(_) => { + set_last_error("code 参数不是合法字符串"); + return 0; + } + }; + + let built = std::panic::catch_unwind(|| { + let runtime = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(r) => r, + Err(e) => return Err(format!("runtime init: {e}")), + }; + let cfg = ConnectConfig::direct(host_s, port as u16, code_s); + match runtime.block_on(Session::connect(cfg)) { + Ok(session) => Ok(SessionBox { + session: Arc::new(session), + runtime, + }), + Err(e) => Err(format!("{e:#}")), + } + }); + + match built { + Ok(Ok(boxed)) => { + set_last_error(""); + Box::into_raw(Box::new(Mutex::new(boxed))) as jlong + } + Ok(Err(msg)) => { + set_last_error(&msg); + 0 + } + Err(_) => { + set_last_error("panic in nativeConnect"); + 0 + } + } +} + +fn set_last_error(msg: &str) { + let mut slot = LAST_ERROR.lock().unwrap(); + if msg.is_empty() { + *slot = None; + } else { + *slot = Some(msg.to_string()); + } +} + +static LAST_ERROR: Mutex> = Mutex::new(None); + +/// 取最近一次 nativeConnect 的错误消息(无错误返回 null)。 +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeLastError<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, +) -> jstring { + let msg = LAST_ERROR.lock().unwrap().take(); + match msg { + Some(m) => env + .new_string(m) + .map(|s| s.into_raw()) + .unwrap_or(std::ptr::null_mut()), + None => std::ptr::null_mut(), + } +} + +/// 取下一帧(阻塞直到有帧或会话结束)。返回 null = 会话结束。 +/// 仅允许解码线程调用;destroy 前必须先停止本线程(见文件头锁纪律)。 +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeNextFrame<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, + handle: jlong, +) -> jbyteArray { + // 短暂持锁:克隆 Arc 与拿 runtime 引用,随后立即释放——等帧不能挡输入。 + let (session, runtime) = { + let Some(boxed) = handle_to_box(handle) else { + return std::ptr::null_mut(); + }; + let guard = lock_box(boxed); + (guard.session.clone(), &guard.runtime as *const tokio::runtime::Runtime) + }; + // SAFETY: 见文件头「锁纪律」。Kotlin 保证解码线程存活期间不 destroy。 + let rt = unsafe { &*runtime }; + + match rt.block_on(session.next_frame()) { + None => std::ptr::null_mut(), + Some(f) => { + let mut buf = vec![0u8; FRAME_HEADER_LEN + f.payload.len()]; + buf[0] = f.keyframe as u8; + buf[1..9].copy_from_slice(&f.seq.to_le_bytes()); + buf[9..17].copy_from_slice(&f.ts_ms.to_le_bytes()); + buf[17..21].copy_from_slice(&f.width.to_le_bytes()); + buf[21..25].copy_from_slice(&f.height.to_le_bytes()); + buf[FRAME_HEADER_LEN..].copy_from_slice(&f.payload); + match env.byte_array_from_slice(&buf) { + Ok(a) => a.into_raw(), + Err(_) => std::ptr::null_mut(), + } + } + } +} + +/// 会话统计打包:高 32 位 rtt_ms,低 32 位 frames_in 低 32 位。-1 = 句柄无效。 +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeStats( + _env: JNIEnv, + _class: JClass, + handle: jlong, +) -> jlong { + let Some(boxed) = handle_to_box(handle) else { + return -1; + }; + let guard = lock_box(boxed); + let st = guard.session.stats(); + // 打包进 i64:高 32 位 rtt_ms、低 32 位 frames_in,Kotlin 侧移位还原 + (((st.rtt_ms & 0xFFFF_FFFF) << 32) | (st.frames_in & 0xFFFF_FFFF)) as jlong +} + +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeSendMouseMove( + _env: JNIEnv, + _class: JClass, + handle: jlong, + x: jint, + y: jint, +) { + with_session(handle, |s| { + s.send_input(InputEvent::MouseMove { + x: x.clamp(0, 65535) as u16, + y: y.clamp(0, 65535) as u16, + }) + }); +} + +/// button: 0=左 1=右 其他=中;down: 1=按下 0=抬起 +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeSendMouseButton( + _env: JNIEnv, + _class: JClass, + handle: jlong, + button: jint, + down: jint, +) { + use common::MouseButton; + let b = match button { + 0 => MouseButton::Left, + 1 => MouseButton::Right, + _ => MouseButton::Middle, + }; + with_session(handle, |s| { + s.send_input(if down != 0 { + InputEvent::MouseDown(b) + } else { + InputEvent::MouseUp(b) + }) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeSendWheel( + _env: JNIEnv, + _class: JClass, + handle: jlong, + dy: jint, +) { + with_session(handle, |s| s.send_input(InputEvent::Wheel { dy })); +} + +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeSendKey( + _env: JNIEnv, + _class: JClass, + handle: jlong, + scancode: jint, + extended: jint, + down: jint, +) { + with_session(handle, |s| { + s.send_input(if down != 0 { + InputEvent::KeyDown { + scancode: scancode as u16, + extended: extended != 0, + } + } else { + InputEvent::KeyUp { + scancode: scancode as u16, + extended: extended != 0, + } + }) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeRequestKeyframe( + _env: JNIEnv, + _class: JClass, + handle: jlong, +) { + with_session(handle, |s| s.request_keyframe()); +} + +/// 释放会话。Kotlin 侧调用后必须把句柄置 0,且先停解码线程。 +#[no_mangle] +pub extern "system" fn Java_com_verify_remote_RustBridge_nativeDestroy( + _env: JNIEnv, + _class: JClass, + handle: jlong, +) { + if handle == 0 { + return; + } + let real = handle as *mut Mutex; + unsafe { + let _ = Box::from_raw(real); + } +} + +fn handle_to_box(handle: jlong) -> Option<&'static Mutex> { + if handle == 0 { + return None; + } + let real = handle as *mut Mutex; + // 悬垂指针无法在 Rust 侧检测:阶段 1 由 Kotlin「destroy 后置 0 + 先停线程」 + // 的顺序纪律保证。违反即 UB,登记在方案风险清单(阶段 2 可加代际表消除)。 + // NonNull::new 是安全函数;解引用才是 unsafe 点 + std::ptr::NonNull::new(real).map(|p| unsafe { p.as_ref() }) +} + +/// 锁中毒容忍:SessionBox 内部无跨线程共享可变状态(Session 各方法内部自带 +/// 并发原语),中毒后继续用是安全的。 +fn lock_box(b: &Mutex) -> std::sync::MutexGuard<'_, SessionBox> { + match b.lock() { + Ok(g) => g, + Err(p) => p.into_inner(), + } +} + +fn with_session(handle: jlong, f: impl FnOnce(&Session)) { + let Some(boxed) = handle_to_box(handle) else { + return; + }; + let guard = lock_box(boxed); + f(&guard.session); +} diff --git a/viewer-core/src/lib.rs b/viewer-core/src/lib.rs index c61140a..f8d9538 100644 --- a/viewer-core/src/lib.rs +++ b/viewer-core/src/lib.rs @@ -149,7 +149,9 @@ enum Command { pub struct Session { tx: mpsc::UnboundedSender, /// 帧接收端。通道关闭 = 会话结束(被控端断开或流错误)。 - frames: mpsc::Receiver, + /// Mutex 包一层是为了让 `next_frame` 取 `&self`:JNI 层里解码线程独占 recv, + /// 输入线程并发调 `send_input`(走无锁的 unbounded tx),两者互不阻塞。 + frames: Arc>>, stats: Arc>, /// 保持 Endpoint 存活(drop 会销毁连接) _endpoint: quinn::Endpoint, @@ -313,7 +315,7 @@ impl Session { Ok(Self { tx: cmd_tx, - frames, + frames: Arc::new(tokio::sync::Mutex::new(frames)), stats, _endpoint: endpoint, conn, @@ -321,8 +323,9 @@ impl Session { } /// 取下一帧。`None` = 会话结束。 - pub async fn next_frame(&mut self) -> Option { - self.frames.recv().await + /// `&self`:JNI 层里解码线程独占 recv,输入线程可并发 `send_input`。 + pub async fn next_frame(&self) -> Option { + self.frames.lock().await.recv().await } /// 发送一个输入事件(非阻塞入队,由写任务串行发出)。