remote/android-ffi/src/lib.rs
郑博文 f28db050fb feat(android-ffi): nativeLastErrorKind 出口带错误分类(3a)
LAST_ERROR 槽位改存 (kind, detail),新增 nativeLastErrorKind();FFI 契约
kind 先取、message 后取(消息读走清空槽位)。kind 编号与 viewer_core::
ConnectErrorKind::code 一致,跨 FFI 只传整数。JNI 符号 12→13 全部导出(字节扫描核)。
2026-10-31 13:29:48 +08:00

377 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<Session>` 与 `&'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::{JCharArray, JClass, JObject, 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<Session>,
runtime: tokio::runtime::Runtime,
}
/// 连接被控端。成功返回句柄(>0),失败返回 0;错误消息经 `nativeLastError()` 取,
/// **错误分类**经 `nativeLastErrorKind()` 取(3a:界面按分类显示中文文案,原始英文作第二行)。
#[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(-1, "host 参数不是合法字符串");
return 0;
}
};
let code_s = match env.get_string(&code) {
Ok(s) => s.to_string_lossy().into_owned(),
Err(_) => {
set_last_error(-1, "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((-1i32, 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,
}),
// ConnectError 的 Display 即原始 detail;kind.code() 与
// viewer_core::ConnectErrorKind::code 的映射表一致(FFI 契约)。
Err(e) => Err((e.kind.code(), e.detail)),
}
});
match built {
Ok(Ok(boxed)) => {
set_last_error(0, "");
Box::into_raw(Box::new(Mutex::new(boxed))) as jlong
}
Ok(Err((kind, msg))) => {
set_last_error(kind, &msg);
0
}
Err(_) => {
set_last_error(-1, "panic in nativeConnect");
0
}
}
}
/// kind = 0 表示清除;-1 表示 FFI 层自身错误(无 viewer-core 分类,Kotlin 走 Other 文案)。
fn set_last_error(kind: i32, msg: &str) {
let mut slot = LAST_ERROR.lock().unwrap();
if msg.is_empty() {
*slot = None;
} else {
*slot = Some((kind, msg.to_string()));
}
}
static LAST_ERROR: Mutex<Option<(i32, String)>> = Mutex::new(None);
/// 取最近一次 nativeConnect 的错误消息(无错误返回 null)。
/// FFI 契约:调用方必须先 `nativeLastErrorKind()` 再 `nativeLastError()`——
/// 本函数读走消息时**清空整个槽位**(kind 一起失效),颠倒顺序会拿到 kind=0。
#[no_mangle]
pub extern "system" fn Java_com_verify_remote_RustBridge_nativeLastError<'local>(
env: JNIEnv<'local>,
_class: JClass<'local>,
) -> jstring {
let mut slot = LAST_ERROR.lock().unwrap();
let msg = slot.as_ref().map(|(_, m)| m.clone());
match msg {
Some(m) => {
*slot = None;
env.new_string(m)
.map(|s| s.into_raw())
.unwrap_or(std::ptr::null_mut())
}
None => std::ptr::null_mut(),
}
}
/// 取最近一次 nativeConnect 的错误分类码(viewer_core::ConnectErrorKind::code)。
/// 只读不清空;0 = 无错误,-1 = FFI 层自身错误(参数非法/panic/runtime 初始化失败,
/// 无 viewer-core 分类,Kotlin 按 Other 文案 + 原始消息显示)。
#[no_mangle]
pub extern "system" fn Java_com_verify_remote_RustBridge_nativeLastErrorKind(
_env: JNIEnv,
_class: JClass,
) -> jint {
let slot = LAST_ERROR.lock().unwrap();
slot.as_ref().map(|(k, _)| *k).unwrap_or(0)
}
/// 取下一帧(阻塞直到有帧或会话结束)。返回 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
}
/// 被控端桌面尺寸,打包成 i64:高 32 位宽、低 32 位高。0 = 句柄无效。
/// Kotlin 侧必须在建解码器之前调用,用返回值定 SurfaceView 宽高比。
#[no_mangle]
pub extern "system" fn Java_com_verify_remote_RustBridge_nativeDesktopSize(
_env: JNIEnv,
_class: JClass,
handle: jlong,
) -> jlong {
let Some(boxed) = handle_to_box(handle) else {
return 0;
};
let guard = lock_box(boxed);
let (w, h) = guard.session.desktop_size();
(((w as u64 & 0xFFFF_FFFF) << 32) | (h as u64 & 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,
}
})
});
}
/// 协议 v3:文本上行。`units` 是 Kotlin 侧的 `CharArray`(UTF-16 code unit)。
///
/// 为什么不用 `JString`:jni 0.21 的 `get_string` 走 Modified-UTF-8 → Rust `String`,
/// 落单代理项(IME 提交边界常见)会在这次往返里丢失。`char[]` 经
/// `GetCharArrayRegion` 按 u16 直接拷贝,零转换(`jchar = u16`,见 jni-sys-0.3.1/lib.rs:15)。
///
/// 截断与空输入的判断在 `viewer_core::Session::send_text` 里做,与被控端同一口径。
/// 失败必须有可见后果:JNI 取数失败走 eprintln(Android 上落 stderr→logcat),
/// 不静默返回成功——用户对着远端打字却没字,最坏的就是「看起来发送了」。
#[no_mangle]
pub extern "system" fn Java_com_verify_remote_RustBridge_nativeSendText<'local>(
// 不需要 `mut`:jni 0.21.1 的 `get_array_length` / `get_char_array_region`
// 都是 `&self`(jnienv.rs:1652 / :1887),与 `new_string` 之类的 `&mut self` 不同。
env: JNIEnv<'local>,
_class: JClass<'local>,
handle: jlong,
units: JObject<'local>,
) {
if units.as_raw().is_null() {
// Kotlin 侧不会传 null,但 JNI 入口的对端数据一律不信任(全局安全规则)
return;
}
let arr = JCharArray::from(units);
let len = match env.get_array_length(&arr) {
Ok(n) if n > 0 => n as usize,
Ok(_) => return,
Err(e) => {
eprintln!("[android-ffi] get_array_length failed: {e}");
return;
}
};
let mut buf = vec![0u16; len];
if let Err(e) = env.get_char_array_region(&arr, 0, &mut buf) {
eprintln!("[android-ffi] get_char_array_region failed: {e}");
return;
}
with_session(handle, |s| s.send_text(&buf));
}
#[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<SessionBox>;
unsafe {
let _ = Box::from_raw(real);
}
}
fn handle_to_box(handle: jlong) -> Option<&'static Mutex<SessionBox>> {
if handle == 0 {
return None;
}
let real = handle as *mut Mutex<SessionBox>;
// 悬垂指针无法在 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<SessionBox>) -> 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);
}