Files
rustdesk/libs/scrap/src/android/ffi.rs
RustDesk b50fde6910 add the base crate and repoint the moved modules at it (#16107)
* add the base crate and repoint the moved modules at it

`libs/base` (crate `base`) takes the parts of hbb_common that only this app
uses: `fs`, `platform`, `keyboard`, `message.proto`, and 145 of the 177
`config::keys` constants. hbb_common keeps what the server names, and the 32
keys it reads itself are re-exported from `base::config::keys` so call sites
still see the full set through one path.

Sources move verbatim. The only edits inside them are `crate::` prefixes that
now have to say `hbb_common::`; `keyboard.rs` and `platform/windows.rs` are
byte-identical. The crate stays on edition 2018, the edition the moved code was
written under. `log`, `lazy_static` and `anyhow` become direct dependencies so
the bare paths in that code resolve exactly as before, and its winapi features
are spelled out rather than left to feature unification.

Two call sites outside Rust and Cargo had to follow the move: the Android
protobuf source dir, which still pointed at hbb_common/protos for message.proto,
and the three AGENTS.md entries that named hbb_common for options, protos and
file transfer.

`scrap`'s `drm` feature now forwards to `base/wayland_probe`. Left pointing at
hbb_common it would still have compiled, silently dropping the Wayland
socket-probe fallback, so that forward is verified by a build with and without
the feature.

`config::keys` carries a test asserting its names stay disjoint from the ones
hbb_common kept: the glob re-export and the local constants share a namespace,
and Rust prefers the local item silently, so a name added to both sides would
otherwise let client and server disagree with no diagnostic.

Verified: macOS and Linux, debug and release, `--all-targets`; the 177 key
constants diffed name-for-name and value-for-value; the generated protobuf types
compared before and after; every `#[cfg]` gate on a moved import checked against
its original; and every file that was `rustfmt`-clean before this change still
is, compared against master file by file. Windows is checked by inspection only
-- it cannot be compiled here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* one `use` per crate, and write the rule down

`fs.rs` came out of the move with two ungated `use hbb_common::` statements,
because the original single `use crate::{...}` had to give up `message_proto`
to the new crate and the rest was left in a second block. Fold it back into one.

A scan of the whole tree for the same shape finds nothing else: every other file
with more than one top-level `use base::` or `use hbb_common::` is split by a
`#[cfg]` that does not cover the whole block, or by `pub use` next to `use`.
Those are the cases that cannot merge, so AGENTS.md now states both the rule and
the exemption.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:46:42 +08:00

513 lines
16 KiB
Rust

use jni::objects::JByteBuffer;
use jni::objects::JString;
use jni::objects::JValue;
use jni::sys::jboolean;
use jni::JNIEnv;
use jni::{
objects::{GlobalRef, JClass, JObject},
strings::JNIString,
JavaVM,
};
use base::message_proto::MultiClipboards;
use hbb_common::protobuf::Message;
use jni::errors::{Error as JniError, Result as JniResult};
use lazy_static::lazy_static;
use serde::Deserialize;
use std::ops::Not;
use std::os::raw::c_void;
use std::sync::atomic::{AtomicPtr, Ordering::SeqCst};
use std::sync::{Mutex, RwLock};
use std::time::{Duration, Instant};
lazy_static! {
static ref JVM: RwLock<Option<JavaVM>> = RwLock::new(None);
static ref MAIN_SERVICE_CTX: RwLock<Option<GlobalRef>> = RwLock::new(None); // MainService -> video service / audio service / info
static ref APPLICATION_CONTEXT: RwLock<Option<GlobalRef>> = RwLock::new(None);
static ref VIDEO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("video", MAX_VIDEO_FRAME_TIMEOUT));
static ref AUDIO_RAW: Mutex<FrameRaw> = Mutex::new(FrameRaw::new("audio", MAX_AUDIO_FRAME_TIMEOUT));
static ref NDK_CONTEXT_INITED: Mutex<bool> = Default::default();
static ref MEDIA_CODEC_INFOS: RwLock<Option<MediaCodecInfos>> = RwLock::new(None);
static ref CLIPBOARD_MANAGER: RwLock<Option<GlobalRef>> = RwLock::new(None);
static ref CLIPBOARDS_HOST: Mutex<Option<MultiClipboards>> = Mutex::new(None);
static ref CLIPBOARDS_CLIENT: Mutex<Option<MultiClipboards>> = Mutex::new(None);
}
const MAX_VIDEO_FRAME_TIMEOUT: Duration = Duration::from_millis(100);
const MAX_AUDIO_FRAME_TIMEOUT: Duration = Duration::from_millis(1000);
struct FrameRaw {
name: &'static str,
ptr: AtomicPtr<u8>,
len: usize,
last_update: Instant,
timeout: Duration,
enable: bool,
}
impl FrameRaw {
fn new(name: &'static str, timeout: Duration) -> Self {
FrameRaw {
name,
ptr: AtomicPtr::default(),
len: 0,
last_update: Instant::now(),
timeout,
enable: false,
}
}
fn set_enable(&mut self, value: bool) {
self.enable = value;
self.ptr.store(std::ptr::null_mut(), SeqCst);
self.len = 0;
}
fn update(&mut self, data: *mut u8, len: usize) {
if self.enable.not() {
return;
}
self.len = len;
self.ptr.store(data, SeqCst);
self.last_update = Instant::now();
}
// take inner data as slice
// release when success
fn take<'a>(&mut self, dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
if self.enable.not() {
return None;
}
let ptr = self.ptr.load(SeqCst);
if ptr.is_null() || self.len == 0 {
None
} else {
if self.last_update.elapsed() > self.timeout {
log::trace!("Failed to take {} raw,timeout!", self.name);
return None;
}
let slice = unsafe { std::slice::from_raw_parts(ptr, self.len) };
self.release();
if last.len() == slice.len() && crate::would_block_if_equal(last, slice).is_err() {
return None;
}
dst.resize(slice.len(), 0);
unsafe {
std::ptr::copy_nonoverlapping(slice.as_ptr(), dst.as_mut_ptr(), slice.len());
}
Some(())
}
}
fn release(&mut self) {
self.len = 0;
self.ptr.store(std::ptr::null_mut(), SeqCst);
}
}
pub fn get_video_raw<'a>(dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
VIDEO_RAW.lock().ok()?.take(dst, last)
}
pub fn get_audio_raw<'a>(dst: &mut Vec<u8>, last: &mut Vec<u8>) -> Option<()> {
AUDIO_RAW.lock().ok()?.take(dst, last)
}
pub fn get_clipboards(client: bool) -> Option<MultiClipboards> {
if client {
CLIPBOARDS_CLIENT.lock().ok()?.take()
} else {
CLIPBOARDS_HOST.lock().ok()?.take()
}
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_onVideoFrameUpdate(
env: JNIEnv,
_class: JClass,
buffer: JObject,
) {
let jb = JByteBuffer::from(buffer);
if let Ok(data) = env.get_direct_buffer_address(&jb) {
if let Ok(len) = env.get_direct_buffer_capacity(&jb) {
VIDEO_RAW.lock().unwrap().update(data, len);
}
}
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_onAudioFrameUpdate(
env: JNIEnv,
_class: JClass,
buffer: JObject,
) {
let jb = JByteBuffer::from(buffer);
if let Ok(data) = env.get_direct_buffer_address(&jb) {
if let Ok(len) = env.get_direct_buffer_capacity(&jb) {
AUDIO_RAW.lock().unwrap().update(data, len);
}
}
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_onClipboardUpdate(
env: JNIEnv,
_class: JClass,
buffer: JByteBuffer,
) {
if let Ok(data) = env.get_direct_buffer_address(&buffer) {
if let Ok(len) = env.get_direct_buffer_capacity(&buffer) {
let data = unsafe { std::slice::from_raw_parts(data, len) };
if let Ok(clips) = MultiClipboards::parse_from_bytes(&data[1..]) {
let is_client = data[0] == 1;
if is_client {
*CLIPBOARDS_CLIENT.lock().unwrap() = Some(clips);
} else {
*CLIPBOARDS_HOST.lock().unwrap() = Some(clips);
}
}
}
}
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_setFrameRawEnable(
env: JNIEnv,
_class: JClass,
name: JString,
value: jboolean,
) {
let mut env = env;
if let Ok(name) = env.get_string(&name) {
let name: String = name.into();
let value = value.eq(&1);
if name.eq("video") {
VIDEO_RAW.lock().unwrap().set_enable(value);
} else if name.eq("audio") {
AUDIO_RAW.lock().unwrap().set_enable(value);
}
};
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObject) {
log::debug!("MainService init from java");
if let Ok(jvm) = env.get_java_vm() {
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
let mut jvm_lock = JVM.write().unwrap();
if jvm_lock.is_none() {
*jvm_lock = Some(jvm);
}
drop(jvm_lock);
if let Ok(context) = env.new_global_ref(ctx) {
let context_jobject = context.as_obj().as_raw() as *mut c_void;
*MAIN_SERVICE_CTX.write().unwrap() = Some(context);
init_ndk_context(java_vm, context_jobject);
}
}
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_setClipboardManager(
env: JNIEnv,
_class: JClass,
clipboard_manager: JObject,
) {
log::debug!("ClipboardManager init from java");
if let Ok(jvm) = env.get_java_vm() {
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
let mut jvm_lock = JVM.write().unwrap();
if jvm_lock.is_none() {
*jvm_lock = Some(jvm);
}
drop(jvm_lock);
if let Ok(manager) = env.new_global_ref(clipboard_manager) {
*CLIPBOARD_MANAGER.write().unwrap() = Some(manager);
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct MediaCodecInfo {
pub name: String,
pub is_encoder: bool,
#[serde(default)]
pub hw: Option<bool>, // api 29+
pub mime_type: String,
pub surface: bool,
pub nv12: bool,
#[serde(default)]
pub low_latency: Option<bool>, // api 30+, decoder
pub min_bitrate: u32,
pub max_bitrate: u32,
pub min_width: usize,
pub max_width: usize,
pub min_height: usize,
pub max_height: usize,
}
#[derive(Debug, Deserialize, Clone)]
pub struct MediaCodecInfos {
pub version: usize,
pub w: usize, // aligned
pub h: usize, // aligned
pub codecs: Vec<MediaCodecInfo>,
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_setCodecInfo(env: JNIEnv, _class: JClass, info: JString) {
let mut env = env;
if let Ok(info) = env.get_string(&info) {
let info: String = info.into();
if let Ok(infos) = serde_json::from_str::<MediaCodecInfos>(&info) {
*MEDIA_CODEC_INFOS.write().unwrap() = Some(infos);
}
}
}
pub fn get_codec_info() -> Option<MediaCodecInfos> {
MEDIA_CODEC_INFOS.read().unwrap().as_ref().cloned()
}
pub fn clear_codec_info() {
*MEDIA_CODEC_INFOS.write().unwrap() = None;
}
// another way to fix "reference table overflow" error caused by new_string and call_main_service_pointer_input frequently calld
// is below, but here I change kind from string to int for performance
/*
env.with_local_frame(10, || {
let kind = env.new_string(kind)?;
env.call_method(
ctx,
"rustPointerInput",
"(Ljava/lang/String;III)V",
&[
JValue::Object(&JObject::from(kind)),
JValue::Int(mask),
JValue::Int(x),
JValue::Int(y),
],
)?;
Ok(JObject::null())
})?;
*/
pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread_as_daemon()?;
let kind = if kind == "touch" { 0 } else { 1 };
env.call_method(
ctx,
"rustPointerInput",
"(IIII)V",
&[
JValue::Int(kind),
JValue::Int(mask),
JValue::Int(x),
JValue::Int(y),
],
)?;
return Ok(());
} else {
return Err(JniError::ThrowFailed(-1));
}
}
pub fn call_main_service_key_event(data: &[u8]) -> JniResult<()> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread_as_daemon()?;
let data = env.byte_array_from_slice(data)?;
env.call_method(
ctx,
"rustKeyEventInput",
"([B)V",
&[JValue::Object(&JObject::from(data))],
)?;
return Ok(());
} else {
return Err(JniError::ThrowFailed(-1));
}
}
fn _call_clipboard_manager<S, T>(name: S, sig: T, args: &[JValue]) -> JniResult<()>
where
S: Into<JNIString>,
T: Into<JNIString> + AsRef<str>,
{
if let (Some(jvm), Some(cm)) = (
JVM.read().unwrap().as_ref(),
CLIPBOARD_MANAGER.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread()?;
env.call_method(cm, name, sig, args)?;
return Ok(());
} else {
return Err(JniError::ThrowFailed(-1));
}
}
pub fn call_clipboard_manager_update_clipboard(data: &[u8]) -> JniResult<()> {
if let (Some(jvm), Some(cm)) = (
JVM.read().unwrap().as_ref(),
CLIPBOARD_MANAGER.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread()?;
let data = env.byte_array_from_slice(data)?;
env.call_method(
cm,
"rustUpdateClipboard",
"([B)V",
&[JValue::Object(&JObject::from(data))],
)?;
return Ok(());
} else {
return Err(JniError::ThrowFailed(-1));
}
}
pub fn call_clipboard_manager_enable_client_clipboard(enable: bool) -> JniResult<()> {
_call_clipboard_manager(
"rustEnableClientClipboard",
"(Z)V",
&[JValue::Bool(jboolean::from(enable))],
)
}
pub fn call_main_service_get_by_name(name: &str) -> JniResult<String> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread_as_daemon()?;
let res = env.with_local_frame(10, |env| -> JniResult<String> {
let name = env.new_string(name)?;
let res = env
.call_method(
ctx,
"rustGetByName",
"(Ljava/lang/String;)Ljava/lang/String;",
&[JValue::Object(&JObject::from(name))],
)?
.l()?;
let res = JString::from(res);
let res = env.get_string(&res)?;
let res = res.to_string_lossy().to_string();
Ok(res)
})?;
Ok(res)
} else {
return Err(JniError::ThrowFailed(-1));
}
}
pub fn call_main_service_set_by_name(
name: &str,
arg1: Option<&str>,
arg2: Option<&str>,
) -> JniResult<()> {
if let (Some(jvm), Some(ctx)) = (
JVM.read().unwrap().as_ref(),
MAIN_SERVICE_CTX.read().unwrap().as_ref(),
) {
let mut env = jvm.attach_current_thread_as_daemon()?;
env.with_local_frame(10, |env| -> JniResult<()> {
let name = env.new_string(name)?;
let arg1 = env.new_string(arg1.unwrap_or(""))?;
let arg2 = env.new_string(arg2.unwrap_or(""))?;
env.call_method(
ctx,
"rustSetByName",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
&[
JValue::Object(&JObject::from(name)),
JValue::Object(&JObject::from(arg1)),
JValue::Object(&JObject::from(arg2)),
],
)?;
Ok(())
})?;
return Ok(());
} else {
return Err(JniError::ThrowFailed(-1));
}
}
// Difference between MainService, MainActivity, JNI_OnLoad:
// jvm is the same, ctx is differen and ctx of JNI_OnLoad is null.
// cpal: all three works
// Service(GetByName, ...): only ctx from MainService works, so use 2 init context functions
// On app start: JNI_OnLoad or MainActivity init context
// On service start first time: MainService replace the context
fn init_ndk_context(java_vm: *mut c_void, context_jobject: *mut c_void) {
let mut lock = NDK_CONTEXT_INITED.lock().unwrap();
if *lock {
unsafe {
ndk_context::release_android_context();
}
*lock = false;
}
unsafe {
ndk_context::initialize_android_context(java_vm, context_jobject);
#[cfg(feature = "hwcodec")]
hwcodec::android::ffmpeg_set_java_vm(java_vm);
}
*lock = true;
}
fn try_init_rustls_platform_verifier(env: &mut JNIEnv, context_jobject: *mut c_void) {
use hbb_common::config::ANDROID_RUSTLS_PLATFORM_VERIFIER_INITIALIZED as INITIALIZED;
use std::sync::atomic::Ordering;
let initialized = INITIALIZED.load(Ordering::Relaxed);
if !initialized {
let ctx_for_rustls = unsafe { JObject::from_raw(context_jobject as jni::sys::jobject) };
if let Err(e) =
hbb_common::rustls_platform_verifier::android::init_hosted(env, ctx_for_rustls)
{
log::error!("Failed to initialize rustls-platform-verifier: {:?}", e);
} else {
INITIALIZED.store(true, Ordering::Relaxed);
log::info!("rustls-platform-verifier initialized successfully");
}
}
}
// https://cjycode.com/flutter_rust_bridge/guides/how-to/ndk-init
#[no_mangle]
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint {
if let Ok(env) = vm.get_env() {
let vm = vm.get_java_vm_pointer() as *mut std::os::raw::c_void;
init_ndk_context(vm, res);
}
jni::JNIVersion::V6.into()
}
#[no_mangle]
pub extern "system" fn Java_ffi_FFI_onAppStart(mut env: JNIEnv, _class: JClass, ctx: JObject) {
if ctx.is_null() {
log::error!("application context is null");
return;
}
if APPLICATION_CONTEXT.read().unwrap().is_some() {
log::info!("application context already initialized");
return;
}
if let Ok(jvm) = env.get_java_vm() {
if let Ok(context) = env.new_global_ref(ctx) {
let java_vm = jvm.get_java_vm_pointer() as *mut c_void;
let context_jobject = context.as_obj().as_raw() as *mut c_void;
*APPLICATION_CONTEXT.write().unwrap() = Some(context);
try_init_rustls_platform_verifier(&mut env, context_jobject);
}
}
}