LAST_ERROR 槽位改存 (kind, detail),新增 nativeLastErrorKind();FFI 契约 kind 先取、message 后取(消息读走清空槽位)。kind 编号与 viewer_core:: ConnectErrorKind::code 一致,跨 FFI 只传整数。JNI 符号 12→13 全部导出(字节扫描核)。
377 lines
13 KiB
Rust
377 lines
13 KiB
Rust
//! 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);
|
||
}
|