feat(android-ffi): viewer-core 的 JNI 薄层,arm64 交叉编译通过(QUIC 栈上手机的第一道关)
方案变更说明(A2 落地修正):原裁决选 UniFFI,但实查 Maven Central 与 maven.mozilla.org 均无 uniffi Kotlin 运行时构件(404),自编译发布成本超过收益。 改为裸 JNI:阶段 0(真机)已验证该链路,接口刻意保持窄(连接/取帧/发输入/销毁), 将来若引入 UniFFI 只替换本层。 线程与锁纪律(决定接口形态): - nativeNextFrame 等帧期间不持会话锁(clone Arc<Session> 后即释放), 桌面静止 500ms 帧间隔不会卡住 UI 线程的输入调用 - destroy 前必须先停解码线程,Kotlin 侧顺序纪律保证(已登记风险清单) - 句柄 = Box 裸指针(永不为 0),destroy 后置 0 帧返回约定:byte[] = [keyframe 标志 1][seq 8][ts 8][w 4][h 4][Annex-B payload], 定长 25 字节头,Kotlin 切片零 JNI 往返。 viewer-core 适配:Session.frames 改 Arc<tokio Mutex>,next_frame 取 &self, 使解码线程与输入线程可并发借用同一 Session。 验证: - 桌面 cargo build -p android-ffi 零警告 - cargo ndk -t arm64-v8a --release:quinn/ring/rustls 交叉编译全部通过 (消除阶段 1 最大未知:QUIC 栈能否编到 Android) - libandroidffi.so 4.2MB,10 个 Java_com_verify_remote_RustBridge_* 符号导出正确
This commit is contained in:
1 parent
34df7b070b
commit
44767ed370
5 files changed
+473
-21
No files matched your search
@@ -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<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::{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<Session>,
|
||||
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<Option<String>> = 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<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);
|
||||
}
|
||||
Reference in new issue
Block a user