From 4342771cba13bb366b18f1a89af6bc41314f4c52 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Thu, 23 Oct 2025 11:28:36 +0200 Subject: [PATCH 01/77] feat: add Adapter `retrieve_peripherals` method --- src/api/mod.rs | 15 +++++++++++++++ src/bluez/adapter.rs | 9 ++++++++- src/corebluetooth/adapter.rs | 9 ++++++++- src/droidplug/adapter.rs | 12 +++++++++++- src/winrtble/adapter.rs | 9 ++++++++- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index bb3acf68..c48650b1 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -209,6 +209,15 @@ pub struct ScanFilter { pub services: Vec, } +/// Parameters of retrieve_peripherals method. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RetrievePeripheralsOptions { + /// retrieve connected peripherals by services + services: Option>, + /// retrieve known peripherals by identifiers + identifiers: Option>, +} + /// The type of write operation to use. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WriteType { @@ -363,6 +372,12 @@ pub trait Central: Send + Sync + Clone { /// may contain peripherals that are no longer available. async fn peripherals(&self) -> Result>; + /// Returns the list of known or connected [`Peripheral`]s + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result>; + /// Returns a particular [`Peripheral`] by its address if it has been discovered. async fn peripheral(&self, id: &PeripheralId) -> Result; diff --git a/src/bluez/adapter.rs b/src/bluez/adapter.rs index 72efb407..4e0964fc 100644 --- a/src/bluez/adapter.rs +++ b/src/bluez/adapter.rs @@ -1,5 +1,5 @@ use super::peripheral::{Peripheral, PeripheralId}; -use crate::api::{Central, CentralEvent, CentralState, ScanFilter}; +use crate::api::{Central, CentralEvent, CentralState, RetrievePeripheralsOptions, ScanFilter}; use crate::{Error, Result}; use async_trait::async_trait; use bluez_async::{ @@ -91,6 +91,13 @@ impl Central for Adapter { .collect()) } + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result> { + Err(Error::NotSupported("Not implemented".to_string())) + } + async fn peripheral(&self, id: &PeripheralId) -> Result { let device = self.session.get_device_info(&id.0).await.map_err(|e| { if let BluetoothError::DbusError(_) = e { diff --git a/src/corebluetooth/adapter.rs b/src/corebluetooth/adapter.rs index b8626bd1..17995b43 100644 --- a/src/corebluetooth/adapter.rs +++ b/src/corebluetooth/adapter.rs @@ -3,7 +3,7 @@ use super::internal::{ CoreBluetoothReplyFuture, }; use super::peripheral::{Peripheral, PeripheralId}; -use crate::api::{Central, CentralEvent, CentralState, ScanFilter}; +use crate::api::{Central, CentralEvent, CentralState, RetrievePeripheralsOptions, ScanFilter}; use crate::common::adapter_manager::AdapterManager; use crate::{Error, Result}; use async_trait::async_trait; @@ -122,6 +122,13 @@ impl Central for Adapter { Ok(self.manager.peripherals()) } + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result> { + Err(Error::NotSupported("Not implemented".to_string())) + } + async fn peripheral(&self, id: &PeripheralId) -> Result { self.manager.peripheral(id).ok_or(Error::DeviceNotFound) } diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index f3d10c90..5a6ed6f2 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -6,7 +6,10 @@ use super::{ peripheral::{Peripheral, PeripheralId}, }; use crate::{ - api::{BDAddr, Central, CentralEvent, CentralState, PeripheralProperties, ScanFilter}, + api::{ + BDAddr, Central, CentralEvent, CentralState, PeripheralProperties, + RetrievePeripheralsOptions, ScanFilter, + }, common::adapter_manager::AdapterManager, Error, Result, }; @@ -158,6 +161,13 @@ impl Central for Adapter { Ok(self.manager.peripherals()) } + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result> { + Err(Error::NotSupported("Not implemented".to_string())) + } + async fn peripheral(&self, address: &PeripheralId) -> Result { self.manager .peripheral(address) diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index d840c403..5a3085f8 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -13,7 +13,7 @@ use super::{ble::watcher::BLEWatcher, peripheral::Peripheral, peripheral::PeripheralId}; use crate::{ - api::{BDAddr, Central, CentralEvent, CentralState, ScanFilter}, + api::{BDAddr, Central, CentralEvent, CentralState, RetrievePeripheralsOptions, ScanFilter}, common::adapter_manager::AdapterManager, Error, Result, }; @@ -118,6 +118,13 @@ impl Central for Adapter { Ok(self.manager.peripherals()) } + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result> { + Err(Error::NotSupported("Not implemented".to_string())) + } + async fn peripheral(&self, id: &PeripheralId) -> Result { self.manager.peripheral(id).ok_or(Error::DeviceNotFound) } From fdcaedbc24ba3da2f38030c46cecadbf761aaa02 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 25 Apr 2026 17:47:22 -0700 Subject: [PATCH 02/77] build: Upgrade jni from 0.19 to 0.20 Breaking changes addressed: - call_method_unchecked takes &[jni::sys::jvalue] instead of &[JValue]; added JValue::to_jni() at all ~40 call sites - JavaType replaced by ReturnType in call_method_unchecked return type parameter (Object/Array/Primitive instead of carrying class strings) - JMethodID no longer has a lifetime parameter - JObject::into_inner() renamed to into_raw() - set_rust_field/get_rust_field/take_rust_field marked unsafe; wrapped all 9 call sites in unsafe blocks Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 8 +- Cargo.toml | 4 +- src/droidplug/adapter.rs | 6 +- src/droidplug/jni/objects.rs | 254 ++++++++++---------------- src/droidplug/jni_utils/exceptions.rs | 12 +- src/droidplug/jni_utils/future.rs | 10 +- src/droidplug/jni_utils/ops.rs | 6 +- src/droidplug/jni_utils/stream.rs | 19 +- src/droidplug/jni_utils/task.rs | 11 +- src/droidplug/jni_utils/uuid.rs | 10 +- src/droidplug/peripheral.rs | 4 +- tests/android/rust/Cargo.toml | 2 +- 12 files changed, 141 insertions(+), 205 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b586906e..3800e7a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -401,7 +401,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -421,9 +421,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jni" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" dependencies = [ "cesu8", "combine", @@ -1103,7 +1103,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c117aa21..627bc5eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,11 +45,11 @@ dbus = "0.9.10" bluez-async = "0.8.2" [target.'cfg(target_os = "android")'.dependencies] -jni = "0.19.0" +jni = "0.20.0" once_cell = "1.21.3" [target.'cfg(not(target_os = "android"))'.dependencies] -jni = { version = "0.19.0", optional = true } +jni = { version = "0.20.0", optional = true } once_cell = { version = "1.21.3", optional = true } [target.'cfg(target_vendor = "apple")'.dependencies] diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index 5e0a4f2b..1ad9f5f4 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -55,7 +55,7 @@ impl Adapter { manager: Arc::new(AdapterManager::default()), internal, }; - env.set_rust_field(obj, "handle", adapter.clone())?; + unsafe { env.set_rust_field(obj, "handle", adapter.clone()) }?; Ok(adapter) } @@ -205,7 +205,7 @@ pub(crate) fn adapter_report_scan_result_internal( obj: JObject, scan_result: JObject, ) -> crate::Result<()> { - let adapter = env.get_rust_field::<_, _, Adapter>(obj, "handle")?; + let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, "handle") }?; adapter.report_scan_result(scan_result)?; Ok(()) } @@ -216,7 +216,7 @@ pub(crate) fn adapter_on_connection_state_changed_internal( addr: JString, connected: jboolean, ) -> crate::Result<()> { - let adapter = env.get_rust_field::<_, _, Adapter>(obj, "handle")?; + let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, "handle") }?; let addr_str = JavaStr::from_env(env, addr)?; let addr_str = addr_str.to_str().map_err(|e| Error::Other(e.into()))?; let addr = BDAddr::from_str(addr_str)?; diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 6e5e051a..839d608e 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -2,8 +2,8 @@ use crate::droidplug::jni_utils::{future::JFuture, stream::JStream, uuid::JUuid} use jni::{ JNIEnv, errors::Result, - objects::{JClass, JList, JMap, JMethodID, JObject, JString}, - signature::{JavaType, Primitive}, + objects::{JClass, JList, JMap, JMethodID, JObject, JString, JValue}, + signature::{Primitive, ReturnType}, strings::JavaStr, sys::jint, }; @@ -14,21 +14,21 @@ use crate::api::{BDAddr, CharPropFlags, PeripheralProperties, ScanFilter}; pub struct JPeripheral<'a: 'b, 'b> { internal: JObject<'a>, - connect: JMethodID<'a>, - disconnect: JMethodID<'a>, - is_connected: JMethodID<'a>, - discover_services: JMethodID<'a>, - read: JMethodID<'a>, - write: JMethodID<'a>, - set_characteristic_notification: JMethodID<'a>, - get_notifications: JMethodID<'a>, - read_descriptor: JMethodID<'a>, - write_descriptor: JMethodID<'a>, - get_device_name: JMethodID<'a>, - request_mtu: JMethodID<'a>, - get_connection_parameters: JMethodID<'a>, - request_connection_priority: JMethodID<'a>, - read_remote_rssi: JMethodID<'a>, + connect: JMethodID, + disconnect: JMethodID, + is_connected: JMethodID, + discover_services: JMethodID, + read: JMethodID, + write: JMethodID, + set_characteristic_notification: JMethodID, + get_notifications: JMethodID, + read_descriptor: JMethodID, + write_descriptor: JMethodID, + get_device_name: JMethodID, + request_mtu: JMethodID, + get_connection_parameters: JMethodID, + request_connection_priority: JMethodID, + read_remote_rssi: JMethodID, env: &'b JNIEnv<'a>, } @@ -166,12 +166,7 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { pub fn connect(&self) -> Result> { let future_obj = self .env - .call_method_unchecked( - self.internal, - self.connect, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.connect, ReturnType::Object, &[])? .l()?; JFuture::from_env(self.env, future_obj) } @@ -179,12 +174,7 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { pub fn disconnect(&self) -> Result> { let future_obj = self .env - .call_method_unchecked( - self.internal, - self.disconnect, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.disconnect, ReturnType::Object, &[])? .l()?; JFuture::from_env(self.env, future_obj) } @@ -194,7 +184,7 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.is_connected, - JavaType::Primitive(Primitive::Boolean), + ReturnType::Primitive(Primitive::Boolean), &[], )? .z() @@ -206,7 +196,7 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.discover_services, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), + ReturnType::Object, &[], )? .l()?; @@ -219,8 +209,8 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.read, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), - &[uuid.into()], + ReturnType::Object, + &[JValue::from(uuid).to_jni()], )? .l()?; JFuture::from_env(self.env, future_obj) @@ -237,8 +227,12 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.write, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), - &[uuid.into(), data.into(), write_type.into()], + ReturnType::Object, + &[ + JValue::from(uuid).to_jni(), + JValue::from(data).to_jni(), + JValue::from(write_type).to_jni(), + ], )? .l()?; JFuture::from_env(self.env, future_obj) @@ -254,8 +248,8 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.set_characteristic_notification, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), - &[uuid.into(), enable.into()], + ReturnType::Object, + &[JValue::from(uuid).to_jni(), JValue::from(enable).to_jni()], )? .l()?; JFuture::from_env(self.env, future_obj) @@ -267,7 +261,7 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.get_notifications, - JavaType::Object("Lio/github/gedgygedgy/rust/stream/Stream;".to_string()), + ReturnType::Object, &[], )? .l()?; @@ -284,8 +278,11 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.read_descriptor, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), - &[characteristic.into(), uuid.into()], + ReturnType::Object, + &[ + JValue::from(characteristic).to_jni(), + JValue::from(uuid).to_jni(), + ], )? .l()?; JFuture::from_env(self.env, future_obj) @@ -294,12 +291,7 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { pub fn get_device_name(&self) -> Result> { let obj = self .env - .call_method_unchecked( - self.internal, - self.get_device_name, - JavaType::Object("Ljava/lang/String;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.get_device_name, ReturnType::Object, &[])? .l()?; if obj.is_null() { Ok(None) @@ -314,8 +306,8 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.request_mtu, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), - &[mtu.into()], + ReturnType::Object, + &[JValue::from(mtu).to_jni()], )? .l() } @@ -326,14 +318,14 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.get_connection_parameters, - JavaType::Array(JavaType::Primitive(Primitive::Int).into()), + ReturnType::Array, &[], )? .l()?; if obj.is_null() { return Ok(None); } - let arr = obj.into_inner(); + let arr = obj.into_raw(); let len = self.env.get_array_length(arr)?; if len < 3 { return Ok(None); @@ -354,7 +346,7 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.read_remote_rssi, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), + ReturnType::Object, &[], )? .l() @@ -365,8 +357,8 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.request_connection_priority, - JavaType::Primitive(Primitive::Boolean), - &[priority.into()], + ReturnType::Primitive(Primitive::Boolean), + &[JValue::from(priority).to_jni()], )? .z() } @@ -382,8 +374,12 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { .call_method_unchecked( self.internal, self.write_descriptor, - JavaType::Object("Lio/github/gedgygedgy/rust/future/Future;".to_string()), - &[characteristic.into(), uuid.into(), data.into()], + ReturnType::Object, + &[ + JValue::from(characteristic).to_jni(), + JValue::from(uuid).to_jni(), + JValue::from(data).to_jni(), + ], )? .l()?; JFuture::from_env(self.env, future_obj) @@ -392,9 +388,9 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { pub struct JBluetoothGattService<'a: 'b, 'b> { internal: JObject<'a>, - get_uuid: JMethodID<'a>, - //is_primary: JMethodID<'a>, - get_characteristics: JMethodID<'a>, + get_uuid: JMethodID, + //is_primary: JMethodID, + get_characteristics: JMethodID, env: &'b JNIEnv<'a>, } @@ -421,7 +417,7 @@ impl<'a: 'b, 'b> JBluetoothGattService<'a, 'b> { .call_method_unchecked( self.internal, self.is_primary, - JavaType::Primitive(Primitive::Boolean), + ReturnType::Primitive(Primitive::Boolean), &[], )? .z() @@ -432,12 +428,7 @@ impl<'a: 'b, 'b> JBluetoothGattService<'a, 'b> { pub fn get_uuid(&self) -> Result { let obj = self .env - .call_method_unchecked( - self.internal, - self.get_uuid, - JavaType::Object("Ljava/util/UUID;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.get_uuid, ReturnType::Object, &[])? .l()?; let uuid_obj = JUuid::from_env(self.env, obj)?; Ok(uuid_obj.as_uuid()?) @@ -449,7 +440,7 @@ impl<'a: 'b, 'b> JBluetoothGattService<'a, 'b> { .call_method_unchecked( self.internal, self.get_characteristics, - JavaType::Object("Ljava/util/List;".to_string()), + ReturnType::Object, &[], )? .l()?; @@ -464,10 +455,10 @@ impl<'a: 'b, 'b> JBluetoothGattService<'a, 'b> { pub struct JBluetoothGattCharacteristic<'a: 'b, 'b> { internal: JObject<'a>, - get_uuid: JMethodID<'a>, - get_properties: JMethodID<'a>, - get_value: JMethodID<'a>, - get_descriptors: JMethodID<'a>, + get_uuid: JMethodID, + get_properties: JMethodID, + get_value: JMethodID, + get_descriptors: JMethodID, env: &'b JNIEnv<'a>, } @@ -493,12 +484,7 @@ impl<'a: 'b, 'b> JBluetoothGattCharacteristic<'a, 'b> { pub fn get_uuid(&self) -> Result { let obj = self .env - .call_method_unchecked( - self.internal, - self.get_uuid, - JavaType::Object("Ljava/util/UUID;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.get_uuid, ReturnType::Object, &[])? .l()?; let uuid_obj = JUuid::from_env(self.env, obj)?; Ok(uuid_obj.as_uuid()?) @@ -510,7 +496,7 @@ impl<'a: 'b, 'b> JBluetoothGattCharacteristic<'a, 'b> { .call_method_unchecked( self.internal, self.get_properties, - JavaType::Primitive(Primitive::Int), + ReturnType::Primitive(Primitive::Int), &[], )? .i()?; @@ -520,25 +506,15 @@ impl<'a: 'b, 'b> JBluetoothGattCharacteristic<'a, 'b> { pub fn get_value(&self) -> Result> { let value = self .env - .call_method_unchecked( - self.internal, - self.get_value, - JavaType::Array(JavaType::Primitive(Primitive::Byte).into()), - &[], - )? + .call_method_unchecked(self.internal, self.get_value, ReturnType::Array, &[])? .l()?; - crate::droidplug::jni_utils::arrays::byte_array_to_vec(self.env, value.into_inner()) + crate::droidplug::jni_utils::arrays::byte_array_to_vec(self.env, value.into_raw()) } pub fn get_descriptors(&self) -> Result> { let obj = self .env - .call_method_unchecked( - self.internal, - self.get_descriptors, - JavaType::Object("Ljava/util/List;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.get_descriptors, ReturnType::Object, &[])? .l()?; let desc_list = JList::from_env(self.env, obj)?; let mut desc_vec = vec![]; @@ -551,7 +527,7 @@ impl<'a: 'b, 'b> JBluetoothGattCharacteristic<'a, 'b> { pub struct JBluetoothGattDescriptor<'a: 'b, 'b> { internal: JObject<'a>, - get_uuid: JMethodID<'a>, + get_uuid: JMethodID, env: &'b JNIEnv<'a>, } @@ -570,12 +546,7 @@ impl<'a: 'b, 'b> JBluetoothGattDescriptor<'a, 'b> { pub fn get_uuid(&self) -> Result { let obj = self .env - .call_method_unchecked( - self.internal, - self.get_uuid, - JavaType::Object("Ljava/util/UUID;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.get_uuid, ReturnType::Object, &[])? .l()?; let uuid_obj = JUuid::from_env(self.env, obj)?; Ok(uuid_obj.as_uuid()?) @@ -584,7 +555,7 @@ impl<'a: 'b, 'b> JBluetoothGattDescriptor<'a, 'b> { pub struct JBluetoothDevice<'a: 'b, 'b> { internal: JObject<'a>, - get_address: JMethodID<'a>, + get_address: JMethodID, env: &'b JNIEnv<'a>, } @@ -603,12 +574,7 @@ impl<'a: 'b, 'b> JBluetoothDevice<'a, 'b> { pub fn get_address(&self) -> Result> { let obj = self .env - .call_method_unchecked( - self.internal, - self.get_address, - JavaType::Object("Ljava/lang/String;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.get_address, ReturnType::Object, &[])? .l()?; Ok(obj.into()) } @@ -653,10 +619,10 @@ impl<'a> From> for JObject<'a> { pub struct JScanResult<'a: 'b, 'b> { internal: JObject<'a>, - get_device: JMethodID<'a>, - get_scan_record: JMethodID<'a>, - get_tx_power: JMethodID<'a>, - get_rssi: JMethodID<'a>, + get_device: JMethodID, + get_scan_record: JMethodID, + get_tx_power: JMethodID, + get_rssi: JMethodID, env: &'b JNIEnv<'a>, } @@ -686,12 +652,7 @@ impl<'a: 'b, 'b> JScanResult<'a, 'b> { pub fn get_device(&self) -> Result> { let obj = self .env - .call_method_unchecked( - self.internal, - self.get_device, - JavaType::Object("Landroid/bluetooth/BluetoothDevice;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.get_device, ReturnType::Object, &[])? .l()?; JBluetoothDevice::from_env(self.env, obj) } @@ -699,12 +660,7 @@ impl<'a: 'b, 'b> JScanResult<'a, 'b> { pub fn get_scan_record(&self) -> Result> { let obj = self .env - .call_method_unchecked( - self.internal, - self.get_scan_record, - JavaType::Object("Landroid/bluetooth/le/ScanRecord;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.get_scan_record, ReturnType::Object, &[])? .l()?; JScanRecord::from_env(self.env, obj) } @@ -714,7 +670,7 @@ impl<'a: 'b, 'b> JScanResult<'a, 'b> { .call_method_unchecked( self.internal, self.get_tx_power, - JavaType::Primitive(Primitive::Int), + ReturnType::Primitive(Primitive::Int), &[], )? .i() @@ -725,7 +681,7 @@ impl<'a: 'b, 'b> JScanResult<'a, 'b> { .call_method_unchecked( self.internal, self.get_rssi, - JavaType::Primitive(Primitive::Int), + ReturnType::Primitive(Primitive::Int), &[], )? .i() @@ -800,7 +756,7 @@ impl<'a: 'b, 'b> TryFrom> for (BDAddr, Option TryFrom> for (BDAddr, Option TryFrom> for (BDAddr, Option { internal: JObject<'a>, - get_device_name: JMethodID<'a>, - get_tx_power_level: JMethodID<'a>, - get_manufacturer_specific_data: JMethodID<'a>, - get_service_data: JMethodID<'a>, - get_service_uuids: JMethodID<'a>, + get_device_name: JMethodID, + get_tx_power_level: JMethodID, + get_manufacturer_specific_data: JMethodID, + get_service_data: JMethodID, + get_service_uuids: JMethodID, env: &'b JNIEnv<'a>, } @@ -909,12 +865,7 @@ impl<'a: 'b, 'b> JScanRecord<'a, 'b> { pub fn get_device_name(&self) -> Result> { let obj = self .env - .call_method_unchecked( - self.internal, - self.get_device_name, - JavaType::Object("Ljava/lang/String;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.get_device_name, ReturnType::Object, &[])? .l()?; Ok(obj.into()) } @@ -924,7 +875,7 @@ impl<'a: 'b, 'b> JScanRecord<'a, 'b> { .call_method_unchecked( self.internal, self.get_tx_power_level, - JavaType::Primitive(Primitive::Int), + ReturnType::Primitive(Primitive::Int), &[], )? .i() @@ -936,7 +887,7 @@ impl<'a: 'b, 'b> JScanRecord<'a, 'b> { .call_method_unchecked( self.internal, self.get_manufacturer_specific_data, - JavaType::Object("Landroid/util/SparseArray;".to_string()), + ReturnType::Object, &[], )? .l()?; @@ -949,7 +900,7 @@ impl<'a: 'b, 'b> JScanRecord<'a, 'b> { .call_method_unchecked( self.internal, self.get_service_data, - JavaType::Object("Ljava/util/Map;".to_string()), + ReturnType::Object, &[], )? .l()?; @@ -962,7 +913,7 @@ impl<'a: 'b, 'b> JScanRecord<'a, 'b> { .call_method_unchecked( self.internal, self.get_service_uuids, - JavaType::Object("Ljava/util/List;".to_string()), + ReturnType::Object, &[], )? .l()?; @@ -973,9 +924,9 @@ impl<'a: 'b, 'b> JScanRecord<'a, 'b> { #[derive(Clone)] pub struct JSparseArray<'a: 'b, 'b> { internal: JObject<'a>, - size: JMethodID<'a>, - key_at: JMethodID<'a>, - value_at: JMethodID<'a>, + size: JMethodID, + key_at: JMethodID, + value_at: JMethodID, env: &'b JNIEnv<'a>, } @@ -1014,7 +965,7 @@ impl<'a: 'b, 'b> JSparseArray<'a, 'b> { .call_method_unchecked( self.internal, self.size, - JavaType::Primitive(Primitive::Int), + ReturnType::Primitive(Primitive::Int), &[], )? .i() @@ -1025,8 +976,8 @@ impl<'a: 'b, 'b> JSparseArray<'a, 'b> { .call_method_unchecked( self.internal, self.key_at, - JavaType::Primitive(Primitive::Int), - &[index.into()], + ReturnType::Primitive(Primitive::Int), + &[JValue::from(index).to_jni()], )? .i() } @@ -1036,8 +987,8 @@ impl<'a: 'b, 'b> JSparseArray<'a, 'b> { .call_method_unchecked( self.internal, self.value_at, - JavaType::Object("Ljava/lang/Object;".to_string()), - &[index.into()], + ReturnType::Object, + &[JValue::from(index).to_jni()], )? .l() } @@ -1078,7 +1029,7 @@ impl<'a: 'b, 'b> Iterator for JSparseArrayIter<'a, 'b> { } pub struct JParcelUuid<'a: 'b, 'b> { internal: JObject<'a>, - get_uuid: JMethodID<'a>, + get_uuid: JMethodID, env: &'b JNIEnv<'a>, } @@ -1097,12 +1048,7 @@ impl<'a: 'b, 'b> JParcelUuid<'a, 'b> { pub fn get_uuid(&self) -> Result> { let obj = self .env - .call_method_unchecked( - self.internal, - self.get_uuid, - JavaType::Object("Ljava/util/UUID;".to_string()), - &[], - )? + .call_method_unchecked(self.internal, self.get_uuid, ReturnType::Object, &[])? .l()?; JUuid::from_env(self.env, obj) } diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index 6f014d52..6f394376 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -126,16 +126,16 @@ impl<'a: 'b, 'b> JPanicException<'a, 'b> { "(Ljava/lang/String;)V", &[msg.into()], )?; - env.set_rust_field(obj, "any", any)?; + unsafe { env.set_rust_field(obj, "any", any) }?; Self::from_env(env, obj.into()) } pub fn get(&self) -> Result>, Error> { - self.env.get_rust_field(self.internal, "any") + unsafe { self.env.get_rust_field(self.internal, "any") } } pub fn take(&self) -> Result, Error> { - self.env.take_rust_field(self.internal, "any") + unsafe { self.env.take_rust_field(self.internal, "any") } } pub fn resume_unwind(&self) -> Result<(), Error> { @@ -542,7 +542,7 @@ mod test { .l() .unwrap(); assert_eq!( - env.get_array_length(suppressed_list.into_inner()).unwrap(), + env.get_array_length(suppressed_list.into_raw()).unwrap(), 0 ); @@ -579,11 +579,11 @@ mod test { .l() .unwrap(); assert_eq!( - env.get_array_length(suppressed_list.into_inner()).unwrap(), + env.get_array_length(suppressed_list.into_raw()).unwrap(), 1 ); let suppressed_ex = env - .get_object_array_element(suppressed_list.into_inner(), 0) + .get_object_array_element(suppressed_list.into_raw(), 0) .unwrap(); assert!(env.is_same_object(old_ex, suppressed_ex).unwrap()); diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 34587a80..ffaf4e0e 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -2,8 +2,8 @@ use super::task::JPollResult; use ::jni::{ JNIEnv, JavaVM, errors::{Error, Result}, - objects::{GlobalRef, JClass, JMethodID, JObject}, - signature::JavaType, + objects::{GlobalRef, JClass, JMethodID, JObject, JValue}, + signature::ReturnType, }; use static_assertions::assert_impl_all; use std::{ @@ -21,7 +21,7 @@ use std::{ /// For a [`Send`] version of this, use [`JSendFuture`]. pub struct JFuture<'a: 'b, 'b> { internal: JObject<'a>, - poll: JMethodID<'a>, + poll: JMethodID, env: &'b JNIEnv<'a>, } @@ -49,8 +49,8 @@ impl<'a: 'b, 'b> JFuture<'a, 'b> { .call_method_unchecked( self.internal, self.poll, - JavaType::Object("io/github/gedgygedgy/rust/task/PollResult".into()), - &[waker.into()], + ReturnType::Object, + &[JValue::from(waker).to_jni()], )? .l()?; JPollResult::from_env(self.env, result) diff --git a/src/droidplug/jni_utils/ops.rs b/src/droidplug/jni_utils/ops.rs index 3b896251..03902913 100644 --- a/src/droidplug/jni_utils/ops.rs +++ b/src/droidplug/jni_utils/ops.rs @@ -282,7 +282,7 @@ fn fn_adapter<'a: 'b, 'b>( "(Z)V", &[local.into()], )?; - env.set_rust_field::<_, _, FnWrapper>(obj, "data", SendSyncWrapper(arc))?; + unsafe { env.set_rust_field::<_, _, FnWrapper>(obj, "data", SendSyncWrapper(arc)) }?; Ok(obj) } @@ -295,7 +295,7 @@ pub(crate) extern "C" fn fn_adapter_call_internal<'a>( ) -> JObject<'a> { use std::panic::AssertUnwindSafe; - let arc = if let Ok(f) = env.get_rust_field::<_, _, FnWrapper>(obj1, "data") { + let arc = if let Ok(f) = unsafe { env.get_rust_field::<_, _, FnWrapper>(obj1, "data") } { AssertUnwindSafe(f.0.clone()) } else { return JObject::null(); @@ -306,6 +306,6 @@ pub(crate) extern "C" fn fn_adapter_call_internal<'a>( pub(crate) extern "C" fn fn_adapter_close_internal(env: JNIEnv, obj: JObject) { let _ = super::exceptions::throw_unwind(&env, || { - let _ = env.take_rust_field::<_, _, FnWrapper>(obj, "data"); + let _ = unsafe { env.take_rust_field::<_, _, FnWrapper>(obj, "data") }; }); } diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index c108247c..636a4fc5 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -2,8 +2,8 @@ use super::task::JPollResult; use ::jni::{ JNIEnv, JavaVM, errors::{Error, Result}, - objects::{GlobalRef, JClass, JMethodID, JObject}, - signature::JavaType, + objects::{GlobalRef, JClass, JMethodID, JObject, JValue}, + signature::ReturnType, }; use futures::stream::Stream; use static_assertions::assert_impl_all; @@ -19,7 +19,7 @@ use std::{ /// For a [`Send`] version of this, use [`JSendStream`]. pub struct JStream<'a: 'b, 'b> { internal: JObject<'a>, - poll_next: JMethodID<'a>, + poll_next: JMethodID, env: &'b JNIEnv<'a>, } @@ -47,8 +47,8 @@ impl<'a: 'b, 'b> JStream<'a, 'b> { .call_method_unchecked( self.internal, self.poll_next, - JavaType::Object("io/github/gedgygedgy/rust/task/PollResult".to_string()), - &[waker.into()], + ReturnType::Object, + &[JValue::from(waker).to_jni()], )? .l()?; let _auto_local = self.env.auto_local(result); @@ -153,7 +153,7 @@ assert_impl_all!(JSendStream: Send); struct JStreamPoll<'a: 'b, 'b> { internal: JObject<'a>, - get: JMethodID<'a>, + get: JMethodID, env: &'b JNIEnv<'a>, } @@ -177,12 +177,7 @@ impl<'a: 'b, 'b> JStreamPoll<'a, 'b> { pub fn get(&self) -> Result> { self.env - .call_method_unchecked( - self.internal, - self.get, - JavaType::Object("java/lang/Object".into()), - &[], - )? + .call_method_unchecked(self.internal, self.get, ReturnType::Object, &[])? .l() } } diff --git a/src/droidplug/jni_utils/task.rs b/src/droidplug/jni_utils/task.rs index c0b3c038..7993282a 100644 --- a/src/droidplug/jni_utils/task.rs +++ b/src/droidplug/jni_utils/task.rs @@ -2,7 +2,7 @@ use ::jni::{ JNIEnv, errors::Result, objects::{JClass, JMethodID, JObject}, - signature::JavaType, + signature::ReturnType, }; use std::task::Waker; @@ -26,7 +26,7 @@ pub fn waker<'a: 'b, 'b>(env: &'b JNIEnv<'a>, waker: Waker) -> Result { internal: JObject<'a>, - get: JMethodID<'a>, + get: JMethodID, env: &'b JNIEnv<'a>, } @@ -50,12 +50,7 @@ impl<'a: 'b, 'b> JPollResult<'a, 'b> { pub fn get(&self) -> Result> { self.env - .call_method_unchecked( - self.internal, - self.get, - JavaType::Object("java/lang/Object".into()), - &[], - )? + .call_method_unchecked(self.internal, self.get, ReturnType::Object, &[])? .l() } } diff --git a/src/droidplug/jni_utils/uuid.rs b/src/droidplug/jni_utils/uuid.rs index d5fdf6fa..2c7b7faf 100644 --- a/src/droidplug/jni_utils/uuid.rs +++ b/src/droidplug/jni_utils/uuid.rs @@ -2,7 +2,7 @@ use jni::{ JNIEnv, errors::Result, objects::{AutoLocal, JMethodID, JObject}, - signature::{JavaType, Primitive}, + signature::{Primitive, ReturnType}, sys::jlong, }; use uuid::Uuid; @@ -11,8 +11,8 @@ use uuid::Uuid; /// to convert to and from a [`Uuid`]. pub struct JUuid<'a: 'b, 'b> { internal: JObject<'a>, - get_least_significant_bits: JMethodID<'a>, - get_most_significant_bits: JMethodID<'a>, + get_least_significant_bits: JMethodID, + get_most_significant_bits: JMethodID, env: &'b JNIEnv<'a>, } @@ -38,7 +38,7 @@ impl<'a: 'b, 'b> JUuid<'a, 'b> { .call_method_unchecked( self.internal, self.get_least_significant_bits, - JavaType::Primitive(Primitive::Long), + ReturnType::Primitive(Primitive::Long), &[], )? .j()? as u64; @@ -47,7 +47,7 @@ impl<'a: 'b, 'b> JUuid<'a, 'b> { .call_method_unchecked( self.internal, self.get_most_significant_bits, - JavaType::Primitive(Primitive::Long), + ReturnType::Primitive(Primitive::Long), &[], )? .j()? as u64; diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 8554376e..40576985 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -390,7 +390,7 @@ impl api::Peripheral for Peripheral { self.with_obj(|env, _obj| { let result = JPollResult::from_env(env, result_ref.as_obj())?; let bytes = get_poll_result(env, result)?; - Ok(byte_array_to_vec(env, bytes.into_inner())?) + Ok(byte_array_to_vec(env, bytes.into_raw())?) }) } @@ -476,7 +476,7 @@ impl api::Peripheral for Peripheral { self.with_obj(|env, _obj| { let result = JPollResult::from_env(env, result_ref.as_obj())?; let bytes = get_poll_result(env, result)?; - Ok(byte_array_to_vec(env, bytes.into_inner())?) + Ok(byte_array_to_vec(env, bytes.into_raw())?) }) } diff --git a/tests/android/rust/Cargo.toml b/tests/android/rust/Cargo.toml index 9afc3077..5ec67c7a 100644 --- a/tests/android/rust/Cargo.toml +++ b/tests/android/rust/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib"] [dependencies] btleplug = { path = "../../.." } -jni = "0.19" +jni = "0.20" once_cell = "1" tokio = { version = "1", features = ["full"] } uuid = "1" From 38e480417732cfe25dd8684644304ff3c9863284 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 25 Apr 2026 18:33:40 -0700 Subject: [PATCH 03/77] build: Upgrade jni from 0.20 to 0.21 Major restructure of the Android JNI backend for jni-rs 0.21 breaking changes. JNIEnv now requires &mut self for all operations and JObject types are no longer Copy/Clone. Key changes: - Remove env field from all JNI wrapper structs, pass &mut JNIEnv per-method-call instead - JSendFuture/JSendStream store JavaVM and obtain env via get_env() on each poll, solving the Future::poll env-passing problem - Replace JList/JMap wrapper usage with direct env.call_method() iteration to avoid &mut borrow conflicts - Extract throw_panic from throw_unwind to enable catch_unwind in ops.rs without double &mut env borrows - Use typed arrays (JByteArray, JObjectArray) per 0.21 API - Wrap call_method_unchecked in unsafe blocks with raw jvalue args - Update test infrastructure for RefCell invariance: explicit RefMut guards, scoped setup before block_on, block-scoped borrows between await points Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 121 +++- Cargo.toml | 4 +- src/droidplug/adapter.rs | 113 +-- src/droidplug/jni/mod.rs | 20 +- src/droidplug/jni/objects.rs | 990 +++++++++++++------------- src/droidplug/jni_utils/arrays.rs | 28 +- src/droidplug/jni_utils/classcache.rs | 14 +- src/droidplug/jni_utils/exceptions.rs | 399 ++++++----- src/droidplug/jni_utils/future.rs | 337 +++++---- src/droidplug/jni_utils/mod.rs | 30 +- src/droidplug/jni_utils/ops.rs | 161 +++-- src/droidplug/jni_utils/stream.rs | 382 ++++++---- src/droidplug/jni_utils/task.rs | 63 +- src/droidplug/jni_utils/uuid.rs | 96 +-- src/droidplug/mod.rs | 2 +- src/droidplug/peripheral.rs | 324 ++++----- tests/android/rust/Cargo.toml | 2 +- 17 files changed, 1657 insertions(+), 1429 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3800e7a8..986a5db5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -336,6 +336,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "hashbrown" version = "0.14.5" @@ -419,18 +425,31 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "java-locator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c46c1fe465c59b1474e665e85e1256c3893dd00927b8d55f63b09044c1e64f" +dependencies = [ + "glob", +] + [[package]] name = "jni" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", + "cfg-if", "combine", + "java-locator", "jni-sys", + "libloading", "log", "thiserror 1.0.69", "walkdir", + "windows-sys 0.45.0", ] [[package]] @@ -476,6 +495,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -1097,6 +1126,22 @@ dependencies = [ "semver", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -1106,6 +1151,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.62.2" @@ -1207,6 +1258,15 @@ 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.59.0" @@ -1234,6 +1294,21 @@ 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" @@ -1276,6 +1351,12 @@ 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" @@ -1288,6 +1369,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[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" @@ -1300,6 +1387,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[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" @@ -1324,6 +1417,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[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" @@ -1336,6 +1435,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[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" @@ -1348,6 +1453,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[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" @@ -1360,6 +1471,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[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 627bc5eb..48e190ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,11 +45,11 @@ dbus = "0.9.10" bluez-async = "0.8.2" [target.'cfg(target_os = "android")'.dependencies] -jni = "0.20.0" +jni = "0.21.0" once_cell = "1.21.3" [target.'cfg(not(target_os = "android"))'.dependencies] -jni = { version = "0.20.0", optional = true } +jni = { version = "0.21.0", optional = true } once_cell = { version = "1.21.3", optional = true } [target.'cfg(target_vendor = "apple")'.dependencies] diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index 1ad9f5f4..ed902ffd 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -1,4 +1,3 @@ -use super::jni_utils::exceptions::try_block; use super::{ jni::{ global_jvm, @@ -13,11 +12,9 @@ use crate::{ }; use async_trait::async_trait; use futures::stream::Stream; -use jni::objects::JClass; use jni::{ JNIEnv, - objects::{GlobalRef, JObject, JString}, - strings::JavaStr, + objects::{GlobalRef, JClass, JObject, JString}, sys::jboolean, }; use std::{ @@ -43,30 +40,31 @@ impl Debug for Adapter { impl Adapter { pub(crate) fn new() -> Result { - let env = global_jvm().get_env()?; + let mut env = global_jvm().get_env()?; let obj = env.new_object( "com/nonpolynomial/btleplug/android/impl/Adapter", "()V", &[], )?; - let internal = env.new_global_ref(obj)?; + let internal = env.new_global_ref(&obj)?; let adapter = Self { manager: Arc::new(AdapterManager::default()), internal, }; - unsafe { env.set_rust_field(obj, "handle", adapter.clone()) }?; + unsafe { env.set_rust_field(&obj, "handle", adapter.clone()) }?; Ok(adapter) } - pub fn report_scan_result(&self, scan_result: JObject) -> Result { - use std::convert::TryInto; - - let env = global_jvm().get_env()?; - let scan_result = JScanResult::from_env(&env, scan_result)?; - - let (addr, properties): (BDAddr, Option) = scan_result.try_into()?; + pub fn report_scan_result( + &self, + env: &mut JNIEnv, + scan_result: JObject, + ) -> Result { + let scan_result = JScanResult::from_env(env, scan_result)?; + let (addr, properties): (BDAddr, Option) = + scan_result.to_peripheral_properties(env)?; match self.manager.peripheral(&PeripheralId(addr)) { Some(p) => match properties { @@ -74,10 +72,7 @@ impl Adapter { self.report_properties(&p, properties, false); Ok(p) } - None => { - //self.manager.emit(CentralEvent::DeviceDisconnected(addr)); - Err(Error::DeviceNotFound) - } + None => Err(Error::DeviceNotFound), }, None => match properties { Some(properties) => { @@ -91,8 +86,9 @@ impl Adapter { } fn add(&self, address: BDAddr) -> Result { - let env = global_jvm().get_env()?; - let peripheral = Peripheral::new(&env, self.internal.as_obj(), address)?; + let mut env = global_jvm().get_env()?; + let local_adapter = env.new_local_ref(&self.internal)?; + let peripheral = Peripheral::new(&mut env, local_adapter, address)?; self.manager.add_peripheral(peripheral.clone()); Ok(peripheral) } @@ -130,7 +126,6 @@ impl Central for Adapter { type Peripheral = Peripheral; async fn adapter_info(&self) -> Result { - // TODO: Get information about the adapter. Ok("Android".to_string()) } @@ -139,39 +134,45 @@ impl Central for Adapter { } async fn start_scan(&self, filter: ScanFilter) -> Result<()> { - let env = global_jvm().get_env()?; - let filter = JScanFilter::new(&env, filter)?; - try_block(&env, || { - env.call_method( - &self.internal, - "startScan", - "(Lcom/nonpolynomial/btleplug/android/impl/ScanFilter;)V", - &[filter.into()], - )?; - Ok(Ok(())) - }) - .catch( - JClass::from( - super::jni_utils::classcache::get_class( + let mut env = global_jvm().get_env()?; + let filter = JScanFilter::new(&mut env, filter)?; + let filter_obj: JObject = filter.into(); + match env.call_method( + &self.internal, + "startScan", + "(Lcom/nonpolynomial/btleplug/android/impl/ScanFilter;)V", + &[(&filter_obj).into()], + ) { + Ok(_) => Ok(()), + Err(jni::errors::Error::JavaException) => { + let ex = env.exception_occurred()?; + env.exception_clear()?; + + let no_adapter_class = super::jni_utils::classcache::get_class( "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", ) - .unwrap() - .as_obj(), - ), - |_ex| Ok(Err(Error::NoAdapterAvailable)), - ) - .catch("java/lang/RuntimeException", |ex| { - let msg = env - .call_method(ex, "getMessage", "()Ljava/lang/String;", &[])? - .l()?; - let msgstr: String = env.get_string(msg.into())?.into(); - Ok(Err(Error::RuntimeError(msgstr))) - }) - .result()? + .unwrap(); + + if env.is_instance_of(&ex, <&JClass>::from(no_adapter_class.as_obj()))? { + Err(Error::NoAdapterAvailable) + } else if env.is_instance_of(&ex, "java/lang/RuntimeException")? { + let msg = env + .call_method(&ex, "getMessage", "()Ljava/lang/String;", &[])? + .l()?; + let jstr: JString = msg.into(); + let msgstr: String = env.get_string(&jstr)?.into(); + Err(Error::RuntimeError(msgstr)) + } else { + env.throw(&ex)?; + Err(jni::errors::Error::JavaException.into()) + } + } + Err(e) => Err(e.into()), + } } async fn stop_scan(&self) -> Result<()> { - let env = global_jvm().get_env()?; + let mut env = global_jvm().get_env()?; env.call_method(&self.internal, "stopScan", "()V", &[])?; Ok(()) } @@ -201,23 +202,25 @@ impl Central for Adapter { } pub(crate) fn adapter_report_scan_result_internal( - env: &JNIEnv, - obj: JObject, + env: &mut JNIEnv, + obj: &JObject, scan_result: JObject, ) -> crate::Result<()> { let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, "handle") }?; - adapter.report_scan_result(scan_result)?; + let adapter_clone = adapter.clone(); + drop(adapter); + adapter_clone.report_scan_result(env, scan_result)?; Ok(()) } pub(crate) fn adapter_on_connection_state_changed_internal( - env: &JNIEnv, - obj: JObject, + env: &mut JNIEnv, + obj: &JObject, addr: JString, connected: jboolean, ) -> crate::Result<()> { let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, "handle") }?; - let addr_str = JavaStr::from_env(env, addr)?; + let addr_str = env.get_string(&addr)?; let addr_str = addr_str.to_str().map_err(|e| Error::Other(e.into()))?; let addr = BDAddr::from_str(addr_str)?; adapter.manager.emit(if connected != 0 { diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index cafe87ad..a5368b73 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -7,10 +7,12 @@ use std::ffi::c_void; static GLOBAL_JVM: OnceCell = OnceCell::new(); -pub fn init(env: &JNIEnv) -> crate::Result<()> { +pub fn init(env: &mut JNIEnv) -> crate::Result<()> { if let Ok(()) = GLOBAL_JVM.set(env.get_java_vm()?) { + let adapter_class = + env.find_class("com/nonpolynomial/btleplug/android/impl/Adapter")?; env.register_native_methods( - "com/nonpolynomial/btleplug/android/impl/Adapter", + &adapter_class, &[ NativeMethod { name: "reportScanResult".into(), @@ -97,8 +99,7 @@ pub fn init(env: &JNIEnv) -> crate::Result<()> { )?; // FnAdapter native method registration - let fn_adapter_class = - env.auto_local(env.find_class("io/github/gedgygedgy/rust/ops/FnAdapter")?); + let fn_adapter_class = env.find_class("io/github/gedgygedgy/rust/ops/FnAdapter")?; env.register_native_methods( &fn_adapter_class, &[ @@ -132,16 +133,17 @@ impl From<::jni::errors::Error> for crate::Error { } } -extern "C" fn adapter_report_scan_result(env: JNIEnv, obj: JObject, scan_result: JObject) { - let _ = super::adapter::adapter_report_scan_result_internal(&env, obj, scan_result); +extern "C" fn adapter_report_scan_result(mut env: JNIEnv, obj: JObject, scan_result: JObject) { + let _ = super::adapter::adapter_report_scan_result_internal(&mut env, &obj, scan_result); } extern "C" fn adapter_on_connection_state_changed( - env: JNIEnv, + mut env: JNIEnv, obj: JObject, addr: JString, connected: jboolean, ) { - let _ = - super::adapter::adapter_on_connection_state_changed_internal(&env, obj, addr, connected); + let _ = super::adapter::adapter_on_connection_state_changed_internal( + &mut env, &obj, addr, connected, + ); } diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 839d608e..f1b264a6 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -2,17 +2,16 @@ use crate::droidplug::jni_utils::{future::JFuture, stream::JStream, uuid::JUuid} use jni::{ JNIEnv, errors::Result, - objects::{JClass, JList, JMap, JMethodID, JObject, JString, JValue}, + objects::{JClass, JMethodID, JObject, JString}, signature::{Primitive, ReturnType}, - strings::JavaStr, - sys::jint, + sys::{jint, jvalue}, }; -use std::{collections::HashMap, convert::TryFrom, iter::Iterator}; +use std::{collections::HashMap, iter::Iterator}; use uuid::Uuid; use crate::api::{BDAddr, CharPropFlags, PeripheralProperties, ScanFilter}; -pub struct JPeripheral<'a: 'b, 'b> { +pub struct JPeripheral<'a> { internal: JObject<'a>, connect: JMethodID, disconnect: JMethodID, @@ -29,10 +28,9 @@ pub struct JPeripheral<'a: 'b, 'b> { get_connection_parameters: JMethodID, request_connection_priority: JMethodID, read_remote_rssi: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> ::std::ops::Deref for JPeripheral<'a, 'b> { +impl<'a> ::std::ops::Deref for JPeripheral<'a> { type Target = JObject<'a>; fn deref(&self) -> &Self::Target { @@ -40,27 +38,19 @@ impl<'a: 'b, 'b> ::std::ops::Deref for JPeripheral<'a, 'b> { } } -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(other: JPeripheral<'a, 'b>) -> JObject<'a> { +impl<'a> From> for JObject<'a> { + fn from(other: JPeripheral<'a>) -> JObject<'a> { other.internal } } -impl<'a: 'b, 'b> JPeripheral<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - //Self::from_env_impl(env, obj) - //let class = env.find_class("com/nonpolynomial/btleplug/android/impl/Peripheral")?; - //Self::from_env_impl(env, obj, class) - Self::from_env_impl(env, obj) - } - - fn from_env_impl(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - //let class = env.auto_local(class); +impl<'a> JPeripheral<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { let class_static = crate::droidplug::jni_utils::classcache::get_class( "com/nonpolynomial/btleplug/android/impl/Peripheral", ) .unwrap(); - let class = JClass::from(class_static.as_obj()); + let class = <&JClass>::from(class_static.as_obj()); let connect = env.get_method_id( class, @@ -140,200 +130,223 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { get_connection_parameters, request_connection_priority, read_remote_rssi, - env, }) } - pub fn new(env: &'b JNIEnv<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { - // let class = env.find_class("com/nonpolynomial/btleplug/android/impl/Peripheral")?; + pub fn new(env: &mut JNIEnv<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { let addr_jstr = env.new_string(format!("{:X}", addr))?; + let class_static = crate::droidplug::jni_utils::classcache::get_class( + "com/nonpolynomial/btleplug/android/impl/Peripheral", + ) + .unwrap(); let obj = env.new_object( - JClass::from( - crate::droidplug::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/Peripheral", - ) - .unwrap() - .as_obj(), - ), - //class.as_obj(), + <&JClass>::from(class_static.as_obj()), "(Lcom/nonpolynomial/btleplug/android/impl/Adapter;Ljava/lang/String;)V", - &[adapter.into(), addr_jstr.into()], + &[(&adapter).into(), (&addr_jstr).into()], )?; - //Self::from_env_impl(env, obj, class) - Self::from_env_impl(env, obj) + Self::from_env(env, obj) } - pub fn connect(&self) -> Result> { - let future_obj = self - .env - .call_method_unchecked(self.internal, self.connect, ReturnType::Object, &[])? - .l()?; - JFuture::from_env(self.env, future_obj) + pub fn connect(&self, env: &mut JNIEnv<'a>) -> Result> { + let future_obj = unsafe { + env.call_method_unchecked(&self.internal, self.connect, ReturnType::Object, &[]) + }? + .l()?; + JFuture::from_env(env, future_obj) } - pub fn disconnect(&self) -> Result> { - let future_obj = self - .env - .call_method_unchecked(self.internal, self.disconnect, ReturnType::Object, &[])? - .l()?; - JFuture::from_env(self.env, future_obj) + pub fn disconnect(&self, env: &mut JNIEnv<'a>) -> Result> { + let future_obj = unsafe { + env.call_method_unchecked(&self.internal, self.disconnect, ReturnType::Object, &[]) + }? + .l()?; + JFuture::from_env(env, future_obj) } - pub fn is_connected(&self) -> Result { - self.env - .call_method_unchecked( - self.internal, + pub fn is_connected(&self, env: &mut JNIEnv<'a>) -> Result { + unsafe { + env.call_method_unchecked( + &self.internal, self.is_connected, ReturnType::Primitive(Primitive::Boolean), &[], - )? - .z() + ) + }? + .z() } - pub fn discover_services(&self) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, + pub fn discover_services(&self, env: &mut JNIEnv<'a>) -> Result> { + let future_obj = unsafe { + env.call_method_unchecked( + &self.internal, self.discover_services, ReturnType::Object, &[], - )? - .l()?; - JFuture::from_env(self.env, future_obj) + ) + }? + .l()?; + JFuture::from_env(env, future_obj) } - pub fn read(&self, uuid: JUuid<'a, 'b>) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, + pub fn read(&self, env: &mut JNIEnv<'a>, uuid: &JUuid<'a>) -> Result> { + let future_obj = unsafe { + env.call_method_unchecked( + &self.internal, self.read, ReturnType::Object, - &[JValue::from(uuid).to_jni()], - )? - .l()?; - JFuture::from_env(self.env, future_obj) + &[jvalue { + l: uuid.as_obj().as_raw(), + }], + ) + }? + .l()?; + JFuture::from_env(env, future_obj) } pub fn write( &self, - uuid: JUuid<'a, 'b>, - data: JObject<'a>, + env: &mut JNIEnv<'a>, + uuid: &JUuid<'a>, + data: &JObject<'a>, write_type: jint, - ) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, + ) -> Result> { + let future_obj = unsafe { + env.call_method_unchecked( + &self.internal, self.write, ReturnType::Object, &[ - JValue::from(uuid).to_jni(), - JValue::from(data).to_jni(), - JValue::from(write_type).to_jni(), + jvalue { + l: uuid.as_obj().as_raw(), + }, + jvalue { + l: data.as_raw(), + }, + jvalue { i: write_type }, ], - )? - .l()?; - JFuture::from_env(self.env, future_obj) + ) + }? + .l()?; + JFuture::from_env(env, future_obj) } pub fn set_characteristic_notification( &self, - uuid: JUuid<'a, 'b>, + env: &mut JNIEnv<'a>, + uuid: &JUuid<'a>, enable: bool, - ) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, + ) -> Result> { + let future_obj = unsafe { + env.call_method_unchecked( + &self.internal, self.set_characteristic_notification, ReturnType::Object, - &[JValue::from(uuid).to_jni(), JValue::from(enable).to_jni()], - )? - .l()?; - JFuture::from_env(self.env, future_obj) + &[ + jvalue { + l: uuid.as_obj().as_raw(), + }, + jvalue { + z: enable as u8, + }, + ], + ) + }? + .l()?; + JFuture::from_env(env, future_obj) } - pub fn get_notifications(&self) -> Result> { - let stream_obj = self - .env - .call_method_unchecked( - self.internal, + pub fn get_notifications(&self, env: &mut JNIEnv<'a>) -> Result> { + let stream_obj = unsafe { + env.call_method_unchecked( + &self.internal, self.get_notifications, ReturnType::Object, &[], - )? - .l()?; - JStream::from_env(self.env, stream_obj) + ) + }? + .l()?; + JStream::from_env(env, stream_obj) } pub fn read_descriptor( &self, - characteristic: JUuid<'a, 'b>, - uuid: JUuid<'a, 'b>, - ) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, + env: &mut JNIEnv<'a>, + characteristic: &JUuid<'a>, + uuid: &JUuid<'a>, + ) -> Result> { + let future_obj = unsafe { + env.call_method_unchecked( + &self.internal, self.read_descriptor, ReturnType::Object, &[ - JValue::from(characteristic).to_jni(), - JValue::from(uuid).to_jni(), + jvalue { + l: characteristic.as_obj().as_raw(), + }, + jvalue { + l: uuid.as_obj().as_raw(), + }, ], - )? - .l()?; - JFuture::from_env(self.env, future_obj) + ) + }? + .l()?; + JFuture::from_env(env, future_obj) } - pub fn get_device_name(&self) -> Result> { - let obj = self - .env - .call_method_unchecked(self.internal, self.get_device_name, ReturnType::Object, &[])? - .l()?; + pub fn get_device_name(&self, env: &mut JNIEnv<'a>) -> Result> { + let obj = unsafe { + env.call_method_unchecked( + &self.internal, + self.get_device_name, + ReturnType::Object, + &[], + ) + }? + .l()?; if obj.is_null() { Ok(None) } else { - let name_str = self.env.get_string(obj.into())?; + let jstr: JString = obj.into(); + let name_str = env.get_string(&jstr)?; Ok(Some(name_str.into())) } } - pub fn request_mtu(&self, mtu: jint) -> Result> { - self.env - .call_method_unchecked( - self.internal, + pub fn request_mtu(&self, env: &mut JNIEnv<'a>, mtu: jint) -> Result> { + unsafe { + env.call_method_unchecked( + &self.internal, self.request_mtu, ReturnType::Object, - &[JValue::from(mtu).to_jni()], - )? - .l() + &[jvalue { i: mtu }], + ) + }? + .l() } - pub fn get_connection_parameters(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, + pub fn get_connection_parameters( + &self, + env: &mut JNIEnv<'a>, + ) -> Result> { + let obj = unsafe { + env.call_method_unchecked( + &self.internal, self.get_connection_parameters, ReturnType::Array, &[], - )? - .l()?; + ) + }? + .l()?; if obj.is_null() { return Ok(None); } - let arr = obj.into_raw(); - let len = self.env.get_array_length(arr)?; + let arr = unsafe { jni::objects::JIntArray::from_raw(obj.into_raw()) }; + let len = env.get_array_length(&arr)?; if len < 3 { return Ok(None); } let mut buf = [0i32; 3]; - self.env.get_int_array_region(arr, 0, &mut buf)?; - // interval is in 1.25ms units → microseconds: × 1250 - // timeout is in 10ms units → microseconds: × 10000 + env.get_int_array_region(&arr, 0, &mut buf)?; Ok(Some(crate::api::ConnectionParameters { interval_us: (buf[0] as u32) * 1250, latency: buf[1] as u16, @@ -341,131 +354,133 @@ impl<'a: 'b, 'b> JPeripheral<'a, 'b> { })) } - pub fn read_remote_rssi(&self) -> Result> { - self.env - .call_method_unchecked( - self.internal, + pub fn read_remote_rssi(&self, env: &mut JNIEnv<'a>) -> Result> { + unsafe { + env.call_method_unchecked( + &self.internal, self.read_remote_rssi, ReturnType::Object, &[], - )? - .l() + ) + }? + .l() } - pub fn request_connection_priority(&self, priority: jint) -> Result { - self.env - .call_method_unchecked( - self.internal, + pub fn request_connection_priority( + &self, + env: &mut JNIEnv<'a>, + priority: jint, + ) -> Result { + unsafe { + env.call_method_unchecked( + &self.internal, self.request_connection_priority, ReturnType::Primitive(Primitive::Boolean), - &[JValue::from(priority).to_jni()], - )? - .z() + &[jvalue { i: priority }], + ) + }? + .z() } pub fn write_descriptor( &self, - characteristic: JUuid<'a, 'b>, - uuid: JUuid<'a, 'b>, - data: JObject<'a>, - ) -> Result> { - let future_obj = self - .env - .call_method_unchecked( - self.internal, + env: &mut JNIEnv<'a>, + characteristic: &JUuid<'a>, + uuid: &JUuid<'a>, + data: &JObject<'a>, + ) -> Result> { + let future_obj = unsafe { + env.call_method_unchecked( + &self.internal, self.write_descriptor, ReturnType::Object, &[ - JValue::from(characteristic).to_jni(), - JValue::from(uuid).to_jni(), - JValue::from(data).to_jni(), + jvalue { + l: characteristic.as_obj().as_raw(), + }, + jvalue { + l: uuid.as_obj().as_raw(), + }, + jvalue { + l: data.as_raw(), + }, ], - )? - .l()?; - JFuture::from_env(self.env, future_obj) + ) + }? + .l()?; + JFuture::from_env(env, future_obj) } } -pub struct JBluetoothGattService<'a: 'b, 'b> { +pub struct JBluetoothGattService<'a> { internal: JObject<'a>, get_uuid: JMethodID, - //is_primary: JMethodID, get_characteristics: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JBluetoothGattService<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/bluetooth/BluetoothGattService")?); +impl<'a> JBluetoothGattService<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = env.find_class("android/bluetooth/BluetoothGattService")?; let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; - //let is_primary = env.get_method_id(&class, "isPrimary", "()Z;")?; let get_characteristics = env.get_method_id(&class, "getCharacteristics", "()Ljava/util/List;")?; Ok(Self { internal: obj, get_uuid, - //is_primary, get_characteristics, - env, }) } pub fn is_primary(&self) -> Result { - /* - self.env - .call_method_unchecked( - self.internal, - self.is_primary, - ReturnType::Primitive(Primitive::Boolean), - &[], - )? - .z() - */ Ok(true) } - pub fn get_uuid(&self) -> Result { - let obj = self - .env - .call_method_unchecked(self.internal, self.get_uuid, ReturnType::Object, &[])? - .l()?; - let uuid_obj = JUuid::from_env(self.env, obj)?; - Ok(uuid_obj.as_uuid()?) + pub fn get_uuid(&self, env: &mut JNIEnv<'a>) -> Result { + let obj = unsafe { + env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) + }? + .l()?; + let uuid_obj = JUuid::from_env(env, obj)?; + uuid_obj.as_uuid(env) } - pub fn get_characteristics(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, + pub fn get_characteristics( + &self, + env: &mut JNIEnv<'a>, + ) -> Result>> { + let obj = unsafe { + env.call_method_unchecked( + &self.internal, self.get_characteristics, ReturnType::Object, &[], - )? - .l()?; - let chr_list = JList::from_env(self.env, obj)?; - let mut chr_vec = vec![]; - for chr in chr_list.iter()? { - chr_vec.push(JBluetoothGattCharacteristic::from_env(self.env, chr)?); + ) + }? + .l()?; + let size = env.call_method(&obj, "size", "()I", &[])?.i()?; + let mut chr_vec = Vec::with_capacity(size as usize); + for i in 0..size { + let chr = env + .call_method(&obj, "get", "(I)Ljava/lang/Object;", &[jni::objects::JValue::from(i)])? + .l()?; + chr_vec.push(JBluetoothGattCharacteristic::from_env(env, chr)?); } Ok(chr_vec) } } -pub struct JBluetoothGattCharacteristic<'a: 'b, 'b> { +pub struct JBluetoothGattCharacteristic<'a> { internal: JObject<'a>, get_uuid: JMethodID, get_properties: JMethodID, get_value: JMethodID, get_descriptors: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JBluetoothGattCharacteristic<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = - env.auto_local(env.find_class("android/bluetooth/BluetoothGattCharacteristic")?); +impl<'a> JBluetoothGattCharacteristic<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = env.find_class("android/bluetooth/BluetoothGattCharacteristic")?; let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; let get_properties = env.get_method_id(&class, "getProperties", "()I")?; @@ -477,105 +492,112 @@ impl<'a: 'b, 'b> JBluetoothGattCharacteristic<'a, 'b> { get_properties, get_value, get_descriptors, - env, }) } - pub fn get_uuid(&self) -> Result { - let obj = self - .env - .call_method_unchecked(self.internal, self.get_uuid, ReturnType::Object, &[])? - .l()?; - let uuid_obj = JUuid::from_env(self.env, obj)?; - Ok(uuid_obj.as_uuid()?) + pub fn get_uuid(&self, env: &mut JNIEnv<'a>) -> Result { + let obj = unsafe { + env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) + }? + .l()?; + let uuid_obj = JUuid::from_env(env, obj)?; + uuid_obj.as_uuid(env) } - pub fn get_properties(&self) -> Result { - let flags = self - .env - .call_method_unchecked( - self.internal, + pub fn get_properties(&self, env: &mut JNIEnv<'a>) -> Result { + let flags = unsafe { + env.call_method_unchecked( + &self.internal, self.get_properties, ReturnType::Primitive(Primitive::Int), &[], - )? - .i()?; + ) + }? + .i()?; Ok(CharPropFlags::from_bits_truncate(flags as u8)) } - pub fn get_value(&self) -> Result> { - let value = self - .env - .call_method_unchecked(self.internal, self.get_value, ReturnType::Array, &[])? - .l()?; - crate::droidplug::jni_utils::arrays::byte_array_to_vec(self.env, value.into_raw()) + pub fn get_value(&self, env: &mut JNIEnv<'a>) -> Result> { + let value = unsafe { + env.call_method_unchecked(&self.internal, self.get_value, ReturnType::Array, &[]) + }? + .l()?; + let value_arr = unsafe { jni::objects::JByteArray::from_raw(value.into_raw()) }; + crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value_arr) } - pub fn get_descriptors(&self) -> Result> { - let obj = self - .env - .call_method_unchecked(self.internal, self.get_descriptors, ReturnType::Object, &[])? - .l()?; - let desc_list = JList::from_env(self.env, obj)?; - let mut desc_vec = vec![]; - for desc in desc_list.iter()? { - desc_vec.push(JBluetoothGattDescriptor::from_env(self.env, desc)?); + pub fn get_descriptors( + &self, + env: &mut JNIEnv<'a>, + ) -> Result>> { + let obj = unsafe { + env.call_method_unchecked( + &self.internal, + self.get_descriptors, + ReturnType::Object, + &[], + ) + }? + .l()?; + let size = env.call_method(&obj, "size", "()I", &[])?.i()?; + let mut desc_vec = Vec::with_capacity(size as usize); + for i in 0..size { + let desc = env + .call_method(&obj, "get", "(I)Ljava/lang/Object;", &[jni::objects::JValue::from(i)])? + .l()?; + desc_vec.push(JBluetoothGattDescriptor::from_env(env, desc)?); } Ok(desc_vec) } } -pub struct JBluetoothGattDescriptor<'a: 'b, 'b> { +pub struct JBluetoothGattDescriptor<'a> { internal: JObject<'a>, get_uuid: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JBluetoothGattDescriptor<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/bluetooth/BluetoothGattDescriptor")?); +impl<'a> JBluetoothGattDescriptor<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = env.find_class("android/bluetooth/BluetoothGattDescriptor")?; let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; Ok(Self { internal: obj, get_uuid, - env, }) } - pub fn get_uuid(&self) -> Result { - let obj = self - .env - .call_method_unchecked(self.internal, self.get_uuid, ReturnType::Object, &[])? - .l()?; - let uuid_obj = JUuid::from_env(self.env, obj)?; - Ok(uuid_obj.as_uuid()?) + pub fn get_uuid(&self, env: &mut JNIEnv<'a>) -> Result { + let obj = unsafe { + env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) + }? + .l()?; + let uuid_obj = JUuid::from_env(env, obj)?; + uuid_obj.as_uuid(env) } } -pub struct JBluetoothDevice<'a: 'b, 'b> { +pub struct JBluetoothDevice<'a> { internal: JObject<'a>, get_address: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JBluetoothDevice<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/bluetooth/BluetoothDevice")?); +impl<'a> JBluetoothDevice<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = env.find_class("android/bluetooth/BluetoothDevice")?; let get_address = env.get_method_id(&class, "getAddress", "()Ljava/lang/String;")?; Ok(Self { internal: obj, get_address, - env, }) } - pub fn get_address(&self) -> Result> { - let obj = self - .env - .call_method_unchecked(self.internal, self.get_address, ReturnType::Object, &[])? - .l()?; + pub fn get_address(&self, env: &mut JNIEnv<'a>) -> Result> { + let obj = unsafe { + env.call_method_unchecked(&self.internal, self.get_address, ReturnType::Object, &[]) + }? + .l()?; Ok(obj.into()) } } @@ -585,27 +607,25 @@ pub struct JScanFilter<'a> { } impl<'a> JScanFilter<'a> { - pub fn new(env: &'a JNIEnv<'a>, filter: ScanFilter) -> Result { + pub fn new(env: &mut JNIEnv<'a>, filter: ScanFilter) -> Result { + let string_class = env.find_class("java/lang/String")?; let uuids = env.new_object_array( filter.services.len() as i32, - env.find_class("java/lang/String")?, - JObject::null(), + &string_class, + &JObject::null(), )?; for (idx, uuid) in filter.services.into_iter().enumerate() { let uuid_str = env.new_string(uuid.to_string())?; - env.set_object_array_element(uuids, idx as i32, uuid_str)?; + env.set_object_array_element(&uuids, idx as i32, &uuid_str)?; } + let class_static = crate::droidplug::jni_utils::classcache::get_class( + "com/nonpolynomial/btleplug/android/impl/ScanFilter", + ) + .unwrap(); let obj = env.new_object( - JClass::from( - crate::droidplug::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/ScanFilter", - ) - .unwrap() - .as_obj(), - ), - //class.as_obj(), + <&JClass>::from(class_static.as_obj()), "([Ljava/lang/String;)V", - &[uuids.into()], + &[(&uuids).into()], )?; Ok(Self { internal: obj }) } @@ -617,18 +637,17 @@ impl<'a> From> for JObject<'a> { } } -pub struct JScanResult<'a: 'b, 'b> { +pub struct JScanResult<'a> { internal: JObject<'a>, get_device: JMethodID, get_scan_record: JMethodID, get_tx_power: JMethodID, get_rssi: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JScanResult<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/bluetooth/le/ScanResult")?); +impl<'a> JScanResult<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = env.find_class("android/bluetooth/le/ScanResult")?; let get_device = env.get_method_id(&class, "getDevice", "()Landroid/bluetooth/BluetoothDevice;")?; @@ -645,86 +664,80 @@ impl<'a: 'b, 'b> JScanResult<'a, 'b> { get_scan_record, get_tx_power, get_rssi, - env, }) } - pub fn get_device(&self) -> Result> { - let obj = self - .env - .call_method_unchecked(self.internal, self.get_device, ReturnType::Object, &[])? - .l()?; - JBluetoothDevice::from_env(self.env, obj) + pub fn get_device(&self, env: &mut JNIEnv<'a>) -> Result> { + let obj = unsafe { + env.call_method_unchecked(&self.internal, self.get_device, ReturnType::Object, &[]) + }? + .l()?; + JBluetoothDevice::from_env(env, obj) } - pub fn get_scan_record(&self) -> Result> { - let obj = self - .env - .call_method_unchecked(self.internal, self.get_scan_record, ReturnType::Object, &[])? - .l()?; - JScanRecord::from_env(self.env, obj) + pub fn get_scan_record(&self, env: &mut JNIEnv<'a>) -> Result> { + let obj = unsafe { + env.call_method_unchecked( + &self.internal, + self.get_scan_record, + ReturnType::Object, + &[], + ) + }? + .l()?; + JScanRecord::from_env(env, obj) } - pub fn get_tx_power(&self) -> Result { - self.env - .call_method_unchecked( - self.internal, + pub fn get_tx_power(&self, env: &mut JNIEnv<'a>) -> Result { + unsafe { + env.call_method_unchecked( + &self.internal, self.get_tx_power, ReturnType::Primitive(Primitive::Int), &[], - )? - .i() + ) + }? + .i() } - pub fn get_rssi(&self) -> Result { - self.env - .call_method_unchecked( - self.internal, + pub fn get_rssi(&self, env: &mut JNIEnv<'a>) -> Result { + unsafe { + env.call_method_unchecked( + &self.internal, self.get_rssi, ReturnType::Primitive(Primitive::Int), &[], - )? - .i() + ) + }? + .i() } -} -impl<'a: 'b, 'b> TryFrom> for (BDAddr, Option) { - type Error = crate::Error; - - fn try_from(result: JScanResult<'a, 'b>) -> std::result::Result { + pub fn to_peripheral_properties( + &self, + env: &mut JNIEnv<'a>, + ) -> std::result::Result<(BDAddr, Option), crate::Error> { use std::str::FromStr; - let device = result.get_device()?; - - let addr_obj = device.get_address()?; - let addr_str = JavaStr::from_env(result.env, addr_obj)?; + let device = self.get_device(env)?; + let addr_jstr = device.get_address(env)?; + let addr_str = env.get_string(&addr_jstr)?; let addr = BDAddr::from_str( addr_str .to_str() - .map_err(|e| Self::Error::Other(e.into()))?, + .map_err(|e| crate::Error::Other(e.into()))?, )?; - let record = result.get_scan_record()?; - let record_obj: &JObject = &record; - let properties = if result - .env - .is_same_object(record_obj.clone(), JObject::null())? - { + let record = self.get_scan_record(env)?; + let record_is_null = env.is_same_object(&*record, JObject::null())?; + let properties = if record_is_null { None } else { - let device_name_obj = record.get_device_name()?; - let device_name = if result - .env - .is_same_object(device_name_obj, JObject::null())? - { + let device_name_obj = record.get_device_name(env)?; + let device_name = if env.is_same_object(&device_name_obj, JObject::null())? { None } else { - let device_name_str = JavaStr::from_env(result.env, device_name_obj)?; - // On Android, there is a chance that a device name may not actually be valid UTF-8. - // We're given the full buffer, regardless of if it's just UTF-8 characters, - // possibly c str with null characters, or whatever. We should try UTF-8 first, if - // that doesn't work out, see if there's a null termination character in it and try - // parsing that. + let device_name_jstr: JString = device_name_obj.into(); + let device_name_str = env.get_string(&device_name_jstr)?; Some( String::from_utf8_lossy(device_name_str.to_bytes()) .chars() @@ -733,65 +746,87 @@ impl<'a: 'b, 'b> TryFrom> for (BDAddr, Option TryFrom> for (BDAddr, Option { +pub struct JScanRecord<'a> { internal: JObject<'a>, get_device_name: JMethodID, get_tx_power_level: JMethodID, get_manufacturer_specific_data: JMethodID, get_service_data: JMethodID, get_service_uuids: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(scan_record: JScanRecord<'a, 'b>) -> Self { +impl<'a> From> for JObject<'a> { + fn from(scan_record: JScanRecord<'a>) -> Self { scan_record.internal } } -impl<'a: 'b, 'b> ::std::ops::Deref for JScanRecord<'a, 'b> { +impl<'a> ::std::ops::Deref for JScanRecord<'a> { type Target = JObject<'a>; fn deref(&self) -> &Self::Target { @@ -837,9 +871,9 @@ impl<'a: 'b, 'b> ::std::ops::Deref for JScanRecord<'a, 'b> { } } -impl<'a: 'b, 'b> JScanRecord<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/bluetooth/le/ScanRecord")?); +impl<'a> JScanRecord<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = env.find_class("android/bluetooth/le/ScanRecord")?; let get_device_name = env.get_method_id(&class, "getDeviceName", "()Ljava/lang/String;")?; let get_tx_power_level = env.get_method_id(&class, "getTxPowerLevel", "()I")?; @@ -858,85 +892,88 @@ impl<'a: 'b, 'b> JScanRecord<'a, 'b> { get_manufacturer_specific_data, get_service_data, get_service_uuids, - env, }) } - pub fn get_device_name(&self) -> Result> { - let obj = self - .env - .call_method_unchecked(self.internal, self.get_device_name, ReturnType::Object, &[])? - .l()?; - Ok(obj.into()) + pub fn get_device_name(&self, env: &mut JNIEnv<'a>) -> Result> { + unsafe { + env.call_method_unchecked( + &self.internal, + self.get_device_name, + ReturnType::Object, + &[], + ) + }? + .l() } - pub fn get_tx_power_level(&self) -> Result { - self.env - .call_method_unchecked( - self.internal, + pub fn get_tx_power_level(&self, env: &mut JNIEnv<'a>) -> Result { + unsafe { + env.call_method_unchecked( + &self.internal, self.get_tx_power_level, ReturnType::Primitive(Primitive::Int), &[], - )? - .i() + ) + }? + .i() } - pub fn get_manufacturer_specific_data(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, + pub fn get_manufacturer_specific_data( + &self, + env: &mut JNIEnv<'a>, + ) -> Result> { + let obj = unsafe { + env.call_method_unchecked( + &self.internal, self.get_manufacturer_specific_data, ReturnType::Object, &[], - )? - .l()?; - JSparseArray::from_env(self.env, obj) + ) + }? + .l()?; + JSparseArray::from_env(env, obj) } - pub fn get_service_data(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, + pub fn get_service_data(&self, env: &mut JNIEnv<'a>) -> Result> { + unsafe { + env.call_method_unchecked( + &self.internal, self.get_service_data, ReturnType::Object, &[], - )? - .l()?; - JMap::from_env(self.env, obj) + ) + }? + .l() } - pub fn get_service_uuids(&self) -> Result> { - let obj = self - .env - .call_method_unchecked( - self.internal, + pub fn get_service_uuids(&self, env: &mut JNIEnv<'a>) -> Result> { + unsafe { + env.call_method_unchecked( + &self.internal, self.get_service_uuids, ReturnType::Object, &[], - )? - .l()?; - JList::from_env(self.env, obj) + ) + }? + .l() } } -#[derive(Clone)] -pub struct JSparseArray<'a: 'b, 'b> { +pub struct JSparseArray<'a> { internal: JObject<'a>, size: JMethodID, key_at: JMethodID, value_at: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(sparse_array: JSparseArray<'a, 'b>) -> Self { +impl<'a> From> for JObject<'a> { + fn from(sparse_array: JSparseArray<'a>) -> Self { sparse_array.internal } } -impl<'a: 'b, 'b> ::std::ops::Deref for JSparseArray<'a, 'b> { +impl<'a> ::std::ops::Deref for JSparseArray<'a> { type Target = JObject<'a>; fn deref(&self) -> &Self::Target { @@ -944,9 +981,9 @@ impl<'a: 'b, 'b> ::std::ops::Deref for JSparseArray<'a, 'b> { } } -impl<'a: 'b, 'b> JSparseArray<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/util/SparseArray")?); +impl<'a> JSparseArray<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = env.find_class("android/util/SparseArray")?; let size = env.get_method_id(&class, "size", "()I")?; let key_at = env.get_method_id(&class, "keyAt", "(I)I")?; @@ -956,100 +993,67 @@ impl<'a: 'b, 'b> JSparseArray<'a, 'b> { size, key_at, value_at, - env, }) } - pub fn size(&self) -> Result { - self.env - .call_method_unchecked( - self.internal, + pub fn size(&self, env: &mut JNIEnv<'a>) -> Result { + unsafe { + env.call_method_unchecked( + &self.internal, self.size, ReturnType::Primitive(Primitive::Int), &[], - )? - .i() + ) + }? + .i() } - pub fn key_at(&self, index: jint) -> Result { - self.env - .call_method_unchecked( - self.internal, + pub fn key_at(&self, env: &mut JNIEnv<'a>, index: jint) -> Result { + unsafe { + env.call_method_unchecked( + &self.internal, self.key_at, ReturnType::Primitive(Primitive::Int), - &[JValue::from(index).to_jni()], - )? - .i() + &[jvalue { i: index }], + ) + }? + .i() } - pub fn value_at(&self, index: jint) -> Result> { - self.env - .call_method_unchecked( - self.internal, + pub fn value_at(&self, env: &mut JNIEnv<'a>, index: jint) -> Result> { + unsafe { + env.call_method_unchecked( + &self.internal, self.value_at, ReturnType::Object, - &[JValue::from(index).to_jni()], - )? - .l() - } - - pub fn iter(&self) -> JSparseArrayIter<'a, 'b> { - JSparseArrayIter { - internal: self.clone(), - index: 0, - } - } -} - -pub struct JSparseArrayIter<'a: 'b, 'b> { - internal: JSparseArray<'a, 'b>, - index: jint, -} - -impl<'a: 'b, 'b> JSparseArrayIter<'a, 'b> { - fn next_internal(&mut self) -> Result)>> { - let size = self.internal.size()?; - Ok(if self.index >= size { - None - } else { - let key = self.internal.key_at(self.index)?; - let value = self.internal.value_at(self.index)?; - self.index += 1; - Some((key, value)) - }) + &[jvalue { i: index }], + ) + }? + .l() } } -impl<'a: 'b, 'b> Iterator for JSparseArrayIter<'a, 'b> { - type Item = Result<(jint, JObject<'a>)>; - - fn next(&mut self) -> Option { - self.next_internal().transpose() - } -} -pub struct JParcelUuid<'a: 'b, 'b> { +pub struct JParcelUuid<'a> { internal: JObject<'a>, get_uuid: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JParcelUuid<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("android/os/ParcelUuid")?); +impl<'a> JParcelUuid<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = env.find_class("android/os/ParcelUuid")?; let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; Ok(Self { internal: obj, get_uuid, - env, }) } - pub fn get_uuid(&self) -> Result> { - let obj = self - .env - .call_method_unchecked(self.internal, self.get_uuid, ReturnType::Object, &[])? - .l()?; - JUuid::from_env(self.env, obj) + pub fn get_uuid(&self, env: &mut JNIEnv<'a>) -> Result> { + let obj = unsafe { + env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) + }? + .l()?; + JUuid::from_env(env, obj) } } diff --git a/src/droidplug/jni_utils/arrays.rs b/src/droidplug/jni_utils/arrays.rs index e471fb9d..435e8777 100644 --- a/src/droidplug/jni_utils/arrays.rs +++ b/src/droidplug/jni_utils/arrays.rs @@ -1,25 +1,26 @@ use jni::{ JNIEnv, errors::Result, - sys::{jbyte, jbyteArray, jint}, + objects::JByteArray, + sys::{jbyte, jint}, }; use std::slice; /// Create a new Java byte array from the given slice. -pub fn slice_to_byte_array<'a, 'b>(env: &'a JNIEnv<'a>, slice: &'b [u8]) -> Result { +pub fn slice_to_byte_array<'local>(env: &mut JNIEnv<'local>, slice: &[u8]) -> Result> { let obj = env.new_byte_array(slice.len() as jint)?; let slice = unsafe { &*(slice as *const [u8] as *const [jbyte]) }; - env.set_byte_array_region(obj, 0, slice)?; + env.set_byte_array_region(&obj, 0, slice)?; Ok(obj) } /// Get a [`Vec`] of bytes from the given Java byte array. -pub fn byte_array_to_vec<'a>(env: &'a JNIEnv<'a>, obj: jbyteArray) -> Result> { - let size = env.get_array_length(obj)? as usize; +pub fn byte_array_to_vec(env: &JNIEnv, array: &JByteArray) -> Result> { + let size = env.get_array_length(array)? as usize; let mut result = Vec::with_capacity(size); unsafe { let result_slice = slice::from_raw_parts_mut(result.as_mut_ptr() as *mut jbyte, size); - env.get_byte_array_region(obj, 0, result_slice)?; + env.get_byte_array_region(array, 0, result_slice)?; result.set_len(size); } Ok(result) @@ -31,23 +32,26 @@ mod test { #[test] fn test_slice_to_byte_array() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); let obj = super::slice_to_byte_array(env, &[1, 2, 3, 4, 5]).unwrap(); - assert_eq!(env.get_array_length(obj).unwrap(), 5); + assert_eq!(env.get_array_length(&obj).unwrap(), 5); let mut bytes = [0i8; 5]; - env.get_byte_array_region(obj, 0, &mut bytes).unwrap(); + env.get_byte_array_region(&obj, 0, &mut bytes).unwrap(); assert_eq!(bytes, [1, 2, 3, 4, 5]); }); } #[test] fn test_byte_array_to_vec() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); let obj = env.new_byte_array(5).unwrap(); - env.set_byte_array_region(obj, 0, &[1, 2, 3, 4, 5]).unwrap(); + env.set_byte_array_region(&obj, 0, &[1, 2, 3, 4, 5]) + .unwrap(); - let vec = super::byte_array_to_vec(env, obj).unwrap(); + let vec = super::byte_array_to_vec(env, &obj).unwrap(); assert_eq!(vec, vec![1, 2, 3, 4, 5]); }); } diff --git a/src/droidplug/jni_utils/classcache.rs b/src/droidplug/jni_utils/classcache.rs index 3b0f63ff..f297e5d8 100644 --- a/src/droidplug/jni_utils/classcache.rs +++ b/src/droidplug/jni_utils/classcache.rs @@ -4,19 +4,15 @@ use once_cell::sync::OnceCell; static CLASSCACHE: OnceCell> = OnceCell::new(); -pub fn find_add_class(env: &JNIEnv, classname: &str) -> Result<()> { +pub fn find_add_class(env: &mut JNIEnv, classname: &str) -> Result<()> { let cache = CLASSCACHE.get_or_init(|| DashMap::new()); - cache.insert( - classname.to_owned(), - env.new_global_ref(env.find_class(classname).unwrap()) - .unwrap(), - ); + let cls = env.find_class(classname)?; + let global = env.new_global_ref(cls)?; + cache.insert(classname.to_owned(), global); Ok(()) } pub fn get_class(classname: &str) -> Option { let cache = CLASSCACHE.get_or_init(|| DashMap::new()); - cache - .get(classname) - .and_then(|pair| Some(pair.value().clone())) + cache.get(classname).map(|pair| pair.value().clone()) } diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index 6f394376..c397723b 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -6,15 +6,13 @@ use jni::{ }; use std::{ any::Any, - convert::TryFrom, panic::{UnwindSafe, catch_unwind, resume_unwind}, sync::MutexGuard, }; /// Result from [`try_block`]. This object can be chained into /// [`catch`](TryCatchResult::catch) calls to catch exceptions. -pub struct TryCatchResult<'a: 'b, 'b, T> { - env: &'b JNIEnv<'a>, +pub struct TryCatchResult { try_result: Result, Error>, catch_result: Option>, } @@ -22,68 +20,61 @@ pub struct TryCatchResult<'a: 'b, 'b, T> { /// Attempt to execute a block of JNI code. If the code causes an exception /// to be thrown, it will be stored in the resulting [`TryCatchResult`] for /// matching with [`catch`](TryCatchResult::catch). -pub fn try_block<'a: 'b, 'b, T>( - env: &'b JNIEnv<'a>, - block: impl FnOnce() -> Result, -) -> TryCatchResult<'a, 'b, T> { +pub fn try_block( + env: &mut JNIEnv, + block: impl FnOnce(&mut JNIEnv) -> Result, +) -> TryCatchResult { TryCatchResult { - env, try_result: (|| { if env.exception_check()? { Err(Error::JavaException) } else { - Ok(block()) + Ok(block(env)) } })(), catch_result: None, } } -impl<'a: 'b, 'b, T> TryCatchResult<'a, 'b, T> { - pub fn catch( +impl TryCatchResult { + pub fn catch<'local>( self, - class: impl Desc<'a, JClass<'a>>, - block: impl FnOnce(JThrowable<'a>) -> Result, + env: &mut JNIEnv<'local>, + class: impl Desc<'local, JClass<'local>>, + block: impl FnOnce(&mut JNIEnv<'local>, JThrowable<'local>) -> Result, ) -> Self { match (self.try_result, self.catch_result) { (Err(e), _) => Self { - env: self.env, try_result: Err(e), catch_result: None, }, (Ok(Ok(r)), _) => Self { - env: self.env, try_result: Ok(Ok(r)), catch_result: None, }, (Ok(Err(e)), Some(r)) => Self { - env: self.env, try_result: Ok(Err(e)), catch_result: Some(r), }, (Ok(Err(Error::JavaException)), None) => { - let env = self.env; let catch_result = (|| { if env.exception_check()? { let ex = env.exception_occurred()?; - let _auto_local = env.auto_local(ex.clone()); env.exception_clear()?; - if env.is_instance_of(ex, class)? { - return block(ex).map(|o| Some(o)); + if env.is_instance_of(&ex, class)? { + return block(env, ex).map(|o| Some(o)); } - env.throw(ex)?; + env.throw(&ex)?; } Ok(None) })() .transpose(); Self { - env, try_result: Ok(Err(Error::JavaException)), catch_result, } } (Ok(Err(e)), None) => Self { - env: self.env, try_result: Ok(Err(e)), catch_result: None, }, @@ -102,62 +93,58 @@ impl<'a: 'b, 'b, T> TryCatchResult<'a, 'b, T> { /// Wrapper for [`JObject`]s that implement /// `io.github.gedgygedgy.rust.panic.PanicException`. -pub struct JPanicException<'a: 'b, 'b> { +pub struct JPanicException<'a> { internal: JThrowable<'a>, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JPanicException<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JThrowable<'a>) -> Result { - Ok(Self { internal: obj, env }) +impl<'a> JPanicException<'a> { + pub fn from_env(obj: JThrowable<'a>) -> Self { + Self { internal: obj } } - pub fn new(env: &'b JNIEnv<'a>, any: Box) -> Result { + pub fn new(env: &mut JNIEnv<'a>, any: Box) -> Result { let msg = if let Some(s) = any.downcast_ref::<&str>() { - env.new_string(s)? + env.new_string(s)?.into() } else if let Some(s) = any.downcast_ref::() { - env.new_string(s)? + env.new_string(s)?.into() } else { - JObject::null().into() + JObject::null() }; let obj = env.new_object( "io/github/gedgygedgy/rust/panic/PanicException", "(Ljava/lang/String;)V", - &[msg.into()], + &[(&msg).into()], )?; - unsafe { env.set_rust_field(obj, "any", any) }?; - Self::from_env(env, obj.into()) + unsafe { env.set_rust_field(&obj, "any", any) }?; + Ok(Self { + internal: obj.into(), + }) } - pub fn get(&self) -> Result>, Error> { - unsafe { self.env.get_rust_field(self.internal, "any") } + pub fn get<'b>( + &self, + env: &'b mut JNIEnv, + ) -> Result>, Error> { + unsafe { env.get_rust_field(&self.internal, "any") } } - pub fn take(&self) -> Result, Error> { - unsafe { self.env.take_rust_field(self.internal, "any") } + pub fn take(&self, env: &mut JNIEnv) -> Result, Error> { + unsafe { env.take_rust_field(&self.internal, "any") } } - pub fn resume_unwind(&self) -> Result<(), Error> { - resume_unwind(self.take()?); + pub fn resume_unwind(&self, env: &mut JNIEnv) -> Result<(), Error> { + resume_unwind(self.take(env)?); } } -impl<'a: 'b, 'b> TryFrom> for Box { - type Error = Error; - - fn try_from(ex: JPanicException<'a, 'b>) -> Result { - ex.take() - } -} - -impl<'a: 'b, 'b> From> for JThrowable<'a> { - fn from(ex: JPanicException<'a, 'b>) -> Self { +impl<'a> From> for JThrowable<'a> { + fn from(ex: JPanicException<'a>) -> Self { ex.internal } } -impl<'a: 'b, 'b> ::std::ops::Deref for JPanicException<'a, 'b> { +impl<'a> ::std::ops::Deref for JPanicException<'a> { type Target = JThrowable<'a>; fn deref(&self) -> &Self::Target { @@ -165,45 +152,53 @@ impl<'a: 'b, 'b> ::std::ops::Deref for JPanicException<'a, 'b> { } } +/// Wraps a caught panic payload in a +/// `io.github.gedgygedgy.rust.panic.PanicException` and throws it. If a Java +/// exception is already pending, it will be added as a suppressed exception. +pub fn throw_panic( + env: &mut JNIEnv, + panic: Box, +) -> Result<(), Error> { + let old_ex = if env.exception_check()? { + let ex = env.exception_occurred()?; + env.exception_clear()?; + Some(ex) + } else { + None + }; + let ex = JPanicException::new(env, panic)?; + + if let Some(old_ex) = old_ex { + env.call_method( + &*ex, + "addSuppressed", + "(Ljava/lang/Throwable;)V", + &[(&old_ex).into()], + )?; + } + let ex: JThrowable = ex.into(); + env.throw(&ex)?; + Ok(()) +} + /// Calls the given closure. If it panics, catch the unwind, wrap it in a /// `io.github.gedgygedgy.rust.panic.PanicException`, and throw it. -pub fn throw_unwind<'a: 'b, 'b, R>( - env: &'b JNIEnv<'a>, +pub fn throw_unwind( + env: &mut JNIEnv, f: impl FnOnce() -> R + UnwindSafe, ) -> Result> { - catch_unwind(f).map_err(|e| { - let old_ex = if env.exception_check()? { - let ex = env.exception_occurred()?; - env.exception_clear()?; - Some(ex) - } else { - None - }; - let ex = JPanicException::new(env, e)?; - - if let Some(old_ex) = old_ex { - env.call_method( - ex.clone(), - "addSuppressed", - "(Ljava/lang/Throwable;)V", - &[old_ex.into()], - )?; - } - let ex: JThrowable = ex.into(); - env.throw(ex)?; - Ok(()) - }) + catch_unwind(f).map_err(|e| throw_panic(env, e)) } #[cfg(test)] mod test { - use jni::{JNIEnv, errors::Error, objects::JThrowable}; + use jni::{JNIEnv, errors::Error, objects::{JObject, JThrowable}}; use super::super::test_utils; use super::try_block; - fn test_catch<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + fn test_catch( + env: &mut JNIEnv, throw_class: Option<&str>, try_result: Result, rethrow: bool, @@ -218,59 +213,72 @@ mod test { let illegal_argument_exception = env .find_class("java/lang/IllegalArgumentException") .unwrap(); - if let Some(ex) = old_ex { + if let Some(ref ex) = old_ex { env.throw(ex).unwrap(); } let ex = throw_class.map(|c| { - let ex: JThrowable = env.new_object(c, "()V", &[]).unwrap().into(); - ex + let obj = env.new_object(c, "()V", &[]).unwrap(); + JThrowable::from(obj) }); - try_block(env, || { - if let Some(t) = ex { + try_block(env, |env| { + if let Some(ref t) = ex { env.throw(t).unwrap(); } try_result }) - .catch(illegal_argument_exception, |caught| { + .catch(env, illegal_argument_exception, |env, caught| { assert!(!env.exception_check().unwrap()); - assert!(env.is_same_object(ex.unwrap(), caught).unwrap()); + assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); Ok(1) }) - .catch("java/lang/ArrayIndexOutOfBoundsException", |caught| { - assert!(!env.exception_check().unwrap()); - assert!(env.is_same_object(ex.unwrap(), caught).unwrap()); - if rethrow { - Err(Error::JavaException) - } else { - Ok(2) - } - }) - .catch("java/lang/IndexOutOfBoundsException", |caught| { - assert!(!env.exception_check().unwrap()); - assert!(env.is_same_object(ex.unwrap(), caught).unwrap()); - if rethrow { - env.throw(caught).unwrap(); - Err(Error::JavaException) - } else { - Ok(3) - } - }) - .catch("java/lang/StringIndexOutOfBoundsException", |caught| { - assert!(!env.exception_check().unwrap()); - assert!(env.is_same_object(ex.unwrap(), caught).unwrap()); - Ok(4) - }) + .catch( + env, + "java/lang/ArrayIndexOutOfBoundsException", + |env, caught| { + assert!(!env.exception_check().unwrap()); + assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); + if rethrow { + Err(Error::JavaException) + } else { + Ok(2) + } + }, + ) + .catch( + env, + "java/lang/IndexOutOfBoundsException", + |env, caught| { + assert!(!env.exception_check().unwrap()); + assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); + if rethrow { + env.throw(&caught).unwrap(); + Err(Error::JavaException) + } else { + Ok(3) + } + }, + ) + .catch( + env, + "java/lang/StringIndexOutOfBoundsException", + |env, caught| { + assert!(!env.exception_check().unwrap()); + assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); + Ok(4) + }, + ) .result() } #[test] fn test_catch_first() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); assert_eq!( test_catch( - &env, + env, Some("java/lang/IllegalArgumentException"), Err(Error::JavaException), false, @@ -284,10 +292,11 @@ mod test { #[test] fn test_catch_second() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); assert_eq!( test_catch( - &env, + env, Some("java/lang/ArrayIndexOutOfBoundsException"), Err(Error::JavaException), false, @@ -301,10 +310,11 @@ mod test { #[test] fn test_catch_third() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); assert_eq!( test_catch( - &env, + env, Some("java/lang/StringIndexOutOfBoundsException"), Err(Error::JavaException), false, @@ -318,17 +328,19 @@ mod test { #[test] fn test_catch_ok() { - test_utils::JVM_ENV.with(|env| { - assert_eq!(test_catch(&env, None, Ok(0), false).unwrap(), 0); + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); + assert_eq!(test_catch(env, None, Ok(0), false).unwrap(), 0); assert!(!env.exception_check().unwrap()); }); } #[test] fn test_catch_none() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); if let Error::JavaException = test_catch( - &env, + env, Some("java/lang/SecurityException"), Err(Error::JavaException), false, @@ -339,7 +351,7 @@ mod test { let ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); assert!( - env.is_instance_of(ex, "java/lang/SecurityException") + env.is_instance_of(&ex, "java/lang/SecurityException") .unwrap() ); } else { @@ -350,7 +362,8 @@ mod test { #[test] fn test_catch_other() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); if let Error::InvalidCtorReturn = test_catch(env, None, Err(Error::InvalidCtorReturn), false).unwrap_err() { @@ -363,7 +376,8 @@ mod test { #[test] fn test_catch_bogus_exception() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); if let Error::JavaException = test_catch(env, None, Err(Error::JavaException), false).unwrap_err() { @@ -376,18 +390,19 @@ mod test { #[test] fn test_catch_prior_exception() { - test_utils::JVM_ENV.with(|env| { - let ex: JThrowable = env - .new_object("java/lang/IllegalArgumentException", "()V", &[]) - .unwrap() - .into(); - env.throw(ex).unwrap(); + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); + let ex = JThrowable::from( + env.new_object("java/lang/IllegalArgumentException", "()V", &[]) + .unwrap(), + ); + env.throw(&ex).unwrap(); - if let Error::JavaException = test_catch(&env, None, Ok(0), false).unwrap_err() { + if let Error::JavaException = test_catch(env, None, Ok(0), false).unwrap_err() { assert!(env.exception_check().unwrap()); let actual_ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); - assert!(env.is_same_object(actual_ex, ex).unwrap()); + assert!(env.is_same_object(&actual_ex, &ex).unwrap()); } else { panic!("JavaException not found"); } @@ -396,9 +411,10 @@ mod test { #[test] fn test_catch_rethrow() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); if let Error::JavaException = test_catch( - &env, + env, Some("java/lang/StringIndexOutOfBoundsException"), Err(Error::JavaException), true, @@ -409,7 +425,7 @@ mod test { let ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); assert!( - env.is_instance_of(ex, "java/lang/StringIndexOutOfBoundsException") + env.is_instance_of(&ex, "java/lang/StringIndexOutOfBoundsException") .unwrap() ); } else { @@ -420,9 +436,10 @@ mod test { #[test] fn test_catch_bogus_rethrow() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); if let Error::JavaException = test_catch( - &env, + env, Some("java/lang/ArrayIndexOutOfBoundsException"), Err(Error::JavaException), true, @@ -438,84 +455,91 @@ mod test { #[test] fn test_panic_exception_static_str() { - test_utils::JVM_ENV.with(|env| { - use jni::{objects::JString, strings::JavaStr}; + test_utils::JVM_ENV.with(|cell| { + let mut guard = cell.borrow_mut(); + let env = &mut *guard; + use jni::objects::JString; - const STATIC_MSG: &'static str = "This is a &'static str"; + const STATIC_MSG: &str = "This is a &'static str"; let ex = super::JPanicException::new(env, Box::new(STATIC_MSG)).unwrap(); { - let any = ex.get().unwrap(); + let any = ex.get(env).unwrap(); assert_eq!(*any.downcast_ref::<&str>().unwrap(), STATIC_MSG); } let msg: JString = env - .call_method(ex.clone(), "getMessage", "()Ljava/lang/String;", &[]) + .call_method(&*ex, "getMessage", "()Ljava/lang/String;", &[]) .unwrap() .l() .unwrap() .into(); - let str = JavaStr::from_env(env, msg).unwrap(); - assert_eq!(str.to_str().unwrap(), STATIC_MSG); + let str = env.get_string(&msg).unwrap(); + assert_eq!(>::from(str), STATIC_MSG); }); } #[test] fn test_panic_exception_string() { - test_utils::JVM_ENV.with(|env| { - use jni::{objects::JString, strings::JavaStr}; + test_utils::JVM_ENV.with(|cell| { + let mut guard = cell.borrow_mut(); + let env = &mut *guard; + use jni::objects::JString; use std::any::Any; - const STRING_MSG: &'static str = "This is a String"; + const STRING_MSG: &str = "This is a String"; let ex = super::JPanicException::new(env, Box::new(STRING_MSG.to_string())).unwrap(); { - let any = ex.get().unwrap(); + let any = ex.get(env).unwrap(); assert_eq!(*any.downcast_ref::().unwrap(), STRING_MSG); } let msg: JString = env - .call_method(ex.clone(), "getMessage", "()Ljava/lang/String;", &[]) + .call_method(&*ex, "getMessage", "()Ljava/lang/String;", &[]) .unwrap() .l() .unwrap() .into(); - let str = JavaStr::from_env(env, msg).unwrap(); - assert_eq!(str.to_str().unwrap(), STRING_MSG); + let str = env.get_string(&msg).unwrap(); + assert_eq!(>::from(str), STRING_MSG); - let any: Box = ex.take().unwrap(); + let any: Box = ex.take(env).unwrap(); assert_eq!(*any.downcast::().unwrap(), STRING_MSG); }); } #[test] fn test_panic_exception_other() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let mut guard = cell.borrow_mut(); + let env = &mut *guard; use jni::objects::JObject; - use std::{any::Any, convert::TryInto}; + use std::any::Any; let ex = super::JPanicException::new(env, Box::new(42)).unwrap(); { - let any = ex.get().unwrap(); + let any = ex.get(env).unwrap(); assert_eq!(*any.downcast_ref::().unwrap(), 42); } let msg = env - .call_method(ex.clone(), "getMessage", "()Ljava/lang/String;", &[]) + .call_method(&*ex, "getMessage", "()Ljava/lang/String;", &[]) .unwrap() .l() .unwrap(); - assert!(env.is_same_object(msg, JObject::null()).unwrap()); + assert!(env.is_same_object(&msg, JObject::null()).unwrap()); - let any: Box = ex.try_into().unwrap(); + let any: Box = ex.take(env).unwrap(); assert_eq!(*any.downcast::().unwrap(), 42); }); } #[test] fn test_throw_unwind_ok() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); let result = super::throw_unwind(env, || 42).unwrap(); assert_eq!(result, 42); assert!(!env.exception_check().unwrap()); @@ -524,7 +548,8 @@ mod test { #[test] fn test_throw_unwind_panic() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); super::throw_unwind(env, || panic!("This is a panic")) .unwrap_err() .unwrap(); @@ -532,22 +557,22 @@ mod test { let ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); assert!( - env.is_instance_of(ex, "io/github/gedgygedgy/rust/panic/PanicException") + env.is_instance_of(&ex, "io/github/gedgygedgy/rust/panic/PanicException") .unwrap() ); let suppressed_list = env - .call_method(ex, "getSuppressed", "()[Ljava/lang/Throwable;", &[]) + .call_method(&ex, "getSuppressed", "()[Ljava/lang/Throwable;", &[]) .unwrap() .l() .unwrap(); - assert_eq!( - env.get_array_length(suppressed_list.into_raw()).unwrap(), - 0 - ); + let suppressed_array = + unsafe { jni::objects::JObjectArray::from_raw(suppressed_list.into_raw()) }; + assert_eq!(env.get_array_length(&suppressed_array).unwrap(), 0); - let ex = super::JPanicException::from_env(env, ex).unwrap(); - let any = ex.take().unwrap(); + let ex_throwable = JThrowable::from(JObject::from(ex)); + let ex = super::JPanicException::from_env(ex_throwable); + let any = ex.take(env).unwrap(); let str = any.downcast::<&str>().unwrap(); assert_eq!(*str, "This is a panic"); }); @@ -555,12 +580,11 @@ mod test { #[test] fn test_throw_unwind_panic_suppress() { - test_utils::JVM_ENV.with(|env| { - let old_ex: JThrowable = env - .new_object("java/lang/Exception", "()V", &[]) - .unwrap() - .into(); - env.throw(old_ex).unwrap(); + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); + let old_ex = + JThrowable::from(env.new_object("java/lang/Exception", "()V", &[]).unwrap()); + env.throw(&old_ex).unwrap(); super::throw_unwind(env, || panic!("This is a panic")) .unwrap_err() @@ -569,26 +593,24 @@ mod test { let ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); assert!( - env.is_instance_of(ex, "io/github/gedgygedgy/rust/panic/PanicException") + env.is_instance_of(&ex, "io/github/gedgygedgy/rust/panic/PanicException") .unwrap() ); let suppressed_list = env - .call_method(ex, "getSuppressed", "()[Ljava/lang/Throwable;", &[]) + .call_method(&ex, "getSuppressed", "()[Ljava/lang/Throwable;", &[]) .unwrap() .l() .unwrap(); - assert_eq!( - env.get_array_length(suppressed_list.into_raw()).unwrap(), - 1 - ); - let suppressed_ex = env - .get_object_array_element(suppressed_list.into_raw(), 0) - .unwrap(); - assert!(env.is_same_object(old_ex, suppressed_ex).unwrap()); - - let ex = super::JPanicException::from_env(env, ex).unwrap(); - let any = ex.take().unwrap(); + let suppressed_array = + unsafe { jni::objects::JObjectArray::from_raw(suppressed_list.into_raw()) }; + assert_eq!(env.get_array_length(&suppressed_array).unwrap(), 1); + let suppressed_ex = env.get_object_array_element(&suppressed_array, 0).unwrap(); + assert!(env.is_same_object(&old_ex, &suppressed_ex).unwrap()); + + let ex_throwable = JThrowable::from(JObject::from(ex)); + let ex = super::JPanicException::from_env(ex_throwable); + let any = ex.take(env).unwrap(); let str = any.downcast::<&str>().unwrap(); assert_eq!(*str, "This is a panic"); }); @@ -597,9 +619,10 @@ mod test { #[test] #[should_panic(expected = "This is a panic")] fn test_panic_exception_resume_unwind() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); let ex = super::JPanicException::new(env, Box::new("This is a panic")).unwrap(); - ex.resume_unwind().unwrap(); + ex.resume_unwind(env).unwrap(); }); } } diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index ffaf4e0e..2a42b404 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -1,67 +1,60 @@ -use super::task::JPollResult; use ::jni::{ JNIEnv, JavaVM, - errors::{Error, Result}, - objects::{GlobalRef, JClass, JMethodID, JObject, JValue}, + errors::Result, + objects::{GlobalRef, JClass, JMethodID, JObject}, signature::ReturnType, + sys::jvalue, }; use static_assertions::assert_impl_all; use std::{ - convert::TryFrom, future::Future, pin::Pin, task::{Context, Poll}, }; /// Wrapper for [`JObject`]s that implement -/// `io.github.gedgygedgy.rust.future.Future`. Implements -/// [`Future`](std::future::Future) to allow asynchronous Rust code to wait for -/// a result from Java code. +/// `io.github.gedgygedgy.rust.future.Future`. Provides a typed interface for +/// calling the Java future's `poll` method. /// -/// For a [`Send`] version of this, use [`JSendFuture`]. -pub struct JFuture<'a: 'b, 'b> { +/// For an async [`Future`](std::future::Future) implementation, convert to +/// [`JSendFuture`] via [`JSendFuture::new`]. +pub struct JFuture<'a> { internal: JObject<'a>, - poll: JMethodID, - env: &'b JNIEnv<'a>, + poll_id: JMethodID, } -impl<'a: 'b, 'b> JFuture<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let poll = env.get_method_id( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/future/Future") - .unwrap() - .as_obj(), - ), +impl<'a> JFuture<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = + super::classcache::get_class("io/github/gedgygedgy/rust/future/Future").unwrap(); + let poll_id = env.get_method_id( + <&JClass>::from(class.as_obj()), "poll", "(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;", )?; Ok(Self { internal: obj, - poll, - env, + poll_id, }) } - pub fn poll(&self, waker: JObject<'a>) -> Result> { - let result = self - .env - .call_method_unchecked( - self.internal, - self.poll, + pub fn poll(&self, env: &mut JNIEnv<'a>, waker: &JObject<'_>) -> Result> { + let result = unsafe { + env.call_method_unchecked( + &self.internal, + self.poll_id, ReturnType::Object, - &[JValue::from(waker).to_jni()], - )? - .l()?; - JPollResult::from_env(self.env, result) - } - - pub fn into_future(self) -> JFutureIntoFuture<'a, 'b> { - JFutureIntoFuture(self) + &[jvalue { + l: waker.as_raw(), + }], + ) + }? + .l()?; + Ok(result) } } -impl<'a: 'b, 'b> ::std::ops::Deref for JFuture<'a, 'b> { +impl<'a> ::std::ops::Deref for JFuture<'a> { type Target = JObject<'a>; fn deref(&self) -> &Self::Target { @@ -69,67 +62,62 @@ impl<'a: 'b, 'b> ::std::ops::Deref for JFuture<'a, 'b> { } } -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(other: JFuture<'a, 'b>) -> JObject<'a> { +impl<'a> From> for JObject<'a> { + fn from(other: JFuture<'a>) -> JObject<'a> { other.internal } } -pub struct JFutureIntoFuture<'a: 'b, 'b>(JFuture<'a, 'b>); - -impl<'a: 'b, 'b> JFutureIntoFuture<'a, 'b> { - fn poll_internal(&self, context: &mut Context<'_>) -> Result>> { - use super::task::waker; - let result = self.0.poll(waker(self.0.env, context.waker().clone())?)?; - Ok( - if self.0.env.is_same_object(result.clone(), JObject::null())? { - Poll::Pending - } else { - Poll::Ready(result) - }, - ) - } -} - -impl<'a: 'b, 'b> Future for JFutureIntoFuture<'a, 'b> { - type Output = Result>; - - fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { - match self.poll_internal(context) { - Ok(Poll::Ready(result)) => Poll::Ready(Ok(result)), - Ok(Poll::Pending) => Poll::Pending, - Err(err) => Poll::Ready(Err(err)), - } - } -} - -impl<'a: 'b, 'b> From> for JFuture<'a, 'b> { - fn from(fut: JFutureIntoFuture<'a, 'b>) -> Self { - fut.0 - } -} - -impl<'a: 'b, 'b> std::ops::Deref for JFutureIntoFuture<'a, 'b> { - type Target = JFuture<'a, 'b>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// [`Send`] version of [`JFuture`]. +/// [`Send`] version of [`JFuture`]. Implements [`Future`](std::future::Future) +/// by obtaining a [`JNIEnv`] from the stored [`JavaVM`] on each poll. pub struct JSendFuture { internal: GlobalRef, + poll_id: JMethodID, vm: JavaVM, } -impl<'a: 'b, 'b> TryFrom> for JSendFuture { - type Error = Error; +impl JSendFuture { + pub fn new(env: &mut JNIEnv, future: &JFuture) -> Result { + Ok(Self { + internal: env.new_global_ref(&future.internal)?, + poll_id: future.poll_id, + vm: env.get_java_vm()?, + }) + } - fn try_from(future: JFuture<'a, 'b>) -> Result { + pub fn from_env(env: &mut JNIEnv, obj: &JObject) -> Result { + let class = + super::classcache::get_class("io/github/gedgygedgy/rust/future/Future").unwrap(); + let poll_id = env.get_method_id( + <&JClass>::from(class.as_obj()), + "poll", + "(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;", + )?; Ok(Self { - internal: future.env.new_global_ref(future.internal)?, - vm: future.env.get_java_vm()?, + internal: env.new_global_ref(obj)?, + poll_id, + vm: env.get_java_vm()?, + }) + } + + fn poll_internal(&self, context: &mut Context<'_>) -> Result>> { + let mut env = self.vm.get_env()?; + let jwaker = super::task::waker(&mut env, context.waker().clone())?; + let result = unsafe { + env.call_method_unchecked( + self.internal.as_obj(), + self.poll_id, + ReturnType::Object, + &[jvalue { + l: jwaker.as_raw(), + }], + ) + }? + .l()?; + Ok(if env.is_same_object(&result, JObject::null())? { + Poll::Pending + } else { + Poll::Ready(Ok(env.new_global_ref(result)?)) }) } } @@ -142,16 +130,6 @@ impl ::std::ops::Deref for JSendFuture { } } -impl JSendFuture { - fn poll_internal(&self, context: &mut Context<'_>) -> Result>> { - let env = self.vm.get_env()?; - let jfuture = JFuture::from_env(&env, self.internal.as_obj())?.into_future(); - jfuture - .poll_internal(context) - .map(|result| result.map(|result| Ok(env.new_global_ref(result)?))) - } -} - impl Future for JSendFuture { type Output = Result; @@ -167,7 +145,7 @@ assert_impl_all!(JSendFuture: Send); #[cfg(test)] mod test { - use super::super::{task::JPollResult, test_utils}; + use super::super::test_utils; use super::{JFuture, JSendFuture}; use std::{ future::Future, @@ -177,9 +155,12 @@ mod test { #[test] fn test_jfuture() { + use super::super::task::JPollResult; use std::sync::Arc; - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); + let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -191,7 +172,9 @@ mod test { let future_obj = env .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) .unwrap(); - let mut future = JFuture::from_env(env, future_obj).unwrap().into_future(); + let future_local = env.new_local_ref(&future_obj).unwrap(); + let jfuture = JFuture::from_env(env, future_local).unwrap(); + let mut future = JSendFuture::new(env, &jfuture).unwrap(); assert!( Future::poll(Pin::new(&mut future), &mut Context::from_waker(&waker)).is_pending() @@ -206,17 +189,23 @@ mod test { assert_eq!(data.value(), false); let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - env.call_method(future_obj, "wake", "(Ljava/lang/Object;)V", &[obj.into()]) - .unwrap(); + env.call_method( + &future_obj, + "wake", + "(Ljava/lang/Object;)V", + &[(&obj).into()], + ) + .unwrap(); assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), true); let poll = Future::poll(Pin::new(&mut future), &mut Context::from_waker(&waker)); if let Poll::Ready(result) = poll { - assert!( - env.is_same_object(result.unwrap().get().unwrap(), obj) - .unwrap() - ); + let global = result.unwrap(); + let local = env.new_local_ref(global.as_obj()).unwrap(); + let poll_result = JPollResult::from_env(env, local).unwrap(); + let result_obj = poll_result.get(env).unwrap(); + assert!(env.is_same_object(&result_obj, &obj).unwrap()); } else { panic!("Poll result should be ready"); } @@ -225,10 +214,11 @@ mod test { let poll = Future::poll(Pin::new(&mut future), &mut Context::from_waker(&waker)); if let Poll::Ready(result) = poll { - assert!( - env.is_same_object(result.unwrap().get().unwrap(), obj) - .unwrap() - ); + let global = result.unwrap(); + let local = env.new_local_ref(global.as_obj()).unwrap(); + let poll_result = JPollResult::from_env(env, local).unwrap(); + let result_obj = poll_result.get(env).unwrap(); + assert!(env.is_same_object(&result_obj, &obj).unwrap()); } else { panic!("Poll result should be ready"); } @@ -239,29 +229,46 @@ mod test { #[test] fn test_jfuture_await() { + use super::super::task::JPollResult; use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|env| { - let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) - .unwrap(); - let future = JFuture::from_env(env, future_obj).unwrap(); - let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + test_utils::JVM_ENV.with(|cell| { + let (future, future_obj_global, obj_global) = { + let env = &mut *cell.borrow_mut(); + let future_obj = env + .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .unwrap(); + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future_local = env.new_local_ref(&future_obj).unwrap(); + let jfuture = JFuture::from_env(env, future_local).unwrap(); + let future = JSendFuture::new(env, &jfuture).unwrap(); + let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj_global = env.new_global_ref(&obj).unwrap(); + (future, future_obj_global, obj_global) + }; block_on(async { join!( async { - env.call_method(future_obj, "wake", "(Ljava/lang/Object;)V", &[obj.into()]) - .unwrap(); + let env = &mut *cell.borrow_mut(); + let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + env.call_method( + &future_local, + "wake", + "(Ljava/lang/Object;)V", + &[(&obj_local).into()], + ) + .unwrap(); }, async { - assert!( - env.is_same_object( - future.into_future().await.unwrap().get().unwrap(), - obj - ) - .unwrap() - ); + let global = future.await.unwrap(); + let env = &mut *cell.borrow_mut(); + let local = env.new_local_ref(global.as_obj()).unwrap(); + let poll_result = JPollResult::from_env(env, local).unwrap(); + let result_obj = poll_result.get(env).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + assert!(env.is_same_object(&result_obj, &obj_local).unwrap()); } ); }); @@ -272,34 +279,53 @@ mod test { fn test_jfuture_await_throw() { use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|env| { - let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) - .unwrap(); - let future = JFuture::from_env(env, future_obj).unwrap(); - let ex = env.new_object("java/lang/Exception", "()V", &[]).unwrap(); + test_utils::JVM_ENV.with(|cell| { + let (future, future_obj_global, ex_global) = { + let env = &mut *cell.borrow_mut(); + let future_obj = env + .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .unwrap(); + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future_local = env.new_local_ref(&future_obj).unwrap(); + let jfuture = JFuture::from_env(env, future_local).unwrap(); + let future = JSendFuture::new(env, &jfuture).unwrap(); + let ex = env.new_object("java/lang/Exception", "()V", &[]).unwrap(); + let ex_global = env.new_global_ref(&ex).unwrap(); + (future, future_obj_global, ex_global) + }; block_on(async { join!( async { + let env = &mut *cell.borrow_mut(); + let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); + let ex_local = env.new_local_ref(ex_global.as_obj()).unwrap(); env.call_method( - future_obj, + &future_local, "wakeWithThrowable", "(Ljava/lang/Throwable;)V", - &[ex.into()], + &[(&ex_local).into()], ) .unwrap(); }, async { - future.into_future().await.unwrap().get().unwrap_err(); + use super::super::task::JPollResult; + + let global = future.await.unwrap(); + let env = &mut *cell.borrow_mut(); + let local = env.new_local_ref(global.as_obj()).unwrap(); + let poll_result = JPollResult::from_env(env, local).unwrap(); + let _err = poll_result.get(env).unwrap_err(); + let future_ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); let actual_ex = env - .call_method(future_ex, "getCause", "()Ljava/lang/Throwable;", &[]) + .call_method(&future_ex, "getCause", "()Ljava/lang/Throwable;", &[]) .unwrap() .l() .unwrap(); - assert!(env.is_same_object(actual_ex, ex).unwrap()); + let ex_local = env.new_local_ref(ex_global.as_obj()).unwrap(); + assert!(env.is_same_object(&actual_ex, &ex_local).unwrap()); } ); }); @@ -308,27 +334,44 @@ mod test { #[test] fn test_jsendfuture_await() { + use super::super::task::JPollResult; use futures::{executor::block_on, join}; - use std::convert::TryInto; - test_utils::JVM_ENV.with(|env| { - let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) - .unwrap(); - let future = JFuture::from_env(env, future_obj).unwrap(); - let future: JSendFuture = future.try_into().unwrap(); - let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + test_utils::JVM_ENV.with(|cell| { + let (future, future_obj_global, obj_global) = { + let env = &mut *cell.borrow_mut(); + let future_obj = env + .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .unwrap(); + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future = JSendFuture::from_env(env, &future_obj).unwrap(); + let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj_global = env.new_global_ref(&obj).unwrap(); + (future, future_obj_global, obj_global) + }; block_on(async { join!( async { - env.call_method(future_obj, "wake", "(Ljava/lang/Object;)V", &[obj.into()]) - .unwrap(); + let env = &mut *cell.borrow_mut(); + let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + env.call_method( + &future_local, + "wake", + "(Ljava/lang/Object;)V", + &[(&obj_local).into()], + ) + .unwrap(); }, async { let global_ref = future.await.unwrap(); - let jpoll = JPollResult::from_env(env, global_ref.as_obj()).unwrap(); - assert!(env.is_same_object(jpoll.get().unwrap(), obj).unwrap()); + let env = &mut *cell.borrow_mut(); + let local = env.new_local_ref(global_ref.as_obj()).unwrap(); + let jpoll = JPollResult::from_env(env, local).unwrap(); + let result_obj = jpoll.get(env).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + assert!(env.is_same_object(&result_obj, &obj_local).unwrap()); } ); }); diff --git a/src/droidplug/jni_utils/mod.rs b/src/droidplug/jni_utils/mod.rs index 84bae6b2..6334cce9 100644 --- a/src/droidplug/jni_utils/mod.rs +++ b/src/droidplug/jni_utils/mod.rs @@ -12,13 +12,14 @@ pub(crate) mod test_utils { use jni::{JNIEnv, JavaVM, objects::GlobalRef}; use lazy_static::lazy_static; use std::{ + cell::RefCell, sync::{Arc, Mutex}, task::{Wake, Waker}, }; use jni::NativeMethod; - fn test_init(env: &JNIEnv) -> jni::errors::Result<()> { + fn test_init(env: &mut JNIEnv) -> jni::errors::Result<()> { use std::ffi::c_void; super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/future/Future")?; super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/future/FutureException")?; @@ -31,7 +32,7 @@ pub(crate) mod test_utils { super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnBiFunctionImpl")?; super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnFunctionImpl")?; - let class = env.auto_local(env.find_class("io/github/gedgygedgy/rust/ops/FnAdapter")?); + let class = env.find_class("io/github/gedgygedgy/rust/ops/FnAdapter")?; env.register_native_methods( &class, &[ @@ -89,8 +90,8 @@ pub(crate) mod test_utils { } thread_local! { - pub static JVM_ENV: JNIEnv<'static> = { - let env = JVM.jvm.attach_current_thread_permanently().unwrap(); + pub static JVM_ENV: RefCell> = { + let mut env = JVM.jvm.attach_current_thread_permanently().unwrap(); let thread = env .call_static_method( @@ -103,13 +104,13 @@ pub(crate) mod test_utils { .l() .unwrap(); env.call_method( - thread, + &thread, "setContextClassLoader", "(Ljava/lang/ClassLoader;)V", - &[JVM.class_loader.as_obj().into()] + &[(&JVM.class_loader).into()] ).unwrap(); - env + RefCell::new(env) } } @@ -125,17 +126,18 @@ pub(crate) mod test_utils { jni_utils_jar.push("libs"); jni_utils_jar.push("btleplug-jni.jar"); + let classpath = format!( + "-Djava.class.path={}", + jni_utils_jar.to_str().unwrap() + ); let jvm_args = InitArgsBuilder::new() - .option(&format!( - "-Djava.class.path={}", - jni_utils_jar.to_str().unwrap() - )) + .option(&classpath) .build() .unwrap(); let jvm = JavaVM::new(jvm_args).unwrap(); - let env = jvm.attach_current_thread_permanently().unwrap(); - test_init(&env).unwrap(); + let mut env = jvm.attach_current_thread_permanently().unwrap(); + test_init(&mut env).unwrap(); let thread = env .call_static_method( @@ -149,7 +151,7 @@ pub(crate) mod test_utils { .unwrap(); let class_loader = env .call_method( - thread, + &thread, "getContextClassLoader", "()Ljava/lang/ClassLoader;", &[], diff --git a/src/droidplug/jni_utils/ops.rs b/src/droidplug/jni_utils/ops.rs index 03902913..311cd4db 100644 --- a/src/droidplug/jni_utils/ops.rs +++ b/src/droidplug/jni_utils/ops.rs @@ -25,89 +25,92 @@ macro_rules! define_fn_adapter { signature: $closure_name:ident: impl for<'c, 'd> Fn$args:tt -> $ret:ty, closure: $closure:expr, ) => { - fn $foi<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + fn $foi<'local>( + env: &mut JNIEnv<'local>, $closure_name: impl for<'c, 'd> FnOnce$args -> $ret + 'static, local: bool, - ) -> Result> { - let adapter = env.auto_local(fn_once_adapter(env, $closure, local)?); + ) -> Result> { + let adapter = fn_once_adapter(env, $closure, local)?; + let class = super::classcache::get_class($ic).unwrap(); env.new_object( - JClass::from(super::classcache::get_class($ic).unwrap().as_obj()), + <&JClass>::from(class.as_obj()), "(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V", &[(&adapter).into()], ) } - pub fn $fo<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + pub fn $fo<'local>( + env: &mut JNIEnv<'local>, f: impl for<'c, 'd> FnOnce$args -> $ret + Send + 'static, - ) -> Result> { + ) -> Result> { $foi(env, f, false) } #[allow(dead_code)] - pub fn $fol<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + pub fn $fol<'local>( + env: &mut JNIEnv<'local>, f: impl for<'c, 'd> FnOnce$args -> $ret + 'static, - ) -> Result> { + ) -> Result> { $foi(env, f, true) } - fn $fmi<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + fn $fmi<'local>( + env: &mut JNIEnv<'local>, mut $closure_name: impl for<'c, 'd> FnMut$args -> $ret + 'static, local: bool, - ) -> Result> { - let adapter = env.auto_local(fn_mut_adapter(env, $closure, local)?); + ) -> Result> { + let adapter = fn_mut_adapter(env, $closure, local)?; + let class = super::classcache::get_class($ic).unwrap(); env.new_object( - JClass::from(super::classcache::get_class($ic).unwrap().as_obj()), + <&JClass>::from(class.as_obj()), "(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V", &[(&adapter).into()], ) } #[allow(dead_code)] - pub fn $fm<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + pub fn $fm<'local>( + env: &mut JNIEnv<'local>, f: impl for<'c, 'd> FnMut$args -> $ret + Send + 'static, - ) -> Result> { + ) -> Result> { $fmi(env, f, false) } #[allow(dead_code)] - pub fn $fml<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + pub fn $fml<'local>( + env: &mut JNIEnv<'local>, f: impl for<'c, 'd> FnMut$args -> $ret + 'static, - ) -> Result> { + ) -> Result> { $fmi(env, f, true) } - fn $fi<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + fn $fi<'local>( + env: &mut JNIEnv<'local>, $closure_name: impl for<'c, 'd> Fn$args -> $ret + 'static, local: bool, - ) -> Result> { - let adapter = env.auto_local(fn_adapter(env, $closure, local)?); + ) -> Result> { + let adapter = fn_adapter(env, $closure, local)?; + let class = super::classcache::get_class($ic).unwrap(); env.new_object( - JClass::from(super::classcache::get_class($ic).unwrap().as_obj()), + <&JClass>::from(class.as_obj()), "(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V", &[(&adapter).into()], ) } #[allow(dead_code)] - pub fn $f<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + pub fn $f<'local>( + env: &mut JNIEnv<'local>, f: impl for<'c, 'd> Fn$args -> $ret + Send + Sync + 'static, - ) -> Result> { + ) -> Result> { $fi(env, f, false) } #[allow(dead_code)] - pub fn $fl<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, + pub fn $fl<'local>( + env: &mut JNIEnv<'local>, f: impl for<'c, 'd> Fn$args -> $ret + 'static, - ) -> Result> { + ) -> Result> { $fi(env, f, true) } }; @@ -129,7 +132,7 @@ define_fn_adapter! { doc_fn_once: "fn_once_runnable", doc_fn: "fn_runnable", doc_noop: "be a no-op", - signature: f: impl for<'c, 'd> Fn(&'d JNIEnv<'c>, JObject<'c>) -> (), + signature: f: impl for<'c, 'd> Fn(&'d mut JNIEnv<'c>, JObject<'c>) -> (), closure: move |env, _obj1, obj2, _arg1, _arg2| { f(env, obj2); JObject::null() @@ -152,7 +155,7 @@ define_fn_adapter! { doc_fn_once: "fn_once_bi_function", doc_fn: "fn_bi_funciton", doc_noop: "return `null`", - signature: f: impl for<'c, 'd> Fn(&'d JNIEnv<'c>, JObject<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, + signature: f: impl for<'c, 'd> Fn(&'d mut JNIEnv<'c>, JObject<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, closure: move |env, _obj1, obj2, arg1, arg2| { f(env, obj2, arg1, arg2) }, @@ -174,7 +177,7 @@ define_fn_adapter! { doc_fn_once: "fn_once_function", doc_fn: "fn_function", doc_noop: "return `null`", - signature: f: impl for<'c, 'd> Fn(&'d JNIEnv<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, + signature: f: impl for<'c, 'd> Fn(&'d mut JNIEnv<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, closure: move |env, _obj1, obj2, arg1, _arg2| { f(env, obj2, arg1) }, @@ -188,7 +191,7 @@ unsafe impl Sync for SendSyncWrapper {} type FnWrapper = SendSyncWrapper< Arc< dyn for<'a, 'b> Fn( - &'b JNIEnv<'a>, + &'b mut JNIEnv<'a>, JObject<'a>, JObject<'a>, JObject<'a>, @@ -198,10 +201,10 @@ type FnWrapper = SendSyncWrapper< >, >; -fn fn_once_adapter<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, +fn fn_once_adapter<'local>( + env: &mut JNIEnv<'local>, f: impl for<'c, 'd> FnOnce( - &'d JNIEnv<'c>, + &'d mut JNIEnv<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -209,7 +212,7 @@ fn fn_once_adapter<'a: 'b, 'b>( ) -> JObject<'c> + 'static, local: bool, -) -> Result> { +) -> Result> { let mutex = Mutex::new(Some(f)); fn_adapter( env, @@ -228,10 +231,10 @@ fn fn_once_adapter<'a: 'b, 'b>( ) } -fn fn_mut_adapter<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, +fn fn_mut_adapter<'local>( + env: &mut JNIEnv<'local>, f: impl for<'c, 'd> FnMut( - &'d JNIEnv<'c>, + &'d mut JNIEnv<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -239,7 +242,7 @@ fn fn_mut_adapter<'a: 'b, 'b>( ) -> JObject<'c> + 'static, local: bool, -) -> Result> { +) -> Result> { let mutex = Mutex::new(f); fn_adapter( env, @@ -251,10 +254,10 @@ fn fn_mut_adapter<'a: 'b, 'b>( ) } -fn fn_adapter<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, +fn fn_adapter<'local>( + env: &mut JNIEnv<'local>, f: impl for<'c, 'd> Fn( - &'d JNIEnv<'c>, + &'d mut JNIEnv<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -262,10 +265,10 @@ fn fn_adapter<'a: 'b, 'b>( ) -> JObject<'c> + 'static, local: bool, -) -> Result> { +) -> Result> { let arc: Arc< dyn for<'c, 'd> Fn( - &'d JNIEnv<'c>, + &'d mut JNIEnv<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -273,39 +276,47 @@ fn fn_adapter<'a: 'b, 'b>( ) -> JObject<'c>, > = Arc::from(f); + let class = super::classcache::get_class("io/github/gedgygedgy/rust/ops/FnAdapter").unwrap(); let obj = env.new_object( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/ops/FnAdapter") - .unwrap() - .as_obj(), - ), + <&JClass>::from(class.as_obj()), "(Z)V", &[local.into()], )?; - unsafe { env.set_rust_field::<_, _, FnWrapper>(obj, "data", SendSyncWrapper(arc)) }?; + unsafe { env.set_rust_field::<_, _, FnWrapper>(&obj, "data", SendSyncWrapper(arc)) }?; Ok(obj) } -pub(crate) extern "C" fn fn_adapter_call_internal<'a>( - env: JNIEnv<'a>, - obj1: JObject<'a>, - obj2: JObject<'a>, - arg1: JObject<'a>, - arg2: JObject<'a>, -) -> JObject<'a> { - use std::panic::AssertUnwindSafe; +pub(crate) extern "C" fn fn_adapter_call_internal<'local>( + mut env: JNIEnv<'local>, + obj1: JObject<'local>, + obj2: JObject<'local>, + arg1: JObject<'local>, + arg2: JObject<'local>, +) -> JObject<'local> { + use std::panic::{AssertUnwindSafe, catch_unwind}; - let arc = if let Ok(f) = unsafe { env.get_rust_field::<_, _, FnWrapper>(obj1, "data") } { - AssertUnwindSafe(f.0.clone()) - } else { - return JObject::null(); - }; - super::exceptions::throw_unwind(&env, || arc(&env, obj1, obj2, arg1, arg2)) - .unwrap_or_else(|_| JObject::null()) + let arc = + if let Ok(f) = unsafe { env.get_rust_field::<_, _, FnWrapper>(&obj1, "data") } { + AssertUnwindSafe(f.0.clone()) + } else { + return JObject::null(); + }; + match catch_unwind(AssertUnwindSafe(|| arc(&mut env, obj1, obj2, arg1, arg2))) { + Ok(result) => result, + Err(panic) => { + let _ = super::exceptions::throw_panic(&mut env, panic); + JObject::null() + } + } } -pub(crate) extern "C" fn fn_adapter_close_internal(env: JNIEnv, obj: JObject) { - let _ = super::exceptions::throw_unwind(&env, || { - let _ = unsafe { env.take_rust_field::<_, _, FnWrapper>(obj, "data") }; - }); +pub(crate) extern "C" fn fn_adapter_close_internal(mut env: JNIEnv, obj: JObject) { + use std::panic::{AssertUnwindSafe, catch_unwind}; + + let result = catch_unwind(AssertUnwindSafe(|| { + let _ = unsafe { env.take_rust_field::<_, _, FnWrapper>(&obj, "data") }; + })); + if let Err(panic) = result { + let _ = super::exceptions::throw_panic(&mut env, panic); + } } diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index 636a4fc5..e8bb85f0 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -1,80 +1,65 @@ use super::task::JPollResult; use ::jni::{ JNIEnv, JavaVM, - errors::{Error, Result}, - objects::{GlobalRef, JClass, JMethodID, JObject, JValue}, + errors::Result, + objects::{GlobalRef, JClass, JMethodID, JObject}, signature::ReturnType, + sys::jvalue, }; use futures::stream::Stream; use static_assertions::assert_impl_all; use std::{ - convert::TryFrom, pin::Pin, task::{Context, Poll}, }; /// Wrapper for [`JObject`]s that implement -/// `io.github.gedgygedgy.rust.stream.Stream`. +/// `io.github.gedgygedgy.rust.stream.Stream`. Provides a typed interface for +/// calling the Java stream's `pollNext` method. /// -/// For a [`Send`] version of this, use [`JSendStream`]. -pub struct JStream<'a: 'b, 'b> { +/// For an async [`Stream`] implementation, convert to [`JSendStream`] via +/// [`JSendStream::new`]. +pub struct JStream<'a> { internal: JObject<'a>, - poll_next: JMethodID, - env: &'b JNIEnv<'a>, + poll_next_id: JMethodID, } -impl<'a: 'b, 'b> JStream<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let poll_next = env.get_method_id( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream") - .unwrap() - .as_obj(), - ), +impl<'a> JStream<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = + super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream").unwrap(); + let poll_next_id = env.get_method_id( + <&JClass>::from(class.as_obj()), "pollNext", "(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;", )?; Ok(Self { internal: obj, - poll_next, - env, + poll_next_id, }) } - fn j_poll_next(&self, waker: JObject<'a>) -> Result>>> { - let result = self - .env - .call_method_unchecked( - self.internal, - self.poll_next, + pub fn poll_next_with_env( + &self, + env: &mut JNIEnv<'a>, + waker: &JObject<'_>, + ) -> Result> { + let result = unsafe { + env.call_method_unchecked( + &self.internal, + self.poll_next_id, ReturnType::Object, - &[JValue::from(waker).to_jni()], - )? - .l()?; - let _auto_local = self.env.auto_local(result); - Ok(if self.env.is_same_object(result, JObject::null())? { - Poll::Pending - } else { - Poll::Ready({ - let poll = JPollResult::from_env(self.env, result)?; - let stream_poll_obj = poll.get()?; - if self.env.is_same_object(stream_poll_obj, JObject::null())? { - None - } else { - let stream_poll = JStreamPoll::from_env(self.env, stream_poll_obj)?; - Some(stream_poll.get()?) - } - }) - }) - } - - fn poll_next_internal(&self, context: &mut Context) -> Result>>> { - use super::task::waker; - self.j_poll_next(waker(self.env, context.waker().clone())?) + &[jvalue { + l: waker.as_raw(), + }], + ) + }? + .l()?; + Ok(result) } } -impl<'a: 'b, 'b> ::std::ops::Deref for JStream<'a, 'b> { +impl<'a> ::std::ops::Deref for JStream<'a> { type Target = JObject<'a>; fn deref(&self) -> &Self::Target { @@ -82,39 +67,77 @@ impl<'a: 'b, 'b> ::std::ops::Deref for JStream<'a, 'b> { } } -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(other: JStream<'a, 'b>) -> JObject<'a> { +impl<'a> From> for JObject<'a> { + fn from(other: JStream<'a>) -> JObject<'a> { other.internal } } -impl<'a: 'b, 'b> Stream for JStream<'a, 'b> { - type Item = Result>; - - fn poll_next(self: Pin<&mut Self>, context: &mut Context) -> Poll> { - match self.poll_next_internal(context) { - Ok(Poll::Ready(result)) => Poll::Ready(result.map(|o| Ok(o))), - Ok(Poll::Pending) => Poll::Pending, - Err(err) => Poll::Ready(Some(Err(err))), - } - } -} - -/// [`Send`] version of [`JStream`]. +/// [`Send`] version of [`JStream`]. Implements [`Stream`] by obtaining a +/// [`JNIEnv`] from the stored [`JavaVM`] on each poll. pub struct JSendStream { internal: GlobalRef, + poll_next_id: JMethodID, vm: JavaVM, } -impl<'a: 'b, 'b> TryFrom> for JSendStream { - type Error = Error; +impl JSendStream { + pub fn new(env: &mut JNIEnv, stream: &JStream) -> Result { + Ok(Self { + internal: env.new_global_ref(&stream.internal)?, + poll_next_id: stream.poll_next_id, + vm: env.get_java_vm()?, + }) + } - fn try_from(stream: JStream<'a, 'b>) -> Result { + pub fn from_env(env: &mut JNIEnv, obj: &JObject) -> Result { + let class = + super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream").unwrap(); + let poll_next_id = env.get_method_id( + <&JClass>::from(class.as_obj()), + "pollNext", + "(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;", + )?; Ok(Self { - internal: stream.env.new_global_ref(stream.internal)?, - vm: stream.env.get_java_vm()?, + internal: env.new_global_ref(obj)?, + poll_next_id, + vm: env.get_java_vm()?, }) } + + fn poll_next_internal( + &self, + context: &mut Context<'_>, + ) -> Result>>> { + let mut env = self.vm.get_env()?; + let jwaker = super::task::waker(&mut env, context.waker().clone())?; + let result = unsafe { + env.call_method_unchecked( + self.internal.as_obj(), + self.poll_next_id, + ReturnType::Object, + &[jvalue { + l: jwaker.as_raw(), + }], + ) + }? + .l()?; + + if env.is_same_object(&result, JObject::null())? { + return Ok(Poll::Pending); + } + + let poll_result = JPollResult::from_env(&mut env, result)?; + let stream_poll_obj = poll_result.get(&mut env)?; + + if env.is_same_object(&stream_poll_obj, JObject::null())? { + return Ok(Poll::Ready(None)); + } + + let stream_poll = JStreamPoll::from_env(&mut env, stream_poll_obj)?; + let obj = stream_poll.get(&mut env)?; + Ok(Poll::Ready(Some(Ok(env.new_global_ref(obj)?)))) + } } impl ::std::ops::Deref for JSendStream { @@ -125,19 +148,6 @@ impl ::std::ops::Deref for JSendStream { } } -impl JSendStream { - fn poll_next_internal( - &self, - context: &mut Context<'_>, - ) -> Result>>> { - let env = self.vm.get_env()?; - let jstream = JStream::from_env(&env, self.internal.as_obj())?; - jstream - .poll_next_internal(context) - .map(|result| result.map(|result| result.map(|obj| env.new_global_ref(obj)))) - } -} - impl Stream for JSendStream { type Item = Result; @@ -151,33 +161,25 @@ impl Stream for JSendStream { assert_impl_all!(JSendStream: Send); -struct JStreamPoll<'a: 'b, 'b> { +struct JStreamPoll<'a> { internal: JObject<'a>, get: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JStreamPoll<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { +impl<'a> JStreamPoll<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = + super::classcache::get_class("io/github/gedgygedgy/rust/stream/StreamPoll").unwrap(); let get = env.get_method_id( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/stream/StreamPoll") - .unwrap() - .as_obj(), - ), + <&JClass>::from(class.as_obj()), "get", "()Ljava/lang/Object;", )?; - Ok(Self { - internal: obj, - get, - env, - }) + Ok(Self { internal: obj, get }) } - pub fn get(&self) -> Result> { - self.env - .call_method_unchecked(self.internal, self.get, ReturnType::Object, &[])? + pub fn get(&self, env: &mut JNIEnv<'a>) -> Result> { + unsafe { env.call_method_unchecked(&self.internal, self.get, ReturnType::Object, &[]) }? .l() } } @@ -185,7 +187,7 @@ impl<'a: 'b, 'b> JStreamPoll<'a, 'b> { #[cfg(test)] mod test { use super::super::test_utils; - use super::JStream; + use super::{JSendStream, JStream}; use futures::stream::Stream; use std::{ pin::Pin, @@ -196,7 +198,9 @@ mod test { fn test_jstream() { use std::sync::Arc; - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); + let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -208,7 +212,9 @@ mod test { let stream_obj = env .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) .unwrap(); - let mut stream = JStream::from_env(env, stream_obj).unwrap(); + let stream_local = env.new_local_ref(&stream_obj).unwrap(); + let jstream = JStream::from_env(env, stream_local).unwrap(); + let mut stream = JSendStream::new(env, &jstream).unwrap(); assert!( Pin::new(&mut stream) @@ -219,22 +225,31 @@ mod test { assert_eq!(data.value(), false); let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj1.into()]) - .unwrap(); + env.call_method( + &stream_obj, + "add", + "(Ljava/lang/Object;)V", + &[(&obj1).into()], + ) + .unwrap(); assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), true); data.set_value(false); let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj2.into()]) - .unwrap(); + env.call_method( + &stream_obj, + "add", + "(Ljava/lang/Object;)V", + &[(&obj2).into()], + ) + .unwrap(); assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - data.set_value(false); let poll = Pin::new(&mut stream).poll_next(&mut Context::from_waker(&waker)); if let Poll::Ready(Some(Ok(actual_obj1))) = poll { - assert!(env.is_same_object(actual_obj1, obj1).unwrap()); + assert!(env.is_same_object(actual_obj1.as_obj(), &obj1).unwrap()); } else { panic!("Poll result should be ready"); } @@ -243,7 +258,7 @@ mod test { let poll = Pin::new(&mut stream).poll_next(&mut Context::from_waker(&waker)); if let Poll::Ready(Some(Ok(actual_obj2))) = poll { - assert!(env.is_same_object(actual_obj2, obj2).unwrap()); + assert!(env.is_same_object(actual_obj2.as_obj(), &obj2).unwrap()); } else { panic!("Poll result should be ready"); } @@ -258,7 +273,7 @@ mod test { assert_eq!(Arc::strong_count(&data), 3); assert_eq!(data.value(), false); - env.call_method(stream_obj, "finish", "()V", &[]).unwrap(); + env.call_method(&stream_obj, "finish", "()V", &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), true); data.set_value(false); @@ -277,33 +292,64 @@ mod test { fn test_jstream_await() { use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|env| { - let stream_obj = env - .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) - .unwrap(); - let mut stream = JStream::from_env(env, stream_obj).unwrap(); - let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + test_utils::JVM_ENV.with(|cell| { + let (mut stream, stream_obj_global, obj1_global, obj2_global) = { + let env = &mut *cell.borrow_mut(); + let stream_obj = env + .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) + .unwrap(); + let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); + let stream_local = env.new_local_ref(&stream_obj).unwrap(); + let jstream = JStream::from_env(env, stream_local).unwrap(); + let stream = JSendStream::new(env, &jstream).unwrap(); + let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj1_global = env.new_global_ref(&obj1).unwrap(); + let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj2_global = env.new_global_ref(&obj2).unwrap(); + (stream, stream_obj_global, obj1_global, obj2_global) + }; block_on(async { join!( async { - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj1.into()]) - .unwrap(); - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj2.into()]) - .unwrap(); - env.call_method(stream_obj, "finish", "()V", &[]).unwrap(); + let env = &mut *cell.borrow_mut(); + let s = env.new_local_ref(stream_obj_global.as_obj()).unwrap(); + let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); + let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); + env.call_method( + &s, + "add", + "(Ljava/lang/Object;)V", + &[(&o1).into()], + ) + .unwrap(); + env.call_method( + &s, + "add", + "(Ljava/lang/Object;)V", + &[(&o2).into()], + ) + .unwrap(); + env.call_method(&s, "finish", "()V", &[]).unwrap(); }, async { use futures::StreamExt; - assert!( - env.is_same_object(stream.next().await.unwrap().unwrap(), obj1) - .unwrap() - ); - assert!( - env.is_same_object(stream.next().await.unwrap().unwrap(), obj2) - .unwrap() - ); + let g1 = stream.next().await.unwrap().unwrap(); + { + let mut guard = cell.borrow_mut(); + let env = &mut *guard; + let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); + assert!(env.is_same_object(g1.as_obj(), &o1).unwrap()); + } + + let g2 = stream.next().await.unwrap().unwrap(); + { + let mut guard = cell.borrow_mut(); + let env = &mut *guard; + let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); + assert!(env.is_same_object(g2.as_obj(), &o2).unwrap()); + } + assert!(stream.next().await.is_none()); } ); @@ -313,44 +359,64 @@ mod test { #[test] fn test_jsendstream_await() { - use super::JSendStream; use futures::{executor::block_on, join}; - use std::convert::TryInto; - test_utils::JVM_ENV.with(|env| { - let stream_obj = env - .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) - .unwrap(); - let stream = JStream::from_env(env, stream_obj).unwrap(); - let mut stream: JSendStream = stream.try_into().unwrap(); - let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); - let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + test_utils::JVM_ENV.with(|cell| { + let (mut stream, stream_obj_global, obj1_global, obj2_global) = { + let env = &mut *cell.borrow_mut(); + let stream_obj = env + .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) + .unwrap(); + let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); + let stream = JSendStream::from_env(env, &stream_obj).unwrap(); + let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj1_global = env.new_global_ref(&obj1).unwrap(); + let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj2_global = env.new_global_ref(&obj2).unwrap(); + (stream, stream_obj_global, obj1_global, obj2_global) + }; block_on(async { join!( async { - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj1.into()]) - .unwrap(); - env.call_method(stream_obj, "add", "(Ljava/lang/Object;)V", &[obj2.into()]) - .unwrap(); - env.call_method(stream_obj, "finish", "()V", &[]).unwrap(); + let env = &mut *cell.borrow_mut(); + let s = env.new_local_ref(stream_obj_global.as_obj()).unwrap(); + let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); + let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); + env.call_method( + &s, + "add", + "(Ljava/lang/Object;)V", + &[(&o1).into()], + ) + .unwrap(); + env.call_method( + &s, + "add", + "(Ljava/lang/Object;)V", + &[(&o2).into()], + ) + .unwrap(); + env.call_method(&s, "finish", "()V", &[]).unwrap(); }, async { use futures::StreamExt; - assert!( - env.is_same_object( - stream.next().await.unwrap().unwrap().as_obj(), - obj1 - ) - .unwrap() - ); - assert!( - env.is_same_object( - stream.next().await.unwrap().unwrap().as_obj(), - obj2 - ) - .unwrap() - ); + let g1 = stream.next().await.unwrap().unwrap(); + { + let mut guard = cell.borrow_mut(); + let env = &mut *guard; + let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); + assert!(env.is_same_object(g1.as_obj(), &o1).unwrap()); + } + + let g2 = stream.next().await.unwrap().unwrap(); + { + let mut guard = cell.borrow_mut(); + let env = &mut *guard; + let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); + assert!(env.is_same_object(g2.as_obj(), &o2).unwrap()); + } + assert!(stream.next().await.is_none()); } ); diff --git a/src/droidplug/jni_utils/task.rs b/src/droidplug/jni_utils/task.rs index 7993282a..95096110 100644 --- a/src/droidplug/jni_utils/task.rs +++ b/src/droidplug/jni_utils/task.rs @@ -7,55 +7,40 @@ use ::jni::{ use std::task::Waker; /// Wraps the given waker in a `io.github.gedgygedgy.rust.task.Waker` object. -pub fn waker<'a: 'b, 'b>(env: &'b JNIEnv<'a>, waker: Waker) -> Result> { +pub fn waker<'a>(env: &mut JNIEnv<'a>, waker: Waker) -> Result> { let runnable = super::ops::fn_once_runnable(env, |_e, _o| waker.wake())?; + let class = super::classcache::get_class("io/github/gedgygedgy/rust/task/Waker").unwrap(); let obj = env.new_object( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/task/Waker") - .unwrap() - .as_obj(), - ), + <&JClass>::from(class.as_obj()), "(Lio/github/gedgygedgy/rust/ops/FnRunnable;)V", - &[runnable.into()], + &[(&runnable).into()], )?; Ok(obj) } /// Wrapper for [`JObject`]s that implement /// `io.github.gedgygedgy.rust.task.PollResult`. -pub struct JPollResult<'a: 'b, 'b> { +pub struct JPollResult<'a> { internal: JObject<'a>, get: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JPollResult<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let get = env.get_method_id( - JClass::from( - super::classcache::get_class("io/github/gedgygedgy/rust/task/PollResult") - .unwrap() - .as_obj(), - ), - "get", - "()Ljava/lang/Object;", - )?; - Ok(Self { - internal: obj, - get, - env, - }) +impl<'a> JPollResult<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = + super::classcache::get_class("io/github/gedgygedgy/rust/task/PollResult").unwrap(); + let get = env.get_method_id(<&JClass>::from(class.as_obj()), "get", "()Ljava/lang/Object;")?; + Ok(Self { internal: obj, get }) } - pub fn get(&self) -> Result> { - self.env - .call_method_unchecked(self.internal, self.get, ReturnType::Object, &[])? + pub fn get(&self, env: &mut JNIEnv<'a>) -> Result> { + unsafe { env.call_method_unchecked(&self.internal, self.get, ReturnType::Object, &[]) }? .l() } } -impl<'a: 'b, 'b> ::std::ops::Deref for JPollResult<'a, 'b> { +impl<'a> ::std::ops::Deref for JPollResult<'a> { type Target = JObject<'a>; fn deref(&self) -> &Self::Target { @@ -63,8 +48,8 @@ impl<'a: 'b, 'b> ::std::ops::Deref for JPollResult<'a, 'b> { } } -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(other: JPollResult<'a, 'b>) -> JObject<'a> { +impl<'a> From> for JObject<'a> { + fn from(other: JPollResult<'a>) -> JObject<'a> { other.internal } } @@ -76,7 +61,9 @@ mod test { #[test] fn test_waker_wake() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); + let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -89,12 +76,12 @@ mod test { assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - env.call_method(jwaker, "wake", "()V", &[]).unwrap(); + env.call_method(&jwaker, "wake", "()V", &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), true); data.set_value(false); - env.call_method(jwaker, "wake", "()V", &[]).unwrap(); + env.call_method(&jwaker, "wake", "()V", &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); }); @@ -102,7 +89,9 @@ mod test { #[test] fn test_waker_close_wake() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); + let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -115,11 +104,11 @@ mod test { assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - env.call_method(jwaker, "close", "()V", &[]).unwrap(); + env.call_method(&jwaker, "close", "()V", &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); - env.call_method(jwaker, "wake", "()V", &[]).unwrap(); + env.call_method(&jwaker, "wake", "()V", &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); }); diff --git a/src/droidplug/jni_utils/uuid.rs b/src/droidplug/jni_utils/uuid.rs index 2c7b7faf..c330ce7b 100644 --- a/src/droidplug/jni_utils/uuid.rs +++ b/src/droidplug/jni_utils/uuid.rs @@ -1,7 +1,7 @@ use jni::{ JNIEnv, errors::Result, - objects::{AutoLocal, JMethodID, JObject}, + objects::{JMethodID, JObject}, signature::{Primitive, ReturnType}, sys::jlong, }; @@ -9,71 +9,69 @@ use uuid::Uuid; /// Wrapper for [`JObject`]s that contain `java.util.UUID`. Provides methods /// to convert to and from a [`Uuid`]. -pub struct JUuid<'a: 'b, 'b> { +pub struct JUuid<'a> { internal: JObject<'a>, get_least_significant_bits: JMethodID, get_most_significant_bits: JMethodID, - env: &'b JNIEnv<'a>, } -impl<'a: 'b, 'b> JUuid<'a, 'b> { - pub fn from_env(env: &'b JNIEnv<'a>, obj: JObject<'a>) -> Result { - let class = env.auto_local(env.find_class("java/util/UUID")?); - Self::from_env_impl(env, obj, class) +impl<'a> JUuid<'a> { + pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + let class = env.find_class("java/util/UUID")?; + let get_least_significant_bits = + env.get_method_id(&class, "getLeastSignificantBits", "()J")?; + let get_most_significant_bits = + env.get_method_id(&class, "getMostSignificantBits", "()J")?; + Ok(Self { + internal: obj, + get_least_significant_bits, + get_most_significant_bits, + }) } - pub fn new(env: &'b JNIEnv<'a>, uuid: Uuid) -> Result { + pub fn new(env: &mut JNIEnv<'a>, uuid: Uuid) -> Result { let val = uuid.as_u128(); let least = (val & 0xFFFFFFFFFFFFFFFF) as jlong; let most = ((val >> 64) & 0xFFFFFFFFFFFFFFFF) as jlong; - let class = env.auto_local(env.find_class("java/util/UUID")?); + let class = env.find_class("java/util/UUID")?; let obj = env.new_object(&class, "(JJ)V", &[most.into(), least.into()])?; - Self::from_env_impl(env, obj, class) + let get_least_significant_bits = + env.get_method_id(&class, "getLeastSignificantBits", "()J")?; + let get_most_significant_bits = + env.get_method_id(&class, "getMostSignificantBits", "()J")?; + Ok(Self { + internal: obj, + get_least_significant_bits, + get_most_significant_bits, + }) } - pub fn as_uuid(&self) -> Result { - let least = self - .env - .call_method_unchecked( - self.internal, + pub fn as_uuid(&self, env: &mut JNIEnv<'a>) -> Result { + let least = unsafe { + env.call_method_unchecked( + &self.internal, self.get_least_significant_bits, ReturnType::Primitive(Primitive::Long), &[], - )? - .j()? as u64; - let most = self - .env - .call_method_unchecked( - self.internal, + ) + }? + .j()? as u64; + let most = unsafe { + env.call_method_unchecked( + &self.internal, self.get_most_significant_bits, ReturnType::Primitive(Primitive::Long), &[], - )? - .j()? as u64; + ) + }? + .j()? as u64; let val = ((most as u128) << 64) | (least as u128); Ok(Uuid::from_u128(val)) } - - fn from_env_impl( - env: &'b JNIEnv<'a>, - obj: JObject<'a>, - class: AutoLocal<'a, 'b>, - ) -> Result { - let get_least_significant_bits = - env.get_method_id(&class, "getLeastSignificantBits", "()J")?; - let get_most_significant_bits = - env.get_method_id(&class, "getMostSignificantBits", "()J")?; - Ok(Self { - internal: obj, - get_least_significant_bits, - get_most_significant_bits, - env, - }) - } } -impl<'a: 'b, 'b> ::std::ops::Deref for JUuid<'a, 'b> { +impl<'a> ::std::ops::Deref for JUuid<'a> { type Target = JObject<'a>; fn deref(&self) -> &Self::Target { @@ -81,8 +79,8 @@ impl<'a: 'b, 'b> ::std::ops::Deref for JUuid<'a, 'b> { } } -impl<'a: 'b, 'b> From> for JObject<'a> { - fn from(other: JUuid<'a, 'b>) -> JObject<'a> { +impl<'a> From> for JObject<'a> { + fn from(other: JUuid<'a>) -> JObject<'a> { other.internal } } @@ -115,7 +113,8 @@ mod test { #[test] fn test_uuid_new() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); for test in TESTS { let most = test.most as jlong; let least = test.least as jlong; @@ -124,12 +123,12 @@ mod test { let obj: JObject = uuid_obj.into(); let actual_most = env - .call_method(obj, "getMostSignificantBits", "()J", &[]) + .call_method(&obj, "getMostSignificantBits", "()J", &[]) .unwrap() .j() .unwrap(); let actual_least = env - .call_method(obj, "getLeastSignificantBits", "()J", &[]) + .call_method(&obj, "getLeastSignificantBits", "()J", &[]) .unwrap() .j() .unwrap(); @@ -141,7 +140,8 @@ mod test { #[test] fn test_uuid_as_uuid() { - test_utils::JVM_ENV.with(|env| { + test_utils::JVM_ENV.with(|cell| { + let env = &mut *cell.borrow_mut(); for test in TESTS { let most = test.most as jlong; let least = test.least as jlong; @@ -151,7 +151,7 @@ mod test { .unwrap(); let uuid_obj = JUuid::from_env(env, obj).unwrap(); - assert_eq!(uuid_obj.as_uuid().unwrap(), Uuid::from_u128(test.uuid)); + assert_eq!(uuid_obj.as_uuid(env).unwrap(), Uuid::from_u128(test.uuid)); } }); } diff --git a/src/droidplug/mod.rs b/src/droidplug/mod.rs index eed9db6a..d759835f 100644 --- a/src/droidplug/mod.rs +++ b/src/droidplug/mod.rs @@ -10,7 +10,7 @@ mod jni_utils; static GLOBAL_ADAPTER: OnceCell = OnceCell::new(); -pub fn init(env: &JNIEnv) -> crate::Result<()> { +pub fn init(env: &mut JNIEnv) -> crate::Result<()> { self::jni::init(env)?; GLOBAL_ADAPTER.get_or_try_init(|| adapter::Adapter::new())?; Ok(()) diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 40576985..797414a3 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -1,6 +1,5 @@ use super::jni_utils::{ arrays::byte_array_to_vec, - exceptions::try_block, future::{JFuture, JSendFuture}, stream::JSendStream, task::JPollResult, @@ -17,7 +16,7 @@ use async_trait::async_trait; use futures::stream::Stream; use jni::{ JNIEnv, - objects::{GlobalRef, JList, JObject}, + objects::{GlobalRef, JClass, JObject, JString, JValue}, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -25,7 +24,6 @@ use serde::{Deserialize, Serialize}; use serde_cr as serde; use std::{ collections::BTreeSet, - convert::TryFrom, fmt::{self, Debug, Display, Formatter}, pin::Pin, sync::atomic::{AtomicU16, Ordering}, @@ -37,7 +35,7 @@ use super::jni::{ global_jvm, objects::{JBluetoothGattCharacteristic, JBluetoothGattService, JPeripheral}, }; -use jni::objects::JClass; + #[cfg_attr( feature = "serde", derive(Serialize, Deserialize), @@ -51,107 +49,74 @@ impl Display for PeripheralId { } } -fn get_poll_result<'a: 'b, 'b>( - env: &'b JNIEnv<'a>, - result: JPollResult<'a, 'b>, +fn get_poll_result<'a>( + env: &mut JNIEnv<'a>, + result_ref: &GlobalRef, ) -> Result> { - try_block(env, || Ok(Ok(result.get()?))) - .catch( - JClass::from( - super::jni_utils::classcache::get_class( - "io/github/gedgygedgy/rust/future/FutureException", - ) - .unwrap() - .as_obj(), - ), - |ex| { + let result_obj = env.new_local_ref(result_ref)?; + let poll_result = JPollResult::from_env(env, result_obj)?; + + match poll_result.get(env) { + Ok(obj) => Ok(obj), + Err(jni::errors::Error::JavaException) => { + let ex = env.exception_occurred()?; + env.exception_clear()?; + + let future_exception_class = super::jni_utils::classcache::get_class( + "io/github/gedgygedgy/rust/future/FutureException", + ) + .unwrap(); + + if env.is_instance_of(&ex, <&JClass>::from(future_exception_class.as_obj()))? { let cause = env - .call_method(ex, "getCause", "()Ljava/lang/Throwable;", &[])? + .call_method(&ex, "getCause", "()Ljava/lang/Throwable;", &[])? .l()?; - if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/NotConnectedException", - ) - .unwrap() - .as_obj(), - ), - )? { - Ok(Err(Error::NotConnected)) - } else if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/PermissionDeniedException", - ) - .unwrap() - .as_obj(), - ), - )? { - Ok(Err(Error::PermissionDenied)) - } else if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/UnexpectedCallbackException", - ) - .unwrap() - .as_obj(), - ), + + let check = |name: &str| -> jni::errors::Result { + let cls = super::jni_utils::classcache::get_class(name).unwrap(); + env.is_instance_of(&cause, <&JClass>::from(cls.as_obj())) + }; + + if check("com/nonpolynomial/btleplug/android/impl/NotConnectedException")? { + Err(Error::NotConnected) + } else if check( + "com/nonpolynomial/btleplug/android/impl/PermissionDeniedException", )? { - Ok(Err(Error::UnexpectedCallback)) - } else if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/UnexpectedCharacteristicException", - ) - .unwrap() - .as_obj(), - ), + Err(Error::PermissionDenied) + } else if check( + "com/nonpolynomial/btleplug/android/impl/UnexpectedCallbackException", )? { - Ok(Err(Error::UnexpectedCharacteristic)) - } else if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/NoSuchCharacteristicException", - ) - .unwrap() - .as_obj(), - ), + Err(Error::UnexpectedCallback) + } else if check( + "com/nonpolynomial/btleplug/android/impl/UnexpectedCharacteristicException", )? { - Ok(Err(Error::NoSuchCharacteristic)) - } else if env.is_instance_of( - cause, - JClass::from( - super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", - ) - .unwrap() - .as_obj(), - ), + Err(Error::UnexpectedCharacteristic) + } else if check( + "com/nonpolynomial/btleplug/android/impl/NoSuchCharacteristicException", )? { - Ok(Err(Error::NoAdapterAvailable)) - } else if env.is_instance_of( - cause, - "java/lang/RuntimeException", + Err(Error::NoSuchCharacteristic) + } else if check( + "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", )? { + Err(Error::NoAdapterAvailable) + } else if env.is_instance_of(&cause, "java/lang/RuntimeException")? { let msg = env - .call_method(cause, "getMessage", "()Ljava/lang/String;", &[]) - .unwrap() - .l() - .unwrap(); - let msgstr:String = env.get_string(msg.into()).unwrap().into(); - Ok(Err(Error::RuntimeError(msgstr))) + .call_method(&cause, "getMessage", "()Ljava/lang/String;", &[])? + .l()?; + let jstr: JString = msg.into(); + let msgstr: String = env.get_string(&jstr)?.into(); + Err(Error::RuntimeError(msgstr)) } else { - env.throw(ex)?; - Err(jni::errors::Error::JavaException) + env.throw(&ex)?; + Err(jni::errors::Error::JavaException.into()) } - }, - ) - .result()? + } else { + env.throw(&ex)?; + Err(jni::errors::Error::JavaException.into()) + } + } + Err(e) => Err(e.into()), + } } #[derive(Debug)] @@ -171,11 +136,12 @@ pub struct Peripheral { } impl Peripheral { - pub(crate) fn new(env: &JNIEnv, adapter: JObject, addr: BDAddr) -> Result { + pub(crate) fn new(env: &mut JNIEnv, adapter: JObject, addr: BDAddr) -> Result { let obj = JPeripheral::new(env, adapter, addr)?; + let internal = env.new_global_ref(&*obj)?; Ok(Self { addr, - internal: env.new_global_ref(obj)?, + internal, shared: Arc::new(Mutex::new(PeripheralShared { services: BTreeSet::new(), characteristics: BTreeSet::new(), @@ -188,20 +154,20 @@ impl Peripheral { pub(crate) fn report_properties(&self, properties: PeripheralProperties) { let mut guard = self.shared.lock().unwrap(); - guard.properties = Some(properties); } fn with_obj( &self, - f: impl FnOnce(&JNIEnv, JPeripheral) -> std::result::Result, + f: impl FnOnce(&mut JNIEnv, &JPeripheral) -> std::result::Result, ) -> std::result::Result where E: From<::jni::errors::Error>, { - let env = global_jvm().get_env()?; - let obj = JPeripheral::from_env(&env, self.internal.as_obj())?; - f(&env, obj) + let mut env = global_jvm().get_env()?; + let local_obj = env.new_local_ref(&self.internal)?; + let obj = JPeripheral::from_env(&mut env, local_obj)?; + f(&mut env, &obj) } async fn set_characteristic_notification( @@ -211,13 +177,11 @@ impl Peripheral { ) -> Result<()> { let future = self.with_obj(|env, obj| { let uuid_obj = JUuid::new(env, characteristic.uuid)?; - JSendFuture::try_from(obj.set_characteristic_notification(uuid_obj, enable)?) + let future = obj.set_characteristic_notification(env, &uuid_obj, enable)?; + JSendFuture::new(env, &future) })?; let result_ref = future.await?; - self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - get_poll_result(env, result).map(|_| {}) - }) + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) } } @@ -229,7 +193,6 @@ impl Debug for Peripheral { #[async_trait] impl api::Peripheral for Peripheral { - /// Returns the unique identifier of the peripheral. fn id(&self) -> PeripheralId { PeripheralId(self.addr) } @@ -253,19 +216,19 @@ impl api::Peripheral for Peripheral { } async fn is_connected(&self) -> Result { - self.with_obj(|_env, obj| Ok(obj.is_connected()?)) + self.with_obj(|env, obj| Ok(obj.is_connected(env)?)) } async fn connect(&self) -> Result<()> { - let future = self.with_obj(|env, obj| JSendFuture::try_from(obj.connect()?))?; - let result_ref = future.await?; - self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - get_poll_result(env, result).map(|_| {}) + let future = self.with_obj(|env, obj| { + let future = obj.connect(env)?; + JSendFuture::new(env, &future) })?; + let result_ref = future.await?; + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {}))?; // Query the system-cached device name and update local_name - self.with_obj(|_env, obj| -> std::result::Result<(), Error> { - if let Ok(Some(name)) = obj.get_device_name() { + self.with_obj(|env, obj| -> std::result::Result<(), Error> { + if let Ok(Some(name)) = obj.get_device_name(env) { let mut guard = self.shared.lock().map_err(Into::::into)?; if let Some(ref mut props) = guard.properties { props.local_name = Some(name); @@ -275,13 +238,14 @@ impl api::Peripheral for Peripheral { })?; // Auto-negotiate maximum MTU (517) after connection let mtu_future = self.with_obj(|env, obj| { - JSendFuture::try_from(JFuture::from_env(env, obj.request_mtu(517)?)?) + let mtu_obj = obj.request_mtu(env, 517)?; + let mtu_future = JFuture::from_env(env, mtu_obj)?; + JSendFuture::new(env, &mtu_future) })?; let mtu_result_ref = mtu_future.await?; self.with_obj(|env, _obj| -> Result<()> { - let mtu_result = JPollResult::from_env(env, mtu_result_ref.as_obj())?; - let mtu_obj = get_poll_result(env, mtu_result)?; - let mtu_val = env.call_method(mtu_obj, "intValue", "()I", &[])?.i()?; + let mtu_obj = get_poll_result(env, &mtu_result_ref)?; + let mtu_val = env.call_method(&mtu_obj, "intValue", "()I", &[])?.i()?; self.mtu.store(mtu_val as u16, Ordering::Relaxed); Ok(()) })?; @@ -289,53 +253,54 @@ impl api::Peripheral for Peripheral { } async fn disconnect(&self) -> Result<()> { - let future = self.with_obj(|env, obj| JSendFuture::try_from(obj.disconnect()?))?; + let future = self.with_obj(|env, obj| { + let future = obj.disconnect(env)?; + JSendFuture::new(env, &future) + })?; let result_ref = future.await?; - self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - get_poll_result(env, result).map(|_| {}) - }) + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) } - /// The set of services we've discovered for this device. This will be empty until - /// `discover_services` is called. fn services(&self) -> BTreeSet { let guard = self.shared.lock().unwrap(); (&guard.services).clone() } async fn discover_services(&self) -> Result<()> { - let future = self.with_obj(|env, obj| JSendFuture::try_from(obj.discover_services()?))?; + let future = self.with_obj(|env, obj| { + let future = obj.discover_services(env)?; + JSendFuture::new(env, &future) + })?; let result_ref = future.await?; self.with_obj(|env, _obj| { use std::iter::FromIterator; - let result = JPollResult::from_env(env, result_ref.as_obj())?; - let obj = get_poll_result(env, result)?; - let list = JList::from_env(env, obj)?; + let obj = get_poll_result(env, &result_ref)?; + let size = env.call_method(&obj, "size", "()I", &[])?.i()?; let mut peripheral_services = Vec::new(); let mut peripheral_characteristics = Vec::new(); - for service in list.iter()? { - let service = JBluetoothGattService::from_env(env, service)?; + for i in 0..size { + let svc_obj = env + .call_method(&obj, "get", "(I)Ljava/lang/Object;", &[JValue::from(i)])? + .l()?; + let service = JBluetoothGattService::from_env(env, svc_obj)?; let mut characteristics = BTreeSet::::new(); - for characteristic in service.get_characteristics()? { + for characteristic in service.get_characteristics(env)? { let mut descriptors = BTreeSet::new(); - for descriptor in characteristic.get_descriptors()? { + for descriptor in characteristic.get_descriptors(env)? { descriptors.insert(Descriptor { - uuid: descriptor.get_uuid()?, - service_uuid: service.get_uuid()?, - characteristic_uuid: characteristic.get_uuid()?, + uuid: descriptor.get_uuid(env)?, + service_uuid: service.get_uuid(env)?, + characteristic_uuid: characteristic.get_uuid(env)?, }); } let char = Characteristic { - service_uuid: service.get_uuid()?, - uuid: characteristic.get_uuid()?, - properties: characteristic.get_properties()?, + service_uuid: service.get_uuid(env)?, + uuid: characteristic.get_uuid(env)?, + properties: characteristic.get_properties(env)?, descriptors: descriptors.clone(), }; - // Only consider the first characteristic of each UUID - // This "should" be unique, but of course it's not enforced if characteristics .iter() .filter(|c| c.service_uuid == char.service_uuid && c.uuid == char.uuid) @@ -347,7 +312,7 @@ impl api::Peripheral for Peripheral { } } peripheral_services.push(Service { - uuid: service.get_uuid()?, + uuid: service.get_uuid(env)?, primary: service.is_primary()?, characteristics, }) @@ -372,25 +337,24 @@ impl api::Peripheral for Peripheral { WriteType::WithResponse => 2, WriteType::WithoutResponse => 1, }; - JSendFuture::try_from(obj.write(uuid, data_obj.into(), write_type)?) + let future = obj.write(env, &uuid, &data_obj.into(), write_type)?; + JSendFuture::new(env, &future) })?; let result_ref = future.await?; - self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - get_poll_result(env, result).map(|_| {}) - }) + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) } async fn read(&self, characteristic: &Characteristic) -> Result> { let future = self.with_obj(|env, obj| { let uuid = JUuid::new(env, characteristic.uuid)?; - JSendFuture::try_from(obj.read(uuid)?) + let future = obj.read(env, &uuid)?; + JSendFuture::new(env, &future) })?; let result_ref = future.await?; self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - let bytes = get_poll_result(env, result)?; - Ok(byte_array_to_vec(env, bytes.into_raw())?) + let bytes_obj = get_poll_result(env, &result_ref)?; + let bytes_arr = unsafe { jni::objects::JByteArray::from_raw(bytes_obj.into_raw()) }; + Ok(byte_array_to_vec(env, &bytes_arr)?) }) } @@ -407,15 +371,19 @@ impl api::Peripheral for Peripheral { async fn notifications(&self) -> Result + Send>>> { use futures::stream::StreamExt; let shared = self.shared.clone(); - let stream = self.with_obj(|_env, obj| JSendStream::try_from(obj.get_notifications()?))?; + let stream = self.with_obj(|env, obj| { + let stream = obj.get_notifications(env)?; + JSendStream::new(env, &stream) + })?; let stream = stream .map(move |item| match item { Ok(item) => { - let env = global_jvm().get_env()?; - let item = item.as_obj(); - let characteristic = JBluetoothGattCharacteristic::from_env(&env, item)?; - let uuid = characteristic.get_uuid()?; - let value = characteristic.get_value()?; + let mut env = global_jvm().get_env()?; + let local_obj = env.new_local_ref(item.as_obj())?; + let characteristic = + JBluetoothGattCharacteristic::from_env(&mut env, local_obj)?; + let uuid = characteristic.get_uuid(&mut env)?; + let value = characteristic.get_value(&mut env)?; let service_uuid = shared .lock() .ok() @@ -441,13 +409,14 @@ impl api::Peripheral for Peripheral { async fn read_rssi(&self) -> Result { let future = self.with_obj(|env, obj| { - JSendFuture::try_from(JFuture::from_env(env, obj.read_remote_rssi()?)?) + let rssi_obj = obj.read_remote_rssi(env)?; + let rssi_future = JFuture::from_env(env, rssi_obj)?; + JSendFuture::new(env, &rssi_future) })?; let result_ref = future.await?; self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - let rssi_obj = get_poll_result(env, result)?; - let rssi_val = env.call_method(rssi_obj, "intValue", "()I", &[])?.i()?; + let rssi_obj = get_poll_result(env, &result_ref)?; + let rssi_val = env.call_method(&rssi_obj, "intValue", "()I", &[])?.i()?; Ok(rssi_val as i16) }) } @@ -457,46 +426,45 @@ impl api::Peripheral for Peripheral { let characteristic = JUuid::new(env, descriptor.characteristic_uuid)?; let uuid = JUuid::new(env, descriptor.uuid)?; let data_obj = super::jni_utils::arrays::slice_to_byte_array(env, data)?; - JSendFuture::try_from(obj.write_descriptor(characteristic, uuid, data_obj.into())?) + let future = obj.write_descriptor(env, &characteristic, &uuid, &data_obj.into())?; + JSendFuture::new(env, &future) })?; let result_ref = future.await?; - self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - get_poll_result(env, result).map(|_| {}) - }) + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) } async fn read_descriptor(&self, descriptor: &Descriptor) -> Result> { let future = self.with_obj(|env, obj| { let characteristic = JUuid::new(env, descriptor.characteristic_uuid)?; let uuid = JUuid::new(env, descriptor.uuid)?; - JSendFuture::try_from(obj.read_descriptor(characteristic, uuid)?) + let future = obj.read_descriptor(env, &characteristic, &uuid)?; + JSendFuture::new(env, &future) })?; let result_ref = future.await?; self.with_obj(|env, _obj| { - let result = JPollResult::from_env(env, result_ref.as_obj())?; - let bytes = get_poll_result(env, result)?; - Ok(byte_array_to_vec(env, bytes.into_raw())?) + let bytes_obj = get_poll_result(env, &result_ref)?; + let bytes_arr = unsafe { jni::objects::JByteArray::from_raw(bytes_obj.into_raw()) }; + Ok(byte_array_to_vec(env, &bytes_arr)?) }) } async fn connection_parameters(&self) -> Result> { - self.with_obj(|_env, obj| { + self.with_obj(|env, obj| { Ok(obj - .get_connection_parameters() + .get_connection_parameters(env) .map_err(|e| Error::Other(format!("{:?}", e).into()))?) }) } async fn request_connection_parameters(&self, preset: ConnectionParameterPreset) -> Result<()> { let priority = match preset { - ConnectionParameterPreset::Balanced => 0, // CONNECTION_PRIORITY_BALANCED - ConnectionParameterPreset::ThroughputOptimized => 1, // CONNECTION_PRIORITY_HIGH - ConnectionParameterPreset::PowerOptimized => 2, // CONNECTION_PRIORITY_LOW_POWER + ConnectionParameterPreset::Balanced => 0, + ConnectionParameterPreset::ThroughputOptimized => 1, + ConnectionParameterPreset::PowerOptimized => 2, }; - self.with_obj(|_env, obj| { + self.with_obj(|env, obj| { let success = obj - .request_connection_priority(priority) + .request_connection_priority(env, priority) .map_err(|e| Error::Other(format!("{:?}", e).into()))?; if success { Ok(()) diff --git a/tests/android/rust/Cargo.toml b/tests/android/rust/Cargo.toml index 5ec67c7a..1aabb571 100644 --- a/tests/android/rust/Cargo.toml +++ b/tests/android/rust/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib"] [dependencies] btleplug = { path = "../../.." } -jni = "0.20" +jni = "0.21" once_cell = "1" tokio = { version = "1", features = ["full"] } uuid = "1" From f59c47e0db622d924e4566be3f3f2551a053fe66 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 18:50:08 -0700 Subject: [PATCH 04/77] fix: Update droidplug and test crate for jni 0.21 lifetime requirements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jni 0.21 tightened lifetime variance on JNIEnv, requiring explicit shared lifetimes wherever JObject and JNIEnv are used together. Changes: - `with_obj` closure: use HRTB `for<'env>` so JNIEnv and JPeripheral share the same lifetime, fixing the invariant mutable reference errors - `Peripheral::new`, `report_scan_result`, `adapter_report_scan_result_internal`, `adapter_report_scan_result` JNI callback: add explicit `'a` lifetime annotations tying env and JObject parameters together - `adapter_on_connection_state_changed_internal`: get address string before acquiring the MutexGuard from `get_rust_field` to avoid double-mutable borrow of env - `JUuid::as_obj().as_raw()` → `uuid.as_raw()` via Deref: `JObject::as_obj()` was removed in jni 0.21; JUuid already Derefs to JObject - Test crate `lib.rs`: update `run_test`, `initBtleplug`, and `jni_test!` macro to use `&mut JNIEnv` as required by the updated API Co-Authored-By: Claude Sonnet 4.6 --- src/droidplug/adapter.rs | 17 ++++----- src/droidplug/jni/mod.rs | 2 +- src/droidplug/jni/objects.rs | 14 +++---- src/droidplug/peripheral.rs | 8 ++-- tests/android/rust/Cargo.lock | 72 ++++++++++++++++++++++++++++++++++- tests/android/rust/src/lib.rs | 10 ++--- 6 files changed, 94 insertions(+), 29 deletions(-) diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index ed902ffd..70ec8088 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -57,10 +57,10 @@ impl Adapter { Ok(adapter) } - pub fn report_scan_result( + pub fn report_scan_result<'a>( &self, - env: &mut JNIEnv, - scan_result: JObject, + env: &mut JNIEnv<'a>, + scan_result: JObject<'a>, ) -> Result { let scan_result = JScanResult::from_env(env, scan_result)?; let (addr, properties): (BDAddr, Option) = @@ -201,10 +201,10 @@ impl Central for Adapter { } } -pub(crate) fn adapter_report_scan_result_internal( - env: &mut JNIEnv, +pub(crate) fn adapter_report_scan_result_internal<'a>( + env: &mut JNIEnv<'a>, obj: &JObject, - scan_result: JObject, + scan_result: JObject<'a>, ) -> crate::Result<()> { let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, "handle") }?; let adapter_clone = adapter.clone(); @@ -219,10 +219,9 @@ pub(crate) fn adapter_on_connection_state_changed_internal( addr: JString, connected: jboolean, ) -> crate::Result<()> { + let addr_str: String = env.get_string(&addr)?.into(); + let addr = BDAddr::from_str(&addr_str)?; let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, "handle") }?; - let addr_str = env.get_string(&addr)?; - let addr_str = addr_str.to_str().map_err(|e| Error::Other(e.into()))?; - let addr = BDAddr::from_str(addr_str)?; adapter.manager.emit(if connected != 0 { CentralEvent::DeviceConnected(PeripheralId(addr)) } else { diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index a5368b73..6c94668d 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -133,7 +133,7 @@ impl From<::jni::errors::Error> for crate::Error { } } -extern "C" fn adapter_report_scan_result(mut env: JNIEnv, obj: JObject, scan_result: JObject) { +extern "C" fn adapter_report_scan_result<'a>(mut env: JNIEnv<'a>, obj: JObject, scan_result: JObject<'a>) { let _ = super::adapter::adapter_report_scan_result_internal(&mut env, &obj, scan_result); } diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index f1b264a6..62dab6f8 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -195,7 +195,7 @@ impl<'a> JPeripheral<'a> { self.read, ReturnType::Object, &[jvalue { - l: uuid.as_obj().as_raw(), + l: uuid.as_raw(), }], ) }? @@ -217,7 +217,7 @@ impl<'a> JPeripheral<'a> { ReturnType::Object, &[ jvalue { - l: uuid.as_obj().as_raw(), + l: uuid.as_raw(), }, jvalue { l: data.as_raw(), @@ -243,7 +243,7 @@ impl<'a> JPeripheral<'a> { ReturnType::Object, &[ jvalue { - l: uuid.as_obj().as_raw(), + l: uuid.as_raw(), }, jvalue { z: enable as u8, @@ -281,10 +281,10 @@ impl<'a> JPeripheral<'a> { ReturnType::Object, &[ jvalue { - l: characteristic.as_obj().as_raw(), + l: characteristic.as_raw(), }, jvalue { - l: uuid.as_obj().as_raw(), + l: uuid.as_raw(), }, ], ) @@ -396,10 +396,10 @@ impl<'a> JPeripheral<'a> { ReturnType::Object, &[ jvalue { - l: characteristic.as_obj().as_raw(), + l: characteristic.as_raw(), }, jvalue { - l: uuid.as_obj().as_raw(), + l: uuid.as_raw(), }, jvalue { l: data.as_raw(), diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 797414a3..b7e25052 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -29,8 +29,6 @@ use std::{ sync::atomic::{AtomicU16, Ordering}, sync::{Arc, Mutex}, }; -use uuid::Uuid; - use super::jni::{ global_jvm, objects::{JBluetoothGattCharacteristic, JBluetoothGattService, JPeripheral}, @@ -72,7 +70,7 @@ fn get_poll_result<'a>( .call_method(&ex, "getCause", "()Ljava/lang/Throwable;", &[])? .l()?; - let check = |name: &str| -> jni::errors::Result { + let mut check = |name: &str| -> jni::errors::Result { let cls = super::jni_utils::classcache::get_class(name).unwrap(); env.is_instance_of(&cause, <&JClass>::from(cls.as_obj())) }; @@ -136,7 +134,7 @@ pub struct Peripheral { } impl Peripheral { - pub(crate) fn new(env: &mut JNIEnv, adapter: JObject, addr: BDAddr) -> Result { + pub(crate) fn new<'a>(env: &mut JNIEnv<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { let obj = JPeripheral::new(env, adapter, addr)?; let internal = env.new_global_ref(&*obj)?; Ok(Self { @@ -159,7 +157,7 @@ impl Peripheral { fn with_obj( &self, - f: impl FnOnce(&mut JNIEnv, &JPeripheral) -> std::result::Result, + f: impl for<'env> FnOnce(&mut JNIEnv<'env>, &JPeripheral<'env>) -> std::result::Result, ) -> std::result::Result where E: From<::jni::errors::Error>, diff --git a/tests/android/rust/Cargo.lock b/tests/android/rust/Cargo.lock index 4d1091fc..ab29b097 100644 --- a/tests/android/rust/Cargo.lock +++ b/tests/android/rust/Cargo.lock @@ -331,16 +331,18 @@ dependencies = [ [[package]] name = "jni" -version = "0.19.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", + "cfg-if", "combine", "jni-sys", "log", "thiserror 1.0.69", "walkdir", + "windows-sys 0.45.0", ] [[package]] @@ -940,6 +942,15 @@ 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.59.0" @@ -967,6 +978,21 @@ 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" @@ -1009,6 +1035,12 @@ 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" @@ -1021,6 +1053,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[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" @@ -1033,6 +1071,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[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" @@ -1057,6 +1101,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[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" @@ -1069,6 +1119,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[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" @@ -1081,6 +1137,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[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" @@ -1093,6 +1155,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[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/tests/android/rust/src/lib.rs b/tests/android/rust/src/lib.rs index f56d58aa..b234357f 100644 --- a/tests/android/rust/src/lib.rs +++ b/tests/android/rust/src/lib.rs @@ -58,7 +58,7 @@ fn runtime() -> &'static Runtime { const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(55); /// Run a test function on the global runtime, converting panics to JNI exceptions. -fn run_test(env: &JNIEnv, test_name: &str, f: impl std::future::Future) { +fn run_test(env: &mut JNIEnv, test_name: &str, f: impl std::future::Future) { log::info!("[START] {}", test_name); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { runtime().block_on(async { @@ -86,7 +86,7 @@ fn run_test(env: &JNIEnv, test_name: &str, f: impl std::future::Future { #[unsafe(no_mangle)] - pub extern "system" fn $jni_name(env: JNIEnv, _class: JClass) { - run_test(&env, stringify!($test_fn), $test_fn()); + pub extern "system" fn $jni_name(mut env: JNIEnv, _class: JClass) { + run_test(&mut env, stringify!($test_fn), $test_fn()); } }; } From 3f8cef15d527d79236014ef8ea623ad8eb202020 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 19:29:44 -0700 Subject: [PATCH 05/77] build: Bump jni from 0.21 to 0.22 Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 194 ++++++++++++---------------------- Cargo.toml | 4 +- tests/android/rust/Cargo.toml | 2 +- 3 files changed, 69 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 986a5db5..2eb3f812 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,7 +58,7 @@ dependencies = [ "log", "serde", "serde-xml-rs", - "thiserror 2.0.18", + "thiserror", "tokio", "uuid", ] @@ -96,7 +96,7 @@ dependencies = [ "serde_bytes", "serde_json", "static_assertions", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "toml", @@ -117,12 +117,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[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.4" @@ -436,27 +430,54 @@ dependencies = [ [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", "java-locator", + "jni-macros", "jni-sys", "libloading", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror", "walkdir", - "windows-sys 0.45.0", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "js-sys" @@ -497,12 +518,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "winapi", + "windows-link", ] [[package]] @@ -712,6 +733,15 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -757,7 +787,7 @@ checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" dependencies = [ "log", "serde", - "thiserror 2.0.18", + "thiserror", "xml", ] @@ -813,6 +843,22 @@ dependencies = [ "serde_core", ] +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -861,33 +907,13 @@ dependencies = [ "winapi-util", ] -[[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.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[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", + "thiserror-impl", ] [[package]] @@ -1126,22 +1152,6 @@ dependencies = [ "semver", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.11" @@ -1151,12 +1161,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows" version = "0.62.2" @@ -1258,15 +1262,6 @@ 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.59.0" @@ -1294,21 +1289,6 @@ 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" @@ -1351,12 +1331,6 @@ 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" @@ -1369,12 +1343,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[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" @@ -1387,12 +1355,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[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" @@ -1417,12 +1379,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[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" @@ -1435,12 +1391,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[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" @@ -1453,12 +1403,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[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" @@ -1471,12 +1415,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[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 48e190ab..3bbb65e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,11 +45,11 @@ dbus = "0.9.10" bluez-async = "0.8.2" [target.'cfg(target_os = "android")'.dependencies] -jni = "0.21.0" +jni = "0.22" once_cell = "1.21.3" [target.'cfg(not(target_os = "android"))'.dependencies] -jni = { version = "0.21.0", optional = true } +jni = { version = "0.22", optional = true } once_cell = { version = "1.21.3", optional = true } [target.'cfg(target_vendor = "apple")'.dependencies] diff --git a/tests/android/rust/Cargo.toml b/tests/android/rust/Cargo.toml index 1aabb571..0ebfb89b 100644 --- a/tests/android/rust/Cargo.toml +++ b/tests/android/rust/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib"] [dependencies] btleplug = { path = "../../.." } -jni = "0.21" +jni = "0.22" once_cell = "1" tokio = { version = "1", features = ["full"] } uuid = "1" From 0b81626eae25cd05daadc88021a6d61c40c8d1a6 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 19:38:24 -0700 Subject: [PATCH 06/77] refactor: Rename JNIEnv to Env and GlobalRef to Global across droidplug Phase 2 of jni 0.22 migration. Non-FFI function signatures now use Env<'a> instead of JNIEnv<'a>. GlobalRef fields in Clone structs are wrapped in Arc>>. Classcache stores Arc>>. extern "C" FFI functions and test_utils are left as-is for later phases. Co-Authored-By: Claude Opus 4.6 --- src/droidplug/adapter.rs | 21 +++--- src/droidplug/jni/mod.rs | 4 +- src/droidplug/jni/objects.rs | 98 +++++++++++++-------------- src/droidplug/jni_utils/arrays.rs | 8 +-- src/droidplug/jni_utils/classcache.rs | 14 ++-- src/droidplug/jni_utils/exceptions.rs | 26 +++---- src/droidplug/jni_utils/future.rs | 28 +++----- src/droidplug/jni_utils/stream.rs | 32 ++++----- src/droidplug/jni_utils/task.rs | 11 ++- src/droidplug/jni_utils/uuid.rs | 10 ++- src/droidplug/mod.rs | 4 +- src/droidplug/peripheral.rs | 18 ++--- 12 files changed, 127 insertions(+), 147 deletions(-) diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index 70ec8088..5470c7bd 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -13,8 +13,8 @@ use crate::{ use async_trait::async_trait; use futures::stream::Stream; use jni::{ - JNIEnv, - objects::{GlobalRef, JClass, JObject, JString}, + Env, + objects::{Global, JClass, JObject, JString}, sys::jboolean, }; use std::{ @@ -27,7 +27,7 @@ use std::{ #[derive(Clone)] pub struct Adapter { manager: Arc>, - internal: GlobalRef, + internal: Arc>>, } impl Debug for Adapter { @@ -47,7 +47,7 @@ impl Adapter { "()V", &[], )?; - let internal = env.new_global_ref(&obj)?; + let internal = Arc::new(env.new_global_ref(&obj)?); let adapter = Self { manager: Arc::new(AdapterManager::default()), internal, @@ -59,8 +59,9 @@ impl Adapter { pub fn report_scan_result<'a>( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, scan_result: JObject<'a>, + ) -> Result { let scan_result = JScanResult::from_env(env, scan_result)?; let (addr, properties): (BDAddr, Option) = @@ -87,7 +88,7 @@ impl Adapter { fn add(&self, address: BDAddr) -> Result { let mut env = global_jvm().get_env()?; - let local_adapter = env.new_local_ref(&self.internal)?; + let local_adapter = env.new_local_ref(self.internal.as_obj())?; let peripheral = Peripheral::new(&mut env, local_adapter, address)?; self.manager.add_peripheral(peripheral.clone()); Ok(peripheral) @@ -138,7 +139,7 @@ impl Central for Adapter { let filter = JScanFilter::new(&mut env, filter)?; let filter_obj: JObject = filter.into(); match env.call_method( - &self.internal, + self.internal.as_obj(), "startScan", "(Lcom/nonpolynomial/btleplug/android/impl/ScanFilter;)V", &[(&filter_obj).into()], @@ -173,7 +174,7 @@ impl Central for Adapter { async fn stop_scan(&self) -> Result<()> { let mut env = global_jvm().get_env()?; - env.call_method(&self.internal, "stopScan", "()V", &[])?; + env.call_method(self.internal.as_obj(), "stopScan", "()V", &[])?; Ok(()) } @@ -202,7 +203,7 @@ impl Central for Adapter { } pub(crate) fn adapter_report_scan_result_internal<'a>( - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, obj: &JObject, scan_result: JObject<'a>, ) -> crate::Result<()> { @@ -214,7 +215,7 @@ pub(crate) fn adapter_report_scan_result_internal<'a>( } pub(crate) fn adapter_on_connection_state_changed_internal( - env: &mut JNIEnv, + env: &mut Env, obj: &JObject, addr: JString, connected: jboolean, diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index 6c94668d..9453eab7 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,13 +1,13 @@ pub mod objects; -use ::jni::{JNIEnv, JavaVM, NativeMethod, objects::JObject}; +use ::jni::{Env, JNIEnv, JavaVM, NativeMethod, objects::JObject}; use jni::{objects::JString, sys::jboolean}; use once_cell::sync::OnceCell; use std::ffi::c_void; static GLOBAL_JVM: OnceCell = OnceCell::new(); -pub fn init(env: &mut JNIEnv) -> crate::Result<()> { +pub fn init(env: &mut Env) -> crate::Result<()> { if let Ok(()) = GLOBAL_JVM.set(env.get_java_vm()?) { let adapter_class = env.find_class("com/nonpolynomial/btleplug/android/impl/Adapter")?; diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 62dab6f8..7e43dbfb 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -1,6 +1,6 @@ use crate::droidplug::jni_utils::{future::JFuture, stream::JStream, uuid::JUuid}; use jni::{ - JNIEnv, + Env, errors::Result, objects::{JClass, JMethodID, JObject, JString}, signature::{Primitive, ReturnType}, @@ -45,7 +45,7 @@ impl<'a> From> for JObject<'a> { } impl<'a> JPeripheral<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class_static = crate::droidplug::jni_utils::classcache::get_class( "com/nonpolynomial/btleplug/android/impl/Peripheral", ) @@ -133,7 +133,7 @@ impl<'a> JPeripheral<'a> { }) } - pub fn new(env: &mut JNIEnv<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { + pub fn new(env: &mut Env<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { let addr_jstr = env.new_string(format!("{:X}", addr))?; let class_static = crate::droidplug::jni_utils::classcache::get_class( "com/nonpolynomial/btleplug/android/impl/Peripheral", @@ -147,7 +147,7 @@ impl<'a> JPeripheral<'a> { Self::from_env(env, obj) } - pub fn connect(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn connect(&self, env: &mut Env<'a>) -> Result> { let future_obj = unsafe { env.call_method_unchecked(&self.internal, self.connect, ReturnType::Object, &[]) }? @@ -155,7 +155,7 @@ impl<'a> JPeripheral<'a> { JFuture::from_env(env, future_obj) } - pub fn disconnect(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn disconnect(&self, env: &mut Env<'a>) -> Result> { let future_obj = unsafe { env.call_method_unchecked(&self.internal, self.disconnect, ReturnType::Object, &[]) }? @@ -163,7 +163,7 @@ impl<'a> JPeripheral<'a> { JFuture::from_env(env, future_obj) } - pub fn is_connected(&self, env: &mut JNIEnv<'a>) -> Result { + pub fn is_connected(&self, env: &mut Env<'a>) -> Result { unsafe { env.call_method_unchecked( &self.internal, @@ -175,7 +175,7 @@ impl<'a> JPeripheral<'a> { .z() } - pub fn discover_services(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn discover_services(&self, env: &mut Env<'a>) -> Result> { let future_obj = unsafe { env.call_method_unchecked( &self.internal, @@ -188,7 +188,7 @@ impl<'a> JPeripheral<'a> { JFuture::from_env(env, future_obj) } - pub fn read(&self, env: &mut JNIEnv<'a>, uuid: &JUuid<'a>) -> Result> { + pub fn read(&self, env: &mut Env<'a>, uuid: &JUuid<'a>) -> Result> { let future_obj = unsafe { env.call_method_unchecked( &self.internal, @@ -205,7 +205,7 @@ impl<'a> JPeripheral<'a> { pub fn write( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, uuid: &JUuid<'a>, data: &JObject<'a>, write_type: jint, @@ -232,7 +232,7 @@ impl<'a> JPeripheral<'a> { pub fn set_characteristic_notification( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, uuid: &JUuid<'a>, enable: bool, ) -> Result> { @@ -255,7 +255,7 @@ impl<'a> JPeripheral<'a> { JFuture::from_env(env, future_obj) } - pub fn get_notifications(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get_notifications(&self, env: &mut Env<'a>) -> Result> { let stream_obj = unsafe { env.call_method_unchecked( &self.internal, @@ -270,7 +270,7 @@ impl<'a> JPeripheral<'a> { pub fn read_descriptor( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, characteristic: &JUuid<'a>, uuid: &JUuid<'a>, ) -> Result> { @@ -293,7 +293,7 @@ impl<'a> JPeripheral<'a> { JFuture::from_env(env, future_obj) } - pub fn get_device_name(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get_device_name(&self, env: &mut Env<'a>) -> Result> { let obj = unsafe { env.call_method_unchecked( &self.internal, @@ -312,7 +312,7 @@ impl<'a> JPeripheral<'a> { } } - pub fn request_mtu(&self, env: &mut JNIEnv<'a>, mtu: jint) -> Result> { + pub fn request_mtu(&self, env: &mut Env<'a>, mtu: jint) -> Result> { unsafe { env.call_method_unchecked( &self.internal, @@ -326,7 +326,7 @@ impl<'a> JPeripheral<'a> { pub fn get_connection_parameters( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, ) -> Result> { let obj = unsafe { env.call_method_unchecked( @@ -354,7 +354,7 @@ impl<'a> JPeripheral<'a> { })) } - pub fn read_remote_rssi(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn read_remote_rssi(&self, env: &mut Env<'a>) -> Result> { unsafe { env.call_method_unchecked( &self.internal, @@ -368,7 +368,7 @@ impl<'a> JPeripheral<'a> { pub fn request_connection_priority( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, priority: jint, ) -> Result { unsafe { @@ -384,7 +384,7 @@ impl<'a> JPeripheral<'a> { pub fn write_descriptor( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, characteristic: &JUuid<'a>, uuid: &JUuid<'a>, data: &JObject<'a>, @@ -419,7 +419,7 @@ pub struct JBluetoothGattService<'a> { } impl<'a> JBluetoothGattService<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = env.find_class("android/bluetooth/BluetoothGattService")?; let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; @@ -436,7 +436,7 @@ impl<'a> JBluetoothGattService<'a> { Ok(true) } - pub fn get_uuid(&self, env: &mut JNIEnv<'a>) -> Result { + pub fn get_uuid(&self, env: &mut Env<'a>) -> Result { let obj = unsafe { env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) }? @@ -447,7 +447,7 @@ impl<'a> JBluetoothGattService<'a> { pub fn get_characteristics( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, ) -> Result>> { let obj = unsafe { env.call_method_unchecked( @@ -479,7 +479,7 @@ pub struct JBluetoothGattCharacteristic<'a> { } impl<'a> JBluetoothGattCharacteristic<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = env.find_class("android/bluetooth/BluetoothGattCharacteristic")?; let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; @@ -495,7 +495,7 @@ impl<'a> JBluetoothGattCharacteristic<'a> { }) } - pub fn get_uuid(&self, env: &mut JNIEnv<'a>) -> Result { + pub fn get_uuid(&self, env: &mut Env<'a>) -> Result { let obj = unsafe { env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) }? @@ -504,7 +504,7 @@ impl<'a> JBluetoothGattCharacteristic<'a> { uuid_obj.as_uuid(env) } - pub fn get_properties(&self, env: &mut JNIEnv<'a>) -> Result { + pub fn get_properties(&self, env: &mut Env<'a>) -> Result { let flags = unsafe { env.call_method_unchecked( &self.internal, @@ -517,7 +517,7 @@ impl<'a> JBluetoothGattCharacteristic<'a> { Ok(CharPropFlags::from_bits_truncate(flags as u8)) } - pub fn get_value(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get_value(&self, env: &mut Env<'a>) -> Result> { let value = unsafe { env.call_method_unchecked(&self.internal, self.get_value, ReturnType::Array, &[]) }? @@ -528,7 +528,7 @@ impl<'a> JBluetoothGattCharacteristic<'a> { pub fn get_descriptors( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, ) -> Result>> { let obj = unsafe { env.call_method_unchecked( @@ -557,7 +557,7 @@ pub struct JBluetoothGattDescriptor<'a> { } impl<'a> JBluetoothGattDescriptor<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = env.find_class("android/bluetooth/BluetoothGattDescriptor")?; let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; @@ -567,7 +567,7 @@ impl<'a> JBluetoothGattDescriptor<'a> { }) } - pub fn get_uuid(&self, env: &mut JNIEnv<'a>) -> Result { + pub fn get_uuid(&self, env: &mut Env<'a>) -> Result { let obj = unsafe { env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) }? @@ -583,7 +583,7 @@ pub struct JBluetoothDevice<'a> { } impl<'a> JBluetoothDevice<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = env.find_class("android/bluetooth/BluetoothDevice")?; let get_address = env.get_method_id(&class, "getAddress", "()Ljava/lang/String;")?; @@ -593,7 +593,7 @@ impl<'a> JBluetoothDevice<'a> { }) } - pub fn get_address(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get_address(&self, env: &mut Env<'a>) -> Result> { let obj = unsafe { env.call_method_unchecked(&self.internal, self.get_address, ReturnType::Object, &[]) }? @@ -607,7 +607,7 @@ pub struct JScanFilter<'a> { } impl<'a> JScanFilter<'a> { - pub fn new(env: &mut JNIEnv<'a>, filter: ScanFilter) -> Result { + pub fn new(env: &mut Env<'a>, filter: ScanFilter) -> Result { let string_class = env.find_class("java/lang/String")?; let uuids = env.new_object_array( filter.services.len() as i32, @@ -646,7 +646,7 @@ pub struct JScanResult<'a> { } impl<'a> JScanResult<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = env.find_class("android/bluetooth/le/ScanResult")?; let get_device = @@ -667,7 +667,7 @@ impl<'a> JScanResult<'a> { }) } - pub fn get_device(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get_device(&self, env: &mut Env<'a>) -> Result> { let obj = unsafe { env.call_method_unchecked(&self.internal, self.get_device, ReturnType::Object, &[]) }? @@ -675,7 +675,7 @@ impl<'a> JScanResult<'a> { JBluetoothDevice::from_env(env, obj) } - pub fn get_scan_record(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get_scan_record(&self, env: &mut Env<'a>) -> Result> { let obj = unsafe { env.call_method_unchecked( &self.internal, @@ -688,7 +688,7 @@ impl<'a> JScanResult<'a> { JScanRecord::from_env(env, obj) } - pub fn get_tx_power(&self, env: &mut JNIEnv<'a>) -> Result { + pub fn get_tx_power(&self, env: &mut Env<'a>) -> Result { unsafe { env.call_method_unchecked( &self.internal, @@ -700,7 +700,7 @@ impl<'a> JScanResult<'a> { .i() } - pub fn get_rssi(&self, env: &mut JNIEnv<'a>) -> Result { + pub fn get_rssi(&self, env: &mut Env<'a>) -> Result { unsafe { env.call_method_unchecked( &self.internal, @@ -714,7 +714,7 @@ impl<'a> JScanResult<'a> { pub fn to_peripheral_properties( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, ) -> std::result::Result<(BDAddr, Option), crate::Error> { use std::str::FromStr; @@ -872,7 +872,7 @@ impl<'a> ::std::ops::Deref for JScanRecord<'a> { } impl<'a> JScanRecord<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = env.find_class("android/bluetooth/le/ScanRecord")?; let get_device_name = env.get_method_id(&class, "getDeviceName", "()Ljava/lang/String;")?; @@ -895,7 +895,7 @@ impl<'a> JScanRecord<'a> { }) } - pub fn get_device_name(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get_device_name(&self, env: &mut Env<'a>) -> Result> { unsafe { env.call_method_unchecked( &self.internal, @@ -907,7 +907,7 @@ impl<'a> JScanRecord<'a> { .l() } - pub fn get_tx_power_level(&self, env: &mut JNIEnv<'a>) -> Result { + pub fn get_tx_power_level(&self, env: &mut Env<'a>) -> Result { unsafe { env.call_method_unchecked( &self.internal, @@ -921,7 +921,7 @@ impl<'a> JScanRecord<'a> { pub fn get_manufacturer_specific_data( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, ) -> Result> { let obj = unsafe { env.call_method_unchecked( @@ -935,7 +935,7 @@ impl<'a> JScanRecord<'a> { JSparseArray::from_env(env, obj) } - pub fn get_service_data(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get_service_data(&self, env: &mut Env<'a>) -> Result> { unsafe { env.call_method_unchecked( &self.internal, @@ -947,7 +947,7 @@ impl<'a> JScanRecord<'a> { .l() } - pub fn get_service_uuids(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get_service_uuids(&self, env: &mut Env<'a>) -> Result> { unsafe { env.call_method_unchecked( &self.internal, @@ -982,7 +982,7 @@ impl<'a> ::std::ops::Deref for JSparseArray<'a> { } impl<'a> JSparseArray<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = env.find_class("android/util/SparseArray")?; let size = env.get_method_id(&class, "size", "()I")?; @@ -996,7 +996,7 @@ impl<'a> JSparseArray<'a> { }) } - pub fn size(&self, env: &mut JNIEnv<'a>) -> Result { + pub fn size(&self, env: &mut Env<'a>) -> Result { unsafe { env.call_method_unchecked( &self.internal, @@ -1008,7 +1008,7 @@ impl<'a> JSparseArray<'a> { .i() } - pub fn key_at(&self, env: &mut JNIEnv<'a>, index: jint) -> Result { + pub fn key_at(&self, env: &mut Env<'a>, index: jint) -> Result { unsafe { env.call_method_unchecked( &self.internal, @@ -1020,7 +1020,7 @@ impl<'a> JSparseArray<'a> { .i() } - pub fn value_at(&self, env: &mut JNIEnv<'a>, index: jint) -> Result> { + pub fn value_at(&self, env: &mut Env<'a>, index: jint) -> Result> { unsafe { env.call_method_unchecked( &self.internal, @@ -1039,7 +1039,7 @@ pub struct JParcelUuid<'a> { } impl<'a> JParcelUuid<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = env.find_class("android/os/ParcelUuid")?; let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; @@ -1049,7 +1049,7 @@ impl<'a> JParcelUuid<'a> { }) } - pub fn get_uuid(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get_uuid(&self, env: &mut Env<'a>) -> Result> { let obj = unsafe { env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) }? diff --git a/src/droidplug/jni_utils/arrays.rs b/src/droidplug/jni_utils/arrays.rs index 435e8777..71918916 100644 --- a/src/droidplug/jni_utils/arrays.rs +++ b/src/droidplug/jni_utils/arrays.rs @@ -1,21 +1,19 @@ use jni::{ - JNIEnv, + Env, errors::Result, objects::JByteArray, sys::{jbyte, jint}, }; use std::slice; -/// Create a new Java byte array from the given slice. -pub fn slice_to_byte_array<'local>(env: &mut JNIEnv<'local>, slice: &[u8]) -> Result> { +pub fn slice_to_byte_array<'local>(env: &mut Env<'local>, slice: &[u8]) -> Result> { let obj = env.new_byte_array(slice.len() as jint)?; let slice = unsafe { &*(slice as *const [u8] as *const [jbyte]) }; env.set_byte_array_region(&obj, 0, slice)?; Ok(obj) } -/// Get a [`Vec`] of bytes from the given Java byte array. -pub fn byte_array_to_vec(env: &JNIEnv, array: &JByteArray) -> Result> { +pub fn byte_array_to_vec(env: &Env, array: &JByteArray) -> Result> { let size = env.get_array_length(array)? as usize; let mut result = Vec::with_capacity(size); unsafe { diff --git a/src/droidplug/jni_utils/classcache.rs b/src/droidplug/jni_utils/classcache.rs index f297e5d8..39732870 100644 --- a/src/droidplug/jni_utils/classcache.rs +++ b/src/droidplug/jni_utils/classcache.rs @@ -1,18 +1,20 @@ use dashmap::DashMap; -use jni::{JNIEnv, errors::Result, objects::GlobalRef}; +use jni::{Env, errors::Result, objects::{Global, JObject}}; use once_cell::sync::OnceCell; +use std::sync::Arc; -static CLASSCACHE: OnceCell> = OnceCell::new(); +static CLASSCACHE: OnceCell>>>> = OnceCell::new(); -pub fn find_add_class(env: &mut JNIEnv, classname: &str) -> Result<()> { +pub fn find_add_class(env: &mut Env, classname: &str) -> Result<()> { let cache = CLASSCACHE.get_or_init(|| DashMap::new()); let cls = env.find_class(classname)?; - let global = env.new_global_ref(cls)?; - cache.insert(classname.to_owned(), global); + let cls_obj: JObject = cls.into(); + let global = env.new_global_ref(&cls_obj)?; + cache.insert(classname.to_owned(), Arc::new(global)); Ok(()) } -pub fn get_class(classname: &str) -> Option { +pub fn get_class(classname: &str) -> Option>>> { let cache = CLASSCACHE.get_or_init(|| DashMap::new()); cache.get(classname).map(|pair| pair.value().clone()) } diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index c397723b..f286a209 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -1,5 +1,5 @@ use jni::{ - JNIEnv, + Env, descriptors::Desc, errors::Error, objects::{JClass, JObject, JThrowable}, @@ -21,8 +21,8 @@ pub struct TryCatchResult { /// to be thrown, it will be stored in the resulting [`TryCatchResult`] for /// matching with [`catch`](TryCatchResult::catch). pub fn try_block( - env: &mut JNIEnv, - block: impl FnOnce(&mut JNIEnv) -> Result, + env: &mut Env, + block: impl FnOnce(&mut Env) -> Result, ) -> TryCatchResult { TryCatchResult { try_result: (|| { @@ -39,9 +39,9 @@ pub fn try_block( impl TryCatchResult { pub fn catch<'local>( self, - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, class: impl Desc<'local, JClass<'local>>, - block: impl FnOnce(&mut JNIEnv<'local>, JThrowable<'local>) -> Result, + block: impl FnOnce(&mut Env<'local>, JThrowable<'local>) -> Result, ) -> Self { match (self.try_result, self.catch_result) { (Err(e), _) => Self { @@ -102,7 +102,7 @@ impl<'a> JPanicException<'a> { Self { internal: obj } } - pub fn new(env: &mut JNIEnv<'a>, any: Box) -> Result { + pub fn new(env: &mut Env<'a>, any: Box) -> Result { let msg = if let Some(s) = any.downcast_ref::<&str>() { env.new_string(s)?.into() } else if let Some(s) = any.downcast_ref::() { @@ -124,16 +124,16 @@ impl<'a> JPanicException<'a> { pub fn get<'b>( &self, - env: &'b mut JNIEnv, + env: &'b mut Env, ) -> Result>, Error> { unsafe { env.get_rust_field(&self.internal, "any") } } - pub fn take(&self, env: &mut JNIEnv) -> Result, Error> { + pub fn take(&self, env: &mut Env) -> Result, Error> { unsafe { env.take_rust_field(&self.internal, "any") } } - pub fn resume_unwind(&self, env: &mut JNIEnv) -> Result<(), Error> { + pub fn resume_unwind(&self, env: &mut Env) -> Result<(), Error> { resume_unwind(self.take(env)?); } } @@ -156,7 +156,7 @@ impl<'a> ::std::ops::Deref for JPanicException<'a> { /// `io.github.gedgygedgy.rust.panic.PanicException` and throws it. If a Java /// exception is already pending, it will be added as a suppressed exception. pub fn throw_panic( - env: &mut JNIEnv, + env: &mut Env, panic: Box, ) -> Result<(), Error> { let old_ex = if env.exception_check()? { @@ -184,7 +184,7 @@ pub fn throw_panic( /// Calls the given closure. If it panics, catch the unwind, wrap it in a /// `io.github.gedgygedgy.rust.panic.PanicException`, and throw it. pub fn throw_unwind( - env: &mut JNIEnv, + env: &mut Env, f: impl FnOnce() -> R + UnwindSafe, ) -> Result> { catch_unwind(f).map_err(|e| throw_panic(env, e)) @@ -192,13 +192,13 @@ pub fn throw_unwind( #[cfg(test)] mod test { - use jni::{JNIEnv, errors::Error, objects::{JObject, JThrowable}}; + use jni::{Env, errors::Error, objects::{JObject, JThrowable}}; use super::super::test_utils; use super::try_block; fn test_catch( - env: &mut JNIEnv, + env: &mut Env, throw_class: Option<&str>, try_result: Result, rethrow: bool, diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 2a42b404..c1b854dd 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -1,7 +1,7 @@ use ::jni::{ - JNIEnv, JavaVM, + Env, JavaVM, errors::Result, - objects::{GlobalRef, JClass, JMethodID, JObject}, + objects::{Global, JClass, JMethodID, JObject}, signature::ReturnType, sys::jvalue, }; @@ -12,19 +12,13 @@ use std::{ task::{Context, Poll}, }; -/// Wrapper for [`JObject`]s that implement -/// `io.github.gedgygedgy.rust.future.Future`. Provides a typed interface for -/// calling the Java future's `poll` method. -/// -/// For an async [`Future`](std::future::Future) implementation, convert to -/// [`JSendFuture`] via [`JSendFuture::new`]. pub struct JFuture<'a> { internal: JObject<'a>, poll_id: JMethodID, } impl<'a> JFuture<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = super::classcache::get_class("io/github/gedgygedgy/rust/future/Future").unwrap(); let poll_id = env.get_method_id( @@ -38,7 +32,7 @@ impl<'a> JFuture<'a> { }) } - pub fn poll(&self, env: &mut JNIEnv<'a>, waker: &JObject<'_>) -> Result> { + pub fn poll(&self, env: &mut Env<'a>, waker: &JObject<'_>) -> Result> { let result = unsafe { env.call_method_unchecked( &self.internal, @@ -68,16 +62,14 @@ impl<'a> From> for JObject<'a> { } } -/// [`Send`] version of [`JFuture`]. Implements [`Future`](std::future::Future) -/// by obtaining a [`JNIEnv`] from the stored [`JavaVM`] on each poll. pub struct JSendFuture { - internal: GlobalRef, + internal: Global>, poll_id: JMethodID, vm: JavaVM, } impl JSendFuture { - pub fn new(env: &mut JNIEnv, future: &JFuture) -> Result { + pub fn new(env: &mut Env, future: &JFuture) -> Result { Ok(Self { internal: env.new_global_ref(&future.internal)?, poll_id: future.poll_id, @@ -85,7 +77,7 @@ impl JSendFuture { }) } - pub fn from_env(env: &mut JNIEnv, obj: &JObject) -> Result { + pub fn from_env(env: &mut Env, obj: &JObject) -> Result { let class = super::classcache::get_class("io/github/gedgygedgy/rust/future/Future").unwrap(); let poll_id = env.get_method_id( @@ -100,7 +92,7 @@ impl JSendFuture { }) } - fn poll_internal(&self, context: &mut Context<'_>) -> Result>> { + fn poll_internal(&self, context: &mut Context<'_>) -> Result>>>> { let mut env = self.vm.get_env()?; let jwaker = super::task::waker(&mut env, context.waker().clone())?; let result = unsafe { @@ -123,7 +115,7 @@ impl JSendFuture { } impl ::std::ops::Deref for JSendFuture { - type Target = GlobalRef; + type Target = Global>; fn deref(&self) -> &Self::Target { &self.internal @@ -131,7 +123,7 @@ impl ::std::ops::Deref for JSendFuture { } impl Future for JSendFuture { - type Output = Result; + type Output = Result>>; fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { match self.poll_internal(context) { diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index e8bb85f0..751a95e5 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -1,8 +1,8 @@ use super::task::JPollResult; use ::jni::{ - JNIEnv, JavaVM, + Env, JavaVM, errors::Result, - objects::{GlobalRef, JClass, JMethodID, JObject}, + objects::{Global, JClass, JMethodID, JObject}, signature::ReturnType, sys::jvalue, }; @@ -13,19 +13,13 @@ use std::{ task::{Context, Poll}, }; -/// Wrapper for [`JObject`]s that implement -/// `io.github.gedgygedgy.rust.stream.Stream`. Provides a typed interface for -/// calling the Java stream's `pollNext` method. -/// -/// For an async [`Stream`] implementation, convert to [`JSendStream`] via -/// [`JSendStream::new`]. pub struct JStream<'a> { internal: JObject<'a>, poll_next_id: JMethodID, } impl<'a> JStream<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream").unwrap(); let poll_next_id = env.get_method_id( @@ -41,7 +35,7 @@ impl<'a> JStream<'a> { pub fn poll_next_with_env( &self, - env: &mut JNIEnv<'a>, + env: &mut Env<'a>, waker: &JObject<'_>, ) -> Result> { let result = unsafe { @@ -73,16 +67,14 @@ impl<'a> From> for JObject<'a> { } } -/// [`Send`] version of [`JStream`]. Implements [`Stream`] by obtaining a -/// [`JNIEnv`] from the stored [`JavaVM`] on each poll. pub struct JSendStream { - internal: GlobalRef, + internal: Global>, poll_next_id: JMethodID, vm: JavaVM, } impl JSendStream { - pub fn new(env: &mut JNIEnv, stream: &JStream) -> Result { + pub fn new(env: &mut Env, stream: &JStream) -> Result { Ok(Self { internal: env.new_global_ref(&stream.internal)?, poll_next_id: stream.poll_next_id, @@ -90,7 +82,7 @@ impl JSendStream { }) } - pub fn from_env(env: &mut JNIEnv, obj: &JObject) -> Result { + pub fn from_env(env: &mut Env, obj: &JObject) -> Result { let class = super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream").unwrap(); let poll_next_id = env.get_method_id( @@ -108,7 +100,7 @@ impl JSendStream { fn poll_next_internal( &self, context: &mut Context<'_>, - ) -> Result>>> { + ) -> Result>>>>> { let mut env = self.vm.get_env()?; let jwaker = super::task::waker(&mut env, context.waker().clone())?; let result = unsafe { @@ -141,7 +133,7 @@ impl JSendStream { } impl ::std::ops::Deref for JSendStream { - type Target = GlobalRef; + type Target = Global>; fn deref(&self) -> &Self::Target { &self.internal @@ -149,7 +141,7 @@ impl ::std::ops::Deref for JSendStream { } impl Stream for JSendStream { - type Item = Result; + type Item = Result>>; fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll> { match self.poll_next_internal(context) { @@ -167,7 +159,7 @@ struct JStreamPoll<'a> { } impl<'a> JStreamPoll<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = super::classcache::get_class("io/github/gedgygedgy/rust/stream/StreamPoll").unwrap(); let get = env.get_method_id( @@ -178,7 +170,7 @@ impl<'a> JStreamPoll<'a> { Ok(Self { internal: obj, get }) } - pub fn get(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get(&self, env: &mut Env<'a>) -> Result> { unsafe { env.call_method_unchecked(&self.internal, self.get, ReturnType::Object, &[]) }? .l() } diff --git a/src/droidplug/jni_utils/task.rs b/src/droidplug/jni_utils/task.rs index 95096110..32339b6e 100644 --- a/src/droidplug/jni_utils/task.rs +++ b/src/droidplug/jni_utils/task.rs @@ -1,13 +1,12 @@ use ::jni::{ - JNIEnv, + Env, errors::Result, objects::{JClass, JMethodID, JObject}, signature::ReturnType, }; use std::task::Waker; -/// Wraps the given waker in a `io.github.gedgygedgy.rust.task.Waker` object. -pub fn waker<'a>(env: &mut JNIEnv<'a>, waker: Waker) -> Result> { +pub fn waker<'a>(env: &mut Env<'a>, waker: Waker) -> Result> { let runnable = super::ops::fn_once_runnable(env, |_e, _o| waker.wake())?; let class = super::classcache::get_class("io/github/gedgygedgy/rust/task/Waker").unwrap(); @@ -19,22 +18,20 @@ pub fn waker<'a>(env: &mut JNIEnv<'a>, waker: Waker) -> Result> { Ok(obj) } -/// Wrapper for [`JObject`]s that implement -/// `io.github.gedgygedgy.rust.task.PollResult`. pub struct JPollResult<'a> { internal: JObject<'a>, get: JMethodID, } impl<'a> JPollResult<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = super::classcache::get_class("io/github/gedgygedgy/rust/task/PollResult").unwrap(); let get = env.get_method_id(<&JClass>::from(class.as_obj()), "get", "()Ljava/lang/Object;")?; Ok(Self { internal: obj, get }) } - pub fn get(&self, env: &mut JNIEnv<'a>) -> Result> { + pub fn get(&self, env: &mut Env<'a>) -> Result> { unsafe { env.call_method_unchecked(&self.internal, self.get, ReturnType::Object, &[]) }? .l() } diff --git a/src/droidplug/jni_utils/uuid.rs b/src/droidplug/jni_utils/uuid.rs index c330ce7b..caa05ff2 100644 --- a/src/droidplug/jni_utils/uuid.rs +++ b/src/droidplug/jni_utils/uuid.rs @@ -1,5 +1,5 @@ use jni::{ - JNIEnv, + Env, errors::Result, objects::{JMethodID, JObject}, signature::{Primitive, ReturnType}, @@ -7,8 +7,6 @@ use jni::{ }; use uuid::Uuid; -/// Wrapper for [`JObject`]s that contain `java.util.UUID`. Provides methods -/// to convert to and from a [`Uuid`]. pub struct JUuid<'a> { internal: JObject<'a>, get_least_significant_bits: JMethodID, @@ -16,7 +14,7 @@ pub struct JUuid<'a> { } impl<'a> JUuid<'a> { - pub fn from_env(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = env.find_class("java/util/UUID")?; let get_least_significant_bits = env.get_method_id(&class, "getLeastSignificantBits", "()J")?; @@ -29,7 +27,7 @@ impl<'a> JUuid<'a> { }) } - pub fn new(env: &mut JNIEnv<'a>, uuid: Uuid) -> Result { + pub fn new(env: &mut Env<'a>, uuid: Uuid) -> Result { let val = uuid.as_u128(); let least = (val & 0xFFFFFFFFFFFFFFFF) as jlong; let most = ((val >> 64) & 0xFFFFFFFFFFFFFFFF) as jlong; @@ -47,7 +45,7 @@ impl<'a> JUuid<'a> { }) } - pub fn as_uuid(&self, env: &mut JNIEnv<'a>) -> Result { + pub fn as_uuid(&self, env: &mut Env<'a>) -> Result { let least = unsafe { env.call_method_unchecked( &self.internal, diff --git a/src/droidplug/mod.rs b/src/droidplug/mod.rs index d759835f..220ba518 100644 --- a/src/droidplug/mod.rs +++ b/src/droidplug/mod.rs @@ -2,7 +2,7 @@ pub mod adapter; pub mod manager; pub mod peripheral; -use ::jni::JNIEnv; +use ::jni::Env; use once_cell::sync::OnceCell; mod jni; @@ -10,7 +10,7 @@ mod jni_utils; static GLOBAL_ADAPTER: OnceCell = OnceCell::new(); -pub fn init(env: &mut JNIEnv) -> crate::Result<()> { +pub fn init(env: &mut Env) -> crate::Result<()> { self::jni::init(env)?; GLOBAL_ADAPTER.get_or_try_init(|| adapter::Adapter::new())?; Ok(()) diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index b7e25052..dc81c2fc 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -15,8 +15,8 @@ use crate::{ use async_trait::async_trait; use futures::stream::Stream; use jni::{ - JNIEnv, - objects::{GlobalRef, JClass, JObject, JString, JValue}, + Env, + objects::{Global, JClass, JObject, JString, JValue}, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -48,8 +48,8 @@ impl Display for PeripheralId { } fn get_poll_result<'a>( - env: &mut JNIEnv<'a>, - result_ref: &GlobalRef, + env: &mut Env<'a>, + result_ref: &Global>, ) -> Result> { let result_obj = env.new_local_ref(result_ref)?; let poll_result = JPollResult::from_env(env, result_obj)?; @@ -128,15 +128,15 @@ struct PeripheralShared { #[derive(Clone)] pub struct Peripheral { addr: BDAddr, - internal: GlobalRef, + internal: Arc>>, shared: Arc>, mtu: Arc, } impl Peripheral { - pub(crate) fn new<'a>(env: &mut JNIEnv<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { + pub(crate) fn new<'a>(env: &mut Env<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { let obj = JPeripheral::new(env, adapter, addr)?; - let internal = env.new_global_ref(&*obj)?; + let internal = Arc::new(env.new_global_ref(&*obj)?); Ok(Self { addr, internal, @@ -157,13 +157,13 @@ impl Peripheral { fn with_obj( &self, - f: impl for<'env> FnOnce(&mut JNIEnv<'env>, &JPeripheral<'env>) -> std::result::Result, + f: impl for<'env> FnOnce(&mut Env<'env>, &JPeripheral<'env>) -> std::result::Result, ) -> std::result::Result where E: From<::jni::errors::Error>, { let mut env = global_jvm().get_env()?; - let local_obj = env.new_local_ref(&self.internal)?; + let local_obj = env.new_local_ref(self.internal.as_obj())?; let obj = JPeripheral::from_env(&mut env, local_obj)?; f(&mut env, &obj) } From b2f0528616008fbe7f70d7f28ae5fab6bfb7f79d Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 19:47:12 -0700 Subject: [PATCH 07/77] refactor: Wrap JNI strings with jni_str! and signatures with jni_sig! Phase 3 of jni 0.22 migration. All string literals passed to JNI API methods (find_class, get_method_id, call_method, new_object, is_instance_of, set/get/take_rust_field, call_static_method) are now wrapped with jni_str!() for names and jni_sig!() for type signatures. classcache::find_add_class uses JNIString for runtime &str conversion. register_native_methods calls wrapped in unsafe blocks (required by 0.22). Co-Authored-By: Claude Opus 4.6 --- src/droidplug/adapter.rs | 22 ++-- src/droidplug/jni/mod.rs | 31 +++--- src/droidplug/jni/objects.rs | 149 +++++++++++++------------- src/droidplug/jni_utils/classcache.rs | 5 +- src/droidplug/jni_utils/exceptions.rs | 49 ++++----- src/droidplug/jni_utils/future.rs | 44 ++++---- src/droidplug/jni_utils/mod.rs | 36 +++---- src/droidplug/jni_utils/ops.rs | 15 +-- src/droidplug/jni_utils/stream.rs | 62 +++++------ src/droidplug/jni_utils/task.rs | 14 +-- src/droidplug/jni_utils/uuid.rs | 23 ++-- src/droidplug/peripheral.rs | 16 +-- 12 files changed, 239 insertions(+), 227 deletions(-) diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index 5470c7bd..2befcf2c 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -13,7 +13,7 @@ use crate::{ use async_trait::async_trait; use futures::stream::Stream; use jni::{ - Env, + Env, jni_sig, jni_str, objects::{Global, JClass, JObject, JString}, sys::jboolean, }; @@ -43,8 +43,8 @@ impl Adapter { let mut env = global_jvm().get_env()?; let obj = env.new_object( - "com/nonpolynomial/btleplug/android/impl/Adapter", - "()V", + jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"), + jni_sig!("()V"), &[], )?; let internal = Arc::new(env.new_global_ref(&obj)?); @@ -52,7 +52,7 @@ impl Adapter { manager: Arc::new(AdapterManager::default()), internal, }; - unsafe { env.set_rust_field(&obj, "handle", adapter.clone()) }?; + unsafe { env.set_rust_field(&obj, jni_str!("handle"), adapter.clone()) }?; Ok(adapter) } @@ -140,8 +140,8 @@ impl Central for Adapter { let filter_obj: JObject = filter.into(); match env.call_method( self.internal.as_obj(), - "startScan", - "(Lcom/nonpolynomial/btleplug/android/impl/ScanFilter;)V", + jni_str!("startScan"), + jni_sig!("(Lcom/nonpolynomial/btleplug/android/impl/ScanFilter;)V"), &[(&filter_obj).into()], ) { Ok(_) => Ok(()), @@ -156,9 +156,9 @@ impl Central for Adapter { if env.is_instance_of(&ex, <&JClass>::from(no_adapter_class.as_obj()))? { Err(Error::NoAdapterAvailable) - } else if env.is_instance_of(&ex, "java/lang/RuntimeException")? { + } else if env.is_instance_of(&ex, jni_str!("java/lang/RuntimeException"))? { let msg = env - .call_method(&ex, "getMessage", "()Ljava/lang/String;", &[])? + .call_method(&ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[])? .l()?; let jstr: JString = msg.into(); let msgstr: String = env.get_string(&jstr)?.into(); @@ -174,7 +174,7 @@ impl Central for Adapter { async fn stop_scan(&self) -> Result<()> { let mut env = global_jvm().get_env()?; - env.call_method(self.internal.as_obj(), "stopScan", "()V", &[])?; + env.call_method(self.internal.as_obj(), jni_str!("stopScan"), jni_sig!("()V"), &[])?; Ok(()) } @@ -207,7 +207,7 @@ pub(crate) fn adapter_report_scan_result_internal<'a>( obj: &JObject, scan_result: JObject<'a>, ) -> crate::Result<()> { - let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, "handle") }?; + let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, jni_str!("handle")) }?; let adapter_clone = adapter.clone(); drop(adapter); adapter_clone.report_scan_result(env, scan_result)?; @@ -222,7 +222,7 @@ pub(crate) fn adapter_on_connection_state_changed_internal( ) -> crate::Result<()> { let addr_str: String = env.get_string(&addr)?.into(); let addr = BDAddr::from_str(&addr_str)?; - let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, "handle") }?; + let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, jni_str!("handle")) }?; adapter.manager.emit(if connected != 0 { CentralEvent::DeviceConnected(PeripheralId(addr)) } else { diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index 9453eab7..80ddf8e7 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,6 +1,6 @@ pub mod objects; -use ::jni::{Env, JNIEnv, JavaVM, NativeMethod, objects::JObject}; +use ::jni::{Env, JNIEnv, JavaVM, NativeMethod, jni_str, jni_sig, objects::JObject}; use jni::{objects::JString, sys::jboolean}; use once_cell::sync::OnceCell; use std::ffi::c_void; @@ -10,22 +10,22 @@ static GLOBAL_JVM: OnceCell = OnceCell::new(); pub fn init(env: &mut Env) -> crate::Result<()> { if let Ok(()) = GLOBAL_JVM.set(env.get_java_vm()?) { let adapter_class = - env.find_class("com/nonpolynomial/btleplug/android/impl/Adapter")?; - env.register_native_methods( + env.find_class(jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"))?; + unsafe { env.register_native_methods( &adapter_class, &[ NativeMethod { - name: "reportScanResult".into(), - sig: "(Landroid/bluetooth/le/ScanResult;)V".into(), + name: jni_str!("reportScanResult").into(), + sig: jni_sig!("(Landroid/bluetooth/le/ScanResult;)V").into(), fn_ptr: adapter_report_scan_result as *mut c_void, }, NativeMethod { - name: "onConnectionStateChanged".into(), - sig: "(Ljava/lang/String;Z)V".into(), + name: jni_str!("onConnectionStateChanged").into(), + sig: jni_sig!("(Ljava/lang/String;Z)V").into(), fn_ptr: adapter_on_connection_state_changed as *mut c_void, }, ], - )?; + )? }; super::jni_utils::classcache::find_add_class( env, "com/nonpolynomial/btleplug/android/impl/Peripheral", @@ -99,24 +99,25 @@ pub fn init(env: &mut Env) -> crate::Result<()> { )?; // FnAdapter native method registration - let fn_adapter_class = env.find_class("io/github/gedgygedgy/rust/ops/FnAdapter")?; - env.register_native_methods( + let fn_adapter_class = env.find_class(jni_str!("io/github/gedgygedgy/rust/ops/FnAdapter"))?; + unsafe { env.register_native_methods( &fn_adapter_class, &[ NativeMethod { - name: "callInternal".into(), + name: jni_str!("callInternal").into(), sig: - "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;" + jni_sig!("(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;") .into(), fn_ptr: super::jni_utils::ops::fn_adapter_call_internal as *mut c_void, }, NativeMethod { - name: "closeInternal".into(), - sig: "()V".into(), + name: jni_str!("closeInternal").into(), + sig: jni_sig!("()V").into(), fn_ptr: super::jni_utils::ops::fn_adapter_close_internal as *mut c_void, }, ], - )?; + )? }; + } Ok(()) } diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 7e43dbfb..5563b755 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -2,6 +2,7 @@ use crate::droidplug::jni_utils::{future::JFuture, stream::JStream, uuid::JUuid} use jni::{ Env, errors::Result, + jni_sig, jni_str, objects::{JClass, JMethodID, JObject, JString}, signature::{Primitive, ReturnType}, sys::{jint, jvalue}, @@ -54,64 +55,64 @@ impl<'a> JPeripheral<'a> { let connect = env.get_method_id( class, - "connect", - "()Lio/github/gedgygedgy/rust/future/Future;", + jni_str!("connect"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), )?; let disconnect = env.get_method_id( class, - "disconnect", - "()Lio/github/gedgygedgy/rust/future/Future;", + jni_str!("disconnect"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), )?; - let is_connected = env.get_method_id(class, "isConnected", "()Z")?; + let is_connected = env.get_method_id(class, jni_str!("isConnected"), jni_sig!("()Z"))?; let discover_services = env.get_method_id( class, - "discoverServices", - "()Lio/github/gedgygedgy/rust/future/Future;", + jni_str!("discoverServices"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), )?; let read = env.get_method_id( class, - "read", - "(Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;", + jni_str!("read"), + jni_sig!("(Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;"), )?; let write = env.get_method_id( class, - "write", - "(Ljava/util/UUID;[BI)Lio/github/gedgygedgy/rust/future/Future;", + jni_str!("write"), + jni_sig!("(Ljava/util/UUID;[BI)Lio/github/gedgygedgy/rust/future/Future;"), )?; let set_characteristic_notification = env.get_method_id( class, - "setCharacteristicNotification", - "(Ljava/util/UUID;Z)Lio/github/gedgygedgy/rust/future/Future;", + jni_str!("setCharacteristicNotification"), + jni_sig!("(Ljava/util/UUID;Z)Lio/github/gedgygedgy/rust/future/Future;"), )?; let get_notifications = env.get_method_id( class, - "getNotifications", - "()Lio/github/gedgygedgy/rust/stream/Stream;", + jni_str!("getNotifications"), + jni_sig!("()Lio/github/gedgygedgy/rust/stream/Stream;"), )?; let read_descriptor = env.get_method_id( class, - "readDescriptor", - "(Ljava/util/UUID;Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;", + jni_str!("readDescriptor"), + jni_sig!("(Ljava/util/UUID;Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;"), )?; let write_descriptor = env.get_method_id( class, - "writeDescriptor", - "(Ljava/util/UUID;Ljava/util/UUID;[B)Lio/github/gedgygedgy/rust/future/Future;", + jni_str!("writeDescriptor"), + jni_sig!("(Ljava/util/UUID;Ljava/util/UUID;[B)Lio/github/gedgygedgy/rust/future/Future;"), )?; - let get_device_name = env.get_method_id(class, "getDeviceName", "()Ljava/lang/String;")?; + let get_device_name = env.get_method_id(class, jni_str!("getDeviceName"), jni_sig!("()Ljava/lang/String;"))?; let request_mtu = env.get_method_id( class, - "requestMtu", - "(I)Lio/github/gedgygedgy/rust/future/Future;", + jni_str!("requestMtu"), + jni_sig!("(I)Lio/github/gedgygedgy/rust/future/Future;"), )?; let get_connection_parameters = - env.get_method_id(class, "getConnectionParameters", "()[I")?; + env.get_method_id(class, jni_str!("getConnectionParameters"), jni_sig!("()[I"))?; let request_connection_priority = - env.get_method_id(class, "requestConnectionPriority", "(I)Z")?; + env.get_method_id(class, jni_str!("requestConnectionPriority"), jni_sig!("(I)Z"))?; let read_remote_rssi = env.get_method_id( class, - "readRemoteRssi", - "()Lio/github/gedgygedgy/rust/future/Future;", + jni_str!("readRemoteRssi"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), )?; Ok(Self { internal: obj, @@ -141,7 +142,7 @@ impl<'a> JPeripheral<'a> { .unwrap(); let obj = env.new_object( <&JClass>::from(class_static.as_obj()), - "(Lcom/nonpolynomial/btleplug/android/impl/Adapter;Ljava/lang/String;)V", + jni_sig!("(Lcom/nonpolynomial/btleplug/android/impl/Adapter;Ljava/lang/String;)V"), &[(&adapter).into(), (&addr_jstr).into()], )?; Self::from_env(env, obj) @@ -420,11 +421,11 @@ pub struct JBluetoothGattService<'a> { impl<'a> JBluetoothGattService<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class("android/bluetooth/BluetoothGattService")?; + let class = env.find_class(jni_str!("android/bluetooth/BluetoothGattService"))?; - let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; + let get_uuid = env.get_method_id(&class, jni_str!("getUuid"), jni_sig!("()Ljava/util/UUID;"))?; let get_characteristics = - env.get_method_id(&class, "getCharacteristics", "()Ljava/util/List;")?; + env.get_method_id(&class, jni_str!("getCharacteristics"), jni_sig!("()Ljava/util/List;"))?; Ok(Self { internal: obj, get_uuid, @@ -458,11 +459,11 @@ impl<'a> JBluetoothGattService<'a> { ) }? .l()?; - let size = env.call_method(&obj, "size", "()I", &[])?.i()?; + let size = env.call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])?.i()?; let mut chr_vec = Vec::with_capacity(size as usize); for i in 0..size { let chr = env - .call_method(&obj, "get", "(I)Ljava/lang/Object;", &[jni::objects::JValue::from(i)])? + .call_method(&obj, jni_str!("get"), jni_sig!("(I)Ljava/lang/Object;"), &[jni::objects::JValue::from(i)])? .l()?; chr_vec.push(JBluetoothGattCharacteristic::from_env(env, chr)?); } @@ -480,12 +481,12 @@ pub struct JBluetoothGattCharacteristic<'a> { impl<'a> JBluetoothGattCharacteristic<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class("android/bluetooth/BluetoothGattCharacteristic")?; + let class = env.find_class(jni_str!("android/bluetooth/BluetoothGattCharacteristic"))?; - let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; - let get_properties = env.get_method_id(&class, "getProperties", "()I")?; - let get_descriptors = env.get_method_id(&class, "getDescriptors", "()Ljava/util/List;")?; - let get_value = env.get_method_id(&class, "getValue", "()[B")?; + let get_uuid = env.get_method_id(&class, jni_str!("getUuid"), jni_sig!("()Ljava/util/UUID;"))?; + let get_properties = env.get_method_id(&class, jni_str!("getProperties"), jni_sig!("()I"))?; + let get_descriptors = env.get_method_id(&class, jni_str!("getDescriptors"), jni_sig!("()Ljava/util/List;"))?; + let get_value = env.get_method_id(&class, jni_str!("getValue"), jni_sig!("()[B"))?; Ok(Self { internal: obj, get_uuid, @@ -539,11 +540,11 @@ impl<'a> JBluetoothGattCharacteristic<'a> { ) }? .l()?; - let size = env.call_method(&obj, "size", "()I", &[])?.i()?; + let size = env.call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])?.i()?; let mut desc_vec = Vec::with_capacity(size as usize); for i in 0..size { let desc = env - .call_method(&obj, "get", "(I)Ljava/lang/Object;", &[jni::objects::JValue::from(i)])? + .call_method(&obj, jni_str!("get"), jni_sig!("(I)Ljava/lang/Object;"), &[jni::objects::JValue::from(i)])? .l()?; desc_vec.push(JBluetoothGattDescriptor::from_env(env, desc)?); } @@ -558,9 +559,9 @@ pub struct JBluetoothGattDescriptor<'a> { impl<'a> JBluetoothGattDescriptor<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class("android/bluetooth/BluetoothGattDescriptor")?; + let class = env.find_class(jni_str!("android/bluetooth/BluetoothGattDescriptor"))?; - let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; + let get_uuid = env.get_method_id(&class, jni_str!("getUuid"), jni_sig!("()Ljava/util/UUID;"))?; Ok(Self { internal: obj, get_uuid, @@ -584,9 +585,9 @@ pub struct JBluetoothDevice<'a> { impl<'a> JBluetoothDevice<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class("android/bluetooth/BluetoothDevice")?; + let class = env.find_class(jni_str!("android/bluetooth/BluetoothDevice"))?; - let get_address = env.get_method_id(&class, "getAddress", "()Ljava/lang/String;")?; + let get_address = env.get_method_id(&class, jni_str!("getAddress"), jni_sig!("()Ljava/lang/String;"))?; Ok(Self { internal: obj, get_address, @@ -608,7 +609,7 @@ pub struct JScanFilter<'a> { impl<'a> JScanFilter<'a> { pub fn new(env: &mut Env<'a>, filter: ScanFilter) -> Result { - let string_class = env.find_class("java/lang/String")?; + let string_class = env.find_class(jni_str!("java/lang/String"))?; let uuids = env.new_object_array( filter.services.len() as i32, &string_class, @@ -624,7 +625,7 @@ impl<'a> JScanFilter<'a> { .unwrap(); let obj = env.new_object( <&JClass>::from(class_static.as_obj()), - "([Ljava/lang/String;)V", + jni_sig!("([Ljava/lang/String;)V"), &[(&uuids).into()], )?; Ok(Self { internal: obj }) @@ -647,17 +648,17 @@ pub struct JScanResult<'a> { impl<'a> JScanResult<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class("android/bluetooth/le/ScanResult")?; + let class = env.find_class(jni_str!("android/bluetooth/le/ScanResult"))?; let get_device = - env.get_method_id(&class, "getDevice", "()Landroid/bluetooth/BluetoothDevice;")?; + env.get_method_id(&class, jni_str!("getDevice"), jni_sig!("()Landroid/bluetooth/BluetoothDevice;"))?; let get_scan_record = env.get_method_id( &class, - "getScanRecord", - "()Landroid/bluetooth/le/ScanRecord;", + jni_str!("getScanRecord"), + jni_sig!("()Landroid/bluetooth/le/ScanRecord;"), )?; - let get_tx_power = env.get_method_id(&class, "getTxPower", "()I")?; - let get_rssi = env.get_method_id(&class, "getRssi", "()I")?; + let get_tx_power = env.get_method_id(&class, jni_str!("getTxPower"), jni_sig!("()I"))?; + let get_rssi = env.get_method_id(&class, jni_str!("getRssi"), jni_sig!("()I"))?; Ok(Self { internal: obj, get_device, @@ -775,28 +776,28 @@ impl<'a> JScanResult<'a> { let mut service_data = HashMap::new(); if !env.is_same_object(&service_data_obj, JObject::null())? { let entry_set = env - .call_method(&service_data_obj, "entrySet", "()Ljava/util/Set;", &[])? + .call_method(&service_data_obj, jni_str!("entrySet"), jni_sig!("()Ljava/util/Set;"), &[])? .l()?; let iter_obj = env .call_method( &entry_set, - "iterator", - "()Ljava/util/Iterator;", + jni_str!("iterator"), + jni_sig!("()Ljava/util/Iterator;"), &[], )? .l()?; while env - .call_method(&iter_obj, "hasNext", "()Z", &[])? + .call_method(&iter_obj, jni_str!("hasNext"), jni_sig!("()Z"), &[])? .z()? { let entry = env - .call_method(&iter_obj, "next", "()Ljava/lang/Object;", &[])? + .call_method(&iter_obj, jni_str!("next"), jni_sig!("()Ljava/lang/Object;"), &[])? .l()?; let key = env - .call_method(&entry, "getKey", "()Ljava/lang/Object;", &[])? + .call_method(&entry, jni_str!("getKey"), jni_sig!("()Ljava/lang/Object;"), &[])? .l()?; let value = env - .call_method(&entry, "getValue", "()Ljava/lang/Object;", &[])? + .call_method(&entry, jni_str!("getValue"), jni_sig!("()Ljava/lang/Object;"), &[])? .l()?; let parcel_uuid = JParcelUuid::from_env(env, key)?; let juuid = parcel_uuid.get_uuid(env)?; @@ -813,14 +814,14 @@ impl<'a> JScanResult<'a> { let mut services = Vec::new(); if !env.is_same_object(&services_obj, JObject::null())? { let size = env - .call_method(&services_obj, "size", "()I", &[])? + .call_method(&services_obj, jni_str!("size"), jni_sig!("()I"), &[])? .i()?; for i in 0..size { let obj = env .call_method( &services_obj, - "get", - "(I)Ljava/lang/Object;", + jni_str!("get"), + jni_sig!("(I)Ljava/lang/Object;"), &[jni::objects::JValue::from(i)], )? .l()?; @@ -873,18 +874,18 @@ impl<'a> ::std::ops::Deref for JScanRecord<'a> { impl<'a> JScanRecord<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class("android/bluetooth/le/ScanRecord")?; + let class = env.find_class(jni_str!("android/bluetooth/le/ScanRecord"))?; - let get_device_name = env.get_method_id(&class, "getDeviceName", "()Ljava/lang/String;")?; - let get_tx_power_level = env.get_method_id(&class, "getTxPowerLevel", "()I")?; + let get_device_name = env.get_method_id(&class, jni_str!("getDeviceName"), jni_sig!("()Ljava/lang/String;"))?; + let get_tx_power_level = env.get_method_id(&class, jni_str!("getTxPowerLevel"), jni_sig!("()I"))?; let get_manufacturer_specific_data = env.get_method_id( &class, - "getManufacturerSpecificData", - "()Landroid/util/SparseArray;", + jni_str!("getManufacturerSpecificData"), + jni_sig!("()Landroid/util/SparseArray;"), )?; - let get_service_data = env.get_method_id(&class, "getServiceData", "()Ljava/util/Map;")?; + let get_service_data = env.get_method_id(&class, jni_str!("getServiceData"), jni_sig!("()Ljava/util/Map;"))?; let get_service_uuids = - env.get_method_id(&class, "getServiceUuids", "()Ljava/util/List;")?; + env.get_method_id(&class, jni_str!("getServiceUuids"), jni_sig!("()Ljava/util/List;"))?; Ok(Self { internal: obj, get_device_name, @@ -983,11 +984,11 @@ impl<'a> ::std::ops::Deref for JSparseArray<'a> { impl<'a> JSparseArray<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class("android/util/SparseArray")?; + let class = env.find_class(jni_str!("android/util/SparseArray"))?; - let size = env.get_method_id(&class, "size", "()I")?; - let key_at = env.get_method_id(&class, "keyAt", "(I)I")?; - let value_at = env.get_method_id(&class, "valueAt", "(I)Ljava/lang/Object;")?; + let size = env.get_method_id(&class, jni_str!("size"), jni_sig!("()I"))?; + let key_at = env.get_method_id(&class, jni_str!("keyAt"), jni_sig!("(I)I"))?; + let value_at = env.get_method_id(&class, jni_str!("valueAt"), jni_sig!("(I)Ljava/lang/Object;"))?; Ok(Self { internal: obj, size, @@ -1040,9 +1041,9 @@ pub struct JParcelUuid<'a> { impl<'a> JParcelUuid<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class("android/os/ParcelUuid")?; + let class = env.find_class(jni_str!("android/os/ParcelUuid"))?; - let get_uuid = env.get_method_id(&class, "getUuid", "()Ljava/util/UUID;")?; + let get_uuid = env.get_method_id(&class, jni_str!("getUuid"), jni_sig!("()Ljava/util/UUID;"))?; Ok(Self { internal: obj, get_uuid, diff --git a/src/droidplug/jni_utils/classcache.rs b/src/droidplug/jni_utils/classcache.rs index 39732870..9aa35eba 100644 --- a/src/droidplug/jni_utils/classcache.rs +++ b/src/droidplug/jni_utils/classcache.rs @@ -1,5 +1,5 @@ use dashmap::DashMap; -use jni::{Env, errors::Result, objects::{Global, JObject}}; +use jni::{Env, errors::Result, objects::{Global, JObject}, strings::JNIString}; use once_cell::sync::OnceCell; use std::sync::Arc; @@ -7,7 +7,8 @@ static CLASSCACHE: OnceCell>>>> = On pub fn find_add_class(env: &mut Env, classname: &str) -> Result<()> { let cache = CLASSCACHE.get_or_init(|| DashMap::new()); - let cls = env.find_class(classname)?; + let jni_name = JNIString::from(classname); + let cls = env.find_class(&jni_name)?; let cls_obj: JObject = cls.into(); let global = env.new_global_ref(&cls_obj)?; cache.insert(classname.to_owned(), Arc::new(global)); diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index f286a209..9391d648 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -2,6 +2,7 @@ use jni::{ Env, descriptors::Desc, errors::Error, + jni_str, jni_sig, objects::{JClass, JObject, JThrowable}, }; use std::{ @@ -112,11 +113,11 @@ impl<'a> JPanicException<'a> { }; let obj = env.new_object( - "io/github/gedgygedgy/rust/panic/PanicException", - "(Ljava/lang/String;)V", + jni_str!("io/github/gedgygedgy/rust/panic/PanicException"), + jni_sig!("(Ljava/lang/String;)V"), &[(&msg).into()], )?; - unsafe { env.set_rust_field(&obj, "any", any) }?; + unsafe { env.set_rust_field(&obj, jni_str!("any"), any) }?; Ok(Self { internal: obj.into(), }) @@ -126,11 +127,11 @@ impl<'a> JPanicException<'a> { &self, env: &'b mut Env, ) -> Result>, Error> { - unsafe { env.get_rust_field(&self.internal, "any") } + unsafe { env.get_rust_field(&self.internal, jni_str!("any")) } } pub fn take(&self, env: &mut Env) -> Result, Error> { - unsafe { env.take_rust_field(&self.internal, "any") } + unsafe { env.take_rust_field(&self.internal, jni_str!("any")) } } pub fn resume_unwind(&self, env: &mut Env) -> Result<(), Error> { @@ -171,8 +172,8 @@ pub fn throw_panic( if let Some(old_ex) = old_ex { env.call_method( &*ex, - "addSuppressed", - "(Ljava/lang/Throwable;)V", + jni_str!("addSuppressed"), + jni_sig!("(Ljava/lang/Throwable;)V"), &[(&old_ex).into()], )?; } @@ -192,7 +193,7 @@ pub fn throw_unwind( #[cfg(test)] mod test { - use jni::{Env, errors::Error, objects::{JObject, JThrowable}}; + use jni::{Env, errors::Error, jni_str, jni_sig, objects::{JObject, JThrowable}, strings::JNIString}; use super::super::test_utils; use super::try_block; @@ -211,14 +212,14 @@ mod test { None }; let illegal_argument_exception = env - .find_class("java/lang/IllegalArgumentException") + .find_class(jni_str!("java/lang/IllegalArgumentException")) .unwrap(); if let Some(ref ex) = old_ex { env.throw(ex).unwrap(); } let ex = throw_class.map(|c| { - let obj = env.new_object(c, "()V", &[]).unwrap(); + let obj = env.new_object(JNIString::from(c), jni_sig!("()V"), &[]).unwrap(); JThrowable::from(obj) }); @@ -235,7 +236,7 @@ mod test { }) .catch( env, - "java/lang/ArrayIndexOutOfBoundsException", + jni_str!("java/lang/ArrayIndexOutOfBoundsException"), |env, caught| { assert!(!env.exception_check().unwrap()); assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); @@ -248,7 +249,7 @@ mod test { ) .catch( env, - "java/lang/IndexOutOfBoundsException", + jni_str!("java/lang/IndexOutOfBoundsException"), |env, caught| { assert!(!env.exception_check().unwrap()); assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); @@ -262,7 +263,7 @@ mod test { ) .catch( env, - "java/lang/StringIndexOutOfBoundsException", + jni_str!("java/lang/StringIndexOutOfBoundsException"), |env, caught| { assert!(!env.exception_check().unwrap()); assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); @@ -351,7 +352,7 @@ mod test { let ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); assert!( - env.is_instance_of(&ex, "java/lang/SecurityException") + env.is_instance_of(&ex, jni_str!("java/lang/SecurityException")) .unwrap() ); } else { @@ -393,7 +394,7 @@ mod test { test_utils::JVM_ENV.with(|cell| { let env = &mut *cell.borrow_mut(); let ex = JThrowable::from( - env.new_object("java/lang/IllegalArgumentException", "()V", &[]) + env.new_object(jni_str!("java/lang/IllegalArgumentException"), jni_sig!("()V"), &[]) .unwrap(), ); env.throw(&ex).unwrap(); @@ -425,7 +426,7 @@ mod test { let ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); assert!( - env.is_instance_of(&ex, "java/lang/StringIndexOutOfBoundsException") + env.is_instance_of(&ex, jni_str!("java/lang/StringIndexOutOfBoundsException")) .unwrap() ); } else { @@ -469,7 +470,7 @@ mod test { } let msg: JString = env - .call_method(&*ex, "getMessage", "()Ljava/lang/String;", &[]) + .call_method(&*ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[]) .unwrap() .l() .unwrap() @@ -496,7 +497,7 @@ mod test { } let msg: JString = env - .call_method(&*ex, "getMessage", "()Ljava/lang/String;", &[]) + .call_method(&*ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[]) .unwrap() .l() .unwrap() @@ -525,7 +526,7 @@ mod test { } let msg = env - .call_method(&*ex, "getMessage", "()Ljava/lang/String;", &[]) + .call_method(&*ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[]) .unwrap() .l() .unwrap(); @@ -557,12 +558,12 @@ mod test { let ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); assert!( - env.is_instance_of(&ex, "io/github/gedgygedgy/rust/panic/PanicException") + env.is_instance_of(&ex, jni_str!("io/github/gedgygedgy/rust/panic/PanicException")) .unwrap() ); let suppressed_list = env - .call_method(&ex, "getSuppressed", "()[Ljava/lang/Throwable;", &[]) + .call_method(&ex, jni_str!("getSuppressed"), jni_sig!("()[Ljava/lang/Throwable;"), &[]) .unwrap() .l() .unwrap(); @@ -583,7 +584,7 @@ mod test { test_utils::JVM_ENV.with(|cell| { let env = &mut *cell.borrow_mut(); let old_ex = - JThrowable::from(env.new_object("java/lang/Exception", "()V", &[]).unwrap()); + JThrowable::from(env.new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]).unwrap()); env.throw(&old_ex).unwrap(); super::throw_unwind(env, || panic!("This is a panic")) @@ -593,12 +594,12 @@ mod test { let ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); assert!( - env.is_instance_of(&ex, "io/github/gedgygedgy/rust/panic/PanicException") + env.is_instance_of(&ex, jni_str!("io/github/gedgygedgy/rust/panic/PanicException")) .unwrap() ); let suppressed_list = env - .call_method(&ex, "getSuppressed", "()[Ljava/lang/Throwable;", &[]) + .call_method(&ex, jni_str!("getSuppressed"), jni_sig!("()[Ljava/lang/Throwable;"), &[]) .unwrap() .l() .unwrap(); diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index c1b854dd..5dfb93ea 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -1,6 +1,7 @@ use ::jni::{ Env, JavaVM, errors::Result, + jni_sig, jni_str, objects::{Global, JClass, JMethodID, JObject}, signature::ReturnType, sys::jvalue, @@ -23,8 +24,8 @@ impl<'a> JFuture<'a> { super::classcache::get_class("io/github/gedgygedgy/rust/future/Future").unwrap(); let poll_id = env.get_method_id( <&JClass>::from(class.as_obj()), - "poll", - "(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;", + jni_str!("poll"), + jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), )?; Ok(Self { internal: obj, @@ -82,8 +83,8 @@ impl JSendFuture { super::classcache::get_class("io/github/gedgygedgy/rust/future/Future").unwrap(); let poll_id = env.get_method_id( <&JClass>::from(class.as_obj()), - "poll", - "(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;", + jni_str!("poll"), + jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), )?; Ok(Self { internal: env.new_global_ref(obj)?, @@ -139,6 +140,7 @@ assert_impl_all!(JSendFuture: Send); mod test { use super::super::test_utils; use super::{JFuture, JSendFuture}; + use jni::{jni_sig, jni_str}; use std::{ future::Future, pin::Pin, @@ -162,7 +164,7 @@ mod test { assert_eq!(data.value(), false); let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) .unwrap(); let future_local = env.new_local_ref(&future_obj).unwrap(); let jfuture = JFuture::from_env(env, future_local).unwrap(); @@ -180,11 +182,11 @@ mod test { assert_eq!(Arc::strong_count(&data), 3); assert_eq!(data.value(), false); - let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); env.call_method( &future_obj, - "wake", - "(Ljava/lang/Object;)V", + jni_str!("wake"), + jni_sig!("(Ljava/lang/Object;)V"), &[(&obj).into()], ) .unwrap(); @@ -228,13 +230,13 @@ mod test { let (future, future_obj_global, obj_global) = { let env = &mut *cell.borrow_mut(); let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) .unwrap(); let future_obj_global = env.new_global_ref(&future_obj).unwrap(); let future_local = env.new_local_ref(&future_obj).unwrap(); let jfuture = JFuture::from_env(env, future_local).unwrap(); let future = JSendFuture::new(env, &jfuture).unwrap(); - let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); let obj_global = env.new_global_ref(&obj).unwrap(); (future, future_obj_global, obj_global) }; @@ -247,8 +249,8 @@ mod test { let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); env.call_method( &future_local, - "wake", - "(Ljava/lang/Object;)V", + jni_str!("wake"), + jni_sig!("(Ljava/lang/Object;)V"), &[(&obj_local).into()], ) .unwrap(); @@ -275,13 +277,13 @@ mod test { let (future, future_obj_global, ex_global) = { let env = &mut *cell.borrow_mut(); let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) .unwrap(); let future_obj_global = env.new_global_ref(&future_obj).unwrap(); let future_local = env.new_local_ref(&future_obj).unwrap(); let jfuture = JFuture::from_env(env, future_local).unwrap(); let future = JSendFuture::new(env, &jfuture).unwrap(); - let ex = env.new_object("java/lang/Exception", "()V", &[]).unwrap(); + let ex = env.new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]).unwrap(); let ex_global = env.new_global_ref(&ex).unwrap(); (future, future_obj_global, ex_global) }; @@ -294,8 +296,8 @@ mod test { let ex_local = env.new_local_ref(ex_global.as_obj()).unwrap(); env.call_method( &future_local, - "wakeWithThrowable", - "(Ljava/lang/Throwable;)V", + jni_str!("wakeWithThrowable"), + jni_sig!("(Ljava/lang/Throwable;)V"), &[(&ex_local).into()], ) .unwrap(); @@ -312,7 +314,7 @@ mod test { let future_ex = env.exception_occurred().unwrap(); env.exception_clear().unwrap(); let actual_ex = env - .call_method(&future_ex, "getCause", "()Ljava/lang/Throwable;", &[]) + .call_method(&future_ex, jni_str!("getCause"), jni_sig!("()Ljava/lang/Throwable;"), &[]) .unwrap() .l() .unwrap(); @@ -333,11 +335,11 @@ mod test { let (future, future_obj_global, obj_global) = { let env = &mut *cell.borrow_mut(); let future_obj = env - .new_object("io/github/gedgygedgy/rust/future/SimpleFuture", "()V", &[]) + .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) .unwrap(); let future_obj_global = env.new_global_ref(&future_obj).unwrap(); let future = JSendFuture::from_env(env, &future_obj).unwrap(); - let obj = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); let obj_global = env.new_global_ref(&obj).unwrap(); (future, future_obj_global, obj_global) }; @@ -350,8 +352,8 @@ mod test { let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); env.call_method( &future_local, - "wake", - "(Ljava/lang/Object;)V", + jni_str!("wake"), + jni_sig!("(Ljava/lang/Object;)V"), &[(&obj_local).into()], ) .unwrap(); diff --git a/src/droidplug/jni_utils/mod.rs b/src/droidplug/jni_utils/mod.rs index 6334cce9..cbe02d2a 100644 --- a/src/droidplug/jni_utils/mod.rs +++ b/src/droidplug/jni_utils/mod.rs @@ -9,7 +9,7 @@ pub mod uuid; #[cfg(test)] pub(crate) mod test_utils { - use jni::{JNIEnv, JavaVM, objects::GlobalRef}; + use jni::{JNIEnv, JavaVM, jni_str, jni_sig, objects::GlobalRef}; use lazy_static::lazy_static; use std::{ cell::RefCell, @@ -32,24 +32,24 @@ pub(crate) mod test_utils { super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnBiFunctionImpl")?; super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnFunctionImpl")?; - let class = env.find_class("io/github/gedgygedgy/rust/ops/FnAdapter")?; - env.register_native_methods( + let class = env.find_class(jni_str!("io/github/gedgygedgy/rust/ops/FnAdapter"))?; + unsafe { env.register_native_methods( &class, &[ NativeMethod { - name: "callInternal".into(), + name: jni_str!("callInternal").into(), sig: - "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;" + jni_sig!("(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;") .into(), fn_ptr: super::ops::fn_adapter_call_internal as *mut c_void, }, NativeMethod { - name: "closeInternal".into(), - sig: "()V".into(), + name: jni_str!("closeInternal").into(), + sig: jni_sig!("()V").into(), fn_ptr: super::ops::fn_adapter_close_internal as *mut c_void, }, ], - )?; + )? }; Ok(()) } @@ -95,9 +95,9 @@ pub(crate) mod test_utils { let thread = env .call_static_method( - "java/lang/Thread", - "currentThread", - "()Ljava/lang/Thread;", + jni_str!("java/lang/Thread"), + jni_str!("currentThread"), + jni_sig!("()Ljava/lang/Thread;"), &[], ) .unwrap() @@ -105,8 +105,8 @@ pub(crate) mod test_utils { .unwrap(); env.call_method( &thread, - "setContextClassLoader", - "(Ljava/lang/ClassLoader;)V", + jni_str!("setContextClassLoader"), + jni_sig!("(Ljava/lang/ClassLoader;)V"), &[(&JVM.class_loader).into()] ).unwrap(); @@ -141,9 +141,9 @@ pub(crate) mod test_utils { let thread = env .call_static_method( - "java/lang/Thread", - "currentThread", - "()Ljava/lang/Thread;", + jni_str!("java/lang/Thread"), + jni_str!("currentThread"), + jni_sig!("()Ljava/lang/Thread;"), &[], ) .unwrap() @@ -152,8 +152,8 @@ pub(crate) mod test_utils { let class_loader = env .call_method( &thread, - "getContextClassLoader", - "()Ljava/lang/ClassLoader;", + jni_str!("getContextClassLoader"), + jni_sig!("()Ljava/lang/ClassLoader;"), &[], ) .unwrap() diff --git a/src/droidplug/jni_utils/ops.rs b/src/droidplug/jni_utils/ops.rs index 311cd4db..4f5aa8b8 100644 --- a/src/droidplug/jni_utils/ops.rs +++ b/src/droidplug/jni_utils/ops.rs @@ -1,6 +1,7 @@ use ::jni::{ JNIEnv, errors::Result, + jni_sig, jni_str, objects::{JClass, JObject}, }; use std::sync::{Arc, Mutex}; @@ -34,7 +35,7 @@ macro_rules! define_fn_adapter { let class = super::classcache::get_class($ic).unwrap(); env.new_object( <&JClass>::from(class.as_obj()), - "(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V", + jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) } @@ -63,7 +64,7 @@ macro_rules! define_fn_adapter { let class = super::classcache::get_class($ic).unwrap(); env.new_object( <&JClass>::from(class.as_obj()), - "(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V", + jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) } @@ -93,7 +94,7 @@ macro_rules! define_fn_adapter { let class = super::classcache::get_class($ic).unwrap(); env.new_object( <&JClass>::from(class.as_obj()), - "(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V", + jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) } @@ -279,10 +280,10 @@ fn fn_adapter<'local>( let class = super::classcache::get_class("io/github/gedgygedgy/rust/ops/FnAdapter").unwrap(); let obj = env.new_object( <&JClass>::from(class.as_obj()), - "(Z)V", + jni_sig!("(Z)V"), &[local.into()], )?; - unsafe { env.set_rust_field::<_, _, FnWrapper>(&obj, "data", SendSyncWrapper(arc)) }?; + unsafe { env.set_rust_field::<_, _, FnWrapper>(&obj, jni_str!("data"), SendSyncWrapper(arc)) }?; Ok(obj) } @@ -296,7 +297,7 @@ pub(crate) extern "C" fn fn_adapter_call_internal<'local>( use std::panic::{AssertUnwindSafe, catch_unwind}; let arc = - if let Ok(f) = unsafe { env.get_rust_field::<_, _, FnWrapper>(&obj1, "data") } { + if let Ok(f) = unsafe { env.get_rust_field::<_, _, FnWrapper>(&obj1, jni_str!("data")) } { AssertUnwindSafe(f.0.clone()) } else { return JObject::null(); @@ -314,7 +315,7 @@ pub(crate) extern "C" fn fn_adapter_close_internal(mut env: JNIEnv, obj: JObject use std::panic::{AssertUnwindSafe, catch_unwind}; let result = catch_unwind(AssertUnwindSafe(|| { - let _ = unsafe { env.take_rust_field::<_, _, FnWrapper>(&obj, "data") }; + let _ = unsafe { env.take_rust_field::<_, _, FnWrapper>(&obj, jni_str!("data")) }; })); if let Err(panic) = result { let _ = super::exceptions::throw_panic(&mut env, panic); diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index 751a95e5..70399ccf 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -2,6 +2,7 @@ use super::task::JPollResult; use ::jni::{ Env, JavaVM, errors::Result, + jni_sig, jni_str, objects::{Global, JClass, JMethodID, JObject}, signature::ReturnType, sys::jvalue, @@ -24,8 +25,8 @@ impl<'a> JStream<'a> { super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream").unwrap(); let poll_next_id = env.get_method_id( <&JClass>::from(class.as_obj()), - "pollNext", - "(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;", + jni_str!("pollNext"), + jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), )?; Ok(Self { internal: obj, @@ -87,8 +88,8 @@ impl JSendStream { super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream").unwrap(); let poll_next_id = env.get_method_id( <&JClass>::from(class.as_obj()), - "pollNext", - "(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;", + jni_str!("pollNext"), + jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), )?; Ok(Self { internal: env.new_global_ref(obj)?, @@ -164,8 +165,8 @@ impl<'a> JStreamPoll<'a> { super::classcache::get_class("io/github/gedgygedgy/rust/stream/StreamPoll").unwrap(); let get = env.get_method_id( <&JClass>::from(class.as_obj()), - "get", - "()Ljava/lang/Object;", + jni_str!("get"), + jni_sig!("()Ljava/lang/Object;"), )?; Ok(Self { internal: obj, get }) } @@ -181,6 +182,7 @@ mod test { use super::super::test_utils; use super::{JSendStream, JStream}; use futures::stream::Stream; + use jni::{jni_sig, jni_str}; use std::{ pin::Pin, task::{Context, Poll}, @@ -202,7 +204,7 @@ mod test { assert_eq!(data.value(), false); let stream_obj = env - .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) + .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) .unwrap(); let stream_local = env.new_local_ref(&stream_obj).unwrap(); let jstream = JStream::from_env(env, stream_local).unwrap(); @@ -216,11 +218,11 @@ mod test { assert_eq!(Arc::strong_count(&data), 3); assert_eq!(data.value(), false); - let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); env.call_method( &stream_obj, - "add", - "(Ljava/lang/Object;)V", + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), &[(&obj1).into()], ) .unwrap(); @@ -228,11 +230,11 @@ mod test { assert_eq!(data.value(), true); data.set_value(false); - let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj2 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); env.call_method( &stream_obj, - "add", - "(Ljava/lang/Object;)V", + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), &[(&obj2).into()], ) .unwrap(); @@ -265,7 +267,7 @@ mod test { assert_eq!(Arc::strong_count(&data), 3); assert_eq!(data.value(), false); - env.call_method(&stream_obj, "finish", "()V", &[]).unwrap(); + env.call_method(&stream_obj, jni_str!("finish"), jni_sig!("()V"), &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), true); data.set_value(false); @@ -288,15 +290,15 @@ mod test { let (mut stream, stream_obj_global, obj1_global, obj2_global) = { let env = &mut *cell.borrow_mut(); let stream_obj = env - .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) + .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) .unwrap(); let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); let stream_local = env.new_local_ref(&stream_obj).unwrap(); let jstream = JStream::from_env(env, stream_local).unwrap(); let stream = JSendStream::new(env, &jstream).unwrap(); - let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); let obj1_global = env.new_global_ref(&obj1).unwrap(); - let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj2 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); let obj2_global = env.new_global_ref(&obj2).unwrap(); (stream, stream_obj_global, obj1_global, obj2_global) }; @@ -310,19 +312,19 @@ mod test { let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); env.call_method( &s, - "add", - "(Ljava/lang/Object;)V", + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), &[(&o1).into()], ) .unwrap(); env.call_method( &s, - "add", - "(Ljava/lang/Object;)V", + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), &[(&o2).into()], ) .unwrap(); - env.call_method(&s, "finish", "()V", &[]).unwrap(); + env.call_method(&s, jni_str!("finish"), jni_sig!("()V"), &[]).unwrap(); }, async { use futures::StreamExt; @@ -357,13 +359,13 @@ mod test { let (mut stream, stream_obj_global, obj1_global, obj2_global) = { let env = &mut *cell.borrow_mut(); let stream_obj = env - .new_object("io/github/gedgygedgy/rust/stream/QueueStream", "()V", &[]) + .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) .unwrap(); let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); let stream = JSendStream::from_env(env, &stream_obj).unwrap(); - let obj1 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); let obj1_global = env.new_global_ref(&obj1).unwrap(); - let obj2 = env.new_object("java/lang/Object", "()V", &[]).unwrap(); + let obj2 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); let obj2_global = env.new_global_ref(&obj2).unwrap(); (stream, stream_obj_global, obj1_global, obj2_global) }; @@ -377,19 +379,19 @@ mod test { let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); env.call_method( &s, - "add", - "(Ljava/lang/Object;)V", + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), &[(&o1).into()], ) .unwrap(); env.call_method( &s, - "add", - "(Ljava/lang/Object;)V", + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), &[(&o2).into()], ) .unwrap(); - env.call_method(&s, "finish", "()V", &[]).unwrap(); + env.call_method(&s, jni_str!("finish"), jni_sig!("()V"), &[]).unwrap(); }, async { use futures::StreamExt; diff --git a/src/droidplug/jni_utils/task.rs b/src/droidplug/jni_utils/task.rs index 32339b6e..bda74312 100644 --- a/src/droidplug/jni_utils/task.rs +++ b/src/droidplug/jni_utils/task.rs @@ -1,6 +1,7 @@ use ::jni::{ Env, errors::Result, + jni_sig, jni_str, objects::{JClass, JMethodID, JObject}, signature::ReturnType, }; @@ -12,7 +13,7 @@ pub fn waker<'a>(env: &mut Env<'a>, waker: Waker) -> Result> { let class = super::classcache::get_class("io/github/gedgygedgy/rust/task/Waker").unwrap(); let obj = env.new_object( <&JClass>::from(class.as_obj()), - "(Lio/github/gedgygedgy/rust/ops/FnRunnable;)V", + jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnRunnable;)V"), &[(&runnable).into()], )?; Ok(obj) @@ -27,7 +28,7 @@ impl<'a> JPollResult<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = super::classcache::get_class("io/github/gedgygedgy/rust/task/PollResult").unwrap(); - let get = env.get_method_id(<&JClass>::from(class.as_obj()), "get", "()Ljava/lang/Object;")?; + let get = env.get_method_id(<&JClass>::from(class.as_obj()), jni_str!("get"), jni_sig!("()Ljava/lang/Object;"))?; Ok(Self { internal: obj, get }) } @@ -54,6 +55,7 @@ impl<'a> From> for JObject<'a> { #[cfg(test)] mod test { use super::super::test_utils; + use jni::{jni_sig, jni_str}; use std::sync::Arc; #[test] @@ -73,12 +75,12 @@ mod test { assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - env.call_method(&jwaker, "wake", "()V", &[]).unwrap(); + env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), true); data.set_value(false); - env.call_method(&jwaker, "wake", "()V", &[]).unwrap(); + env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); }); @@ -101,11 +103,11 @@ mod test { assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - env.call_method(&jwaker, "close", "()V", &[]).unwrap(); + env.call_method(&jwaker, jni_str!("close"), jni_sig!("()V"), &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); - env.call_method(&jwaker, "wake", "()V", &[]).unwrap(); + env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); }); diff --git a/src/droidplug/jni_utils/uuid.rs b/src/droidplug/jni_utils/uuid.rs index caa05ff2..fd7dbaa0 100644 --- a/src/droidplug/jni_utils/uuid.rs +++ b/src/droidplug/jni_utils/uuid.rs @@ -1,6 +1,7 @@ use jni::{ Env, errors::Result, + jni_str, jni_sig, objects::{JMethodID, JObject}, signature::{Primitive, ReturnType}, sys::jlong, @@ -15,11 +16,11 @@ pub struct JUuid<'a> { impl<'a> JUuid<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class("java/util/UUID")?; + let class = env.find_class(jni_str!("java/util/UUID"))?; let get_least_significant_bits = - env.get_method_id(&class, "getLeastSignificantBits", "()J")?; + env.get_method_id(&class, jni_str!("getLeastSignificantBits"), jni_sig!("()J"))?; let get_most_significant_bits = - env.get_method_id(&class, "getMostSignificantBits", "()J")?; + env.get_method_id(&class, jni_str!("getMostSignificantBits"), jni_sig!("()J"))?; Ok(Self { internal: obj, get_least_significant_bits, @@ -32,12 +33,12 @@ impl<'a> JUuid<'a> { let least = (val & 0xFFFFFFFFFFFFFFFF) as jlong; let most = ((val >> 64) & 0xFFFFFFFFFFFFFFFF) as jlong; - let class = env.find_class("java/util/UUID")?; - let obj = env.new_object(&class, "(JJ)V", &[most.into(), least.into()])?; + let class = env.find_class(jni_str!("java/util/UUID"))?; + let obj = env.new_object(&class, jni_sig!("(JJ)V"), &[most.into(), least.into()])?; let get_least_significant_bits = - env.get_method_id(&class, "getLeastSignificantBits", "()J")?; + env.get_method_id(&class, jni_str!("getLeastSignificantBits"), jni_sig!("()J"))?; let get_most_significant_bits = - env.get_method_id(&class, "getMostSignificantBits", "()J")?; + env.get_method_id(&class, jni_str!("getMostSignificantBits"), jni_sig!("()J"))?; Ok(Self { internal: obj, get_least_significant_bits, @@ -87,7 +88,7 @@ impl<'a> From> for JObject<'a> { mod test { use super::super::test_utils; use super::JUuid; - use jni::{objects::JObject, sys::jlong}; + use jni::{jni_str, jni_sig, objects::JObject, sys::jlong}; use uuid::Uuid; struct UuidTest { @@ -121,12 +122,12 @@ mod test { let obj: JObject = uuid_obj.into(); let actual_most = env - .call_method(&obj, "getMostSignificantBits", "()J", &[]) + .call_method(&obj, jni_str!("getMostSignificantBits"), jni_sig!("()J"), &[]) .unwrap() .j() .unwrap(); let actual_least = env - .call_method(&obj, "getLeastSignificantBits", "()J", &[]) + .call_method(&obj, jni_str!("getLeastSignificantBits"), jni_sig!("()J"), &[]) .unwrap() .j() .unwrap(); @@ -145,7 +146,7 @@ mod test { let least = test.least as jlong; let obj = env - .new_object("java/util/UUID", "(JJ)V", &[most.into(), least.into()]) + .new_object(jni_str!("java/util/UUID"), jni_sig!("(JJ)V"), &[most.into(), least.into()]) .unwrap(); let uuid_obj = JUuid::from_env(env, obj).unwrap(); diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index dc81c2fc..f8d6dc5f 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -15,7 +15,7 @@ use crate::{ use async_trait::async_trait; use futures::stream::Stream; use jni::{ - Env, + Env, jni_sig, jni_str, objects::{Global, JClass, JObject, JString, JValue}, }; #[cfg(feature = "serde")] @@ -67,7 +67,7 @@ fn get_poll_result<'a>( if env.is_instance_of(&ex, <&JClass>::from(future_exception_class.as_obj()))? { let cause = env - .call_method(&ex, "getCause", "()Ljava/lang/Throwable;", &[])? + .call_method(&ex, jni_str!("getCause"), jni_sig!("()Ljava/lang/Throwable;"), &[])? .l()?; let mut check = |name: &str| -> jni::errors::Result { @@ -97,9 +97,9 @@ fn get_poll_result<'a>( "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", )? { Err(Error::NoAdapterAvailable) - } else if env.is_instance_of(&cause, "java/lang/RuntimeException")? { + } else if env.is_instance_of(&cause, jni_str!("java/lang/RuntimeException"))? { let msg = env - .call_method(&cause, "getMessage", "()Ljava/lang/String;", &[])? + .call_method(&cause, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[])? .l()?; let jstr: JString = msg.into(); let msgstr: String = env.get_string(&jstr)?.into(); @@ -243,7 +243,7 @@ impl api::Peripheral for Peripheral { let mtu_result_ref = mtu_future.await?; self.with_obj(|env, _obj| -> Result<()> { let mtu_obj = get_poll_result(env, &mtu_result_ref)?; - let mtu_val = env.call_method(&mtu_obj, "intValue", "()I", &[])?.i()?; + let mtu_val = env.call_method(&mtu_obj, jni_str!("intValue"), jni_sig!("()I"), &[])?.i()?; self.mtu.store(mtu_val as u16, Ordering::Relaxed); Ok(()) })?; @@ -274,13 +274,13 @@ impl api::Peripheral for Peripheral { use std::iter::FromIterator; let obj = get_poll_result(env, &result_ref)?; - let size = env.call_method(&obj, "size", "()I", &[])?.i()?; + let size = env.call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])?.i()?; let mut peripheral_services = Vec::new(); let mut peripheral_characteristics = Vec::new(); for i in 0..size { let svc_obj = env - .call_method(&obj, "get", "(I)Ljava/lang/Object;", &[JValue::from(i)])? + .call_method(&obj, jni_str!("get"), jni_sig!("(I)Ljava/lang/Object;"), &[JValue::from(i)])? .l()?; let service = JBluetoothGattService::from_env(env, svc_obj)?; let mut characteristics = BTreeSet::::new(); @@ -414,7 +414,7 @@ impl api::Peripheral for Peripheral { let result_ref = future.await?; self.with_obj(|env, _obj| { let rssi_obj = get_poll_result(env, &result_ref)?; - let rssi_val = env.call_method(&rssi_obj, "intValue", "()I", &[])?.i()?; + let rssi_val = env.call_method(&rssi_obj, jni_str!("intValue"), jni_sig!("()I"), &[])?.i()?; Ok(rssi_val as i16) }) } From 6a19506529576607a9a633255bb73a534320ffd9 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 20:04:11 -0700 Subject: [PATCH 08/77] refactor: Migrate to callback-based attach_current_thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all deprecated get_env() and attach_current_thread_permanently() calls with jni 0.22's callback-based attach_current_thread(|env| {...}). Production code: adapter.rs (4 sites), peripheral.rs with_obj + notifications closure (2 sites), future.rs poll_internal (1 site), stream.rs poll_next_internal (1 site). Test infrastructure: Replace RefCell> thread-local with with_env(f) helper that wraps attach_current_thread. This is a fundamental rethink — 0.22's callback model means Env can't escape the closure, so the old pattern of storing env in a RefCell is dead. All 25 test call sites migrated from JVM_ENV.with(|cell|) to with_env(|env|). Block_on/join tests use nested with_env calls for independent env access. Co-Authored-By: Claude Opus 4.6 --- src/droidplug/adapter.rs | 53 +++--- src/droidplug/jni_utils/arrays.rs | 12 +- src/droidplug/jni_utils/exceptions.rs | 105 ++++++------ src/droidplug/jni_utils/future.rs | 202 +++++++++++------------ src/droidplug/jni_utils/mod.rs | 106 ++++++------ src/droidplug/jni_utils/stream.rs | 225 +++++++++++++------------- src/droidplug/jni_utils/task.rs | 14 +- src/droidplug/jni_utils/uuid.rs | 12 +- src/droidplug/peripheral.rs | 50 +++--- 9 files changed, 392 insertions(+), 387 deletions(-) diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index 2befcf2c..0bbf4f7d 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -40,21 +40,21 @@ impl Debug for Adapter { impl Adapter { pub(crate) fn new() -> Result { - let mut env = global_jvm().get_env()?; - - let obj = env.new_object( - jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"), - jni_sig!("()V"), - &[], - )?; - let internal = Arc::new(env.new_global_ref(&obj)?); - let adapter = Self { - manager: Arc::new(AdapterManager::default()), - internal, - }; - unsafe { env.set_rust_field(&obj, jni_str!("handle"), adapter.clone()) }?; - - Ok(adapter) + global_jvm().attach_current_thread(|env| { + let obj = env.new_object( + jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"), + jni_sig!("()V"), + &[], + )?; + let internal = Arc::new(env.new_global_ref(&obj)?); + let adapter = Self { + manager: Arc::new(AdapterManager::default()), + internal, + }; + unsafe { env.set_rust_field(&obj, jni_str!("handle"), adapter.clone()) }?; + + Ok(adapter) + }) } pub fn report_scan_result<'a>( @@ -87,11 +87,12 @@ impl Adapter { } fn add(&self, address: BDAddr) -> Result { - let mut env = global_jvm().get_env()?; - let local_adapter = env.new_local_ref(self.internal.as_obj())?; - let peripheral = Peripheral::new(&mut env, local_adapter, address)?; - self.manager.add_peripheral(peripheral.clone()); - Ok(peripheral) + global_jvm().attach_current_thread(|env| { + let local_adapter = env.new_local_ref(self.internal.as_obj())?; + let peripheral = Peripheral::new(env, local_adapter, address)?; + self.manager.add_peripheral(peripheral.clone()); + Ok(peripheral) + }) } fn report_properties( @@ -135,8 +136,8 @@ impl Central for Adapter { } async fn start_scan(&self, filter: ScanFilter) -> Result<()> { - let mut env = global_jvm().get_env()?; - let filter = JScanFilter::new(&mut env, filter)?; + global_jvm().attach_current_thread(|env| { + let filter = JScanFilter::new(env, filter)?; let filter_obj: JObject = filter.into(); match env.call_method( self.internal.as_obj(), @@ -170,12 +171,14 @@ impl Central for Adapter { } Err(e) => Err(e.into()), } + }) } async fn stop_scan(&self) -> Result<()> { - let mut env = global_jvm().get_env()?; - env.call_method(self.internal.as_obj(), jni_str!("stopScan"), jni_sig!("()V"), &[])?; - Ok(()) + global_jvm().attach_current_thread(|env| { + env.call_method(self.internal.as_obj(), jni_str!("stopScan"), jni_sig!("()V"), &[])?; + Ok(()) + }) } async fn peripherals(&self) -> Result> { diff --git a/src/droidplug/jni_utils/arrays.rs b/src/droidplug/jni_utils/arrays.rs index 71918916..a523c9a4 100644 --- a/src/droidplug/jni_utils/arrays.rs +++ b/src/droidplug/jni_utils/arrays.rs @@ -30,27 +30,27 @@ mod test { #[test] fn test_slice_to_byte_array() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { let obj = super::slice_to_byte_array(env, &[1, 2, 3, 4, 5]).unwrap(); assert_eq!(env.get_array_length(&obj).unwrap(), 5); let mut bytes = [0i8; 5]; env.get_byte_array_region(&obj, 0, &mut bytes).unwrap(); assert_eq!(bytes, [1, 2, 3, 4, 5]); - }); + Ok(()) + }).unwrap(); } #[test] fn test_byte_array_to_vec() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { let obj = env.new_byte_array(5).unwrap(); env.set_byte_array_region(&obj, 0, &[1, 2, 3, 4, 5]) .unwrap(); let vec = super::byte_array_to_vec(env, &obj).unwrap(); assert_eq!(vec, vec![1, 2, 3, 4, 5]); - }); + Ok(()) + }).unwrap(); } } diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index 9391d648..9f1393ef 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -275,8 +275,7 @@ mod test { #[test] fn test_catch_first() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { assert_eq!( test_catch( env, @@ -288,13 +287,13 @@ mod test { 1 ); assert!(!env.exception_check().unwrap()); - }); + Ok(()) + }).unwrap(); } #[test] fn test_catch_second() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { assert_eq!( test_catch( env, @@ -306,13 +305,13 @@ mod test { 2 ); assert!(!env.exception_check().unwrap()); - }); + Ok(()) + }).unwrap(); } #[test] fn test_catch_third() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { assert_eq!( test_catch( env, @@ -324,22 +323,22 @@ mod test { 3 ); assert!(!env.exception_check().unwrap()); - }); + Ok(()) + }).unwrap(); } #[test] fn test_catch_ok() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { assert_eq!(test_catch(env, None, Ok(0), false).unwrap(), 0); assert!(!env.exception_check().unwrap()); - }); + Ok(()) + }).unwrap(); } #[test] fn test_catch_none() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { if let Error::JavaException = test_catch( env, Some("java/lang/SecurityException"), @@ -358,13 +357,13 @@ mod test { } else { panic!("No JavaException"); } - }); + Ok(()) + }).unwrap(); } #[test] fn test_catch_other() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { if let Error::InvalidCtorReturn = test_catch(env, None, Err(Error::InvalidCtorReturn), false).unwrap_err() { @@ -372,13 +371,13 @@ mod test { } else { panic!("InvalidCtorReturn not found"); } - }); + Ok(()) + }).unwrap(); } #[test] fn test_catch_bogus_exception() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { if let Error::JavaException = test_catch(env, None, Err(Error::JavaException), false).unwrap_err() { @@ -386,13 +385,13 @@ mod test { } else { panic!("JavaException not found"); } - }); + Ok(()) + }).unwrap(); } #[test] fn test_catch_prior_exception() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { let ex = JThrowable::from( env.new_object(jni_str!("java/lang/IllegalArgumentException"), jni_sig!("()V"), &[]) .unwrap(), @@ -407,13 +406,13 @@ mod test { } else { panic!("JavaException not found"); } - }); + Ok(()) + }).unwrap(); } #[test] fn test_catch_rethrow() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { if let Error::JavaException = test_catch( env, Some("java/lang/StringIndexOutOfBoundsException"), @@ -432,13 +431,13 @@ mod test { } else { panic!("JavaException not found"); } - }); + Ok(()) + }).unwrap(); } #[test] fn test_catch_bogus_rethrow() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { if let Error::JavaException = test_catch( env, Some("java/lang/ArrayIndexOutOfBoundsException"), @@ -451,14 +450,13 @@ mod test { } else { panic!("JavaException not found"); } - }); + Ok(()) + }).unwrap(); } #[test] fn test_panic_exception_static_str() { - test_utils::JVM_ENV.with(|cell| { - let mut guard = cell.borrow_mut(); - let env = &mut *guard; + test_utils::with_env(|env| { use jni::objects::JString; const STATIC_MSG: &str = "This is a &'static str"; @@ -477,14 +475,13 @@ mod test { .into(); let str = env.get_string(&msg).unwrap(); assert_eq!(>::from(str), STATIC_MSG); - }); + Ok(()) + }).unwrap(); } #[test] fn test_panic_exception_string() { - test_utils::JVM_ENV.with(|cell| { - let mut guard = cell.borrow_mut(); - let env = &mut *guard; + test_utils::with_env(|env| { use jni::objects::JString; use std::any::Any; @@ -507,14 +504,13 @@ mod test { let any: Box = ex.take(env).unwrap(); assert_eq!(*any.downcast::().unwrap(), STRING_MSG); - }); + Ok(()) + }).unwrap(); } #[test] fn test_panic_exception_other() { - test_utils::JVM_ENV.with(|cell| { - let mut guard = cell.borrow_mut(); - let env = &mut *guard; + test_utils::with_env(|env| { use jni::objects::JObject; use std::any::Any; @@ -534,23 +530,23 @@ mod test { let any: Box = ex.take(env).unwrap(); assert_eq!(*any.downcast::().unwrap(), 42); - }); + Ok(()) + }).unwrap(); } #[test] fn test_throw_unwind_ok() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { let result = super::throw_unwind(env, || 42).unwrap(); assert_eq!(result, 42); assert!(!env.exception_check().unwrap()); - }); + Ok(()) + }).unwrap(); } #[test] fn test_throw_unwind_panic() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { super::throw_unwind(env, || panic!("This is a panic")) .unwrap_err() .unwrap(); @@ -576,13 +572,13 @@ mod test { let any = ex.take(env).unwrap(); let str = any.downcast::<&str>().unwrap(); assert_eq!(*str, "This is a panic"); - }); + Ok(()) + }).unwrap(); } #[test] fn test_throw_unwind_panic_suppress() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { let old_ex = JThrowable::from(env.new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]).unwrap()); env.throw(&old_ex).unwrap(); @@ -614,16 +610,17 @@ mod test { let any = ex.take(env).unwrap(); let str = any.downcast::<&str>().unwrap(); assert_eq!(*str, "This is a panic"); - }); + Ok(()) + }).unwrap(); } #[test] #[should_panic(expected = "This is a panic")] fn test_panic_exception_resume_unwind() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { let ex = super::JPanicException::new(env, Box::new("This is a panic")).unwrap(); ex.resume_unwind(env).unwrap(); - }); + Ok(()) + }).unwrap(); } } diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 5dfb93ea..f3960576 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -94,23 +94,24 @@ impl JSendFuture { } fn poll_internal(&self, context: &mut Context<'_>) -> Result>>>> { - let mut env = self.vm.get_env()?; - let jwaker = super::task::waker(&mut env, context.waker().clone())?; - let result = unsafe { - env.call_method_unchecked( - self.internal.as_obj(), - self.poll_id, - ReturnType::Object, - &[jvalue { - l: jwaker.as_raw(), - }], - ) - }? - .l()?; - Ok(if env.is_same_object(&result, JObject::null())? { - Poll::Pending - } else { - Poll::Ready(Ok(env.new_global_ref(result)?)) + self.vm.attach_current_thread(|env| { + let jwaker = super::task::waker(env, context.waker().clone())?; + let result = unsafe { + env.call_method_unchecked( + self.internal.as_obj(), + self.poll_id, + ReturnType::Object, + &[jvalue { + l: jwaker.as_raw(), + }], + ) + }? + .l()?; + Ok(if env.is_same_object(&result, JObject::null())? { + Poll::Pending + } else { + Poll::Ready(Ok(env.new_global_ref(result)?)) + }) }) } } @@ -152,9 +153,7 @@ mod test { use super::super::task::JPollResult; use std::sync::Arc; - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); - + test_utils::with_env(|env| { let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -218,7 +217,9 @@ mod test { } assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), true); - }); + + Ok(()) + }).unwrap(); } #[test] @@ -226,25 +227,23 @@ mod test { use super::super::task::JPollResult; use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|cell| { - let (future, future_obj_global, obj_global) = { - let env = &mut *cell.borrow_mut(); - let future_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) - .unwrap(); - let future_obj_global = env.new_global_ref(&future_obj).unwrap(); - let future_local = env.new_local_ref(&future_obj).unwrap(); - let jfuture = JFuture::from_env(env, future_local).unwrap(); - let future = JSendFuture::new(env, &jfuture).unwrap(); - let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); - let obj_global = env.new_global_ref(&obj).unwrap(); - (future, future_obj_global, obj_global) - }; - - block_on(async { - join!( - async { - let env = &mut *cell.borrow_mut(); + let (future, future_obj_global, obj_global) = test_utils::with_env(|env| { + let future_obj = env + .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) + .unwrap(); + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future_local = env.new_local_ref(&future_obj).unwrap(); + let jfuture = JFuture::from_env(env, future_local).unwrap(); + let future = JSendFuture::new(env, &jfuture).unwrap(); + let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj_global = env.new_global_ref(&obj).unwrap(); + Ok((future, future_obj_global, obj_global)) + }).unwrap(); + + block_on(async { + join!( + async { + test_utils::with_env(|env| { let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); env.call_method( @@ -254,18 +253,21 @@ mod test { &[(&obj_local).into()], ) .unwrap(); - }, - async { - let global = future.await.unwrap(); - let env = &mut *cell.borrow_mut(); + Ok(()) + }).unwrap(); + }, + async { + let global = future.await.unwrap(); + test_utils::with_env(|env| { let local = env.new_local_ref(global.as_obj()).unwrap(); let poll_result = JPollResult::from_env(env, local).unwrap(); let result_obj = poll_result.get(env).unwrap(); let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); assert!(env.is_same_object(&result_obj, &obj_local).unwrap()); - } - ); - }); + Ok(()) + }).unwrap(); + } + ); }); } @@ -273,25 +275,23 @@ mod test { fn test_jfuture_await_throw() { use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|cell| { - let (future, future_obj_global, ex_global) = { - let env = &mut *cell.borrow_mut(); - let future_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) - .unwrap(); - let future_obj_global = env.new_global_ref(&future_obj).unwrap(); - let future_local = env.new_local_ref(&future_obj).unwrap(); - let jfuture = JFuture::from_env(env, future_local).unwrap(); - let future = JSendFuture::new(env, &jfuture).unwrap(); - let ex = env.new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]).unwrap(); - let ex_global = env.new_global_ref(&ex).unwrap(); - (future, future_obj_global, ex_global) - }; - - block_on(async { - join!( - async { - let env = &mut *cell.borrow_mut(); + let (future, future_obj_global, ex_global) = test_utils::with_env(|env| { + let future_obj = env + .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) + .unwrap(); + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future_local = env.new_local_ref(&future_obj).unwrap(); + let jfuture = JFuture::from_env(env, future_local).unwrap(); + let future = JSendFuture::new(env, &jfuture).unwrap(); + let ex = env.new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]).unwrap(); + let ex_global = env.new_global_ref(&ex).unwrap(); + Ok((future, future_obj_global, ex_global)) + }).unwrap(); + + block_on(async { + join!( + async { + test_utils::with_env(|env| { let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); let ex_local = env.new_local_ref(ex_global.as_obj()).unwrap(); env.call_method( @@ -301,12 +301,14 @@ mod test { &[(&ex_local).into()], ) .unwrap(); - }, - async { - use super::super::task::JPollResult; - - let global = future.await.unwrap(); - let env = &mut *cell.borrow_mut(); + Ok(()) + }).unwrap(); + }, + async { + use super::super::task::JPollResult; + + let global = future.await.unwrap(); + test_utils::with_env(|env| { let local = env.new_local_ref(global.as_obj()).unwrap(); let poll_result = JPollResult::from_env(env, local).unwrap(); let _err = poll_result.get(env).unwrap_err(); @@ -320,9 +322,10 @@ mod test { .unwrap(); let ex_local = env.new_local_ref(ex_global.as_obj()).unwrap(); assert!(env.is_same_object(&actual_ex, &ex_local).unwrap()); - } - ); - }); + Ok(()) + }).unwrap(); + } + ); }); } @@ -331,23 +334,21 @@ mod test { use super::super::task::JPollResult; use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|cell| { - let (future, future_obj_global, obj_global) = { - let env = &mut *cell.borrow_mut(); - let future_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) - .unwrap(); - let future_obj_global = env.new_global_ref(&future_obj).unwrap(); - let future = JSendFuture::from_env(env, &future_obj).unwrap(); - let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); - let obj_global = env.new_global_ref(&obj).unwrap(); - (future, future_obj_global, obj_global) - }; - - block_on(async { - join!( - async { - let env = &mut *cell.borrow_mut(); + let (future, future_obj_global, obj_global) = test_utils::with_env(|env| { + let future_obj = env + .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) + .unwrap(); + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future = JSendFuture::from_env(env, &future_obj).unwrap(); + let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj_global = env.new_global_ref(&obj).unwrap(); + Ok((future, future_obj_global, obj_global)) + }).unwrap(); + + block_on(async { + join!( + async { + test_utils::with_env(|env| { let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); env.call_method( @@ -357,18 +358,21 @@ mod test { &[(&obj_local).into()], ) .unwrap(); - }, - async { - let global_ref = future.await.unwrap(); - let env = &mut *cell.borrow_mut(); + Ok(()) + }).unwrap(); + }, + async { + let global_ref = future.await.unwrap(); + test_utils::with_env(|env| { let local = env.new_local_ref(global_ref.as_obj()).unwrap(); let jpoll = JPollResult::from_env(env, local).unwrap(); let result_obj = jpoll.get(env).unwrap(); let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); assert!(env.is_same_object(&result_obj, &obj_local).unwrap()); - } - ); - }); + Ok(()) + }).unwrap(); + } + ); }); } } diff --git a/src/droidplug/jni_utils/mod.rs b/src/droidplug/jni_utils/mod.rs index cbe02d2a..e7641a35 100644 --- a/src/droidplug/jni_utils/mod.rs +++ b/src/droidplug/jni_utils/mod.rs @@ -9,17 +9,17 @@ pub mod uuid; #[cfg(test)] pub(crate) mod test_utils { - use jni::{JNIEnv, JavaVM, jni_str, jni_sig, objects::GlobalRef}; + use jni::{Env, JavaVM, jni_str, jni_sig, objects::Global, objects::JObject}; use lazy_static::lazy_static; use std::{ - cell::RefCell, + cell::Cell, sync::{Arc, Mutex}, task::{Wake, Waker}, }; use jni::NativeMethod; - fn test_init(env: &mut JNIEnv) -> jni::errors::Result<()> { + fn test_init(env: &mut Env) -> jni::errors::Result<()> { use std::ffi::c_void; super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/future/Future")?; super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/future/FutureException")?; @@ -86,32 +86,37 @@ pub(crate) mod test_utils { struct GlobalJVM { jvm: JavaVM, - class_loader: GlobalRef, + class_loader: Global>, } thread_local! { - pub static JVM_ENV: RefCell> = { - let mut env = JVM.jvm.attach_current_thread_permanently().unwrap(); - - let thread = env - .call_static_method( - jni_str!("java/lang/Thread"), - jni_str!("currentThread"), - jni_sig!("()Ljava/lang/Thread;"), - &[], - ) - .unwrap() - .l() - .unwrap(); - env.call_method( - &thread, - jni_str!("setContextClassLoader"), - jni_sig!("(Ljava/lang/ClassLoader;)V"), - &[(&JVM.class_loader).into()] - ).unwrap(); - - RefCell::new(env) - } + static CLASS_LOADER_SET: Cell = const { Cell::new(false) }; + } + + pub fn with_env(f: F) -> jni::errors::Result + where + F: FnOnce(&mut Env) -> jni::errors::Result, + { + JVM.jvm.attach_current_thread(|env| { + if !CLASS_LOADER_SET.with(|c| c.get()) { + let thread = env + .call_static_method( + jni_str!("java/lang/Thread"), + jni_str!("currentThread"), + jni_sig!("()Ljava/lang/Thread;"), + &[], + )? + .l()?; + env.call_method( + &thread, + jni_str!("setContextClassLoader"), + jni_sig!("(Ljava/lang/ClassLoader;)V"), + &[JVM.class_loader.as_obj().into()], + )?; + CLASS_LOADER_SET.with(|c| c.set(true)); + } + f(env) + }) } lazy_static! { @@ -136,30 +141,31 @@ pub(crate) mod test_utils { .unwrap(); let jvm = JavaVM::new(jvm_args).unwrap(); - let mut env = jvm.attach_current_thread_permanently().unwrap(); - test_init(&mut env).unwrap(); - - let thread = env - .call_static_method( - jni_str!("java/lang/Thread"), - jni_str!("currentThread"), - jni_sig!("()Ljava/lang/Thread;"), - &[], - ) - .unwrap() - .l() - .unwrap(); - let class_loader = env - .call_method( - &thread, - jni_str!("getContextClassLoader"), - jni_sig!("()Ljava/lang/ClassLoader;"), - &[], - ) - .unwrap() - .l() - .unwrap(); - let class_loader = env.new_global_ref(class_loader).unwrap(); + let class_loader = jvm.attach_current_thread(|env| { + test_init(env).unwrap(); + + let thread = env + .call_static_method( + jni_str!("java/lang/Thread"), + jni_str!("currentThread"), + jni_sig!("()Ljava/lang/Thread;"), + &[], + ) + .unwrap() + .l() + .unwrap(); + let class_loader = env + .call_method( + &thread, + jni_str!("getContextClassLoader"), + jni_sig!("()Ljava/lang/ClassLoader;"), + &[], + ) + .unwrap() + .l() + .unwrap(); + Ok::<_, jni::errors::Error>(env.new_global_ref(class_loader).unwrap()) + }).unwrap(); GlobalJVM { jvm, class_loader } }; diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index 70399ccf..0352bc85 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -102,34 +102,35 @@ impl JSendStream { &self, context: &mut Context<'_>, ) -> Result>>>>> { - let mut env = self.vm.get_env()?; - let jwaker = super::task::waker(&mut env, context.waker().clone())?; - let result = unsafe { - env.call_method_unchecked( - self.internal.as_obj(), - self.poll_next_id, - ReturnType::Object, - &[jvalue { - l: jwaker.as_raw(), - }], - ) - }? - .l()?; - - if env.is_same_object(&result, JObject::null())? { - return Ok(Poll::Pending); - } + self.vm.attach_current_thread(|env| { + let jwaker = super::task::waker(env, context.waker().clone())?; + let result = unsafe { + env.call_method_unchecked( + self.internal.as_obj(), + self.poll_next_id, + ReturnType::Object, + &[jvalue { + l: jwaker.as_raw(), + }], + ) + }? + .l()?; + + if env.is_same_object(&result, JObject::null())? { + return Ok(Poll::Pending); + } - let poll_result = JPollResult::from_env(&mut env, result)?; - let stream_poll_obj = poll_result.get(&mut env)?; + let poll_result = JPollResult::from_env(env, result)?; + let stream_poll_obj = poll_result.get(env)?; - if env.is_same_object(&stream_poll_obj, JObject::null())? { - return Ok(Poll::Ready(None)); - } + if env.is_same_object(&stream_poll_obj, JObject::null())? { + return Ok(Poll::Ready(None)); + } - let stream_poll = JStreamPoll::from_env(&mut env, stream_poll_obj)?; - let obj = stream_poll.get(&mut env)?; - Ok(Poll::Ready(Some(Ok(env.new_global_ref(obj)?)))) + let stream_poll = JStreamPoll::from_env(env, stream_poll_obj)?; + let obj = stream_poll.get(env)?; + Ok(Poll::Ready(Some(Ok(env.new_global_ref(obj)?)))) + }) } } @@ -192,9 +193,7 @@ mod test { fn test_jstream() { use std::sync::Arc; - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); - + test_utils::with_env(|env| { let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -279,34 +278,34 @@ mod test { } assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - }); + + Ok(()) + }).unwrap(); } #[test] fn test_jstream_await() { use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|cell| { - let (mut stream, stream_obj_global, obj1_global, obj2_global) = { - let env = &mut *cell.borrow_mut(); - let stream_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) - .unwrap(); - let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); - let stream_local = env.new_local_ref(&stream_obj).unwrap(); - let jstream = JStream::from_env(env, stream_local).unwrap(); - let stream = JSendStream::new(env, &jstream).unwrap(); - let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); - let obj1_global = env.new_global_ref(&obj1).unwrap(); - let obj2 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); - let obj2_global = env.new_global_ref(&obj2).unwrap(); - (stream, stream_obj_global, obj1_global, obj2_global) - }; - - block_on(async { - join!( - async { - let env = &mut *cell.borrow_mut(); + let (mut stream, stream_obj_global, obj1_global, obj2_global) = test_utils::with_env(|env| { + let stream_obj = env + .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) + .unwrap(); + let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); + let stream_local = env.new_local_ref(&stream_obj).unwrap(); + let jstream = JStream::from_env(env, stream_local).unwrap(); + let stream = JSendStream::new(env, &jstream).unwrap(); + let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj1_global = env.new_global_ref(&obj1).unwrap(); + let obj2 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj2_global = env.new_global_ref(&obj2).unwrap(); + Ok((stream, stream_obj_global, obj1_global, obj2_global)) + }).unwrap(); + + block_on(async { + join!( + async { + test_utils::with_env(|env| { let s = env.new_local_ref(stream_obj_global.as_obj()).unwrap(); let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); @@ -325,29 +324,28 @@ mod test { ) .unwrap(); env.call_method(&s, jni_str!("finish"), jni_sig!("()V"), &[]).unwrap(); - }, - async { - use futures::StreamExt; - let g1 = stream.next().await.unwrap().unwrap(); - { - let mut guard = cell.borrow_mut(); - let env = &mut *guard; - let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); - assert!(env.is_same_object(g1.as_obj(), &o1).unwrap()); - } - - let g2 = stream.next().await.unwrap().unwrap(); - { - let mut guard = cell.borrow_mut(); - let env = &mut *guard; - let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); - assert!(env.is_same_object(g2.as_obj(), &o2).unwrap()); - } - - assert!(stream.next().await.is_none()); - } - ); - }); + Ok(()) + }).unwrap(); + }, + async { + use futures::StreamExt; + let g1 = stream.next().await.unwrap().unwrap(); + test_utils::with_env(|env| { + let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); + assert!(env.is_same_object(g1.as_obj(), &o1).unwrap()); + Ok(()) + }).unwrap(); + + let g2 = stream.next().await.unwrap().unwrap(); + test_utils::with_env(|env| { + let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); + assert!(env.is_same_object(g2.as_obj(), &o2).unwrap()); + Ok(()) + }).unwrap(); + + assert!(stream.next().await.is_none()); + } + ); }); } @@ -355,25 +353,23 @@ mod test { fn test_jsendstream_await() { use futures::{executor::block_on, join}; - test_utils::JVM_ENV.with(|cell| { - let (mut stream, stream_obj_global, obj1_global, obj2_global) = { - let env = &mut *cell.borrow_mut(); - let stream_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) - .unwrap(); - let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); - let stream = JSendStream::from_env(env, &stream_obj).unwrap(); - let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); - let obj1_global = env.new_global_ref(&obj1).unwrap(); - let obj2 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); - let obj2_global = env.new_global_ref(&obj2).unwrap(); - (stream, stream_obj_global, obj1_global, obj2_global) - }; - - block_on(async { - join!( - async { - let env = &mut *cell.borrow_mut(); + let (mut stream, stream_obj_global, obj1_global, obj2_global) = test_utils::with_env(|env| { + let stream_obj = env + .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) + .unwrap(); + let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); + let stream = JSendStream::from_env(env, &stream_obj).unwrap(); + let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj1_global = env.new_global_ref(&obj1).unwrap(); + let obj2 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj2_global = env.new_global_ref(&obj2).unwrap(); + Ok((stream, stream_obj_global, obj1_global, obj2_global)) + }).unwrap(); + + block_on(async { + join!( + async { + test_utils::with_env(|env| { let s = env.new_local_ref(stream_obj_global.as_obj()).unwrap(); let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); @@ -392,29 +388,28 @@ mod test { ) .unwrap(); env.call_method(&s, jni_str!("finish"), jni_sig!("()V"), &[]).unwrap(); - }, - async { - use futures::StreamExt; - let g1 = stream.next().await.unwrap().unwrap(); - { - let mut guard = cell.borrow_mut(); - let env = &mut *guard; - let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); - assert!(env.is_same_object(g1.as_obj(), &o1).unwrap()); - } - - let g2 = stream.next().await.unwrap().unwrap(); - { - let mut guard = cell.borrow_mut(); - let env = &mut *guard; - let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); - assert!(env.is_same_object(g2.as_obj(), &o2).unwrap()); - } - - assert!(stream.next().await.is_none()); - } - ); - }); + Ok(()) + }).unwrap(); + }, + async { + use futures::StreamExt; + let g1 = stream.next().await.unwrap().unwrap(); + test_utils::with_env(|env| { + let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); + assert!(env.is_same_object(g1.as_obj(), &o1).unwrap()); + Ok(()) + }).unwrap(); + + let g2 = stream.next().await.unwrap().unwrap(); + test_utils::with_env(|env| { + let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); + assert!(env.is_same_object(g2.as_obj(), &o2).unwrap()); + Ok(()) + }).unwrap(); + + assert!(stream.next().await.is_none()); + } + ); }); } } diff --git a/src/droidplug/jni_utils/task.rs b/src/droidplug/jni_utils/task.rs index bda74312..f2393434 100644 --- a/src/droidplug/jni_utils/task.rs +++ b/src/droidplug/jni_utils/task.rs @@ -60,9 +60,7 @@ mod test { #[test] fn test_waker_wake() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); - + test_utils::with_env(|env| { let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -83,14 +81,13 @@ mod test { env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); - }); + Ok(()) + }).unwrap(); } #[test] fn test_waker_close_wake() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); - + test_utils::with_env(|env| { let data = Arc::new(test_utils::TestWakerData::new()); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); @@ -110,6 +107,7 @@ mod test { env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]).unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); - }); + Ok(()) + }).unwrap(); } } diff --git a/src/droidplug/jni_utils/uuid.rs b/src/droidplug/jni_utils/uuid.rs index fd7dbaa0..48618684 100644 --- a/src/droidplug/jni_utils/uuid.rs +++ b/src/droidplug/jni_utils/uuid.rs @@ -112,8 +112,7 @@ mod test { #[test] fn test_uuid_new() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { for test in TESTS { let most = test.most as jlong; let least = test.least as jlong; @@ -134,13 +133,13 @@ mod test { assert_eq!(actual_most, most); assert_eq!(actual_least, least); } - }); + Ok(()) + }).unwrap(); } #[test] fn test_uuid_as_uuid() { - test_utils::JVM_ENV.with(|cell| { - let env = &mut *cell.borrow_mut(); + test_utils::with_env(|env| { for test in TESTS { let most = test.most as jlong; let least = test.least as jlong; @@ -152,6 +151,7 @@ mod test { assert_eq!(uuid_obj.as_uuid(env).unwrap(), Uuid::from_u128(test.uuid)); } - }); + Ok(()) + }).unwrap(); } } diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index f8d6dc5f..50bedcea 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -162,10 +162,11 @@ impl Peripheral { where E: From<::jni::errors::Error>, { - let mut env = global_jvm().get_env()?; - let local_obj = env.new_local_ref(self.internal.as_obj())?; - let obj = JPeripheral::from_env(&mut env, local_obj)?; - f(&mut env, &obj) + global_jvm().attach_current_thread(|env| { + let local_obj = env.new_local_ref(self.internal.as_obj())?; + let obj = JPeripheral::from_env(env, local_obj)?; + f(env, &obj) + }) } async fn set_characteristic_notification( @@ -376,27 +377,28 @@ impl api::Peripheral for Peripheral { let stream = stream .map(move |item| match item { Ok(item) => { - let mut env = global_jvm().get_env()?; - let local_obj = env.new_local_ref(item.as_obj())?; - let characteristic = - JBluetoothGattCharacteristic::from_env(&mut env, local_obj)?; - let uuid = characteristic.get_uuid(&mut env)?; - let value = characteristic.get_value(&mut env)?; - let service_uuid = shared - .lock() - .ok() - .and_then(|guard| { - guard - .services - .iter() - .find(|s| s.characteristics.iter().any(|c| c.uuid == uuid)) - .map(|s| s.uuid) + global_jvm().attach_current_thread(|env| { + let local_obj = env.new_local_ref(item.as_obj())?; + let characteristic = + JBluetoothGattCharacteristic::from_env(env, local_obj)?; + let uuid = characteristic.get_uuid(env)?; + let value = characteristic.get_value(env)?; + let service_uuid = shared + .lock() + .ok() + .and_then(|guard| { + guard + .services + .iter() + .find(|s| s.characteristics.iter().any(|c| c.uuid == uuid)) + .map(|s| s.uuid) + }) + .unwrap_or_default(); + Ok(ValueNotification { + uuid, + service_uuid, + value, }) - .unwrap_or_default(); - Ok(ValueNotification { - uuid, - service_uuid, - value, }) } Err(err) => Err(err), From 79050533882a9eea8e5ba527e5112b358116ce1c Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 20:25:03 -0700 Subject: [PATCH 09/77] refactor: Migrate extern C functions to EnvUnowned + with_env pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the jni 0.22 FFI migration: - ops.rs: JNIEnv → Env for non-FFI code, EnvUnowned + with_env() for extern "C" fn_adapter_call_internal and fn_adapter_close_internal - jni/mod.rs: extern "C" callbacks migrated to EnvUnowned + with_env() - classcache: Store Global> instead of Global> so &Global satisfies Desc for new_object/get_method_id - Fix <&JClass>::from(class.as_obj()) → class.as_ref() across all files - arrays.rs: new_byte_array now takes usize, remove jint cast Co-Authored-By: Claude Opus 4.6 --- src/droidplug/jni/mod.rs | 22 ++++-- src/droidplug/jni_utils/arrays.rs | 4 +- src/droidplug/jni_utils/classcache.rs | 9 ++- src/droidplug/jni_utils/future.rs | 6 +- src/droidplug/jni_utils/ops.rs | 100 ++++++++++++++------------ src/droidplug/jni_utils/stream.rs | 8 +-- src/droidplug/jni_utils/task.rs | 6 +- 7 files changed, 86 insertions(+), 69 deletions(-) diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index 80ddf8e7..0c128e1c 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,6 +1,6 @@ pub mod objects; -use ::jni::{Env, JNIEnv, JavaVM, NativeMethod, jni_str, jni_sig, objects::JObject}; +use ::jni::{Env, EnvUnowned, JavaVM, NativeMethod, jni_str, jni_sig, objects::JObject}; use jni::{objects::JString, sys::jboolean}; use once_cell::sync::OnceCell; use std::ffi::c_void; @@ -134,17 +134,25 @@ impl From<::jni::errors::Error> for crate::Error { } } -extern "C" fn adapter_report_scan_result<'a>(mut env: JNIEnv<'a>, obj: JObject, scan_result: JObject<'a>) { - let _ = super::adapter::adapter_report_scan_result_internal(&mut env, &obj, scan_result); +extern "C" fn adapter_report_scan_result<'a>(mut env: EnvUnowned<'a>, obj: JObject, scan_result: JObject<'a>) { + let outcome = env.with_env(|env| { + let _ = super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result); + Ok::<(), jni::errors::Error>(()) + }); + let _ = outcome.into_outcome(); } extern "C" fn adapter_on_connection_state_changed( - mut env: JNIEnv, + mut env: EnvUnowned, obj: JObject, addr: JString, connected: jboolean, ) { - let _ = super::adapter::adapter_on_connection_state_changed_internal( - &mut env, &obj, addr, connected, - ); + let outcome = env.with_env(|env| { + let _ = super::adapter::adapter_on_connection_state_changed_internal( + env, &obj, addr, connected, + ); + Ok::<(), jni::errors::Error>(()) + }); + let _ = outcome.into_outcome(); } diff --git a/src/droidplug/jni_utils/arrays.rs b/src/droidplug/jni_utils/arrays.rs index a523c9a4..cd39c37c 100644 --- a/src/droidplug/jni_utils/arrays.rs +++ b/src/droidplug/jni_utils/arrays.rs @@ -2,12 +2,12 @@ use jni::{ Env, errors::Result, objects::JByteArray, - sys::{jbyte, jint}, + sys::jbyte, }; use std::slice; pub fn slice_to_byte_array<'local>(env: &mut Env<'local>, slice: &[u8]) -> Result> { - let obj = env.new_byte_array(slice.len() as jint)?; + let obj = env.new_byte_array(slice.len())?; let slice = unsafe { &*(slice as *const [u8] as *const [jbyte]) }; env.set_byte_array_region(&obj, 0, slice)?; Ok(obj) diff --git a/src/droidplug/jni_utils/classcache.rs b/src/droidplug/jni_utils/classcache.rs index 9aa35eba..a7db0ee5 100644 --- a/src/droidplug/jni_utils/classcache.rs +++ b/src/droidplug/jni_utils/classcache.rs @@ -1,21 +1,20 @@ use dashmap::DashMap; -use jni::{Env, errors::Result, objects::{Global, JObject}, strings::JNIString}; +use jni::{Env, errors::Result, objects::{Global, JClass}, strings::JNIString}; use once_cell::sync::OnceCell; use std::sync::Arc; -static CLASSCACHE: OnceCell>>>> = OnceCell::new(); +static CLASSCACHE: OnceCell>>>> = OnceCell::new(); pub fn find_add_class(env: &mut Env, classname: &str) -> Result<()> { let cache = CLASSCACHE.get_or_init(|| DashMap::new()); let jni_name = JNIString::from(classname); let cls = env.find_class(&jni_name)?; - let cls_obj: JObject = cls.into(); - let global = env.new_global_ref(&cls_obj)?; + let global = env.new_global_ref(&cls)?; cache.insert(classname.to_owned(), Arc::new(global)); Ok(()) } -pub fn get_class(classname: &str) -> Option>>> { +pub fn get_class(classname: &str) -> Option>>> { let cache = CLASSCACHE.get_or_init(|| DashMap::new()); cache.get(classname).map(|pair| pair.value().clone()) } diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index f3960576..5b5f4785 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -2,7 +2,7 @@ use ::jni::{ Env, JavaVM, errors::Result, jni_sig, jni_str, - objects::{Global, JClass, JMethodID, JObject}, + objects::{Global, JMethodID, JObject}, signature::ReturnType, sys::jvalue, }; @@ -23,7 +23,7 @@ impl<'a> JFuture<'a> { let class = super::classcache::get_class("io/github/gedgygedgy/rust/future/Future").unwrap(); let poll_id = env.get_method_id( - <&JClass>::from(class.as_obj()), + class.as_ref(), jni_str!("poll"), jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), )?; @@ -82,7 +82,7 @@ impl JSendFuture { let class = super::classcache::get_class("io/github/gedgygedgy/rust/future/Future").unwrap(); let poll_id = env.get_method_id( - <&JClass>::from(class.as_obj()), + class.as_ref(), jni_str!("poll"), jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), )?; diff --git a/src/droidplug/jni_utils/ops.rs b/src/droidplug/jni_utils/ops.rs index 4f5aa8b8..a09cb6b0 100644 --- a/src/droidplug/jni_utils/ops.rs +++ b/src/droidplug/jni_utils/ops.rs @@ -1,8 +1,8 @@ use ::jni::{ - JNIEnv, + Env, EnvUnowned, errors::Result, jni_sig, jni_str, - objects::{JClass, JObject}, + objects::JObject, }; use std::sync::{Arc, Mutex}; @@ -27,21 +27,21 @@ macro_rules! define_fn_adapter { closure: $closure:expr, ) => { fn $foi<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, $closure_name: impl for<'c, 'd> FnOnce$args -> $ret + 'static, local: bool, ) -> Result> { let adapter = fn_once_adapter(env, $closure, local)?; let class = super::classcache::get_class($ic).unwrap(); env.new_object( - <&JClass>::from(class.as_obj()), + class.as_ref(), jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) } pub fn $fo<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, f: impl for<'c, 'd> FnOnce$args -> $ret + Send + 'static, ) -> Result> { $foi(env, f, false) @@ -49,21 +49,21 @@ macro_rules! define_fn_adapter { #[allow(dead_code)] pub fn $fol<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, f: impl for<'c, 'd> FnOnce$args -> $ret + 'static, ) -> Result> { $foi(env, f, true) } fn $fmi<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, mut $closure_name: impl for<'c, 'd> FnMut$args -> $ret + 'static, local: bool, ) -> Result> { let adapter = fn_mut_adapter(env, $closure, local)?; let class = super::classcache::get_class($ic).unwrap(); env.new_object( - <&JClass>::from(class.as_obj()), + class.as_ref(), jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) @@ -71,7 +71,7 @@ macro_rules! define_fn_adapter { #[allow(dead_code)] pub fn $fm<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, f: impl for<'c, 'd> FnMut$args -> $ret + Send + 'static, ) -> Result> { $fmi(env, f, false) @@ -79,21 +79,21 @@ macro_rules! define_fn_adapter { #[allow(dead_code)] pub fn $fml<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, f: impl for<'c, 'd> FnMut$args -> $ret + 'static, ) -> Result> { $fmi(env, f, true) } fn $fi<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, $closure_name: impl for<'c, 'd> Fn$args -> $ret + 'static, local: bool, ) -> Result> { let adapter = fn_adapter(env, $closure, local)?; let class = super::classcache::get_class($ic).unwrap(); env.new_object( - <&JClass>::from(class.as_obj()), + class.as_ref(), jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) @@ -101,7 +101,7 @@ macro_rules! define_fn_adapter { #[allow(dead_code)] pub fn $f<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, f: impl for<'c, 'd> Fn$args -> $ret + Send + Sync + 'static, ) -> Result> { $fi(env, f, false) @@ -109,7 +109,7 @@ macro_rules! define_fn_adapter { #[allow(dead_code)] pub fn $fl<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, f: impl for<'c, 'd> Fn$args -> $ret + 'static, ) -> Result> { $fi(env, f, true) @@ -133,7 +133,7 @@ define_fn_adapter! { doc_fn_once: "fn_once_runnable", doc_fn: "fn_runnable", doc_noop: "be a no-op", - signature: f: impl for<'c, 'd> Fn(&'d mut JNIEnv<'c>, JObject<'c>) -> (), + signature: f: impl for<'c, 'd> Fn(&'d mut Env<'c>, JObject<'c>) -> (), closure: move |env, _obj1, obj2, _arg1, _arg2| { f(env, obj2); JObject::null() @@ -156,7 +156,7 @@ define_fn_adapter! { doc_fn_once: "fn_once_bi_function", doc_fn: "fn_bi_funciton", doc_noop: "return `null`", - signature: f: impl for<'c, 'd> Fn(&'d mut JNIEnv<'c>, JObject<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, + signature: f: impl for<'c, 'd> Fn(&'d mut Env<'c>, JObject<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, closure: move |env, _obj1, obj2, arg1, arg2| { f(env, obj2, arg1, arg2) }, @@ -178,7 +178,7 @@ define_fn_adapter! { doc_fn_once: "fn_once_function", doc_fn: "fn_function", doc_noop: "return `null`", - signature: f: impl for<'c, 'd> Fn(&'d mut JNIEnv<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, + signature: f: impl for<'c, 'd> Fn(&'d mut Env<'c>, JObject<'c>, JObject<'c>) -> JObject<'c>, closure: move |env, _obj1, obj2, arg1, _arg2| { f(env, obj2, arg1) }, @@ -192,7 +192,7 @@ unsafe impl Sync for SendSyncWrapper {} type FnWrapper = SendSyncWrapper< Arc< dyn for<'a, 'b> Fn( - &'b mut JNIEnv<'a>, + &'b mut Env<'a>, JObject<'a>, JObject<'a>, JObject<'a>, @@ -203,9 +203,9 @@ type FnWrapper = SendSyncWrapper< >; fn fn_once_adapter<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, f: impl for<'c, 'd> FnOnce( - &'d mut JNIEnv<'c>, + &'d mut Env<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -233,9 +233,9 @@ fn fn_once_adapter<'local>( } fn fn_mut_adapter<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, f: impl for<'c, 'd> FnMut( - &'d mut JNIEnv<'c>, + &'d mut Env<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -256,9 +256,9 @@ fn fn_mut_adapter<'local>( } fn fn_adapter<'local>( - env: &mut JNIEnv<'local>, + env: &mut Env<'local>, f: impl for<'c, 'd> Fn( - &'d mut JNIEnv<'c>, + &'d mut Env<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -269,7 +269,7 @@ fn fn_adapter<'local>( ) -> Result> { let arc: Arc< dyn for<'c, 'd> Fn( - &'d mut JNIEnv<'c>, + &'d mut Env<'c>, JObject<'c>, JObject<'c>, JObject<'c>, @@ -279,7 +279,7 @@ fn fn_adapter<'local>( let class = super::classcache::get_class("io/github/gedgygedgy/rust/ops/FnAdapter").unwrap(); let obj = env.new_object( - <&JClass>::from(class.as_obj()), + class.as_ref(), jni_sig!("(Z)V"), &[local.into()], )?; @@ -288,7 +288,7 @@ fn fn_adapter<'local>( } pub(crate) extern "C" fn fn_adapter_call_internal<'local>( - mut env: JNIEnv<'local>, + mut env: EnvUnowned<'local>, obj1: JObject<'local>, obj2: JObject<'local>, arg1: JObject<'local>, @@ -296,28 +296,38 @@ pub(crate) extern "C" fn fn_adapter_call_internal<'local>( ) -> JObject<'local> { use std::panic::{AssertUnwindSafe, catch_unwind}; - let arc = - if let Ok(f) = unsafe { env.get_rust_field::<_, _, FnWrapper>(&obj1, jni_str!("data")) } { - AssertUnwindSafe(f.0.clone()) - } else { - return JObject::null(); - }; - match catch_unwind(AssertUnwindSafe(|| arc(&mut env, obj1, obj2, arg1, arg2))) { - Ok(result) => result, - Err(panic) => { - let _ = super::exceptions::throw_panic(&mut env, panic); - JObject::null() + let outcome = env.with_env(|env| -> std::result::Result, jni::errors::Error> { + let arc = + if let Ok(f) = unsafe { env.get_rust_field::<_, _, FnWrapper>(&obj1, jni_str!("data")) } { + AssertUnwindSafe(f.0.clone()) + } else { + return Ok(JObject::null()); + }; + match catch_unwind(AssertUnwindSafe(|| arc(env, obj1, obj2, arg1, arg2))) { + Ok(result) => Ok(result), + Err(panic) => { + let _ = super::exceptions::throw_panic(env, panic); + Ok(JObject::null()) + } } + }); + match outcome.into_outcome() { + jni::Outcome::Ok(obj) => obj, + _ => JObject::null(), } } -pub(crate) extern "C" fn fn_adapter_close_internal(mut env: JNIEnv, obj: JObject) { +pub(crate) extern "C" fn fn_adapter_close_internal(mut env: EnvUnowned, obj: JObject) { use std::panic::{AssertUnwindSafe, catch_unwind}; - let result = catch_unwind(AssertUnwindSafe(|| { - let _ = unsafe { env.take_rust_field::<_, _, FnWrapper>(&obj, jni_str!("data")) }; - })); - if let Err(panic) = result { - let _ = super::exceptions::throw_panic(&mut env, panic); - } + let outcome = env.with_env(|env| { + let result = catch_unwind(AssertUnwindSafe(|| { + let _ = unsafe { env.take_rust_field::<_, _, FnWrapper>(&obj, jni_str!("data")) }; + })); + if let Err(panic) = result { + let _ = super::exceptions::throw_panic(env, panic); + } + Ok::<(), jni::errors::Error>(()) + }); + let _ = outcome.into_outcome(); } diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index 0352bc85..af1d4704 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -3,7 +3,7 @@ use ::jni::{ Env, JavaVM, errors::Result, jni_sig, jni_str, - objects::{Global, JClass, JMethodID, JObject}, + objects::{Global, JMethodID, JObject}, signature::ReturnType, sys::jvalue, }; @@ -24,7 +24,7 @@ impl<'a> JStream<'a> { let class = super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream").unwrap(); let poll_next_id = env.get_method_id( - <&JClass>::from(class.as_obj()), + class.as_ref(), jni_str!("pollNext"), jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), )?; @@ -87,7 +87,7 @@ impl JSendStream { let class = super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream").unwrap(); let poll_next_id = env.get_method_id( - <&JClass>::from(class.as_obj()), + class.as_ref(), jni_str!("pollNext"), jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), )?; @@ -165,7 +165,7 @@ impl<'a> JStreamPoll<'a> { let class = super::classcache::get_class("io/github/gedgygedgy/rust/stream/StreamPoll").unwrap(); let get = env.get_method_id( - <&JClass>::from(class.as_obj()), + class.as_ref(), jni_str!("get"), jni_sig!("()Ljava/lang/Object;"), )?; diff --git a/src/droidplug/jni_utils/task.rs b/src/droidplug/jni_utils/task.rs index f2393434..e412f55b 100644 --- a/src/droidplug/jni_utils/task.rs +++ b/src/droidplug/jni_utils/task.rs @@ -2,7 +2,7 @@ use ::jni::{ Env, errors::Result, jni_sig, jni_str, - objects::{JClass, JMethodID, JObject}, + objects::{JMethodID, JObject}, signature::ReturnType, }; use std::task::Waker; @@ -12,7 +12,7 @@ pub fn waker<'a>(env: &mut Env<'a>, waker: Waker) -> Result> { let class = super::classcache::get_class("io/github/gedgygedgy/rust/task/Waker").unwrap(); let obj = env.new_object( - <&JClass>::from(class.as_obj()), + class.as_ref(), jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnRunnable;)V"), &[(&runnable).into()], )?; @@ -28,7 +28,7 @@ impl<'a> JPollResult<'a> { pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { let class = super::classcache::get_class("io/github/gedgygedgy/rust/task/PollResult").unwrap(); - let get = env.get_method_id(<&JClass>::from(class.as_obj()), jni_str!("get"), jni_sig!("()Ljava/lang/Object;"))?; + let get = env.get_method_id(class.as_ref(), jni_str!("get"), jni_sig!("()Ljava/lang/Object;"))?; Ok(Self { internal: obj, get }) } From a2f581679a6700cd6169f5f5b992cf56e2b9652b Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 20:40:45 -0700 Subject: [PATCH 10/77] fix: Update exception handling and NativeMethod for jni 0.22 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - exception_check() now returns bool (was Result), remove ? - exception_occurred() now returns Option (was Result), use .unwrap() - exception_clear() now returns () (was Result<()>), remove ?/.unwrap() - throw() now returns Err(JavaException) on success; use match or let _ = instead of .unwrap()/.? to preserve original control flow semantics - JObject → JThrowable/JString: use env.cast_local::() (From impls removed) - NativeMethod struct fields replaced by unsafe from_raw_parts constructor - JObjectArray::from_raw now requires env param and element type param - JavaStr → MUTF8Chars in test string assertions (String::from(chars)) Co-Authored-By: Claude Opus 4.6 --- src/droidplug/adapter.rs | 10 +- src/droidplug/jni/mod.rs | 42 ++++---- src/droidplug/jni_utils/exceptions.rs | 143 ++++++++++++++------------ src/droidplug/jni_utils/future.rs | 2 +- src/droidplug/jni_utils/mod.rs | 22 ++-- src/droidplug/peripheral.rs | 14 +-- 6 files changed, 118 insertions(+), 115 deletions(-) diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index 0bbf4f7d..ef01d690 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; use futures::stream::Stream; use jni::{ Env, jni_sig, jni_str, - objects::{Global, JClass, JObject, JString}, + objects::{Global, JObject, JString}, sys::jboolean, }; use std::{ @@ -147,15 +147,15 @@ impl Central for Adapter { ) { Ok(_) => Ok(()), Err(jni::errors::Error::JavaException) => { - let ex = env.exception_occurred()?; - env.exception_clear()?; + let ex = env.exception_occurred().unwrap(); + env.exception_clear(); let no_adapter_class = super::jni_utils::classcache::get_class( "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", ) .unwrap(); - if env.is_instance_of(&ex, <&JClass>::from(no_adapter_class.as_obj()))? { + if env.is_instance_of(&ex, no_adapter_class.as_ref())? { Err(Error::NoAdapterAvailable) } else if env.is_instance_of(&ex, jni_str!("java/lang/RuntimeException"))? { let msg = env @@ -165,7 +165,7 @@ impl Central for Adapter { let msgstr: String = env.get_string(&jstr)?.into(); Err(Error::RuntimeError(msgstr)) } else { - env.throw(&ex)?; + let _ = env.throw(&ex); Err(jni::errors::Error::JavaException.into()) } } diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index 0c128e1c..b2f3b7f5 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -14,16 +14,16 @@ pub fn init(env: &mut Env) -> crate::Result<()> { unsafe { env.register_native_methods( &adapter_class, &[ - NativeMethod { - name: jni_str!("reportScanResult").into(), - sig: jni_sig!("(Landroid/bluetooth/le/ScanResult;)V").into(), - fn_ptr: adapter_report_scan_result as *mut c_void, - }, - NativeMethod { - name: jni_str!("onConnectionStateChanged").into(), - sig: jni_sig!("(Ljava/lang/String;Z)V").into(), - fn_ptr: adapter_on_connection_state_changed as *mut c_void, - }, + NativeMethod::from_raw_parts( + jni_str!("reportScanResult"), + jni_str!("(Landroid/bluetooth/le/ScanResult;)V"), + adapter_report_scan_result as *mut c_void, + ), + NativeMethod::from_raw_parts( + jni_str!("onConnectionStateChanged"), + jni_str!("(Ljava/lang/String;Z)V"), + adapter_on_connection_state_changed as *mut c_void, + ), ], )? }; super::jni_utils::classcache::find_add_class( @@ -103,18 +103,16 @@ pub fn init(env: &mut Env) -> crate::Result<()> { unsafe { env.register_native_methods( &fn_adapter_class, &[ - NativeMethod { - name: jni_str!("callInternal").into(), - sig: - jni_sig!("(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;") - .into(), - fn_ptr: super::jni_utils::ops::fn_adapter_call_internal as *mut c_void, - }, - NativeMethod { - name: jni_str!("closeInternal").into(), - sig: jni_sig!("()V").into(), - fn_ptr: super::jni_utils::ops::fn_adapter_close_internal as *mut c_void, - }, + NativeMethod::from_raw_parts( + jni_str!("callInternal"), + jni_str!("(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"), + super::jni_utils::ops::fn_adapter_call_internal as *mut c_void, + ), + NativeMethod::from_raw_parts( + jni_str!("closeInternal"), + jni_str!("()V"), + super::jni_utils::ops::fn_adapter_close_internal as *mut c_void, + ), ], )? }; diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index 9f1393ef..c9668f5d 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -27,7 +27,7 @@ pub fn try_block( ) -> TryCatchResult { TryCatchResult { try_result: (|| { - if env.exception_check()? { + if env.exception_check() { Err(Error::JavaException) } else { Ok(block(env)) @@ -59,13 +59,19 @@ impl TryCatchResult { }, (Ok(Err(Error::JavaException)), None) => { let catch_result = (|| { - if env.exception_check()? { - let ex = env.exception_occurred()?; - env.exception_clear()?; - if env.is_instance_of(&ex, class)? { - return block(env, ex).map(|o| Some(o)); + if env.exception_check() { + if let Some(ex) = env.exception_occurred() { + env.exception_clear(); + if env.is_instance_of(&ex, class)? { + return block(env, ex).map(|o| Some(o)); + } + // Rethrow — throw() returns Err(JavaException) on success + match env.throw(&ex) { + Err(Error::JavaException) => {} + Err(e) => return Err(e), + Ok(()) => {} + } } - env.throw(&ex)?; } Ok(None) })() @@ -118,8 +124,9 @@ impl<'a> JPanicException<'a> { &[(&msg).into()], )?; unsafe { env.set_rust_field(&obj, jni_str!("any"), any) }?; + let throwable = env.cast_local::(obj)?; Ok(Self { - internal: obj.into(), + internal: throwable, }) } @@ -160,10 +167,10 @@ pub fn throw_panic( env: &mut Env, panic: Box, ) -> Result<(), Error> { - let old_ex = if env.exception_check()? { - let ex = env.exception_occurred()?; - env.exception_clear()?; - Some(ex) + let old_ex = if env.exception_check() { + let ex = env.exception_occurred(); + env.exception_clear(); + ex } else { None }; @@ -178,8 +185,12 @@ pub fn throw_panic( )?; } let ex: JThrowable = ex.into(); - env.throw(&ex)?; - Ok(()) + // throw() returns Err(JavaException) on success in jni 0.22 + match env.throw(&ex) { + Err(Error::JavaException) => Ok(()), + Err(e) => Err(e), + Ok(()) => Ok(()), + } } /// Calls the given closure. If it panics, catch the unwind, wrap it in a @@ -204,10 +215,10 @@ mod test { try_result: Result, rethrow: bool, ) -> Result { - let old_ex = if env.exception_check().unwrap() { - let ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); - Some(ex) + let old_ex = if env.exception_check() { + let ex = env.exception_occurred(); + env.exception_clear(); + ex } else { None }; @@ -215,22 +226,22 @@ mod test { .find_class(jni_str!("java/lang/IllegalArgumentException")) .unwrap(); if let Some(ref ex) = old_ex { - env.throw(ex).unwrap(); + let _ = env.throw(ex); } let ex = throw_class.map(|c| { let obj = env.new_object(JNIString::from(c), jni_sig!("()V"), &[]).unwrap(); - JThrowable::from(obj) + env.cast_local::(obj).unwrap() }); try_block(env, |env| { if let Some(ref t) = ex { - env.throw(t).unwrap(); + let _ = env.throw(t); } try_result }) .catch(env, illegal_argument_exception, |env, caught| { - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); Ok(1) }) @@ -238,7 +249,7 @@ mod test { env, jni_str!("java/lang/ArrayIndexOutOfBoundsException"), |env, caught| { - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); if rethrow { Err(Error::JavaException) @@ -251,10 +262,10 @@ mod test { env, jni_str!("java/lang/IndexOutOfBoundsException"), |env, caught| { - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); if rethrow { - env.throw(&caught).unwrap(); + let _ = env.throw(&caught); Err(Error::JavaException) } else { Ok(3) @@ -265,7 +276,7 @@ mod test { env, jni_str!("java/lang/StringIndexOutOfBoundsException"), |env, caught| { - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); assert!(env.is_same_object(&caught, ex.as_ref().unwrap()).unwrap()); Ok(4) }, @@ -286,7 +297,7 @@ mod test { .unwrap(), 1 ); - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); Ok(()) }).unwrap(); } @@ -304,7 +315,7 @@ mod test { .unwrap(), 2 ); - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); Ok(()) }).unwrap(); } @@ -322,7 +333,7 @@ mod test { .unwrap(), 3 ); - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); Ok(()) }).unwrap(); } @@ -331,7 +342,7 @@ mod test { fn test_catch_ok() { test_utils::with_env(|env| { assert_eq!(test_catch(env, None, Ok(0), false).unwrap(), 0); - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); Ok(()) }).unwrap(); } @@ -347,9 +358,9 @@ mod test { ) .unwrap_err() { - assert!(env.exception_check().unwrap()); + assert!(env.exception_check()); let ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); assert!( env.is_instance_of(&ex, jni_str!("java/lang/SecurityException")) .unwrap() @@ -367,7 +378,7 @@ mod test { if let Error::InvalidCtorReturn = test_catch(env, None, Err(Error::InvalidCtorReturn), false).unwrap_err() { - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); } else { panic!("InvalidCtorReturn not found"); } @@ -381,7 +392,7 @@ mod test { if let Error::JavaException = test_catch(env, None, Err(Error::JavaException), false).unwrap_err() { - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); } else { panic!("JavaException not found"); } @@ -392,16 +403,14 @@ mod test { #[test] fn test_catch_prior_exception() { test_utils::with_env(|env| { - let ex = JThrowable::from( - env.new_object(jni_str!("java/lang/IllegalArgumentException"), jni_sig!("()V"), &[]) - .unwrap(), - ); - env.throw(&ex).unwrap(); + let obj = env.new_object(jni_str!("java/lang/IllegalArgumentException"), jni_sig!("()V"), &[]).unwrap(); + let ex = env.cast_local::(obj).unwrap(); + let _ = env.throw(&ex); if let Error::JavaException = test_catch(env, None, Ok(0), false).unwrap_err() { - assert!(env.exception_check().unwrap()); + assert!(env.exception_check()); let actual_ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); assert!(env.is_same_object(&actual_ex, &ex).unwrap()); } else { panic!("JavaException not found"); @@ -421,9 +430,9 @@ mod test { ) .unwrap_err() { - assert!(env.exception_check().unwrap()); + assert!(env.exception_check()); let ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); assert!( env.is_instance_of(&ex, jni_str!("java/lang/StringIndexOutOfBoundsException")) .unwrap() @@ -446,7 +455,7 @@ mod test { ) .unwrap_err() { - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); } else { panic!("JavaException not found"); } @@ -467,14 +476,14 @@ mod test { assert_eq!(*any.downcast_ref::<&str>().unwrap(), STATIC_MSG); } - let msg: JString = env + let msg_obj = env .call_method(&*ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[]) .unwrap() .l() - .unwrap() - .into(); - let str = env.get_string(&msg).unwrap(); - assert_eq!(>::from(str), STATIC_MSG); + .unwrap(); + let msg = env.cast_local::(msg_obj).unwrap(); + let chars = env.get_string(&msg).unwrap(); + assert_eq!(String::from(chars), STATIC_MSG); Ok(()) }).unwrap(); } @@ -493,14 +502,14 @@ mod test { assert_eq!(*any.downcast_ref::().unwrap(), STRING_MSG); } - let msg: JString = env + let msg_obj = env .call_method(&*ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[]) .unwrap() .l() - .unwrap() - .into(); - let str = env.get_string(&msg).unwrap(); - assert_eq!(>::from(str), STRING_MSG); + .unwrap(); + let msg = env.cast_local::(msg_obj).unwrap(); + let chars = env.get_string(&msg).unwrap(); + assert_eq!(String::from(chars), STRING_MSG); let any: Box = ex.take(env).unwrap(); assert_eq!(*any.downcast::().unwrap(), STRING_MSG); @@ -539,7 +548,7 @@ mod test { test_utils::with_env(|env| { let result = super::throw_unwind(env, || 42).unwrap(); assert_eq!(result, 42); - assert!(!env.exception_check().unwrap()); + assert!(!env.exception_check()); Ok(()) }).unwrap(); } @@ -550,9 +559,9 @@ mod test { super::throw_unwind(env, || panic!("This is a panic")) .unwrap_err() .unwrap(); - assert!(env.exception_check().unwrap()); + assert!(env.exception_check()); let ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); assert!( env.is_instance_of(&ex, jni_str!("io/github/gedgygedgy/rust/panic/PanicException")) .unwrap() @@ -564,11 +573,10 @@ mod test { .l() .unwrap(); let suppressed_array = - unsafe { jni::objects::JObjectArray::from_raw(suppressed_list.into_raw()) }; + unsafe { jni::objects::JObjectArray::::from_raw(env, suppressed_list.into_raw()) }; assert_eq!(env.get_array_length(&suppressed_array).unwrap(), 0); - let ex_throwable = JThrowable::from(JObject::from(ex)); - let ex = super::JPanicException::from_env(ex_throwable); + let ex = super::JPanicException::from_env(ex); let any = ex.take(env).unwrap(); let str = any.downcast::<&str>().unwrap(); assert_eq!(*str, "This is a panic"); @@ -579,16 +587,16 @@ mod test { #[test] fn test_throw_unwind_panic_suppress() { test_utils::with_env(|env| { - let old_ex = - JThrowable::from(env.new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]).unwrap()); - env.throw(&old_ex).unwrap(); + let obj = env.new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]).unwrap(); + let old_ex = env.cast_local::(obj).unwrap(); + let _ = env.throw(&old_ex); super::throw_unwind(env, || panic!("This is a panic")) .unwrap_err() .unwrap(); - assert!(env.exception_check().unwrap()); + assert!(env.exception_check()); let ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); assert!( env.is_instance_of(&ex, jni_str!("io/github/gedgygedgy/rust/panic/PanicException")) .unwrap() @@ -600,13 +608,12 @@ mod test { .l() .unwrap(); let suppressed_array = - unsafe { jni::objects::JObjectArray::from_raw(suppressed_list.into_raw()) }; + unsafe { jni::objects::JObjectArray::::from_raw(env, suppressed_list.into_raw()) }; assert_eq!(env.get_array_length(&suppressed_array).unwrap(), 1); let suppressed_ex = env.get_object_array_element(&suppressed_array, 0).unwrap(); assert!(env.is_same_object(&old_ex, &suppressed_ex).unwrap()); - let ex_throwable = JThrowable::from(JObject::from(ex)); - let ex = super::JPanicException::from_env(ex_throwable); + let ex = super::JPanicException::from_env(ex); let any = ex.take(env).unwrap(); let str = any.downcast::<&str>().unwrap(); assert_eq!(*str, "This is a panic"); diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 5b5f4785..2564fcd1 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -314,7 +314,7 @@ mod test { let _err = poll_result.get(env).unwrap_err(); let future_ex = env.exception_occurred().unwrap(); - env.exception_clear().unwrap(); + env.exception_clear(); let actual_ex = env .call_method(&future_ex, jni_str!("getCause"), jni_sig!("()Ljava/lang/Throwable;"), &[]) .unwrap() diff --git a/src/droidplug/jni_utils/mod.rs b/src/droidplug/jni_utils/mod.rs index e7641a35..437f8907 100644 --- a/src/droidplug/jni_utils/mod.rs +++ b/src/droidplug/jni_utils/mod.rs @@ -36,18 +36,16 @@ pub(crate) mod test_utils { unsafe { env.register_native_methods( &class, &[ - NativeMethod { - name: jni_str!("callInternal").into(), - sig: - jni_sig!("(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;") - .into(), - fn_ptr: super::ops::fn_adapter_call_internal as *mut c_void, - }, - NativeMethod { - name: jni_str!("closeInternal").into(), - sig: jni_sig!("()V").into(), - fn_ptr: super::ops::fn_adapter_close_internal as *mut c_void, - }, + NativeMethod::from_raw_parts( + jni_str!("callInternal"), + jni_str!("(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"), + super::ops::fn_adapter_call_internal as *mut c_void, + ), + NativeMethod::from_raw_parts( + jni_str!("closeInternal"), + jni_str!("()V"), + super::ops::fn_adapter_close_internal as *mut c_void, + ), ], )? }; Ok(()) diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 50bedcea..10b896cd 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -16,7 +16,7 @@ use async_trait::async_trait; use futures::stream::Stream; use jni::{ Env, jni_sig, jni_str, - objects::{Global, JClass, JObject, JString, JValue}, + objects::{Global, JObject, JString, JValue}, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -57,22 +57,22 @@ fn get_poll_result<'a>( match poll_result.get(env) { Ok(obj) => Ok(obj), Err(jni::errors::Error::JavaException) => { - let ex = env.exception_occurred()?; - env.exception_clear()?; + let ex = env.exception_occurred().unwrap(); + env.exception_clear(); let future_exception_class = super::jni_utils::classcache::get_class( "io/github/gedgygedgy/rust/future/FutureException", ) .unwrap(); - if env.is_instance_of(&ex, <&JClass>::from(future_exception_class.as_obj()))? { + if env.is_instance_of(&ex, future_exception_class.as_ref())? { let cause = env .call_method(&ex, jni_str!("getCause"), jni_sig!("()Ljava/lang/Throwable;"), &[])? .l()?; let mut check = |name: &str| -> jni::errors::Result { let cls = super::jni_utils::classcache::get_class(name).unwrap(); - env.is_instance_of(&cause, <&JClass>::from(cls.as_obj())) + env.is_instance_of(&cause, cls.as_ref()) }; if check("com/nonpolynomial/btleplug/android/impl/NotConnectedException")? { @@ -105,11 +105,11 @@ fn get_poll_result<'a>( let msgstr: String = env.get_string(&jstr)?.into(); Err(Error::RuntimeError(msgstr)) } else { - env.throw(&ex)?; + let _ = env.throw(&ex); Err(jni::errors::Error::JavaException.into()) } } else { - env.throw(&ex)?; + let _ = env.throw(&ex); Err(jni::errors::Error::JavaException.into()) } } From 16aaa204070f35dfc47da5ba03d1bd4c1a01870b Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 21:36:50 -0700 Subject: [PATCH 11/77] fix: Modernize deprecated jni 0.22 APIs and fix Android-only build errors Migrate deprecated array methods to type methods (JByteArray::set_region, JPrimitiveArray::len, JObjectArray::get_element), replace removed From for JString with cast_local, update from_raw calls to pass env, fix jboolean (now bool) comparisons, resolve JObject Send lifetime issue in async connect by scoping Global references, and replace env.get_string with JString::mutf8_chars throughout. Co-Authored-By: Claude Opus 4.6 --- src/droidplug/adapter.rs | 8 ++-- src/droidplug/jni/mod.rs | 2 +- src/droidplug/jni/objects.rs | 55 ++++++++++++--------------- src/droidplug/jni_utils/arrays.rs | 12 +++--- src/droidplug/jni_utils/exceptions.rs | 10 ++--- src/droidplug/peripheral.rs | 48 ++++++++++++----------- 6 files changed, 67 insertions(+), 68 deletions(-) diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index ef01d690..98d0c79c 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -161,8 +161,8 @@ impl Central for Adapter { let msg = env .call_method(&ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[])? .l()?; - let jstr: JString = msg.into(); - let msgstr: String = env.get_string(&jstr)?.into(); + let jstr = env.cast_local::(msg)?; + let msgstr = String::from(jstr.mutf8_chars(env)?); Err(Error::RuntimeError(msgstr)) } else { let _ = env.throw(&ex); @@ -223,10 +223,10 @@ pub(crate) fn adapter_on_connection_state_changed_internal( addr: JString, connected: jboolean, ) -> crate::Result<()> { - let addr_str: String = env.get_string(&addr)?.into(); + let addr_str = String::from(addr.mutf8_chars(env)?); let addr = BDAddr::from_str(&addr_str)?; let adapter = unsafe { env.get_rust_field::<_, _, Adapter>(obj, jni_str!("handle")) }?; - adapter.manager.emit(if connected != 0 { + adapter.manager.emit(if connected { CentralEvent::DeviceConnected(PeripheralId(addr)) } else { CentralEvent::DeviceDisconnected(PeripheralId(addr)) diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index b2f3b7f5..be039f70 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,6 +1,6 @@ pub mod objects; -use ::jni::{Env, EnvUnowned, JavaVM, NativeMethod, jni_str, jni_sig, objects::JObject}; +use ::jni::{Env, EnvUnowned, JavaVM, NativeMethod, jni_str, objects::JObject}; use jni::{objects::JString, sys::jboolean}; use once_cell::sync::OnceCell; use std::ffi::c_void; diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 5563b755..c512bfde 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -3,7 +3,7 @@ use jni::{ Env, errors::Result, jni_sig, jni_str, - objects::{JClass, JMethodID, JObject, JString}, + objects::{JMethodID, JObject, JString}, signature::{Primitive, ReturnType}, sys::{jint, jvalue}, }; @@ -51,7 +51,7 @@ impl<'a> JPeripheral<'a> { "com/nonpolynomial/btleplug/android/impl/Peripheral", ) .unwrap(); - let class = <&JClass>::from(class_static.as_obj()); + let class = &**class_static; let connect = env.get_method_id( class, @@ -141,7 +141,7 @@ impl<'a> JPeripheral<'a> { ) .unwrap(); let obj = env.new_object( - <&JClass>::from(class_static.as_obj()), + &**class_static, jni_sig!("(Lcom/nonpolynomial/btleplug/android/impl/Adapter;Ljava/lang/String;)V"), &[(&adapter).into(), (&addr_jstr).into()], )?; @@ -247,7 +247,7 @@ impl<'a> JPeripheral<'a> { l: uuid.as_raw(), }, jvalue { - z: enable as u8, + z: enable, }, ], ) @@ -307,9 +307,9 @@ impl<'a> JPeripheral<'a> { if obj.is_null() { Ok(None) } else { - let jstr: JString = obj.into(); - let name_str = env.get_string(&jstr)?; - Ok(Some(name_str.into())) + let jstr = env.cast_local::(obj)?; + let name_str = jstr.mutf8_chars(env)?; + Ok(Some(String::from(name_str))) } } @@ -341,13 +341,13 @@ impl<'a> JPeripheral<'a> { if obj.is_null() { return Ok(None); } - let arr = unsafe { jni::objects::JIntArray::from_raw(obj.into_raw()) }; - let len = env.get_array_length(&arr)?; + let arr = unsafe { jni::objects::JIntArray::from_raw(env, obj.into_raw()) }; + let len = arr.len(env)?; if len < 3 { return Ok(None); } let mut buf = [0i32; 3]; - env.get_int_array_region(&arr, 0, &mut buf)?; + arr.get_region(env, 0, &mut buf)?; Ok(Some(crate::api::ConnectionParameters { interval_us: (buf[0] as u32) * 1250, latency: buf[1] as u16, @@ -523,7 +523,7 @@ impl<'a> JBluetoothGattCharacteristic<'a> { env.call_method_unchecked(&self.internal, self.get_value, ReturnType::Array, &[]) }? .l()?; - let value_arr = unsafe { jni::objects::JByteArray::from_raw(value.into_raw()) }; + let value_arr = unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value_arr) } @@ -599,7 +599,7 @@ impl<'a> JBluetoothDevice<'a> { env.call_method_unchecked(&self.internal, self.get_address, ReturnType::Object, &[]) }? .l()?; - Ok(obj.into()) + env.cast_local::(obj) } } @@ -609,22 +609,21 @@ pub struct JScanFilter<'a> { impl<'a> JScanFilter<'a> { pub fn new(env: &mut Env<'a>, filter: ScanFilter) -> Result { - let string_class = env.find_class(jni_str!("java/lang/String"))?; - let uuids = env.new_object_array( - filter.services.len() as i32, - &string_class, - &JObject::null(), + let uuids = jni::objects::JObjectArray::::new( + env, + filter.services.len(), + &JString::default(), )?; for (idx, uuid) in filter.services.into_iter().enumerate() { let uuid_str = env.new_string(uuid.to_string())?; - env.set_object_array_element(&uuids, idx as i32, &uuid_str)?; + uuids.set_element(env, idx, &uuid_str)?; } let class_static = crate::droidplug::jni_utils::classcache::get_class( "com/nonpolynomial/btleplug/android/impl/ScanFilter", ) .unwrap(); let obj = env.new_object( - <&JClass>::from(class_static.as_obj()), + &**class_static, jni_sig!("([Ljava/lang/String;)V"), &[(&uuids).into()], )?; @@ -721,12 +720,8 @@ impl<'a> JScanResult<'a> { let device = self.get_device(env)?; let addr_jstr = device.get_address(env)?; - let addr_str = env.get_string(&addr_jstr)?; - let addr = BDAddr::from_str( - addr_str - .to_str() - .map_err(|e| crate::Error::Other(e.into()))?, - )?; + let addr_str = String::from(addr_jstr.mutf8_chars(env)?); + let addr = BDAddr::from_str(&addr_str)?; let record = self.get_scan_record(env)?; let record_is_null = env.is_same_object(&*record, JObject::null())?; @@ -737,10 +732,10 @@ impl<'a> JScanResult<'a> { let device_name = if env.is_same_object(&device_name_obj, JObject::null())? { None } else { - let device_name_jstr: JString = device_name_obj.into(); - let device_name_str = env.get_string(&device_name_jstr)?; + let device_name_jstr = env.cast_local::(device_name_obj)?; + let device_name_str = String::from(device_name_jstr.mutf8_chars(env)?); Some( - String::from_utf8_lossy(device_name_str.to_bytes()) + device_name_str .chars() .filter(|&c| c != '\u{fffd}') .collect(), @@ -765,7 +760,7 @@ impl<'a> JScanResult<'a> { let key = manufacturer_specific_data_obj.key_at(env, i)?; let value = manufacturer_specific_data_obj.value_at(env, i)?; let value_arr = - unsafe { jni::objects::JByteArray::from_raw(value.into_raw()) }; + unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; let data = crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value_arr)?; manufacturer_data.insert(key as u16, data); @@ -803,7 +798,7 @@ impl<'a> JScanResult<'a> { let juuid = parcel_uuid.get_uuid(env)?; let uuid = juuid.as_uuid(env)?; let value_arr = - unsafe { jni::objects::JByteArray::from_raw(value.into_raw()) }; + unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; let data = crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value_arr)?; service_data.insert(uuid, data); diff --git a/src/droidplug/jni_utils/arrays.rs b/src/droidplug/jni_utils/arrays.rs index cd39c37c..eb208be1 100644 --- a/src/droidplug/jni_utils/arrays.rs +++ b/src/droidplug/jni_utils/arrays.rs @@ -9,16 +9,16 @@ use std::slice; pub fn slice_to_byte_array<'local>(env: &mut Env<'local>, slice: &[u8]) -> Result> { let obj = env.new_byte_array(slice.len())?; let slice = unsafe { &*(slice as *const [u8] as *const [jbyte]) }; - env.set_byte_array_region(&obj, 0, slice)?; + obj.set_region(env, 0, slice)?; Ok(obj) } pub fn byte_array_to_vec(env: &Env, array: &JByteArray) -> Result> { - let size = env.get_array_length(array)? as usize; + let size = array.len(env)?; let mut result = Vec::with_capacity(size); unsafe { let result_slice = slice::from_raw_parts_mut(result.as_mut_ptr() as *mut jbyte, size); - env.get_byte_array_region(array, 0, result_slice)?; + array.get_region(env, 0, result_slice)?; result.set_len(size); } Ok(result) @@ -32,10 +32,10 @@ mod test { fn test_slice_to_byte_array() { test_utils::with_env(|env| { let obj = super::slice_to_byte_array(env, &[1, 2, 3, 4, 5]).unwrap(); - assert_eq!(env.get_array_length(&obj).unwrap(), 5); + assert_eq!(obj.len(env).unwrap(), 5); let mut bytes = [0i8; 5]; - env.get_byte_array_region(&obj, 0, &mut bytes).unwrap(); + obj.get_region(env, 0, &mut bytes).unwrap(); assert_eq!(bytes, [1, 2, 3, 4, 5]); Ok(()) }).unwrap(); @@ -45,7 +45,7 @@ mod test { fn test_byte_array_to_vec() { test_utils::with_env(|env| { let obj = env.new_byte_array(5).unwrap(); - env.set_byte_array_region(&obj, 0, &[1, 2, 3, 4, 5]) + obj.set_region(env, 0, &[1, 2, 3, 4, 5]) .unwrap(); let vec = super::byte_array_to_vec(env, &obj).unwrap(); diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index c9668f5d..28afbc88 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -482,7 +482,7 @@ mod test { .l() .unwrap(); let msg = env.cast_local::(msg_obj).unwrap(); - let chars = env.get_string(&msg).unwrap(); + let chars = msg.mutf8_chars(env).unwrap(); assert_eq!(String::from(chars), STATIC_MSG); Ok(()) }).unwrap(); @@ -508,7 +508,7 @@ mod test { .l() .unwrap(); let msg = env.cast_local::(msg_obj).unwrap(); - let chars = env.get_string(&msg).unwrap(); + let chars = msg.mutf8_chars(env).unwrap(); assert_eq!(String::from(chars), STRING_MSG); let any: Box = ex.take(env).unwrap(); @@ -574,7 +574,7 @@ mod test { .unwrap(); let suppressed_array = unsafe { jni::objects::JObjectArray::::from_raw(env, suppressed_list.into_raw()) }; - assert_eq!(env.get_array_length(&suppressed_array).unwrap(), 0); + assert_eq!(suppressed_array.len(env).unwrap(), 0); let ex = super::JPanicException::from_env(ex); let any = ex.take(env).unwrap(); @@ -609,8 +609,8 @@ mod test { .unwrap(); let suppressed_array = unsafe { jni::objects::JObjectArray::::from_raw(env, suppressed_list.into_raw()) }; - assert_eq!(env.get_array_length(&suppressed_array).unwrap(), 1); - let suppressed_ex = env.get_object_array_element(&suppressed_array, 0).unwrap(); + assert_eq!(suppressed_array.len(env).unwrap(), 1); + let suppressed_ex = suppressed_array.get_element(env, 0).unwrap(); assert!(env.is_same_object(&old_ex, &suppressed_ex).unwrap()); let ex = super::JPanicException::from_env(ex); diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 10b896cd..81cd54fb 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -101,8 +101,8 @@ fn get_poll_result<'a>( let msg = env .call_method(&cause, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[])? .l()?; - let jstr: JString = msg.into(); - let msgstr: String = env.get_string(&jstr)?.into(); + let jstr = env.cast_local::(msg)?; + let msgstr = String::from(jstr.mutf8_chars(env)?); Err(Error::RuntimeError(msgstr)) } else { let _ = env.throw(&ex); @@ -219,12 +219,14 @@ impl api::Peripheral for Peripheral { } async fn connect(&self) -> Result<()> { - let future = self.with_obj(|env, obj| { - let future = obj.connect(env)?; - JSendFuture::new(env, &future) - })?; - let result_ref = future.await?; - self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {}))?; + { + let future = self.with_obj(|env, obj| { + let future = obj.connect(env)?; + JSendFuture::new(env, &future) + })?; + let result_ref = future.await?; + self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {}))?; + } // Query the system-cached device name and update local_name self.with_obj(|env, obj| -> std::result::Result<(), Error> { if let Ok(Some(name)) = obj.get_device_name(env) { @@ -236,18 +238,20 @@ impl api::Peripheral for Peripheral { Ok(()) })?; // Auto-negotiate maximum MTU (517) after connection - let mtu_future = self.with_obj(|env, obj| { - let mtu_obj = obj.request_mtu(env, 517)?; - let mtu_future = JFuture::from_env(env, mtu_obj)?; - JSendFuture::new(env, &mtu_future) - })?; - let mtu_result_ref = mtu_future.await?; - self.with_obj(|env, _obj| -> Result<()> { - let mtu_obj = get_poll_result(env, &mtu_result_ref)?; - let mtu_val = env.call_method(&mtu_obj, jni_str!("intValue"), jni_sig!("()I"), &[])?.i()?; - self.mtu.store(mtu_val as u16, Ordering::Relaxed); - Ok(()) - })?; + { + let mtu_future = self.with_obj(|env, obj| { + let mtu_obj = obj.request_mtu(env, 517)?; + let mtu_future = JFuture::from_env(env, mtu_obj)?; + JSendFuture::new(env, &mtu_future) + })?; + let mtu_result_ref = mtu_future.await?; + self.with_obj(|env, _obj| -> Result<()> { + let mtu_obj = get_poll_result(env, &mtu_result_ref)?; + let mtu_val = env.call_method(&mtu_obj, jni_str!("intValue"), jni_sig!("()I"), &[])?.i()?; + self.mtu.store(mtu_val as u16, Ordering::Relaxed); + Ok(()) + })?; + } Ok(()) } @@ -352,7 +356,7 @@ impl api::Peripheral for Peripheral { let result_ref = future.await?; self.with_obj(|env, _obj| { let bytes_obj = get_poll_result(env, &result_ref)?; - let bytes_arr = unsafe { jni::objects::JByteArray::from_raw(bytes_obj.into_raw()) }; + let bytes_arr = unsafe { jni::objects::JByteArray::from_raw(env, bytes_obj.into_raw()) }; Ok(byte_array_to_vec(env, &bytes_arr)?) }) } @@ -443,7 +447,7 @@ impl api::Peripheral for Peripheral { let result_ref = future.await?; self.with_obj(|env, _obj| { let bytes_obj = get_poll_result(env, &result_ref)?; - let bytes_arr = unsafe { jni::objects::JByteArray::from_raw(bytes_obj.into_raw()) }; + let bytes_arr = unsafe { jni::objects::JByteArray::from_raw(env, bytes_obj.into_raw()) }; Ok(byte_array_to_vec(env, &bytes_arr)?) }) } From 0d7a9f0ce1d81345562420d10a4560157b6f9d1c Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 21:36:55 -0700 Subject: [PATCH 12/77] fix: Migrate Android test crate to jni 0.22 EnvUnowned + with_env pattern Replace deprecated JNIEnv with EnvUnowned in extern "system" FFI entry points, use with_env/into_outcome for env access, and update throw_new to use jni_str! and JNIString for 0.22 string type requirements. Co-Authored-By: Claude Opus 4.6 --- tests/android/rust/Cargo.lock | 172 ++++++++++++++-------------------- tests/android/rust/src/lib.rs | 23 +++-- 2 files changed, 85 insertions(+), 110 deletions(-) diff --git a/tests/android/rust/Cargo.lock b/tests/android/rust/Cargo.lock index ab29b097..ea6c57cf 100644 --- a/tests/android/rust/Cargo.lock +++ b/tests/android/rust/Cargo.lock @@ -69,7 +69,7 @@ dependencies = [ "log", "serde", "serde-xml-rs", - "thiserror 2.0.18", + "thiserror", "tokio", "uuid", ] @@ -100,7 +100,7 @@ dependencies = [ "objc2-foundation", "once_cell", "static_assertions", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "uuid", @@ -134,12 +134,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[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.4" @@ -331,25 +325,52 @@ dependencies = [ [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", + "jni-macros", "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror", "walkdir", - "windows-sys 0.45.0", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "js-sys" @@ -550,6 +571,15 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -571,6 +601,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -589,7 +625,7 @@ checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" dependencies = [ "log", "serde", - "thiserror 2.0.18", + "thiserror", "xml", ] @@ -623,6 +659,22 @@ dependencies = [ "libc", ] +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -662,33 +714,13 @@ dependencies = [ "unicode-ident", ] -[[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.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[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", + "thiserror-impl", ] [[package]] @@ -942,15 +974,6 @@ 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.59.0" @@ -978,21 +1001,6 @@ 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" @@ -1035,12 +1043,6 @@ 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" @@ -1053,12 +1055,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[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" @@ -1071,12 +1067,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[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" @@ -1101,12 +1091,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[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" @@ -1119,12 +1103,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[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" @@ -1137,12 +1115,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[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" @@ -1155,12 +1127,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[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/tests/android/rust/src/lib.rs b/tests/android/rust/src/lib.rs index b234357f..a57ef926 100644 --- a/tests/android/rust/src/lib.rs +++ b/tests/android/rust/src/lib.rs @@ -41,7 +41,7 @@ pub fn find_descriptor( } use jni::objects::JClass; -use jni::JNIEnv; +use jni::{Env, EnvUnowned, jni_str}; use std::sync::OnceLock; use tokio::runtime::Runtime; @@ -58,7 +58,7 @@ fn runtime() -> &'static Runtime { const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(55); /// Run a test function on the global runtime, converting panics to JNI exceptions. -fn run_test(env: &mut JNIEnv, test_name: &str, f: impl std::future::Future) { +fn run_test(env: &mut Env, test_name: &str, f: impl std::future::Future) { log::info!("[START] {}", test_name); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { runtime().block_on(async { @@ -78,7 +78,8 @@ fn run_test(env: &mut JNIEnv, test_name: &str, f: impl std::future::Future(()) + }); + let _ = outcome.into_outcome(); } // ── Test JNI exports ──────────────────────────────────────────────── @@ -105,8 +110,12 @@ pub extern "system" fn Java_com_nonpolynomial_btleplug_test_NativeTests_initBtle macro_rules! jni_test { ($jni_name:ident, $test_fn:path) => { #[unsafe(no_mangle)] - pub extern "system" fn $jni_name(mut env: JNIEnv, _class: JClass) { - run_test(&mut env, stringify!($test_fn), $test_fn()); + pub extern "system" fn $jni_name(mut env: EnvUnowned, _class: JClass) { + let outcome = env.with_env(|env| { + run_test(env, stringify!($test_fn), $test_fn()); + Ok::<_, jni::errors::Error>(()) + }); + let _ = outcome.into_outcome(); } }; } From dfdcf34ee9cb45febadd984ba336e2461b8b550a Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 22:19:34 -0700 Subject: [PATCH 13/77] refactor: Replace JNI boilerplate with bind_java_type! macros and JavaVM::singleton() Convert all hand-rolled JNI wrapper types (struct + from_env + Deref + From impls) to jni 0.22's bind_java_type! macro. Replace GLOBAL_JVM OnceCell with JavaVM::singleton(). Simplify JSendFuture/JSendStream by dropping cached JMethodID fields in favour of cast_local per poll call. Net reduction of ~780 lines of boilerplate. Co-Authored-By: Claude Opus 4.6 --- src/droidplug/adapter.rs | 12 +- src/droidplug/jni/mod.rs | 24 +- src/droidplug/jni/objects.rs | 952 ++++++------------------------ src/droidplug/jni_utils/future.rs | 101 +--- src/droidplug/jni_utils/stream.rs | 123 +--- src/droidplug/jni_utils/task.rs | 42 +- src/droidplug/jni_utils/uuid.rs | 87 +-- src/droidplug/peripheral.rs | 20 +- 8 files changed, 289 insertions(+), 1072 deletions(-) diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index 98d0c79c..ec424397 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -1,6 +1,6 @@ use super::{ jni::{ - global_jvm, + jvm, objects::{JScanFilter, JScanResult}, }, peripheral::{Peripheral, PeripheralId}, @@ -40,7 +40,7 @@ impl Debug for Adapter { impl Adapter { pub(crate) fn new() -> Result { - global_jvm().attach_current_thread(|env| { + jvm()?.attach_current_thread(|env| { let obj = env.new_object( jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"), jni_sig!("()V"), @@ -63,7 +63,7 @@ impl Adapter { scan_result: JObject<'a>, ) -> Result { - let scan_result = JScanResult::from_env(env, scan_result)?; + let scan_result = env.cast_local::(scan_result)?; let (addr, properties): (BDAddr, Option) = scan_result.to_peripheral_properties(env)?; @@ -87,7 +87,7 @@ impl Adapter { } fn add(&self, address: BDAddr) -> Result { - global_jvm().attach_current_thread(|env| { + jvm()?.attach_current_thread(|env| { let local_adapter = env.new_local_ref(self.internal.as_obj())?; let peripheral = Peripheral::new(env, local_adapter, address)?; self.manager.add_peripheral(peripheral.clone()); @@ -136,7 +136,7 @@ impl Central for Adapter { } async fn start_scan(&self, filter: ScanFilter) -> Result<()> { - global_jvm().attach_current_thread(|env| { + jvm()?.attach_current_thread(|env| { let filter = JScanFilter::new(env, filter)?; let filter_obj: JObject = filter.into(); match env.call_method( @@ -175,7 +175,7 @@ impl Central for Adapter { } async fn stop_scan(&self) -> Result<()> { - global_jvm().attach_current_thread(|env| { + jvm()?.attach_current_thread(|env| { env.call_method(self.internal.as_obj(), jni_str!("stopScan"), jni_sig!("()V"), &[])?; Ok(()) }) diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index be039f70..989c6795 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,14 +1,24 @@ pub mod objects; -use ::jni::{Env, EnvUnowned, JavaVM, NativeMethod, jni_str, objects::JObject}; +use ::jni::{Env, EnvUnowned, NativeMethod, jni_str, objects::JObject}; use jni::{objects::JString, sys::jboolean}; -use once_cell::sync::OnceCell; use std::ffi::c_void; +use std::sync::Once; -static GLOBAL_JVM: OnceCell = OnceCell::new(); +static INIT: Once = Once::new(); pub fn init(env: &mut Env) -> crate::Result<()> { - if let Ok(()) = GLOBAL_JVM.set(env.get_java_vm()?) { + let mut init_result: crate::Result<()> = Ok(()); + INIT.call_once(|| { + if let Err(e) = init_inner(env) { + init_result = Err(e); + } + }); + init_result +} + +fn init_inner(env: &mut Env) -> crate::Result<()> { + { let adapter_class = env.find_class(jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"))?; unsafe { env.register_native_methods( @@ -120,10 +130,8 @@ pub fn init(env: &mut Env) -> crate::Result<()> { Ok(()) } -pub fn global_jvm() -> &'static JavaVM { - GLOBAL_JVM.get().expect( - "Droidplug has not been initialized. Please initialize it with btleplug::platform::init().", - ) +pub fn jvm() -> crate::Result { + jni::JavaVM::singleton().map_err(|e| crate::Error::Other(Box::new(e))) } impl From<::jni::errors::Error> for crate::Error { diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index c512bfde..da319d8f 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -1,309 +1,113 @@ use crate::droidplug::jni_utils::{future::JFuture, stream::JStream, uuid::JUuid}; use jni::{ Env, + bind_java_type, errors::Result, jni_sig, jni_str, - objects::{JMethodID, JObject, JString}, - signature::{Primitive, ReturnType}, - sys::{jint, jvalue}, + objects::{JObject, JString}, + sys::jint, }; use std::{collections::HashMap, iter::Iterator}; use uuid::Uuid; use crate::api::{BDAddr, CharPropFlags, PeripheralProperties, ScanFilter}; -pub struct JPeripheral<'a> { - internal: JObject<'a>, - connect: JMethodID, - disconnect: JMethodID, - is_connected: JMethodID, - discover_services: JMethodID, - read: JMethodID, - write: JMethodID, - set_characteristic_notification: JMethodID, - get_notifications: JMethodID, - read_descriptor: JMethodID, - write_descriptor: JMethodID, - get_device_name: JMethodID, - request_mtu: JMethodID, - get_connection_parameters: JMethodID, - request_connection_priority: JMethodID, - read_remote_rssi: JMethodID, -} - -impl<'a> ::std::ops::Deref for JPeripheral<'a> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } -} - -impl<'a> From> for JObject<'a> { - fn from(other: JPeripheral<'a>) -> JObject<'a> { - other.internal - } +bind_java_type! { + pub JPeripheral => com.nonpolynomial.btleplug.android.impl.Peripheral, + constructors { + fn with_adapter(adapter: JObject, address: JString), + }, + methods { + priv fn connect_raw() -> JObject { name = "connect" }, + priv fn disconnect_raw() -> JObject { name = "disconnect" }, + fn is_connected() -> jboolean, + priv fn discover_services_raw() -> JObject { name = "discoverServices" }, + priv fn read_raw(uuid: JObject) -> JObject { name = "read" }, + priv fn write_raw(uuid: JObject, data: JObject, write_type: jint) -> JObject { name = "write" }, + priv fn set_characteristic_notification_raw(uuid: JObject, enable: jboolean) -> JObject { + name = "setCharacteristicNotification", + }, + priv fn get_notifications_raw() -> JObject { name = "getNotifications" }, + priv fn read_descriptor_raw(characteristic: JObject, uuid: JObject) -> JObject { name = "readDescriptor" }, + priv fn write_descriptor_raw(characteristic: JObject, uuid: JObject, data: JObject) -> JObject { + name = "writeDescriptor", + }, + priv fn get_device_name_raw() -> JObject { name = "getDeviceName" }, + fn request_mtu(mtu: jint) -> JObject, + priv fn get_connection_parameters_raw() -> JObject { name = "getConnectionParameters" }, + fn request_connection_priority(priority: jint) -> jboolean, + fn read_remote_rssi() -> JObject, + }, } -impl<'a> JPeripheral<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class_static = crate::droidplug::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/Peripheral", - ) - .unwrap(); - let class = &**class_static; - - let connect = env.get_method_id( - class, - jni_str!("connect"), - jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), - )?; - let disconnect = env.get_method_id( - class, - jni_str!("disconnect"), - jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), - )?; - let is_connected = env.get_method_id(class, jni_str!("isConnected"), jni_sig!("()Z"))?; - let discover_services = env.get_method_id( - class, - jni_str!("discoverServices"), - jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), - )?; - let read = env.get_method_id( - class, - jni_str!("read"), - jni_sig!("(Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;"), - )?; - let write = env.get_method_id( - class, - jni_str!("write"), - jni_sig!("(Ljava/util/UUID;[BI)Lio/github/gedgygedgy/rust/future/Future;"), - )?; - let set_characteristic_notification = env.get_method_id( - class, - jni_str!("setCharacteristicNotification"), - jni_sig!("(Ljava/util/UUID;Z)Lio/github/gedgygedgy/rust/future/Future;"), - )?; - let get_notifications = env.get_method_id( - class, - jni_str!("getNotifications"), - jni_sig!("()Lio/github/gedgygedgy/rust/stream/Stream;"), - )?; - let read_descriptor = env.get_method_id( - class, - jni_str!("readDescriptor"), - jni_sig!("(Ljava/util/UUID;Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;"), - )?; - let write_descriptor = env.get_method_id( - class, - jni_str!("writeDescriptor"), - jni_sig!("(Ljava/util/UUID;Ljava/util/UUID;[B)Lio/github/gedgygedgy/rust/future/Future;"), - )?; - let get_device_name = env.get_method_id(class, jni_str!("getDeviceName"), jni_sig!("()Ljava/lang/String;"))?; - let request_mtu = env.get_method_id( - class, - jni_str!("requestMtu"), - jni_sig!("(I)Lio/github/gedgygedgy/rust/future/Future;"), - )?; - let get_connection_parameters = - env.get_method_id(class, jni_str!("getConnectionParameters"), jni_sig!("()[I"))?; - let request_connection_priority = - env.get_method_id(class, jni_str!("requestConnectionPriority"), jni_sig!("(I)Z"))?; - let read_remote_rssi = env.get_method_id( - class, - jni_str!("readRemoteRssi"), - jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), - )?; - Ok(Self { - internal: obj, - connect, - disconnect, - is_connected, - discover_services, - read, - write, - set_characteristic_notification, - get_notifications, - read_descriptor, - write_descriptor, - get_device_name, - request_mtu, - get_connection_parameters, - request_connection_priority, - read_remote_rssi, - }) - } - - pub fn new(env: &mut Env<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { +impl JPeripheral<'_> { + pub fn create<'local>(env: &mut Env<'local>, adapter: JObject<'local>, addr: BDAddr) -> Result> { let addr_jstr = env.new_string(format!("{:X}", addr))?; - let class_static = crate::droidplug::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/Peripheral", - ) - .unwrap(); - let obj = env.new_object( - &**class_static, - jni_sig!("(Lcom/nonpolynomial/btleplug/android/impl/Adapter;Ljava/lang/String;)V"), - &[(&adapter).into(), (&addr_jstr).into()], - )?; - Self::from_env(env, obj) - } - - pub fn connect(&self, env: &mut Env<'a>) -> Result> { - let future_obj = unsafe { - env.call_method_unchecked(&self.internal, self.connect, ReturnType::Object, &[]) - }? - .l()?; - JFuture::from_env(env, future_obj) + JPeripheral::with_adapter(env, &adapter, &addr_jstr) } +} - pub fn disconnect(&self, env: &mut Env<'a>) -> Result> { - let future_obj = unsafe { - env.call_method_unchecked(&self.internal, self.disconnect, ReturnType::Object, &[]) - }? - .l()?; - JFuture::from_env(env, future_obj) +impl<'local> JPeripheral<'local> { + pub fn connect(&self, env: &mut Env<'local>) -> Result> { + env.cast_local::(self.connect_raw(env)?) } - pub fn is_connected(&self, env: &mut Env<'a>) -> Result { - unsafe { - env.call_method_unchecked( - &self.internal, - self.is_connected, - ReturnType::Primitive(Primitive::Boolean), - &[], - ) - }? - .z() + pub fn disconnect(&self, env: &mut Env<'local>) -> Result> { + env.cast_local::(self.disconnect_raw(env)?) } - pub fn discover_services(&self, env: &mut Env<'a>) -> Result> { - let future_obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.discover_services, - ReturnType::Object, - &[], - ) - }? - .l()?; - JFuture::from_env(env, future_obj) + pub fn discover_services(&self, env: &mut Env<'local>) -> Result> { + env.cast_local::(self.discover_services_raw(env)?) } - pub fn read(&self, env: &mut Env<'a>, uuid: &JUuid<'a>) -> Result> { - let future_obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.read, - ReturnType::Object, - &[jvalue { - l: uuid.as_raw(), - }], - ) - }? - .l()?; - JFuture::from_env(env, future_obj) + pub fn read(&self, env: &mut Env<'local>, uuid: &JUuid<'local>) -> Result> { + env.cast_local::(self.read_raw(env, uuid)?) } pub fn write( &self, - env: &mut Env<'a>, - uuid: &JUuid<'a>, - data: &JObject<'a>, + env: &mut Env<'local>, + uuid: &JUuid<'local>, + data: &JObject<'local>, write_type: jint, - ) -> Result> { - let future_obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.write, - ReturnType::Object, - &[ - jvalue { - l: uuid.as_raw(), - }, - jvalue { - l: data.as_raw(), - }, - jvalue { i: write_type }, - ], - ) - }? - .l()?; - JFuture::from_env(env, future_obj) + ) -> Result> { + env.cast_local::(self.write_raw(env, uuid, data, write_type)?) } pub fn set_characteristic_notification( &self, - env: &mut Env<'a>, - uuid: &JUuid<'a>, + env: &mut Env<'local>, + uuid: &JUuid<'local>, enable: bool, - ) -> Result> { - let future_obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.set_characteristic_notification, - ReturnType::Object, - &[ - jvalue { - l: uuid.as_raw(), - }, - jvalue { - z: enable, - }, - ], - ) - }? - .l()?; - JFuture::from_env(env, future_obj) + ) -> Result> { + env.cast_local::(self.set_characteristic_notification_raw(env, uuid, enable)?) } - pub fn get_notifications(&self, env: &mut Env<'a>) -> Result> { - let stream_obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.get_notifications, - ReturnType::Object, - &[], - ) - }? - .l()?; - JStream::from_env(env, stream_obj) + pub fn get_notifications(&self, env: &mut Env<'local>) -> Result> { + env.cast_local::(self.get_notifications_raw(env)?) } pub fn read_descriptor( &self, - env: &mut Env<'a>, - characteristic: &JUuid<'a>, - uuid: &JUuid<'a>, - ) -> Result> { - let future_obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.read_descriptor, - ReturnType::Object, - &[ - jvalue { - l: characteristic.as_raw(), - }, - jvalue { - l: uuid.as_raw(), - }, - ], - ) - }? - .l()?; - JFuture::from_env(env, future_obj) + env: &mut Env<'local>, + characteristic: &JUuid<'local>, + uuid: &JUuid<'local>, + ) -> Result> { + env.cast_local::(self.read_descriptor_raw(env, characteristic, uuid)?) + } + + pub fn write_descriptor( + &self, + env: &mut Env<'local>, + characteristic: &JUuid<'local>, + uuid: &JUuid<'local>, + data: &JObject<'local>, + ) -> Result> { + env.cast_local::(self.write_descriptor_raw(env, characteristic, uuid, data)?) } - pub fn get_device_name(&self, env: &mut Env<'a>) -> Result> { - let obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.get_device_name, - ReturnType::Object, - &[], - ) - }? - .l()?; + pub fn get_device_name(&self, env: &mut Env<'local>) -> Result> { + let obj = self.get_device_name_raw(env)?; if obj.is_null() { Ok(None) } else { @@ -313,31 +117,11 @@ impl<'a> JPeripheral<'a> { } } - pub fn request_mtu(&self, env: &mut Env<'a>, mtu: jint) -> Result> { - unsafe { - env.call_method_unchecked( - &self.internal, - self.request_mtu, - ReturnType::Object, - &[jvalue { i: mtu }], - ) - }? - .l() - } - pub fn get_connection_parameters( &self, - env: &mut Env<'a>, + env: &mut Env<'local>, ) -> Result> { - let obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.get_connection_parameters, - ReturnType::Array, - &[], - ) - }? - .l()?; + let obj = self.get_connection_parameters_raw(env)?; if obj.is_null() { return Ok(None); } @@ -354,253 +138,123 @@ impl<'a> JPeripheral<'a> { supervision_timeout_us: (buf[2] as u32) * 10_000, })) } - - pub fn read_remote_rssi(&self, env: &mut Env<'a>) -> Result> { - unsafe { - env.call_method_unchecked( - &self.internal, - self.read_remote_rssi, - ReturnType::Object, - &[], - ) - }? - .l() - } - - pub fn request_connection_priority( - &self, - env: &mut Env<'a>, - priority: jint, - ) -> Result { - unsafe { - env.call_method_unchecked( - &self.internal, - self.request_connection_priority, - ReturnType::Primitive(Primitive::Boolean), - &[jvalue { i: priority }], - ) - }? - .z() - } - - pub fn write_descriptor( - &self, - env: &mut Env<'a>, - characteristic: &JUuid<'a>, - uuid: &JUuid<'a>, - data: &JObject<'a>, - ) -> Result> { - let future_obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.write_descriptor, - ReturnType::Object, - &[ - jvalue { - l: characteristic.as_raw(), - }, - jvalue { - l: uuid.as_raw(), - }, - jvalue { - l: data.as_raw(), - }, - ], - ) - }? - .l()?; - JFuture::from_env(env, future_obj) - } } -pub struct JBluetoothGattService<'a> { - internal: JObject<'a>, - get_uuid: JMethodID, - get_characteristics: JMethodID, +bind_java_type! { + pub JBluetoothGattService => android.bluetooth.BluetoothGattService, + methods { + fn get_uuid_obj() -> JObject { + name = "getUuid", + }, + fn get_characteristics_obj() -> JObject { + name = "getCharacteristics", + }, + }, } -impl<'a> JBluetoothGattService<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class(jni_str!("android/bluetooth/BluetoothGattService"))?; - - let get_uuid = env.get_method_id(&class, jni_str!("getUuid"), jni_sig!("()Ljava/util/UUID;"))?; - let get_characteristics = - env.get_method_id(&class, jni_str!("getCharacteristics"), jni_sig!("()Ljava/util/List;"))?; - Ok(Self { - internal: obj, - get_uuid, - get_characteristics, - }) - } - +impl<'local> JBluetoothGattService<'local> { pub fn is_primary(&self) -> Result { Ok(true) } - pub fn get_uuid(&self, env: &mut Env<'a>) -> Result { - let obj = unsafe { - env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) - }? - .l()?; - let uuid_obj = JUuid::from_env(env, obj)?; + pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { + let obj = self.get_uuid_obj(env)?; + let uuid_obj = env.cast_local::(obj)?; uuid_obj.as_uuid(env) } pub fn get_characteristics( &self, - env: &mut Env<'a>, - ) -> Result>> { - let obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.get_characteristics, - ReturnType::Object, - &[], - ) - }? - .l()?; + env: &mut Env<'local>, + ) -> Result>> { + let obj = self.get_characteristics_obj(env)?; let size = env.call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])?.i()?; let mut chr_vec = Vec::with_capacity(size as usize); for i in 0..size { let chr = env .call_method(&obj, jni_str!("get"), jni_sig!("(I)Ljava/lang/Object;"), &[jni::objects::JValue::from(i)])? .l()?; - chr_vec.push(JBluetoothGattCharacteristic::from_env(env, chr)?); + chr_vec.push(env.cast_local::(chr)?); } Ok(chr_vec) } } -pub struct JBluetoothGattCharacteristic<'a> { - internal: JObject<'a>, - get_uuid: JMethodID, - get_properties: JMethodID, - get_value: JMethodID, - get_descriptors: JMethodID, +bind_java_type! { + pub JBluetoothGattCharacteristic => android.bluetooth.BluetoothGattCharacteristic, + methods { + fn get_uuid_obj() -> JObject { + name = "getUuid", + }, + fn get_properties_raw() -> jint { + name = "getProperties", + }, + fn get_value_obj() -> JObject { + name = "getValue", + }, + fn get_descriptors_obj() -> JObject { + name = "getDescriptors", + }, + }, } -impl<'a> JBluetoothGattCharacteristic<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class(jni_str!("android/bluetooth/BluetoothGattCharacteristic"))?; - - let get_uuid = env.get_method_id(&class, jni_str!("getUuid"), jni_sig!("()Ljava/util/UUID;"))?; - let get_properties = env.get_method_id(&class, jni_str!("getProperties"), jni_sig!("()I"))?; - let get_descriptors = env.get_method_id(&class, jni_str!("getDescriptors"), jni_sig!("()Ljava/util/List;"))?; - let get_value = env.get_method_id(&class, jni_str!("getValue"), jni_sig!("()[B"))?; - Ok(Self { - internal: obj, - get_uuid, - get_properties, - get_value, - get_descriptors, - }) - } - - pub fn get_uuid(&self, env: &mut Env<'a>) -> Result { - let obj = unsafe { - env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) - }? - .l()?; - let uuid_obj = JUuid::from_env(env, obj)?; +impl<'local> JBluetoothGattCharacteristic<'local> { + pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { + let obj = self.get_uuid_obj(env)?; + let uuid_obj = env.cast_local::(obj)?; uuid_obj.as_uuid(env) } - pub fn get_properties(&self, env: &mut Env<'a>) -> Result { - let flags = unsafe { - env.call_method_unchecked( - &self.internal, - self.get_properties, - ReturnType::Primitive(Primitive::Int), - &[], - ) - }? - .i()?; + pub fn get_properties(&self, env: &mut Env<'local>) -> Result { + let flags = self.get_properties_raw(env)?; Ok(CharPropFlags::from_bits_truncate(flags as u8)) } - pub fn get_value(&self, env: &mut Env<'a>) -> Result> { - let value = unsafe { - env.call_method_unchecked(&self.internal, self.get_value, ReturnType::Array, &[]) - }? - .l()?; + pub fn get_value(&self, env: &mut Env<'local>) -> Result> { + let value = self.get_value_obj(env)?; let value_arr = unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value_arr) } pub fn get_descriptors( &self, - env: &mut Env<'a>, - ) -> Result>> { - let obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.get_descriptors, - ReturnType::Object, - &[], - ) - }? - .l()?; + env: &mut Env<'local>, + ) -> Result>> { + let obj = self.get_descriptors_obj(env)?; let size = env.call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])?.i()?; let mut desc_vec = Vec::with_capacity(size as usize); for i in 0..size { let desc = env .call_method(&obj, jni_str!("get"), jni_sig!("(I)Ljava/lang/Object;"), &[jni::objects::JValue::from(i)])? .l()?; - desc_vec.push(JBluetoothGattDescriptor::from_env(env, desc)?); + desc_vec.push(env.cast_local::(desc)?); } Ok(desc_vec) } } -pub struct JBluetoothGattDescriptor<'a> { - internal: JObject<'a>, - get_uuid: JMethodID, +bind_java_type! { + pub JBluetoothGattDescriptor => android.bluetooth.BluetoothGattDescriptor, + methods { + fn get_uuid_obj() -> JObject { + name = "getUuid", + }, + }, } -impl<'a> JBluetoothGattDescriptor<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class(jni_str!("android/bluetooth/BluetoothGattDescriptor"))?; - - let get_uuid = env.get_method_id(&class, jni_str!("getUuid"), jni_sig!("()Ljava/util/UUID;"))?; - Ok(Self { - internal: obj, - get_uuid, - }) - } - - pub fn get_uuid(&self, env: &mut Env<'a>) -> Result { - let obj = unsafe { - env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) - }? - .l()?; - let uuid_obj = JUuid::from_env(env, obj)?; +impl<'local> JBluetoothGattDescriptor<'local> { + pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { + let obj = self.get_uuid_obj(env)?; + let uuid_obj = env.cast_local::(obj)?; uuid_obj.as_uuid(env) } } -pub struct JBluetoothDevice<'a> { - internal: JObject<'a>, - get_address: JMethodID, -} - -impl<'a> JBluetoothDevice<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class(jni_str!("android/bluetooth/BluetoothDevice"))?; - - let get_address = env.get_method_id(&class, jni_str!("getAddress"), jni_sig!("()Ljava/lang/String;"))?; - Ok(Self { - internal: obj, - get_address, - }) - } - - pub fn get_address(&self, env: &mut Env<'a>) -> Result> { - let obj = unsafe { - env.call_method_unchecked(&self.internal, self.get_address, ReturnType::Object, &[]) - }? - .l()?; - env.cast_local::(obj) - } +bind_java_type! { + pub JBluetoothDevice => android.bluetooth.BluetoothDevice, + methods { + fn get_address() -> JString, + }, } pub struct JScanFilter<'a> { @@ -637,84 +291,33 @@ impl<'a> From> for JObject<'a> { } } -pub struct JScanResult<'a> { - internal: JObject<'a>, - get_device: JMethodID, - get_scan_record: JMethodID, - get_tx_power: JMethodID, - get_rssi: JMethodID, +bind_java_type! { + pub JScanResult => android.bluetooth.le.ScanResult, + methods { + fn get_device_obj() -> JObject { + name = "getDevice", + }, + fn get_scan_record_obj() -> JObject { + name = "getScanRecord", + }, + fn get_tx_power() -> jint, + fn get_rssi() -> jint, + }, } -impl<'a> JScanResult<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class(jni_str!("android/bluetooth/le/ScanResult"))?; - - let get_device = - env.get_method_id(&class, jni_str!("getDevice"), jni_sig!("()Landroid/bluetooth/BluetoothDevice;"))?; - let get_scan_record = env.get_method_id( - &class, - jni_str!("getScanRecord"), - jni_sig!("()Landroid/bluetooth/le/ScanRecord;"), - )?; - let get_tx_power = env.get_method_id(&class, jni_str!("getTxPower"), jni_sig!("()I"))?; - let get_rssi = env.get_method_id(&class, jni_str!("getRssi"), jni_sig!("()I"))?; - Ok(Self { - internal: obj, - get_device, - get_scan_record, - get_tx_power, - get_rssi, - }) - } - - pub fn get_device(&self, env: &mut Env<'a>) -> Result> { - let obj = unsafe { - env.call_method_unchecked(&self.internal, self.get_device, ReturnType::Object, &[]) - }? - .l()?; - JBluetoothDevice::from_env(env, obj) - } - - pub fn get_scan_record(&self, env: &mut Env<'a>) -> Result> { - let obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.get_scan_record, - ReturnType::Object, - &[], - ) - }? - .l()?; - JScanRecord::from_env(env, obj) - } - - pub fn get_tx_power(&self, env: &mut Env<'a>) -> Result { - unsafe { - env.call_method_unchecked( - &self.internal, - self.get_tx_power, - ReturnType::Primitive(Primitive::Int), - &[], - ) - }? - .i() +impl<'local> JScanResult<'local> { + pub fn get_device(&self, env: &mut Env<'local>) -> Result> { + let obj = self.get_device_obj(env)?; + env.cast_local::(obj) } - pub fn get_rssi(&self, env: &mut Env<'a>) -> Result { - unsafe { - env.call_method_unchecked( - &self.internal, - self.get_rssi, - ReturnType::Primitive(Primitive::Int), - &[], - ) - }? - .i() + pub fn get_scan_record(&self, env: &mut Env<'local>) -> Result> { + self.get_scan_record_obj(env) } pub fn to_peripheral_properties( &self, - env: &mut Env<'a>, + env: &mut Env<'local>, ) -> std::result::Result<(BDAddr, Option), crate::Error> { use std::str::FromStr; @@ -723,11 +326,11 @@ impl<'a> JScanResult<'a> { let addr_str = String::from(addr_jstr.mutf8_chars(env)?); let addr = BDAddr::from_str(&addr_str)?; - let record = self.get_scan_record(env)?; - let record_is_null = env.is_same_object(&*record, JObject::null())?; - let properties = if record_is_null { + let record_obj = self.get_scan_record(env)?; + let properties = if record_obj.is_null() { None } else { + let record = env.cast_local::(record_obj)?; let device_name_obj = record.get_device_name(env)?; let device_name = if env.is_same_object(&device_name_obj, JObject::null())? { None @@ -752,13 +355,14 @@ impl<'a> JScanResult<'a> { let rssi = Some(self.get_rssi(env)? as i16); - let manufacturer_specific_data_obj = record.get_manufacturer_specific_data(env)?; + let mfr_data_obj = record.get_manufacturer_specific_data(env)?; let mut manufacturer_data = HashMap::new(); - if !env.is_same_object(&*manufacturer_specific_data_obj, JObject::null())? { - let size = manufacturer_specific_data_obj.size(env)?; + if !mfr_data_obj.is_null() { + let sparse_arr = env.cast_local::(mfr_data_obj)?; + let size = sparse_arr.size(env)?; for i in 0..size { - let key = manufacturer_specific_data_obj.key_at(env, i)?; - let value = manufacturer_specific_data_obj.value_at(env, i)?; + let key = sparse_arr.key_at(env, i)?; + let value = sparse_arr.value_at(env, i)?; let value_arr = unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; let data = @@ -794,7 +398,7 @@ impl<'a> JScanResult<'a> { let value = env .call_method(&entry, jni_str!("getValue"), jni_sig!("()Ljava/lang/Object;"), &[])? .l()?; - let parcel_uuid = JParcelUuid::from_env(env, key)?; + let parcel_uuid = env.cast_local::(key)?; let juuid = parcel_uuid.get_uuid(env)?; let uuid = juuid.as_uuid(env)?; let value_arr = @@ -820,7 +424,7 @@ impl<'a> JScanResult<'a> { &[jni::objects::JValue::from(i)], )? .l()?; - let parcel_uuid = JParcelUuid::from_env(env, obj)?; + let parcel_uuid = env.cast_local::(obj)?; let juuid = parcel_uuid.get_uuid(env)?; let uuid = juuid.as_uuid(env)?; services.push(uuid); @@ -844,212 +448,38 @@ impl<'a> JScanResult<'a> { } } -pub struct JScanRecord<'a> { - internal: JObject<'a>, - get_device_name: JMethodID, - get_tx_power_level: JMethodID, - get_manufacturer_specific_data: JMethodID, - get_service_data: JMethodID, - get_service_uuids: JMethodID, +bind_java_type! { + pub JScanRecord => android.bluetooth.le.ScanRecord, + methods { + fn get_device_name() -> JObject, + fn get_tx_power_level() -> jint, + fn get_manufacturer_specific_data() -> JObject, + fn get_service_data() -> JObject, + fn get_service_uuids() -> JObject, + }, } -impl<'a> From> for JObject<'a> { - fn from(scan_record: JScanRecord<'a>) -> Self { - scan_record.internal - } +bind_java_type! { + pub JSparseArray => android.util.SparseArray, + methods { + fn size() -> jint, + fn key_at(index: jint) -> jint, + fn value_at(index: jint) -> JObject, + }, } -impl<'a> ::std::ops::Deref for JScanRecord<'a> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } +bind_java_type! { + pub JParcelUuid => android.os.ParcelUuid, + methods { + fn get_uuid_obj() -> JObject { + name = "getUuid", + }, + }, } -impl<'a> JScanRecord<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class(jni_str!("android/bluetooth/le/ScanRecord"))?; - - let get_device_name = env.get_method_id(&class, jni_str!("getDeviceName"), jni_sig!("()Ljava/lang/String;"))?; - let get_tx_power_level = env.get_method_id(&class, jni_str!("getTxPowerLevel"), jni_sig!("()I"))?; - let get_manufacturer_specific_data = env.get_method_id( - &class, - jni_str!("getManufacturerSpecificData"), - jni_sig!("()Landroid/util/SparseArray;"), - )?; - let get_service_data = env.get_method_id(&class, jni_str!("getServiceData"), jni_sig!("()Ljava/util/Map;"))?; - let get_service_uuids = - env.get_method_id(&class, jni_str!("getServiceUuids"), jni_sig!("()Ljava/util/List;"))?; - Ok(Self { - internal: obj, - get_device_name, - get_tx_power_level, - get_manufacturer_specific_data, - get_service_data, - get_service_uuids, - }) - } - - pub fn get_device_name(&self, env: &mut Env<'a>) -> Result> { - unsafe { - env.call_method_unchecked( - &self.internal, - self.get_device_name, - ReturnType::Object, - &[], - ) - }? - .l() - } - - pub fn get_tx_power_level(&self, env: &mut Env<'a>) -> Result { - unsafe { - env.call_method_unchecked( - &self.internal, - self.get_tx_power_level, - ReturnType::Primitive(Primitive::Int), - &[], - ) - }? - .i() - } - - pub fn get_manufacturer_specific_data( - &self, - env: &mut Env<'a>, - ) -> Result> { - let obj = unsafe { - env.call_method_unchecked( - &self.internal, - self.get_manufacturer_specific_data, - ReturnType::Object, - &[], - ) - }? - .l()?; - JSparseArray::from_env(env, obj) - } - - pub fn get_service_data(&self, env: &mut Env<'a>) -> Result> { - unsafe { - env.call_method_unchecked( - &self.internal, - self.get_service_data, - ReturnType::Object, - &[], - ) - }? - .l() - } - - pub fn get_service_uuids(&self, env: &mut Env<'a>) -> Result> { - unsafe { - env.call_method_unchecked( - &self.internal, - self.get_service_uuids, - ReturnType::Object, - &[], - ) - }? - .l() - } -} - -pub struct JSparseArray<'a> { - internal: JObject<'a>, - size: JMethodID, - key_at: JMethodID, - value_at: JMethodID, -} - -impl<'a> From> for JObject<'a> { - fn from(sparse_array: JSparseArray<'a>) -> Self { - sparse_array.internal - } -} - -impl<'a> ::std::ops::Deref for JSparseArray<'a> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } -} - -impl<'a> JSparseArray<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class(jni_str!("android/util/SparseArray"))?; - - let size = env.get_method_id(&class, jni_str!("size"), jni_sig!("()I"))?; - let key_at = env.get_method_id(&class, jni_str!("keyAt"), jni_sig!("(I)I"))?; - let value_at = env.get_method_id(&class, jni_str!("valueAt"), jni_sig!("(I)Ljava/lang/Object;"))?; - Ok(Self { - internal: obj, - size, - key_at, - value_at, - }) - } - - pub fn size(&self, env: &mut Env<'a>) -> Result { - unsafe { - env.call_method_unchecked( - &self.internal, - self.size, - ReturnType::Primitive(Primitive::Int), - &[], - ) - }? - .i() - } - - pub fn key_at(&self, env: &mut Env<'a>, index: jint) -> Result { - unsafe { - env.call_method_unchecked( - &self.internal, - self.key_at, - ReturnType::Primitive(Primitive::Int), - &[jvalue { i: index }], - ) - }? - .i() - } - - pub fn value_at(&self, env: &mut Env<'a>, index: jint) -> Result> { - unsafe { - env.call_method_unchecked( - &self.internal, - self.value_at, - ReturnType::Object, - &[jvalue { i: index }], - ) - }? - .l() - } -} - -pub struct JParcelUuid<'a> { - internal: JObject<'a>, - get_uuid: JMethodID, -} - -impl<'a> JParcelUuid<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class(jni_str!("android/os/ParcelUuid"))?; - - let get_uuid = env.get_method_id(&class, jni_str!("getUuid"), jni_sig!("()Ljava/util/UUID;"))?; - Ok(Self { - internal: obj, - get_uuid, - }) - } - - pub fn get_uuid(&self, env: &mut Env<'a>) -> Result> { - let obj = unsafe { - env.call_method_unchecked(&self.internal, self.get_uuid, ReturnType::Object, &[]) - }? - .l()?; - JUuid::from_env(env, obj) +impl<'local> JParcelUuid<'local> { + pub fn get_uuid(&self, env: &mut Env<'local>) -> Result> { + let obj = self.get_uuid_obj(env)?; + env.cast_local::(obj) } } diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 2564fcd1..62b6766c 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -1,10 +1,8 @@ use ::jni::{ Env, JavaVM, + bind_java_type, errors::Result, - jni_sig, jni_str, - objects::{Global, JMethodID, JObject}, - signature::ReturnType, - sys::jvalue, + objects::{Global, JObject}, }; use static_assertions::assert_impl_all; use std::{ @@ -13,82 +11,29 @@ use std::{ task::{Context, Poll}, }; -pub struct JFuture<'a> { - internal: JObject<'a>, - poll_id: JMethodID, -} - -impl<'a> JFuture<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = - super::classcache::get_class("io/github/gedgygedgy/rust/future/Future").unwrap(); - let poll_id = env.get_method_id( - class.as_ref(), - jni_str!("poll"), - jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), - )?; - Ok(Self { - internal: obj, - poll_id, - }) - } - - pub fn poll(&self, env: &mut Env<'a>, waker: &JObject<'_>) -> Result> { - let result = unsafe { - env.call_method_unchecked( - &self.internal, - self.poll_id, - ReturnType::Object, - &[jvalue { - l: waker.as_raw(), - }], - ) - }? - .l()?; - Ok(result) - } -} - -impl<'a> ::std::ops::Deref for JFuture<'a> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } -} - -impl<'a> From> for JObject<'a> { - fn from(other: JFuture<'a>) -> JObject<'a> { - other.internal - } +bind_java_type! { + pub JFuture => io.github.gedgygedgy.rust.future.Future, + methods { + fn poll(waker: JObject) -> JObject, + }, } pub struct JSendFuture { internal: Global>, - poll_id: JMethodID, vm: JavaVM, } impl JSendFuture { pub fn new(env: &mut Env, future: &JFuture) -> Result { Ok(Self { - internal: env.new_global_ref(&future.internal)?, - poll_id: future.poll_id, + internal: env.new_global_ref(&**future)?, vm: env.get_java_vm()?, }) } pub fn from_env(env: &mut Env, obj: &JObject) -> Result { - let class = - super::classcache::get_class("io/github/gedgygedgy/rust/future/Future").unwrap(); - let poll_id = env.get_method_id( - class.as_ref(), - jni_str!("poll"), - jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), - )?; Ok(Self { internal: env.new_global_ref(obj)?, - poll_id, vm: env.get_java_vm()?, }) } @@ -96,17 +41,9 @@ impl JSendFuture { fn poll_internal(&self, context: &mut Context<'_>) -> Result>>>> { self.vm.attach_current_thread(|env| { let jwaker = super::task::waker(env, context.waker().clone())?; - let result = unsafe { - env.call_method_unchecked( - self.internal.as_obj(), - self.poll_id, - ReturnType::Object, - &[jvalue { - l: jwaker.as_raw(), - }], - ) - }? - .l()?; + let local = env.new_local_ref(self.internal.as_obj())?; + let jfuture = env.cast_local::(local)?; + let result = jfuture.poll(env, &jwaker)?; Ok(if env.is_same_object(&result, JObject::null())? { Poll::Pending } else { @@ -166,7 +103,7 @@ mod test { .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) .unwrap(); let future_local = env.new_local_ref(&future_obj).unwrap(); - let jfuture = JFuture::from_env(env, future_local).unwrap(); + let jfuture = env.cast_local::(future_local).unwrap(); let mut future = JSendFuture::new(env, &jfuture).unwrap(); assert!( @@ -196,7 +133,7 @@ mod test { if let Poll::Ready(result) = poll { let global = result.unwrap(); let local = env.new_local_ref(global.as_obj()).unwrap(); - let poll_result = JPollResult::from_env(env, local).unwrap(); + let poll_result = env.cast_local::(local).unwrap(); let result_obj = poll_result.get(env).unwrap(); assert!(env.is_same_object(&result_obj, &obj).unwrap()); } else { @@ -209,7 +146,7 @@ mod test { if let Poll::Ready(result) = poll { let global = result.unwrap(); let local = env.new_local_ref(global.as_obj()).unwrap(); - let poll_result = JPollResult::from_env(env, local).unwrap(); + let poll_result = env.cast_local::(local).unwrap(); let result_obj = poll_result.get(env).unwrap(); assert!(env.is_same_object(&result_obj, &obj).unwrap()); } else { @@ -233,7 +170,7 @@ mod test { .unwrap(); let future_obj_global = env.new_global_ref(&future_obj).unwrap(); let future_local = env.new_local_ref(&future_obj).unwrap(); - let jfuture = JFuture::from_env(env, future_local).unwrap(); + let jfuture = env.cast_local::(future_local).unwrap(); let future = JSendFuture::new(env, &jfuture).unwrap(); let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); let obj_global = env.new_global_ref(&obj).unwrap(); @@ -260,7 +197,7 @@ mod test { let global = future.await.unwrap(); test_utils::with_env(|env| { let local = env.new_local_ref(global.as_obj()).unwrap(); - let poll_result = JPollResult::from_env(env, local).unwrap(); + let poll_result = env.cast_local::(local).unwrap(); let result_obj = poll_result.get(env).unwrap(); let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); assert!(env.is_same_object(&result_obj, &obj_local).unwrap()); @@ -281,7 +218,7 @@ mod test { .unwrap(); let future_obj_global = env.new_global_ref(&future_obj).unwrap(); let future_local = env.new_local_ref(&future_obj).unwrap(); - let jfuture = JFuture::from_env(env, future_local).unwrap(); + let jfuture = env.cast_local::(future_local).unwrap(); let future = JSendFuture::new(env, &jfuture).unwrap(); let ex = env.new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]).unwrap(); let ex_global = env.new_global_ref(&ex).unwrap(); @@ -310,7 +247,7 @@ mod test { let global = future.await.unwrap(); test_utils::with_env(|env| { let local = env.new_local_ref(global.as_obj()).unwrap(); - let poll_result = JPollResult::from_env(env, local).unwrap(); + let poll_result = env.cast_local::(local).unwrap(); let _err = poll_result.get(env).unwrap_err(); let future_ex = env.exception_occurred().unwrap(); @@ -365,7 +302,7 @@ mod test { let global_ref = future.await.unwrap(); test_utils::with_env(|env| { let local = env.new_local_ref(global_ref.as_obj()).unwrap(); - let jpoll = JPollResult::from_env(env, local).unwrap(); + let jpoll = env.cast_local::(local).unwrap(); let result_obj = jpoll.get(env).unwrap(); let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); assert!(env.is_same_object(&result_obj, &obj_local).unwrap()); diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index af1d4704..644ff7c4 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -1,11 +1,9 @@ use super::task::JPollResult; use ::jni::{ Env, JavaVM, + bind_java_type, errors::Result, - jni_sig, jni_str, - objects::{Global, JMethodID, JObject}, - signature::ReturnType, - sys::jvalue, + objects::{Global, JObject}, }; use futures::stream::Stream; use static_assertions::assert_impl_all; @@ -14,86 +12,36 @@ use std::{ task::{Context, Poll}, }; -pub struct JStream<'a> { - internal: JObject<'a>, - poll_next_id: JMethodID, +bind_java_type! { + pub JStream => io.github.gedgygedgy.rust.stream.Stream, + methods { + fn poll_next(waker: JObject) -> JObject, + }, } -impl<'a> JStream<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = - super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream").unwrap(); - let poll_next_id = env.get_method_id( - class.as_ref(), - jni_str!("pollNext"), - jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), - )?; - Ok(Self { - internal: obj, - poll_next_id, - }) - } - - pub fn poll_next_with_env( - &self, - env: &mut Env<'a>, - waker: &JObject<'_>, - ) -> Result> { - let result = unsafe { - env.call_method_unchecked( - &self.internal, - self.poll_next_id, - ReturnType::Object, - &[jvalue { - l: waker.as_raw(), - }], - ) - }? - .l()?; - Ok(result) - } -} - -impl<'a> ::std::ops::Deref for JStream<'a> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } -} - -impl<'a> From> for JObject<'a> { - fn from(other: JStream<'a>) -> JObject<'a> { - other.internal - } +bind_java_type! { + JStreamPoll => io.github.gedgygedgy.rust.stream.StreamPoll, + methods { + fn get() -> JObject, + }, } pub struct JSendStream { internal: Global>, - poll_next_id: JMethodID, vm: JavaVM, } impl JSendStream { pub fn new(env: &mut Env, stream: &JStream) -> Result { Ok(Self { - internal: env.new_global_ref(&stream.internal)?, - poll_next_id: stream.poll_next_id, + internal: env.new_global_ref(&**stream)?, vm: env.get_java_vm()?, }) } pub fn from_env(env: &mut Env, obj: &JObject) -> Result { - let class = - super::classcache::get_class("io/github/gedgygedgy/rust/stream/Stream").unwrap(); - let poll_next_id = env.get_method_id( - class.as_ref(), - jni_str!("pollNext"), - jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), - )?; Ok(Self { internal: env.new_global_ref(obj)?, - poll_next_id, vm: env.get_java_vm()?, }) } @@ -104,30 +52,22 @@ impl JSendStream { ) -> Result>>>>> { self.vm.attach_current_thread(|env| { let jwaker = super::task::waker(env, context.waker().clone())?; - let result = unsafe { - env.call_method_unchecked( - self.internal.as_obj(), - self.poll_next_id, - ReturnType::Object, - &[jvalue { - l: jwaker.as_raw(), - }], - ) - }? - .l()?; + let local = env.new_local_ref(self.internal.as_obj())?; + let jstream = env.cast_local::(local)?; + let result = jstream.poll_next(env, &jwaker)?; if env.is_same_object(&result, JObject::null())? { return Ok(Poll::Pending); } - let poll_result = JPollResult::from_env(env, result)?; + let poll_result = env.cast_local::(result)?; let stream_poll_obj = poll_result.get(env)?; if env.is_same_object(&stream_poll_obj, JObject::null())? { return Ok(Poll::Ready(None)); } - let stream_poll = JStreamPoll::from_env(env, stream_poll_obj)?; + let stream_poll = env.cast_local::(stream_poll_obj)?; let obj = stream_poll.get(env)?; Ok(Poll::Ready(Some(Ok(env.new_global_ref(obj)?)))) }) @@ -155,29 +95,6 @@ impl Stream for JSendStream { assert_impl_all!(JSendStream: Send); -struct JStreamPoll<'a> { - internal: JObject<'a>, - get: JMethodID, -} - -impl<'a> JStreamPoll<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = - super::classcache::get_class("io/github/gedgygedgy/rust/stream/StreamPoll").unwrap(); - let get = env.get_method_id( - class.as_ref(), - jni_str!("get"), - jni_sig!("()Ljava/lang/Object;"), - )?; - Ok(Self { internal: obj, get }) - } - - pub fn get(&self, env: &mut Env<'a>) -> Result> { - unsafe { env.call_method_unchecked(&self.internal, self.get, ReturnType::Object, &[]) }? - .l() - } -} - #[cfg(test)] mod test { use super::super::test_utils; @@ -206,7 +123,7 @@ mod test { .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) .unwrap(); let stream_local = env.new_local_ref(&stream_obj).unwrap(); - let jstream = JStream::from_env(env, stream_local).unwrap(); + let jstream = env.cast_local::(stream_local).unwrap(); let mut stream = JSendStream::new(env, &jstream).unwrap(); assert!( @@ -293,7 +210,7 @@ mod test { .unwrap(); let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); let stream_local = env.new_local_ref(&stream_obj).unwrap(); - let jstream = JStream::from_env(env, stream_local).unwrap(); + let jstream = env.cast_local::(stream_local).unwrap(); let stream = JSendStream::new(env, &jstream).unwrap(); let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); let obj1_global = env.new_global_ref(&obj1).unwrap(); diff --git a/src/droidplug/jni_utils/task.rs b/src/droidplug/jni_utils/task.rs index e412f55b..92a777c4 100644 --- a/src/droidplug/jni_utils/task.rs +++ b/src/droidplug/jni_utils/task.rs @@ -1,9 +1,9 @@ use ::jni::{ Env, + bind_java_type, errors::Result, - jni_sig, jni_str, - objects::{JMethodID, JObject}, - signature::ReturnType, + jni_sig, + objects::JObject, }; use std::task::Waker; @@ -19,37 +19,11 @@ pub fn waker<'a>(env: &mut Env<'a>, waker: Waker) -> Result> { Ok(obj) } -pub struct JPollResult<'a> { - internal: JObject<'a>, - get: JMethodID, -} - -impl<'a> JPollResult<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = - super::classcache::get_class("io/github/gedgygedgy/rust/task/PollResult").unwrap(); - let get = env.get_method_id(class.as_ref(), jni_str!("get"), jni_sig!("()Ljava/lang/Object;"))?; - Ok(Self { internal: obj, get }) - } - - pub fn get(&self, env: &mut Env<'a>) -> Result> { - unsafe { env.call_method_unchecked(&self.internal, self.get, ReturnType::Object, &[]) }? - .l() - } -} - -impl<'a> ::std::ops::Deref for JPollResult<'a> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } -} - -impl<'a> From> for JObject<'a> { - fn from(other: JPollResult<'a>) -> JObject<'a> { - other.internal - } +bind_java_type! { + pub JPollResult => io.github.gedgygedgy.rust.task.PollResult, + methods { + fn get() -> JObject, + }, } #[cfg(test)] diff --git a/src/droidplug/jni_utils/uuid.rs b/src/droidplug/jni_utils/uuid.rs index 48618684..840c1538 100644 --- a/src/droidplug/jni_utils/uuid.rs +++ b/src/droidplug/jni_utils/uuid.rs @@ -1,89 +1,40 @@ use jni::{ Env, + bind_java_type, errors::Result, - jni_str, jni_sig, - objects::{JMethodID, JObject}, - signature::{Primitive, ReturnType}, sys::jlong, }; use uuid::Uuid; -pub struct JUuid<'a> { - internal: JObject<'a>, - get_least_significant_bits: JMethodID, - get_most_significant_bits: JMethodID, +bind_java_type! { + pub JUuid => java.util.UUID, + constructors { + fn with_bits(most_significant_bits: jlong, least_significant_bits: jlong), + }, + methods { + fn get_least_significant_bits() -> jlong, + fn get_most_significant_bits() -> jlong, + }, } -impl<'a> JUuid<'a> { - pub fn from_env(env: &mut Env<'a>, obj: JObject<'a>) -> Result { - let class = env.find_class(jni_str!("java/util/UUID"))?; - let get_least_significant_bits = - env.get_method_id(&class, jni_str!("getLeastSignificantBits"), jni_sig!("()J"))?; - let get_most_significant_bits = - env.get_method_id(&class, jni_str!("getMostSignificantBits"), jni_sig!("()J"))?; - Ok(Self { - internal: obj, - get_least_significant_bits, - get_most_significant_bits, - }) - } - - pub fn new(env: &mut Env<'a>, uuid: Uuid) -> Result { +impl JUuid<'_> { + pub fn new<'local>(env: &mut Env<'local>, uuid: Uuid) -> Result> { let val = uuid.as_u128(); let least = (val & 0xFFFFFFFFFFFFFFFF) as jlong; let most = ((val >> 64) & 0xFFFFFFFFFFFFFFFF) as jlong; - - let class = env.find_class(jni_str!("java/util/UUID"))?; - let obj = env.new_object(&class, jni_sig!("(JJ)V"), &[most.into(), least.into()])?; - let get_least_significant_bits = - env.get_method_id(&class, jni_str!("getLeastSignificantBits"), jni_sig!("()J"))?; - let get_most_significant_bits = - env.get_method_id(&class, jni_str!("getMostSignificantBits"), jni_sig!("()J"))?; - Ok(Self { - internal: obj, - get_least_significant_bits, - get_most_significant_bits, - }) + JUuid::with_bits(env, most, least) } +} - pub fn as_uuid(&self, env: &mut Env<'a>) -> Result { - let least = unsafe { - env.call_method_unchecked( - &self.internal, - self.get_least_significant_bits, - ReturnType::Primitive(Primitive::Long), - &[], - ) - }? - .j()? as u64; - let most = unsafe { - env.call_method_unchecked( - &self.internal, - self.get_most_significant_bits, - ReturnType::Primitive(Primitive::Long), - &[], - ) - }? - .j()? as u64; +impl<'local> JUuid<'local> { + pub fn as_uuid(&self, env: &mut Env<'local>) -> Result { + let least = self.get_least_significant_bits(env)? as u64; + let most = self.get_most_significant_bits(env)? as u64; let val = ((most as u128) << 64) | (least as u128); Ok(Uuid::from_u128(val)) } } -impl<'a> ::std::ops::Deref for JUuid<'a> { - type Target = JObject<'a>; - - fn deref(&self) -> &Self::Target { - &self.internal - } -} - -impl<'a> From> for JObject<'a> { - fn from(other: JUuid<'a>) -> JObject<'a> { - other.internal - } -} - #[cfg(test)] mod test { use super::super::test_utils; @@ -147,7 +98,7 @@ mod test { let obj = env .new_object(jni_str!("java/util/UUID"), jni_sig!("(JJ)V"), &[most.into(), least.into()]) .unwrap(); - let uuid_obj = JUuid::from_env(env, obj).unwrap(); + let uuid_obj = env.cast_local::(obj).unwrap(); assert_eq!(uuid_obj.as_uuid(env).unwrap(), Uuid::from_u128(test.uuid)); } diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 81cd54fb..20547a7a 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -30,7 +30,7 @@ use std::{ sync::{Arc, Mutex}, }; use super::jni::{ - global_jvm, + jvm, objects::{JBluetoothGattCharacteristic, JBluetoothGattService, JPeripheral}, }; @@ -52,7 +52,7 @@ fn get_poll_result<'a>( result_ref: &Global>, ) -> Result> { let result_obj = env.new_local_ref(result_ref)?; - let poll_result = JPollResult::from_env(env, result_obj)?; + let poll_result = env.cast_local::(result_obj)?; match poll_result.get(env) { Ok(obj) => Ok(obj), @@ -135,7 +135,7 @@ pub struct Peripheral { impl Peripheral { pub(crate) fn new<'a>(env: &mut Env<'a>, adapter: JObject<'a>, addr: BDAddr) -> Result { - let obj = JPeripheral::new(env, adapter, addr)?; + let obj = JPeripheral::create(env, adapter, addr)?; let internal = Arc::new(env.new_global_ref(&*obj)?); Ok(Self { addr, @@ -162,9 +162,9 @@ impl Peripheral { where E: From<::jni::errors::Error>, { - global_jvm().attach_current_thread(|env| { + jvm()?.attach_current_thread(|env| { let local_obj = env.new_local_ref(self.internal.as_obj())?; - let obj = JPeripheral::from_env(env, local_obj)?; + let obj = env.cast_local::(local_obj)?; f(env, &obj) }) } @@ -241,7 +241,7 @@ impl api::Peripheral for Peripheral { { let mtu_future = self.with_obj(|env, obj| { let mtu_obj = obj.request_mtu(env, 517)?; - let mtu_future = JFuture::from_env(env, mtu_obj)?; + let mtu_future = env.cast_local::(mtu_obj)?; JSendFuture::new(env, &mtu_future) })?; let mtu_result_ref = mtu_future.await?; @@ -287,7 +287,7 @@ impl api::Peripheral for Peripheral { let svc_obj = env .call_method(&obj, jni_str!("get"), jni_sig!("(I)Ljava/lang/Object;"), &[JValue::from(i)])? .l()?; - let service = JBluetoothGattService::from_env(env, svc_obj)?; + let service = env.cast_local::(svc_obj)?; let mut characteristics = BTreeSet::::new(); for characteristic in service.get_characteristics(env)? { let mut descriptors = BTreeSet::new(); @@ -381,10 +381,10 @@ impl api::Peripheral for Peripheral { let stream = stream .map(move |item| match item { Ok(item) => { - global_jvm().attach_current_thread(|env| { + jvm()?.attach_current_thread(|env| { let local_obj = env.new_local_ref(item.as_obj())?; let characteristic = - JBluetoothGattCharacteristic::from_env(env, local_obj)?; + env.cast_local::(local_obj)?; let uuid = characteristic.get_uuid(env)?; let value = characteristic.get_value(env)?; let service_uuid = shared @@ -414,7 +414,7 @@ impl api::Peripheral for Peripheral { async fn read_rssi(&self) -> Result { let future = self.with_obj(|env, obj| { let rssi_obj = obj.read_remote_rssi(env)?; - let rssi_future = JFuture::from_env(env, rssi_obj)?; + let rssi_future = env.cast_local::(rssi_obj)?; JSendFuture::new(env, &rssi_future) })?; let result_ref = future.await?; From 93a3e34beb6fef96cc98363a14a76357022b069e Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 22:22:37 -0700 Subject: [PATCH 14/77] refactor: Replace adapter extern C callbacks with native_method! macro The macro generates extern "system" trampolines with automatic catch_unwind and error-to-Java-exception propagation, replacing the manual EnvUnowned pattern that silently swallowed errors. Co-Authored-By: Claude Opus 4.6 --- src/droidplug/jni/mod.rs | 54 +++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index 989c6795..21adee1d 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,6 +1,6 @@ pub mod objects; -use ::jni::{Env, EnvUnowned, NativeMethod, jni_str, objects::JObject}; +use ::jni::{Env, NativeMethod, jni_str, native_method, objects::JObject}; use jni::{objects::JString, sys::jboolean}; use std::ffi::c_void; use std::sync::Once; @@ -24,16 +24,16 @@ fn init_inner(env: &mut Env) -> crate::Result<()> { unsafe { env.register_native_methods( &adapter_class, &[ - NativeMethod::from_raw_parts( - jni_str!("reportScanResult"), - jni_str!("(Landroid/bluetooth/le/ScanResult;)V"), - adapter_report_scan_result as *mut c_void, - ), - NativeMethod::from_raw_parts( - jni_str!("onConnectionStateChanged"), - jni_str!("(Ljava/lang/String;Z)V"), - adapter_on_connection_state_changed as *mut c_void, - ), + native_method! { + name = "reportScanResult", + sig = (scan_result: JObject) -> (), + fn = adapter_report_scan_result, + }, + native_method! { + name = "onConnectionStateChanged", + sig = (addr: JString, connected: jboolean) -> (), + fn = adapter_on_connection_state_changed, + }, ], )? }; super::jni_utils::classcache::find_add_class( @@ -140,25 +140,21 @@ impl From<::jni::errors::Error> for crate::Error { } } -extern "C" fn adapter_report_scan_result<'a>(mut env: EnvUnowned<'a>, obj: JObject, scan_result: JObject<'a>) { - let outcome = env.with_env(|env| { - let _ = super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result); - Ok::<(), jni::errors::Error>(()) - }); - let _ = outcome.into_outcome(); +fn adapter_report_scan_result<'local>( + env: &mut Env<'local>, + obj: JObject<'local>, + scan_result: JObject<'local>, +) -> jni::errors::Result<()> { + super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result) + .map_err(|e| jni::errors::Error::Other(Box::new(e))) } -extern "C" fn adapter_on_connection_state_changed( - mut env: EnvUnowned, - obj: JObject, - addr: JString, +fn adapter_on_connection_state_changed<'local>( + env: &mut Env<'local>, + obj: JObject<'local>, + addr: JString<'local>, connected: jboolean, -) { - let outcome = env.with_env(|env| { - let _ = super::adapter::adapter_on_connection_state_changed_internal( - env, &obj, addr, connected, - ); - Ok::<(), jni::errors::Error>(()) - }); - let _ = outcome.into_outcome(); +) -> jni::errors::Result<()> { + super::adapter::adapter_on_connection_state_changed_internal(env, &obj, addr, connected) + .map_err(|e| jni::errors::Error::Other(Box::new(e))) } From 3c1b3a0fa9d65b2c9beed1d3a1e88cd842bbcd83 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 22:30:01 -0700 Subject: [PATCH 15/77] refactor: Eliminate classcache module, use bind_java_type! class caching Replace the hand-rolled DashMap-based class cache with bind_java_type!'s built-in global class caching via Reference::lookup_class(). Add bare bind_java_type! declarations for exception types, FnAdapter, and other utility classes previously only tracked in classcache. Delete classcache.rs entirely. Co-Authored-By: Claude Opus 4.6 --- src/droidplug/adapter.rs | 10 +-- src/droidplug/jni/mod.rs | 102 ++++++++------------------ src/droidplug/jni/objects.rs | 40 ++++++++-- src/droidplug/jni_utils/classcache.rs | 20 ----- src/droidplug/jni_utils/future.rs | 4 + src/droidplug/jni_utils/mod.rs | 41 ++++++----- src/droidplug/jni_utils/ops.rs | 43 +++++++---- src/droidplug/jni_utils/stream.rs | 2 +- src/droidplug/jni_utils/task.rs | 10 ++- src/droidplug/peripheral.rs | 46 ++++++------ 10 files changed, 154 insertions(+), 164 deletions(-) delete mode 100644 src/droidplug/jni_utils/classcache.rs diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index ec424397..af98ce09 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -150,12 +150,12 @@ impl Central for Adapter { let ex = env.exception_occurred().unwrap(); env.exception_clear(); - let no_adapter_class = super::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", - ) - .unwrap(); + let no_adapter_class = ::lookup_class( + env, + &Default::default(), + )?; - if env.is_instance_of(&ex, no_adapter_class.as_ref())? { + if env.is_instance_of(&ex, &*no_adapter_class)? { Err(Error::NoAdapterAvailable) } else if env.is_instance_of(&ex, jni_str!("java/lang/RuntimeException"))? { let msg = env diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index 21adee1d..27531fc4 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,6 +1,6 @@ pub mod objects; -use ::jni::{Env, NativeMethod, jni_str, native_method, objects::JObject}; +use ::jni::{Env, NativeMethod, jni_str, native_method, objects::{JObject, Reference}}; use jni::{objects::JString, sys::jboolean}; use std::ffi::c_void; use std::sync::Once; @@ -36,82 +36,38 @@ fn init_inner(env: &mut Env) -> crate::Result<()> { }, ], )? }; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/Peripheral", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/ScanFilter", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/NotConnectedException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/PermissionDeniedException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/UnexpectedCallbackException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/UnexpectedCharacteristicException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/NoSuchCharacteristicException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", - )?; + use objects::*; + use super::jni_utils::{ + future::{JFuture, JFutureException}, + ops::{JFnAdapter, JFnRunnableImpl, JFnBiFunctionImpl, JFnFunctionImpl}, + stream::{JStream, JStreamPoll}, + task::{JPollResult, JWaker}, + }; - // jni-utils class caching - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/future/Future", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/future/FutureException", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/ops/FnAdapter", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/stream/Stream", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/stream/StreamPoll", - )?; - super::jni_utils::classcache::find_add_class(env, "io/github/gedgygedgy/rust/task/Waker")?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/task/PollResult", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/ops/FnRunnableImpl", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/ops/FnBiFunctionImpl", - )?; - super::jni_utils::classcache::find_add_class( - env, - "io/github/gedgygedgy/rust/ops/FnFunctionImpl", - )?; + let loader = jni::objects::LoaderContext::default(); + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; // FnAdapter native method registration - let fn_adapter_class = env.find_class(jni_str!("io/github/gedgygedgy/rust/ops/FnAdapter"))?; + let fn_adapter_class = ::lookup_class(env, &loader)?; unsafe { env.register_native_methods( - &fn_adapter_class, + &*fn_adapter_class, &[ NativeMethod::from_raw_parts( jni_str!("callInternal"), diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index da319d8f..74509fa3 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -3,7 +3,7 @@ use jni::{ Env, bind_java_type, errors::Result, - jni_sig, jni_str, + jni_sig, objects::{JObject, JString}, sys::jint, }; @@ -12,6 +12,34 @@ use uuid::Uuid; use crate::api::{BDAddr, CharPropFlags, PeripheralProperties, ScanFilter}; +bind_java_type! { + pub JNotConnectedException => com.nonpolynomial.btleplug.android.impl.NotConnectedException, +} + +bind_java_type! { + pub JPermissionDeniedException => com.nonpolynomial.btleplug.android.impl.PermissionDeniedException, +} + +bind_java_type! { + pub JUnexpectedCallbackException => com.nonpolynomial.btleplug.android.impl.UnexpectedCallbackException, +} + +bind_java_type! { + pub JUnexpectedCharacteristicException => com.nonpolynomial.btleplug.android.impl.UnexpectedCharacteristicException, +} + +bind_java_type! { + pub JNoSuchCharacteristicException => com.nonpolynomial.btleplug.android.impl.NoSuchCharacteristicException, +} + +bind_java_type! { + pub JNoBluetoothAdapterException => com.nonpolynomial.btleplug.android.impl.NoBluetoothAdapterException, +} + +bind_java_type! { + pub JScanFilterClass => com.nonpolynomial.btleplug.android.impl.ScanFilter, +} + bind_java_type! { pub JPeripheral => com.nonpolynomial.btleplug.android.impl.Peripheral, constructors { @@ -272,12 +300,12 @@ impl<'a> JScanFilter<'a> { let uuid_str = env.new_string(uuid.to_string())?; uuids.set_element(env, idx, &uuid_str)?; } - let class_static = crate::droidplug::jni_utils::classcache::get_class( - "com/nonpolynomial/btleplug/android/impl/ScanFilter", - ) - .unwrap(); + let class = ::lookup_class( + env, + &Default::default(), + )?; let obj = env.new_object( - &**class_static, + &*class, jni_sig!("([Ljava/lang/String;)V"), &[(&uuids).into()], )?; diff --git a/src/droidplug/jni_utils/classcache.rs b/src/droidplug/jni_utils/classcache.rs deleted file mode 100644 index a7db0ee5..00000000 --- a/src/droidplug/jni_utils/classcache.rs +++ /dev/null @@ -1,20 +0,0 @@ -use dashmap::DashMap; -use jni::{Env, errors::Result, objects::{Global, JClass}, strings::JNIString}; -use once_cell::sync::OnceCell; -use std::sync::Arc; - -static CLASSCACHE: OnceCell>>>> = OnceCell::new(); - -pub fn find_add_class(env: &mut Env, classname: &str) -> Result<()> { - let cache = CLASSCACHE.get_or_init(|| DashMap::new()); - let jni_name = JNIString::from(classname); - let cls = env.find_class(&jni_name)?; - let global = env.new_global_ref(&cls)?; - cache.insert(classname.to_owned(), Arc::new(global)); - Ok(()) -} - -pub fn get_class(classname: &str) -> Option>>> { - let cache = CLASSCACHE.get_or_init(|| DashMap::new()); - cache.get(classname).map(|pair| pair.value().clone()) -} diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 62b6766c..3bba3720 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -18,6 +18,10 @@ bind_java_type! { }, } +bind_java_type! { + pub JFutureException => io.github.gedgygedgy.rust.future.FutureException, +} + pub struct JSendFuture { internal: Global>, vm: JavaVM, diff --git a/src/droidplug/jni_utils/mod.rs b/src/droidplug/jni_utils/mod.rs index 437f8907..0102d8d8 100644 --- a/src/droidplug/jni_utils/mod.rs +++ b/src/droidplug/jni_utils/mod.rs @@ -1,5 +1,4 @@ pub mod arrays; -pub mod classcache; pub mod exceptions; pub mod future; pub mod ops; @@ -9,32 +8,38 @@ pub mod uuid; #[cfg(test)] pub(crate) mod test_utils { - use jni::{Env, JavaVM, jni_str, jni_sig, objects::Global, objects::JObject}; + use jni::{Env, JavaVM, NativeMethod, jni_str, jni_sig, objects::{Global, JObject, Reference}}; use lazy_static::lazy_static; use std::{ cell::Cell, + ffi::c_void, sync::{Arc, Mutex}, task::{Wake, Waker}, }; - use jni::NativeMethod; - fn test_init(env: &mut Env) -> jni::errors::Result<()> { - use std::ffi::c_void; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/future/Future")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/future/FutureException")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnAdapter")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/stream/Stream")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/stream/StreamPoll")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/task/Waker")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/task/PollResult")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnRunnableImpl")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnBiFunctionImpl")?; - super::classcache::find_add_class(env, "io/github/gedgygedgy/rust/ops/FnFunctionImpl")?; - - let class = env.find_class(jni_str!("io/github/gedgygedgy/rust/ops/FnAdapter"))?; + use super::{ + future::{JFuture, JFutureException}, + ops::{JFnAdapter, JFnRunnableImpl, JFnBiFunctionImpl, JFnFunctionImpl}, + stream::{JStream, JStreamPoll}, + task::{JPollResult, JWaker}, + }; + + let loader = jni::objects::LoaderContext::default(); + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + ::lookup_class(env, &loader)?; + + let fn_adapter_class = ::lookup_class(env, &loader)?; unsafe { env.register_native_methods( - &class, + &*fn_adapter_class, &[ NativeMethod::from_raw_parts( jni_str!("callInternal"), diff --git a/src/droidplug/jni_utils/ops.rs b/src/droidplug/jni_utils/ops.rs index a09cb6b0..7ed0db1a 100644 --- a/src/droidplug/jni_utils/ops.rs +++ b/src/droidplug/jni_utils/ops.rs @@ -1,11 +1,28 @@ use ::jni::{ Env, EnvUnowned, + bind_java_type, errors::Result, jni_sig, jni_str, - objects::JObject, + objects::{JObject, Reference}, }; use std::sync::{Arc, Mutex}; +bind_java_type! { + pub JFnAdapter => io.github.gedgygedgy.rust.ops.FnAdapter, +} + +bind_java_type! { + pub JFnRunnableImpl => io.github.gedgygedgy.rust.ops.FnRunnableImpl, +} + +bind_java_type! { + pub JFnBiFunctionImpl => io.github.gedgygedgy.rust.ops.FnBiFunctionImpl, +} + +bind_java_type! { + pub JFnFunctionImpl => io.github.gedgygedgy.rust.ops.FnFunctionImpl, +} + macro_rules! define_fn_adapter { ( fn_once: $fo:ident, @@ -17,7 +34,7 @@ macro_rules! define_fn_adapter { fn: $f:ident, fn_local: $fl:ident, fn_internal: $fi:ident, - impl_class: $ic:literal, + impl_type: $it:ty, doc_class: $dc:literal, doc_method: $dm:literal, doc_fn_once: $dfo:literal, @@ -32,9 +49,9 @@ macro_rules! define_fn_adapter { local: bool, ) -> Result> { let adapter = fn_once_adapter(env, $closure, local)?; - let class = super::classcache::get_class($ic).unwrap(); + let class = <$it as Reference>::lookup_class(env, &Default::default())?; env.new_object( - class.as_ref(), + &*class, jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) @@ -61,9 +78,9 @@ macro_rules! define_fn_adapter { local: bool, ) -> Result> { let adapter = fn_mut_adapter(env, $closure, local)?; - let class = super::classcache::get_class($ic).unwrap(); + let class = <$it as Reference>::lookup_class(env, &Default::default())?; env.new_object( - class.as_ref(), + &*class, jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) @@ -91,9 +108,9 @@ macro_rules! define_fn_adapter { local: bool, ) -> Result> { let adapter = fn_adapter(env, $closure, local)?; - let class = super::classcache::get_class($ic).unwrap(); + let class = <$it as Reference>::lookup_class(env, &Default::default())?; env.new_object( - class.as_ref(), + &*class, jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnAdapter;)V"), &[(&adapter).into()], ) @@ -127,7 +144,7 @@ define_fn_adapter! { fn: fn_runnable, fn_local: fn_runnable_local, fn_internal: fn_runnable_internal, - impl_class: "io/github/gedgygedgy/rust/ops/FnRunnableImpl", + impl_type: JFnRunnableImpl, doc_class: "io.github.gedgygedgy.rust.ops.FnRunnable", doc_method: "run()", doc_fn_once: "fn_once_runnable", @@ -150,7 +167,7 @@ define_fn_adapter! { fn: fn_bi_function, fn_local: fn_bi_function_local, fn_internal: fn_bi_function_internal, - impl_class: "io/github/gedgygedgy/rust/ops/FnBiFunctionImpl", + impl_type: JFnBiFunctionImpl, doc_class: "io.github.gedgygedgy.rust.ops.FnBiFunction", doc_method: "apply()", doc_fn_once: "fn_once_bi_function", @@ -172,7 +189,7 @@ define_fn_adapter! { fn: fn_function, fn_local: fn_function_local, fn_internal: fn_function_internal, - impl_class: "io/github/gedgygedgy/rust/ops/FnFunctionImpl", + impl_type: JFnFunctionImpl, doc_class: "io.github.gedgygedgy.rust.ops.FnFunction", doc_method: "apply()", doc_fn_once: "fn_once_function", @@ -277,9 +294,9 @@ fn fn_adapter<'local>( ) -> JObject<'c>, > = Arc::from(f); - let class = super::classcache::get_class("io/github/gedgygedgy/rust/ops/FnAdapter").unwrap(); + let class = ::lookup_class(env, &Default::default())?; let obj = env.new_object( - class.as_ref(), + &*class, jni_sig!("(Z)V"), &[local.into()], )?; diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index 644ff7c4..fa424bbc 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -20,7 +20,7 @@ bind_java_type! { } bind_java_type! { - JStreamPoll => io.github.gedgygedgy.rust.stream.StreamPoll, + pub JStreamPoll => io.github.gedgygedgy.rust.stream.StreamPoll, methods { fn get() -> JObject, }, diff --git a/src/droidplug/jni_utils/task.rs b/src/droidplug/jni_utils/task.rs index 92a777c4..587dba93 100644 --- a/src/droidplug/jni_utils/task.rs +++ b/src/droidplug/jni_utils/task.rs @@ -3,16 +3,20 @@ use ::jni::{ bind_java_type, errors::Result, jni_sig, - objects::JObject, + objects::{JObject, Reference}, }; use std::task::Waker; +bind_java_type! { + pub JWaker => io.github.gedgygedgy.rust.task.Waker, +} + pub fn waker<'a>(env: &mut Env<'a>, waker: Waker) -> Result> { let runnable = super::ops::fn_once_runnable(env, |_e, _o| waker.wake())?; - let class = super::classcache::get_class("io/github/gedgygedgy/rust/task/Waker").unwrap(); + let class = ::lookup_class(env, &Default::default())?; let obj = env.new_object( - class.as_ref(), + &*class, jni_sig!("(Lio/github/gedgygedgy/rust/ops/FnRunnable;)V"), &[(&runnable).into()], )?; diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 20547a7a..10806bda 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -60,42 +60,38 @@ fn get_poll_result<'a>( let ex = env.exception_occurred().unwrap(); env.exception_clear(); - let future_exception_class = super::jni_utils::classcache::get_class( - "io/github/gedgygedgy/rust/future/FutureException", - ) - .unwrap(); + use jni::objects::Reference; + use super::jni::objects::*; - if env.is_instance_of(&ex, future_exception_class.as_ref())? { + let future_ex_class = ::lookup_class( + env, &Default::default(), + )?; + + if env.is_instance_of(&ex, &*future_ex_class)? { let cause = env .call_method(&ex, jni_str!("getCause"), jni_sig!("()Ljava/lang/Throwable;"), &[])? .l()?; - let mut check = |name: &str| -> jni::errors::Result { - let cls = super::jni_utils::classcache::get_class(name).unwrap(); - env.is_instance_of(&cause, cls.as_ref()) - }; + macro_rules! check_exception { + ($type:ty, $env:expr, $cause:expr) => { + $env.is_instance_of( + $cause, + &*<$type as Reference>::lookup_class($env, &Default::default())?, + )? + }; + } - if check("com/nonpolynomial/btleplug/android/impl/NotConnectedException")? { + if check_exception!(JNotConnectedException, env, &cause) { Err(Error::NotConnected) - } else if check( - "com/nonpolynomial/btleplug/android/impl/PermissionDeniedException", - )? { + } else if check_exception!(JPermissionDeniedException, env, &cause) { Err(Error::PermissionDenied) - } else if check( - "com/nonpolynomial/btleplug/android/impl/UnexpectedCallbackException", - )? { + } else if check_exception!(JUnexpectedCallbackException, env, &cause) { Err(Error::UnexpectedCallback) - } else if check( - "com/nonpolynomial/btleplug/android/impl/UnexpectedCharacteristicException", - )? { + } else if check_exception!(JUnexpectedCharacteristicException, env, &cause) { Err(Error::UnexpectedCharacteristic) - } else if check( - "com/nonpolynomial/btleplug/android/impl/NoSuchCharacteristicException", - )? { + } else if check_exception!(JNoSuchCharacteristicException, env, &cause) { Err(Error::NoSuchCharacteristic) - } else if check( - "com/nonpolynomial/btleplug/android/impl/NoBluetoothAdapterException", - )? { + } else if check_exception!(JNoBluetoothAdapterException, env, &cause) { Err(Error::NoAdapterAvailable) } else if env.is_instance_of(&cause, jni_str!("java/lang/RuntimeException"))? { let msg = env From 70b0ca4489c5c9d82d4bb65cb14c1f3424921ed6 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 23:03:32 -0700 Subject: [PATCH 16/77] fix: Fix Android cross-compilation issues with bind_java_type! macros - Use string literal syntax for Java class paths containing "impl" keyword (bind_java_type! parses dot-separated idents, and "impl" is reserved in Rust) - Use block syntax for methods with name overrides (inline sig + block is not supported, must use { name = "...", sig = (...) -> T } form) - Fix borrow checker issues in JPeripheral wrapper methods by splitting raw call and cast_local into separate statements - Fix error type mismatches in with_obj and notification stream by unifying on crate::Result Co-Authored-By: Claude Opus 4.6 --- src/droidplug/jni/mod.rs | 8 +-- src/droidplug/jni/objects.rs | 109 ++++++++++++++++------------------- src/droidplug/peripheral.rs | 39 ++++++------- 3 files changed, 73 insertions(+), 83 deletions(-) diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index 27531fc4..32a10f08 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -101,8 +101,8 @@ fn adapter_report_scan_result<'local>( obj: JObject<'local>, scan_result: JObject<'local>, ) -> jni::errors::Result<()> { - super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result) - .map_err(|e| jni::errors::Error::Other(Box::new(e))) + let _ = super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result); + Ok(()) } fn adapter_on_connection_state_changed<'local>( @@ -111,6 +111,6 @@ fn adapter_on_connection_state_changed<'local>( addr: JString<'local>, connected: jboolean, ) -> jni::errors::Result<()> { - super::adapter::adapter_on_connection_state_changed_internal(env, &obj, addr, connected) - .map_err(|e| jni::errors::Error::Other(Box::new(e))) + let _ = super::adapter::adapter_on_connection_state_changed_internal(env, &obj, addr, connected); + Ok(()) } diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 74509fa3..6dcc3368 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -3,7 +3,7 @@ use jni::{ Env, bind_java_type, errors::Result, - jni_sig, + jni_sig, jni_str, objects::{JObject, JString}, sys::jint, }; @@ -13,56 +13,58 @@ use uuid::Uuid; use crate::api::{BDAddr, CharPropFlags, PeripheralProperties, ScanFilter}; bind_java_type! { - pub JNotConnectedException => com.nonpolynomial.btleplug.android.impl.NotConnectedException, + pub JNotConnectedException => "com.nonpolynomial.btleplug.android.impl.NotConnectedException", } bind_java_type! { - pub JPermissionDeniedException => com.nonpolynomial.btleplug.android.impl.PermissionDeniedException, + pub JPermissionDeniedException => "com.nonpolynomial.btleplug.android.impl.PermissionDeniedException", } bind_java_type! { - pub JUnexpectedCallbackException => com.nonpolynomial.btleplug.android.impl.UnexpectedCallbackException, + pub JUnexpectedCallbackException => "com.nonpolynomial.btleplug.android.impl.UnexpectedCallbackException", } bind_java_type! { - pub JUnexpectedCharacteristicException => com.nonpolynomial.btleplug.android.impl.UnexpectedCharacteristicException, + pub JUnexpectedCharacteristicException => "com.nonpolynomial.btleplug.android.impl.UnexpectedCharacteristicException", } bind_java_type! { - pub JNoSuchCharacteristicException => com.nonpolynomial.btleplug.android.impl.NoSuchCharacteristicException, + pub JNoSuchCharacteristicException => "com.nonpolynomial.btleplug.android.impl.NoSuchCharacteristicException", } bind_java_type! { - pub JNoBluetoothAdapterException => com.nonpolynomial.btleplug.android.impl.NoBluetoothAdapterException, + pub JNoBluetoothAdapterException => "com.nonpolynomial.btleplug.android.impl.NoBluetoothAdapterException", } bind_java_type! { - pub JScanFilterClass => com.nonpolynomial.btleplug.android.impl.ScanFilter, + pub JScanFilterClass => "com.nonpolynomial.btleplug.android.impl.ScanFilter", } bind_java_type! { - pub JPeripheral => com.nonpolynomial.btleplug.android.impl.Peripheral, + pub JPeripheral => "com.nonpolynomial.btleplug.android.impl.Peripheral", constructors { fn with_adapter(adapter: JObject, address: JString), }, methods { - priv fn connect_raw() -> JObject { name = "connect" }, - priv fn disconnect_raw() -> JObject { name = "disconnect" }, + priv fn connect_raw { name = "connect", sig = () -> JObject }, + priv fn disconnect_raw { name = "disconnect", sig = () -> JObject }, fn is_connected() -> jboolean, - priv fn discover_services_raw() -> JObject { name = "discoverServices" }, - priv fn read_raw(uuid: JObject) -> JObject { name = "read" }, - priv fn write_raw(uuid: JObject, data: JObject, write_type: jint) -> JObject { name = "write" }, - priv fn set_characteristic_notification_raw(uuid: JObject, enable: jboolean) -> JObject { + priv fn discover_services_raw { name = "discoverServices", sig = () -> JObject }, + priv fn read_raw { name = "read", sig = (uuid: JObject) -> JObject }, + priv fn write_raw { name = "write", sig = (uuid: JObject, data: JObject, write_type: jint) -> JObject }, + priv fn set_characteristic_notification_raw { name = "setCharacteristicNotification", + sig = (uuid: JObject, enable: jboolean) -> JObject, }, - priv fn get_notifications_raw() -> JObject { name = "getNotifications" }, - priv fn read_descriptor_raw(characteristic: JObject, uuid: JObject) -> JObject { name = "readDescriptor" }, - priv fn write_descriptor_raw(characteristic: JObject, uuid: JObject, data: JObject) -> JObject { + priv fn get_notifications_raw { name = "getNotifications", sig = () -> JObject }, + priv fn read_descriptor_raw { name = "readDescriptor", sig = (characteristic: JObject, uuid: JObject) -> JObject }, + priv fn write_descriptor_raw { name = "writeDescriptor", + sig = (characteristic: JObject, uuid: JObject, data: JObject) -> JObject, }, - priv fn get_device_name_raw() -> JObject { name = "getDeviceName" }, + priv fn get_device_name_raw { name = "getDeviceName", sig = () -> JObject }, fn request_mtu(mtu: jint) -> JObject, - priv fn get_connection_parameters_raw() -> JObject { name = "getConnectionParameters" }, + priv fn get_connection_parameters_raw { name = "getConnectionParameters", sig = () -> JObject }, fn request_connection_priority(priority: jint) -> jboolean, fn read_remote_rssi() -> JObject, }, @@ -77,19 +79,23 @@ impl JPeripheral<'_> { impl<'local> JPeripheral<'local> { pub fn connect(&self, env: &mut Env<'local>) -> Result> { - env.cast_local::(self.connect_raw(env)?) + let raw = self.connect_raw(env)?; + env.cast_local::(raw) } pub fn disconnect(&self, env: &mut Env<'local>) -> Result> { - env.cast_local::(self.disconnect_raw(env)?) + let raw = self.disconnect_raw(env)?; + env.cast_local::(raw) } pub fn discover_services(&self, env: &mut Env<'local>) -> Result> { - env.cast_local::(self.discover_services_raw(env)?) + let raw = self.discover_services_raw(env)?; + env.cast_local::(raw) } pub fn read(&self, env: &mut Env<'local>, uuid: &JUuid<'local>) -> Result> { - env.cast_local::(self.read_raw(env, uuid)?) + let raw = self.read_raw(env, uuid)?; + env.cast_local::(raw) } pub fn write( @@ -99,7 +105,8 @@ impl<'local> JPeripheral<'local> { data: &JObject<'local>, write_type: jint, ) -> Result> { - env.cast_local::(self.write_raw(env, uuid, data, write_type)?) + let raw = self.write_raw(env, uuid, data, write_type)?; + env.cast_local::(raw) } pub fn set_characteristic_notification( @@ -108,11 +115,13 @@ impl<'local> JPeripheral<'local> { uuid: &JUuid<'local>, enable: bool, ) -> Result> { - env.cast_local::(self.set_characteristic_notification_raw(env, uuid, enable)?) + let raw = self.set_characteristic_notification_raw(env, uuid, enable)?; + env.cast_local::(raw) } pub fn get_notifications(&self, env: &mut Env<'local>) -> Result> { - env.cast_local::(self.get_notifications_raw(env)?) + let raw = self.get_notifications_raw(env)?; + env.cast_local::(raw) } pub fn read_descriptor( @@ -121,7 +130,8 @@ impl<'local> JPeripheral<'local> { characteristic: &JUuid<'local>, uuid: &JUuid<'local>, ) -> Result> { - env.cast_local::(self.read_descriptor_raw(env, characteristic, uuid)?) + let raw = self.read_descriptor_raw(env, characteristic, uuid)?; + env.cast_local::(raw) } pub fn write_descriptor( @@ -131,7 +141,8 @@ impl<'local> JPeripheral<'local> { uuid: &JUuid<'local>, data: &JObject<'local>, ) -> Result> { - env.cast_local::(self.write_descriptor_raw(env, characteristic, uuid, data)?) + let raw = self.write_descriptor_raw(env, characteristic, uuid, data)?; + env.cast_local::(raw) } pub fn get_device_name(&self, env: &mut Env<'local>) -> Result> { @@ -171,12 +182,8 @@ impl<'local> JPeripheral<'local> { bind_java_type! { pub JBluetoothGattService => android.bluetooth.BluetoothGattService, methods { - fn get_uuid_obj() -> JObject { - name = "getUuid", - }, - fn get_characteristics_obj() -> JObject { - name = "getCharacteristics", - }, + fn get_uuid_obj { name = "getUuid", sig = () -> JObject }, + fn get_characteristics_obj { name = "getCharacteristics", sig = () -> JObject }, }, } @@ -211,18 +218,10 @@ impl<'local> JBluetoothGattService<'local> { bind_java_type! { pub JBluetoothGattCharacteristic => android.bluetooth.BluetoothGattCharacteristic, methods { - fn get_uuid_obj() -> JObject { - name = "getUuid", - }, - fn get_properties_raw() -> jint { - name = "getProperties", - }, - fn get_value_obj() -> JObject { - name = "getValue", - }, - fn get_descriptors_obj() -> JObject { - name = "getDescriptors", - }, + fn get_uuid_obj { name = "getUuid", sig = () -> JObject }, + fn get_properties_raw { name = "getProperties", sig = () -> jint }, + fn get_value_obj { name = "getValue", sig = () -> JObject }, + fn get_descriptors_obj { name = "getDescriptors", sig = () -> JObject }, }, } @@ -264,9 +263,7 @@ impl<'local> JBluetoothGattCharacteristic<'local> { bind_java_type! { pub JBluetoothGattDescriptor => android.bluetooth.BluetoothGattDescriptor, methods { - fn get_uuid_obj() -> JObject { - name = "getUuid", - }, + fn get_uuid_obj { name = "getUuid", sig = () -> JObject }, }, } @@ -322,12 +319,8 @@ impl<'a> From> for JObject<'a> { bind_java_type! { pub JScanResult => android.bluetooth.le.ScanResult, methods { - fn get_device_obj() -> JObject { - name = "getDevice", - }, - fn get_scan_record_obj() -> JObject { - name = "getScanRecord", - }, + fn get_device_obj { name = "getDevice", sig = () -> JObject }, + fn get_scan_record_obj { name = "getScanRecord", sig = () -> JObject }, fn get_tx_power() -> jint, fn get_rssi() -> jint, }, @@ -499,9 +492,7 @@ bind_java_type! { bind_java_type! { pub JParcelUuid => android.os.ParcelUuid, methods { - fn get_uuid_obj() -> JObject { - name = "getUuid", - }, + fn get_uuid_obj { name = "getUuid", sig = () -> JObject }, }, } diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 10806bda..13361b78 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -151,13 +151,10 @@ impl Peripheral { guard.properties = Some(properties); } - fn with_obj( + fn with_obj( &self, - f: impl for<'env> FnOnce(&mut Env<'env>, &JPeripheral<'env>) -> std::result::Result, - ) -> std::result::Result - where - E: From<::jni::errors::Error>, - { + f: impl for<'env> FnOnce(&mut Env<'env>, &JPeripheral<'env>) -> Result, + ) -> Result { jvm()?.attach_current_thread(|env| { let local_obj = env.new_local_ref(self.internal.as_obj())?; let obj = env.cast_local::(local_obj)?; @@ -173,7 +170,7 @@ impl Peripheral { let future = self.with_obj(|env, obj| { let uuid_obj = JUuid::new(env, characteristic.uuid)?; let future = obj.set_characteristic_notification(env, &uuid_obj, enable)?; - JSendFuture::new(env, &future) + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) @@ -218,7 +215,7 @@ impl api::Peripheral for Peripheral { { let future = self.with_obj(|env, obj| { let future = obj.connect(env)?; - JSendFuture::new(env, &future) + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {}))?; @@ -238,7 +235,7 @@ impl api::Peripheral for Peripheral { let mtu_future = self.with_obj(|env, obj| { let mtu_obj = obj.request_mtu(env, 517)?; let mtu_future = env.cast_local::(mtu_obj)?; - JSendFuture::new(env, &mtu_future) + Ok(JSendFuture::new(env, &mtu_future)?) })?; let mtu_result_ref = mtu_future.await?; self.with_obj(|env, _obj| -> Result<()> { @@ -254,7 +251,7 @@ impl api::Peripheral for Peripheral { async fn disconnect(&self) -> Result<()> { let future = self.with_obj(|env, obj| { let future = obj.disconnect(env)?; - JSendFuture::new(env, &future) + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) @@ -268,7 +265,7 @@ impl api::Peripheral for Peripheral { async fn discover_services(&self) -> Result<()> { let future = self.with_obj(|env, obj| { let future = obj.discover_services(env)?; - JSendFuture::new(env, &future) + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| { @@ -337,7 +334,7 @@ impl api::Peripheral for Peripheral { WriteType::WithoutResponse => 1, }; let future = obj.write(env, &uuid, &data_obj.into(), write_type)?; - JSendFuture::new(env, &future) + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) @@ -347,7 +344,7 @@ impl api::Peripheral for Peripheral { let future = self.with_obj(|env, obj| { let uuid = JUuid::new(env, characteristic.uuid)?; let future = obj.read(env, &uuid)?; - JSendFuture::new(env, &future) + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| { @@ -372,12 +369,13 @@ impl api::Peripheral for Peripheral { let shared = self.shared.clone(); let stream = self.with_obj(|env, obj| { let stream = obj.get_notifications(env)?; - JSendStream::new(env, &stream) + Ok(JSendStream::new(env, &stream)?) })?; let stream = stream .map(move |item| match item { Ok(item) => { - jvm()?.attach_current_thread(|env| { + let vm = jvm()?; + let result: crate::Result<_> = vm.attach_current_thread(|env| -> jni::errors::Result<_> { let local_obj = env.new_local_ref(item.as_obj())?; let characteristic = env.cast_local::(local_obj)?; @@ -399,9 +397,10 @@ impl api::Peripheral for Peripheral { service_uuid, value, }) - }) + }).map_err(Into::into); + result } - Err(err) => Err(err), + Err(err) => Err(err.into()), }) .filter_map(|item| async { item.ok() }); Ok(Box::pin(stream)) @@ -411,7 +410,7 @@ impl api::Peripheral for Peripheral { let future = self.with_obj(|env, obj| { let rssi_obj = obj.read_remote_rssi(env)?; let rssi_future = env.cast_local::(rssi_obj)?; - JSendFuture::new(env, &rssi_future) + Ok(JSendFuture::new(env, &rssi_future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| { @@ -427,7 +426,7 @@ impl api::Peripheral for Peripheral { let uuid = JUuid::new(env, descriptor.uuid)?; let data_obj = super::jni_utils::arrays::slice_to_byte_array(env, data)?; let future = obj.write_descriptor(env, &characteristic, &uuid, &data_obj.into())?; - JSendFuture::new(env, &future) + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| get_poll_result(env, &result_ref).map(|_| {})) @@ -438,7 +437,7 @@ impl api::Peripheral for Peripheral { let characteristic = JUuid::new(env, descriptor.characteristic_uuid)?; let uuid = JUuid::new(env, descriptor.uuid)?; let future = obj.read_descriptor(env, &characteristic, &uuid)?; - JSendFuture::new(env, &future) + Ok(JSendFuture::new(env, &future)?) })?; let result_ref = future.await?; self.with_obj(|env, _obj| { From 1194685868a7775a4582db0e20254af5af417efc Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 26 Apr 2026 23:47:08 -0700 Subject: [PATCH 17/77] fix: Fix JNI signature mismatches caused by bind_java_type! JObject mapping bind_java_type! maps JObject to Ljava/lang/Object; in JNI signatures, but Java methods using domain-specific types (UUID, Future, Stream, ScanResult, byte[], List, Map, etc.) require exact signature matches. This caused runtime "Method not found" errors and a SIGSEGV in the scan callback. Changes: - objects.rs: Move all methods with domain-typed params/returns out of bind_java_type! into manual env.call_method() with correct JNI signatures. Keep bind_java_type! for class definitions and primitive-only methods. - future.rs: Move JFuture::poll to manual impl with correct Waker/PollResult sigs - stream.rs: Move JStream::poll_next to manual impl with correct Waker/PollResult sigs - mod.rs: Fix reportScanResult to use extern "C" + EnvUnowned + with_env for raw JNI ABI compatibility (from_raw_parts requires raw C calling convention). Add env.get_java_vm() to seed JavaVM singleton during init. - peripheral_finder.rs: Add catch_unwind and logging to adapter background thread for Android debugging (silent thread death caused confusing RecvError) Verified: 28/29 Android integration tests pass on Pixel 9a. The single failure (testPropertiesContainPeripheralInfo TX power) is a test-peripheral issue. Co-Authored-By: Claude Opus 4.6 --- src/droidplug/jni/mod.rs | 29 +++--- src/droidplug/jni/objects.rs | 159 ++++++++++++++++++------------ src/droidplug/jni_utils/future.rs | 15 ++- src/droidplug/jni_utils/stream.rs | 15 ++- tests/common/peripheral_finder.rs | 48 +++++---- 5 files changed, 167 insertions(+), 99 deletions(-) diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index 32a10f08..3046b39c 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,6 +1,6 @@ pub mod objects; -use ::jni::{Env, NativeMethod, jni_str, native_method, objects::{JObject, Reference}}; +use ::jni::{Env, EnvUnowned, NativeMethod, jni_str, native_method, objects::{JObject, Reference}}; use jni::{objects::JString, sys::jboolean}; use std::ffi::c_void; use std::sync::Once; @@ -18,17 +18,21 @@ pub fn init(env: &mut Env) -> crate::Result<()> { } fn init_inner(env: &mut Env) -> crate::Result<()> { + // Seed the JavaVM singleton so JavaVM::singleton() works from any thread. + env.get_java_vm()?; { let adapter_class = env.find_class(jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"))?; unsafe { env.register_native_methods( &adapter_class, &[ - native_method! { - name = "reportScanResult", - sig = (scan_result: JObject) -> (), - fn = adapter_report_scan_result, - }, + // Can't use native_method! here — JObject maps to Ljava/lang/Object; but the + // Java side declares the parameter as ScanResult. JNI requires exact signature match. + NativeMethod::from_raw_parts( + jni_str!("reportScanResult"), + jni_str!("(Landroid/bluetooth/le/ScanResult;)V"), + adapter_report_scan_result as *mut c_void, + ), native_method! { name = "onConnectionStateChanged", sig = (addr: JString, connected: jboolean) -> (), @@ -96,13 +100,16 @@ impl From<::jni::errors::Error> for crate::Error { } } -fn adapter_report_scan_result<'local>( - env: &mut Env<'local>, +extern "C" fn adapter_report_scan_result<'local>( + mut env: EnvUnowned<'local>, obj: JObject<'local>, scan_result: JObject<'local>, -) -> jni::errors::Result<()> { - let _ = super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result); - Ok(()) +) { + let outcome = env.with_env(|env| { + let _ = super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result); + Ok::<_, jni::errors::Error>(()) + }); + let _ = outcome.into_outcome(); } fn adapter_on_connection_state_changed<'local>( diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 6dcc3368..3b53cfbe 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -4,7 +4,7 @@ use jni::{ bind_java_type, errors::Result, jni_sig, jni_str, - objects::{JObject, JString}, + objects::{JObject, JString, Reference}, sys::jint, }; use std::{collections::HashMap, iter::Iterator}; @@ -40,61 +40,53 @@ bind_java_type! { pub JScanFilterClass => "com.nonpolynomial.btleplug.android.impl.ScanFilter", } +// JPeripheral: bind_java_type! for class definition only. Methods use domain-specific +// Java types (UUID, Future, Stream, byte[]) whose JNI signatures can't be expressed +// through the macro's Rust-to-JNI type mapping (JObject → Ljava/lang/Object; is wrong). bind_java_type! { pub JPeripheral => "com.nonpolynomial.btleplug.android.impl.Peripheral", - constructors { - fn with_adapter(adapter: JObject, address: JString), - }, methods { - priv fn connect_raw { name = "connect", sig = () -> JObject }, - priv fn disconnect_raw { name = "disconnect", sig = () -> JObject }, fn is_connected() -> jboolean, - priv fn discover_services_raw { name = "discoverServices", sig = () -> JObject }, - priv fn read_raw { name = "read", sig = (uuid: JObject) -> JObject }, - priv fn write_raw { name = "write", sig = (uuid: JObject, data: JObject, write_type: jint) -> JObject }, - priv fn set_characteristic_notification_raw { - name = "setCharacteristicNotification", - sig = (uuid: JObject, enable: jboolean) -> JObject, - }, - priv fn get_notifications_raw { name = "getNotifications", sig = () -> JObject }, - priv fn read_descriptor_raw { name = "readDescriptor", sig = (characteristic: JObject, uuid: JObject) -> JObject }, - priv fn write_descriptor_raw { - name = "writeDescriptor", - sig = (characteristic: JObject, uuid: JObject, data: JObject) -> JObject, - }, - priv fn get_device_name_raw { name = "getDeviceName", sig = () -> JObject }, - fn request_mtu(mtu: jint) -> JObject, - priv fn get_connection_parameters_raw { name = "getConnectionParameters", sig = () -> JObject }, fn request_connection_priority(priority: jint) -> jboolean, - fn read_remote_rssi() -> JObject, }, } impl JPeripheral<'_> { pub fn create<'local>(env: &mut Env<'local>, adapter: JObject<'local>, addr: BDAddr) -> Result> { let addr_jstr = env.new_string(format!("{:X}", addr))?; - JPeripheral::with_adapter(env, &adapter, &addr_jstr) + let class = JPeripheral::lookup_class(env, &Default::default())?; + let obj = env.new_object( + &*class, + jni_sig!("(Lcom/nonpolynomial/btleplug/android/impl/Adapter;Ljava/lang/String;)V"), + &[(&adapter).into(), (&addr_jstr).into()], + )?; + env.cast_local::(obj) } } impl<'local> JPeripheral<'local> { pub fn connect(&self, env: &mut Env<'local>) -> Result> { - let raw = self.connect_raw(env)?; + let raw = env.call_method(self, jni_str!("connect"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[])?.l()?; env.cast_local::(raw) } pub fn disconnect(&self, env: &mut Env<'local>) -> Result> { - let raw = self.disconnect_raw(env)?; + let raw = env.call_method(self, jni_str!("disconnect"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[])?.l()?; env.cast_local::(raw) } pub fn discover_services(&self, env: &mut Env<'local>) -> Result> { - let raw = self.discover_services_raw(env)?; + let raw = env.call_method(self, jni_str!("discoverServices"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[])?.l()?; env.cast_local::(raw) } pub fn read(&self, env: &mut Env<'local>, uuid: &JUuid<'local>) -> Result> { - let raw = self.read_raw(env, uuid)?; + let raw = env.call_method(self, jni_str!("read"), + jni_sig!("(Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;"), + &[uuid.into()])?.l()?; env.cast_local::(raw) } @@ -105,7 +97,9 @@ impl<'local> JPeripheral<'local> { data: &JObject<'local>, write_type: jint, ) -> Result> { - let raw = self.write_raw(env, uuid, data, write_type)?; + let raw = env.call_method(self, jni_str!("write"), + jni_sig!("(Ljava/util/UUID;[BI)Lio/github/gedgygedgy/rust/future/Future;"), + &[uuid.into(), data.into(), write_type.into()])?.l()?; env.cast_local::(raw) } @@ -115,12 +109,15 @@ impl<'local> JPeripheral<'local> { uuid: &JUuid<'local>, enable: bool, ) -> Result> { - let raw = self.set_characteristic_notification_raw(env, uuid, enable)?; + let raw = env.call_method(self, jni_str!("setCharacteristicNotification"), + jni_sig!("(Ljava/util/UUID;Z)Lio/github/gedgygedgy/rust/future/Future;"), + &[uuid.into(), enable.into()])?.l()?; env.cast_local::(raw) } pub fn get_notifications(&self, env: &mut Env<'local>) -> Result> { - let raw = self.get_notifications_raw(env)?; + let raw = env.call_method(self, jni_str!("getNotifications"), + jni_sig!("()Lio/github/gedgygedgy/rust/stream/Stream;"), &[])?.l()?; env.cast_local::(raw) } @@ -130,7 +127,9 @@ impl<'local> JPeripheral<'local> { characteristic: &JUuid<'local>, uuid: &JUuid<'local>, ) -> Result> { - let raw = self.read_descriptor_raw(env, characteristic, uuid)?; + let raw = env.call_method(self, jni_str!("readDescriptor"), + jni_sig!("(Ljava/util/UUID;Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;"), + &[characteristic.into(), uuid.into()])?.l()?; env.cast_local::(raw) } @@ -141,12 +140,15 @@ impl<'local> JPeripheral<'local> { uuid: &JUuid<'local>, data: &JObject<'local>, ) -> Result> { - let raw = self.write_descriptor_raw(env, characteristic, uuid, data)?; + let raw = env.call_method(self, jni_str!("writeDescriptor"), + jni_sig!("(Ljava/util/UUID;Ljava/util/UUID;[B)Lio/github/gedgygedgy/rust/future/Future;"), + &[characteristic.into(), uuid.into(), data.into()])?.l()?; env.cast_local::(raw) } pub fn get_device_name(&self, env: &mut Env<'local>) -> Result> { - let obj = self.get_device_name_raw(env)?; + let obj = env.call_method(self, jni_str!("getDeviceName"), + jni_sig!("()Ljava/lang/String;"), &[])?.l()?; if obj.is_null() { Ok(None) } else { @@ -156,11 +158,19 @@ impl<'local> JPeripheral<'local> { } } + pub fn request_mtu(&self, env: &mut Env<'local>, mtu: jint) -> Result> { + let raw = env.call_method(self, jni_str!("requestMtu"), + jni_sig!("(I)Lio/github/gedgygedgy/rust/future/Future;"), + &[mtu.into()])?.l()?; + env.cast_local::(raw) + } + pub fn get_connection_parameters( &self, env: &mut Env<'local>, ) -> Result> { - let obj = self.get_connection_parameters_raw(env)?; + let obj = env.call_method(self, jni_str!("getConnectionParameters"), + jni_sig!("()[I"), &[])?.l()?; if obj.is_null() { return Ok(None); } @@ -177,14 +187,19 @@ impl<'local> JPeripheral<'local> { supervision_timeout_us: (buf[2] as u32) * 10_000, })) } + + pub fn read_remote_rssi(&self, env: &mut Env<'local>) -> Result> { + let raw = env.call_method(self, jni_str!("readRemoteRssi"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[])?.l()?; + env.cast_local::(raw) + } } +// Android SDK types: class definition only, methods use manual JNI signatures +// because return types (UUID, List, byte[]) don't map to JObject. + bind_java_type! { pub JBluetoothGattService => android.bluetooth.BluetoothGattService, - methods { - fn get_uuid_obj { name = "getUuid", sig = () -> JObject }, - fn get_characteristics_obj { name = "getCharacteristics", sig = () -> JObject }, - }, } impl<'local> JBluetoothGattService<'local> { @@ -193,7 +208,8 @@ impl<'local> JBluetoothGattService<'local> { } pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { - let obj = self.get_uuid_obj(env)?; + let obj = env.call_method(self, jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), &[])?.l()?; let uuid_obj = env.cast_local::(obj)?; uuid_obj.as_uuid(env) } @@ -202,7 +218,8 @@ impl<'local> JBluetoothGattService<'local> { &self, env: &mut Env<'local>, ) -> Result>> { - let obj = self.get_characteristics_obj(env)?; + let obj = env.call_method(self, jni_str!("getCharacteristics"), + jni_sig!("()Ljava/util/List;"), &[])?.l()?; let size = env.call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])?.i()?; let mut chr_vec = Vec::with_capacity(size as usize); for i in 0..size { @@ -218,16 +235,14 @@ impl<'local> JBluetoothGattService<'local> { bind_java_type! { pub JBluetoothGattCharacteristic => android.bluetooth.BluetoothGattCharacteristic, methods { - fn get_uuid_obj { name = "getUuid", sig = () -> JObject }, fn get_properties_raw { name = "getProperties", sig = () -> jint }, - fn get_value_obj { name = "getValue", sig = () -> JObject }, - fn get_descriptors_obj { name = "getDescriptors", sig = () -> JObject }, }, } impl<'local> JBluetoothGattCharacteristic<'local> { pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { - let obj = self.get_uuid_obj(env)?; + let obj = env.call_method(self, jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), &[])?.l()?; let uuid_obj = env.cast_local::(obj)?; uuid_obj.as_uuid(env) } @@ -238,7 +253,8 @@ impl<'local> JBluetoothGattCharacteristic<'local> { } pub fn get_value(&self, env: &mut Env<'local>) -> Result> { - let value = self.get_value_obj(env)?; + let value = env.call_method(self, jni_str!("getValue"), + jni_sig!("()[B"), &[])?.l()?; let value_arr = unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value_arr) } @@ -247,7 +263,8 @@ impl<'local> JBluetoothGattCharacteristic<'local> { &self, env: &mut Env<'local>, ) -> Result>> { - let obj = self.get_descriptors_obj(env)?; + let obj = env.call_method(self, jni_str!("getDescriptors"), + jni_sig!("()Ljava/util/List;"), &[])?.l()?; let size = env.call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])?.i()?; let mut desc_vec = Vec::with_capacity(size as usize); for i in 0..size { @@ -262,14 +279,12 @@ impl<'local> JBluetoothGattCharacteristic<'local> { bind_java_type! { pub JBluetoothGattDescriptor => android.bluetooth.BluetoothGattDescriptor, - methods { - fn get_uuid_obj { name = "getUuid", sig = () -> JObject }, - }, } impl<'local> JBluetoothGattDescriptor<'local> { pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { - let obj = self.get_uuid_obj(env)?; + let obj = env.call_method(self, jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), &[])?.l()?; let uuid_obj = env.cast_local::(obj)?; uuid_obj.as_uuid(env) } @@ -297,7 +312,7 @@ impl<'a> JScanFilter<'a> { let uuid_str = env.new_string(uuid.to_string())?; uuids.set_element(env, idx, &uuid_str)?; } - let class = ::lookup_class( + let class = ::lookup_class( env, &Default::default(), )?; @@ -319,8 +334,6 @@ impl<'a> From> for JObject<'a> { bind_java_type! { pub JScanResult => android.bluetooth.le.ScanResult, methods { - fn get_device_obj { name = "getDevice", sig = () -> JObject }, - fn get_scan_record_obj { name = "getScanRecord", sig = () -> JObject }, fn get_tx_power() -> jint, fn get_rssi() -> jint, }, @@ -328,12 +341,14 @@ bind_java_type! { impl<'local> JScanResult<'local> { pub fn get_device(&self, env: &mut Env<'local>) -> Result> { - let obj = self.get_device_obj(env)?; + let obj = env.call_method(self, jni_str!("getDevice"), + jni_sig!("()Landroid/bluetooth/BluetoothDevice;"), &[])?.l()?; env.cast_local::(obj) } pub fn get_scan_record(&self, env: &mut Env<'local>) -> Result> { - self.get_scan_record_obj(env) + env.call_method(self, jni_str!("getScanRecord"), + jni_sig!("()Landroid/bluetooth/le/ScanRecord;"), &[])?.l() } pub fn to_peripheral_properties( @@ -472,14 +487,32 @@ impl<'local> JScanResult<'local> { bind_java_type! { pub JScanRecord => android.bluetooth.le.ScanRecord, methods { - fn get_device_name() -> JObject, fn get_tx_power_level() -> jint, - fn get_manufacturer_specific_data() -> JObject, - fn get_service_data() -> JObject, - fn get_service_uuids() -> JObject, }, } +impl<'local> JScanRecord<'local> { + pub fn get_device_name(&self, env: &mut Env<'local>) -> Result> { + env.call_method(self, jni_str!("getDeviceName"), + jni_sig!("()Ljava/lang/String;"), &[])?.l() + } + + pub fn get_manufacturer_specific_data(&self, env: &mut Env<'local>) -> Result> { + env.call_method(self, jni_str!("getManufacturerSpecificData"), + jni_sig!("()Landroid/util/SparseArray;"), &[])?.l() + } + + pub fn get_service_data(&self, env: &mut Env<'local>) -> Result> { + env.call_method(self, jni_str!("getServiceData"), + jni_sig!("()Ljava/util/Map;"), &[])?.l() + } + + pub fn get_service_uuids(&self, env: &mut Env<'local>) -> Result> { + env.call_method(self, jni_str!("getServiceUuids"), + jni_sig!("()Ljava/util/List;"), &[])?.l() + } +} + bind_java_type! { pub JSparseArray => android.util.SparseArray, methods { @@ -491,14 +524,12 @@ bind_java_type! { bind_java_type! { pub JParcelUuid => android.os.ParcelUuid, - methods { - fn get_uuid_obj { name = "getUuid", sig = () -> JObject }, - }, } impl<'local> JParcelUuid<'local> { pub fn get_uuid(&self, env: &mut Env<'local>) -> Result> { - let obj = self.get_uuid_obj(env)?; + let obj = env.call_method(self, jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), &[])?.l()?; env.cast_local::(obj) } } diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 3bba3720..5abcd5f8 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -2,6 +2,7 @@ use ::jni::{ Env, JavaVM, bind_java_type, errors::Result, + jni_sig, jni_str, objects::{Global, JObject}, }; use static_assertions::assert_impl_all; @@ -13,9 +14,17 @@ use std::{ bind_java_type! { pub JFuture => io.github.gedgygedgy.rust.future.Future, - methods { - fn poll(waker: JObject) -> JObject, - }, +} + +impl<'local> JFuture<'local> { + pub fn poll(&self, env: &mut Env<'local>, waker: &JObject<'local>) -> Result> { + env.call_method( + self, + jni_str!("poll"), + jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), + &[waker.into()], + )?.l() + } } bind_java_type! { diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index fa424bbc..512918d9 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -3,6 +3,7 @@ use ::jni::{ Env, JavaVM, bind_java_type, errors::Result, + jni_sig, jni_str, objects::{Global, JObject}, }; use futures::stream::Stream; @@ -14,9 +15,17 @@ use std::{ bind_java_type! { pub JStream => io.github.gedgygedgy.rust.stream.Stream, - methods { - fn poll_next(waker: JObject) -> JObject, - }, +} + +impl<'local> JStream<'local> { + pub fn poll_next(&self, env: &mut Env<'local>, waker: &JObject<'local>) -> Result> { + env.call_method( + self, + jni_str!("pollNext"), + jni_sig!("(Lio/github/gedgygedgy/rust/task/Waker;)Lio/github/gedgygedgy/rust/task/PollResult;"), + &[waker.into()], + )?.l() + } } bind_java_type! { diff --git a/tests/common/peripheral_finder.rs b/tests/common/peripheral_finder.rs index 3806c3cb..9d54f224 100644 --- a/tests/common/peripheral_finder.rs +++ b/tests/common/peripheral_finder.rs @@ -33,24 +33,36 @@ pub async fn get_adapter() -> &'static Adapter { std::thread::Builder::new() .name("btleplug-test-adapter".into()) .spawn(move || { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("failed to create adapter runtime"); - rt.block_on(async { - let manager = Manager::new().await.expect("failed to create BLE manager"); - let adapters = manager.adapters().await.expect("failed to get adapters"); - // Leak the manager so it (and the underlying CBCentralManager) - // lives forever. OnceCell keeps the Adapter alive; we need the - // Manager alive too since the Adapter borrows from it internally - // on some platforms. - std::mem::forget(manager); - let adapter = adapters.into_iter().next().expect("no BLE adapters found"); - tx.send(adapter).ok(); - // Block forever so the runtime (and its spawned event loop) - // stays alive. - std::future::pending::<()>().await; - }); + log::info!("adapter thread: starting"); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("failed to create adapter runtime"); + rt.block_on(async { + log::info!("adapter thread: creating manager"); + let manager = Manager::new().await.expect("failed to create BLE manager"); + log::info!("adapter thread: getting adapters"); + let adapters = manager.adapters().await.expect("failed to get adapters"); + log::info!("adapter thread: got {} adapters", adapters.len()); + std::mem::forget(manager); + let adapter = adapters.into_iter().next().expect("no BLE adapters found"); + log::info!("adapter thread: sending adapter"); + tx.send(adapter).ok(); + log::info!("adapter thread: blocking forever"); + std::future::pending::<()>().await; + }); + })); + if let Err(panic) = result { + let msg = if let Some(s) = panic.downcast_ref::<&str>() { + s.to_string() + } else if let Some(s) = panic.downcast_ref::() { + s.clone() + } else { + "unknown panic".to_string() + }; + log::error!("adapter thread PANICKED: {}", msg); + } }) .expect("failed to spawn adapter thread"); rx.await From 9e8a10d12574410569e8615e65f68fa23d7b26b6 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Fri, 22 May 2026 17:03:04 -0700 Subject: [PATCH 18/77] fix: Propagate JNI callback failures instead of silently discarding them Replace the deprecated `into_outcome()` pattern with jni-rs 0.22's `resolve::()` for scan result reporting, FnAdapter call/close callbacks, and test initialization. In `adapter_on_connection_state_changed`, errors were previously swallowed with `let _ =`. Now Rust-side errors are thrown as Java RuntimeExceptions when no JNI exception is already pending. --- src/droidplug/jni/mod.rs | 16 ++++++++++------ src/droidplug/jni_utils/ops.rs | 18 ++++++++---------- tests/android/rust/src/lib.rs | 14 ++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index 3046b39c..eb3ea6e2 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,6 +1,7 @@ pub mod objects; use ::jni::{Env, EnvUnowned, NativeMethod, jni_str, native_method, objects::{JObject, Reference}}; +use ::jni::errors::ThrowRuntimeExAndDefault; use jni::{objects::JString, sys::jboolean}; use std::ffi::c_void; use std::sync::Once; @@ -105,11 +106,8 @@ extern "C" fn adapter_report_scan_result<'local>( obj: JObject<'local>, scan_result: JObject<'local>, ) { - let outcome = env.with_env(|env| { - let _ = super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result); - Ok::<_, jni::errors::Error>(()) - }); - let _ = outcome.into_outcome(); + env.with_env(|env| super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result)) + .resolve::(); } fn adapter_on_connection_state_changed<'local>( @@ -118,6 +116,12 @@ fn adapter_on_connection_state_changed<'local>( addr: JString<'local>, connected: jboolean, ) -> jni::errors::Result<()> { - let _ = super::adapter::adapter_on_connection_state_changed_internal(env, &obj, addr, connected); + if let Err(e) = + super::adapter::adapter_on_connection_state_changed_internal(env, &obj, addr, connected) + { + if !env.exception_check() { + let _ = env.throw(format!("Rust error: {e}")); + } + } Ok(()) } diff --git a/src/droidplug/jni_utils/ops.rs b/src/droidplug/jni_utils/ops.rs index 7ed0db1a..6e149330 100644 --- a/src/droidplug/jni_utils/ops.rs +++ b/src/droidplug/jni_utils/ops.rs @@ -5,6 +5,7 @@ use ::jni::{ jni_sig, jni_str, objects::{JObject, Reference}, }; +use ::jni::errors::ThrowRuntimeExAndDefault; use std::sync::{Arc, Mutex}; bind_java_type! { @@ -313,7 +314,7 @@ pub(crate) extern "C" fn fn_adapter_call_internal<'local>( ) -> JObject<'local> { use std::panic::{AssertUnwindSafe, catch_unwind}; - let outcome = env.with_env(|env| -> std::result::Result, jni::errors::Error> { + env.with_env(|env| -> std::result::Result, jni::errors::Error> { let arc = if let Ok(f) = unsafe { env.get_rust_field::<_, _, FnWrapper>(&obj1, jni_str!("data")) } { AssertUnwindSafe(f.0.clone()) @@ -327,24 +328,21 @@ pub(crate) extern "C" fn fn_adapter_call_internal<'local>( Ok(JObject::null()) } } - }); - match outcome.into_outcome() { - jni::Outcome::Ok(obj) => obj, - _ => JObject::null(), - } + }) + .resolve::() } pub(crate) extern "C" fn fn_adapter_close_internal(mut env: EnvUnowned, obj: JObject) { use std::panic::{AssertUnwindSafe, catch_unwind}; - let outcome = env.with_env(|env| { + env.with_env(|env| { let result = catch_unwind(AssertUnwindSafe(|| { let _ = unsafe { env.take_rust_field::<_, _, FnWrapper>(&obj, jni_str!("data")) }; })); if let Err(panic) = result { - let _ = super::exceptions::throw_panic(env, panic); + super::exceptions::throw_panic(env, panic)?; } Ok::<(), jni::errors::Error>(()) - }); - let _ = outcome.into_outcome(); + }) + .resolve::(); } diff --git a/tests/android/rust/src/lib.rs b/tests/android/rust/src/lib.rs index a57ef926..3378f7ea 100644 --- a/tests/android/rust/src/lib.rs +++ b/tests/android/rust/src/lib.rs @@ -42,6 +42,7 @@ pub fn find_descriptor( use jni::objects::JClass; use jni::{Env, EnvUnowned, jni_str}; +use jni::errors::ThrowRuntimeExAndDefault; use std::sync::OnceLock; use tokio::runtime::Runtime; @@ -95,11 +96,8 @@ pub extern "system" fn Java_com_nonpolynomial_btleplug_test_NativeTests_initBtle .with_max_level(log::LevelFilter::Debug) .with_tag("btleplug-test"), ); - let outcome = env.with_env(|env| { - btleplug::platform::init(env).expect("failed to initialize btleplug"); - Ok::<_, jni::errors::Error>(()) - }); - let _ = outcome.into_outcome(); + env.with_env(|env| btleplug::platform::init(env)) + .resolve::(); } // ── Test JNI exports ──────────────────────────────────────────────── @@ -111,11 +109,11 @@ macro_rules! jni_test { ($jni_name:ident, $test_fn:path) => { #[unsafe(no_mangle)] pub extern "system" fn $jni_name(mut env: EnvUnowned, _class: JClass) { - let outcome = env.with_env(|env| { + env.with_env(|env| { run_test(env, stringify!($test_fn), $test_fn()); Ok::<_, jni::errors::Error>(()) - }); - let _ = outcome.into_outcome(); + }) + .resolve::(); } }; } From 7dd9823dfcbbdaeb19ed3d64c8213569a23d474a Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 24 May 2026 22:16:48 -0700 Subject: [PATCH 19/77] chore: run rustfmt --- src/droidplug/adapter.rs | 8 +- src/droidplug/jni/mod.rs | 52 +++-- src/droidplug/jni/objects.rs | 324 ++++++++++++++++++++------ src/droidplug/jni_utils/arrays.rs | 21 +- src/droidplug/jni_utils/exceptions.rs | 141 +++++++---- src/droidplug/jni_utils/future.rs | 85 +++++-- src/droidplug/jni_utils/mod.rs | 75 +++--- src/droidplug/jni_utils/ops.rs | 34 ++- src/droidplug/jni_utils/stream.rs | 125 ++++++---- src/droidplug/jni_utils/task.rs | 21 +- src/droidplug/jni_utils/uuid.rs | 35 ++- src/droidplug/peripheral.rs | 101 +++++--- tests/common/peripheral_finder.rs | 9 +- 13 files changed, 704 insertions(+), 327 deletions(-) diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index af98ce09..3ed016e7 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -61,7 +61,6 @@ impl Adapter { &self, env: &mut Env<'a>, scan_result: JObject<'a>, - ) -> Result { let scan_result = env.cast_local::(scan_result)?; let (addr, properties): (BDAddr, Option) = @@ -176,7 +175,12 @@ impl Central for Adapter { async fn stop_scan(&self) -> Result<()> { jvm()?.attach_current_thread(|env| { - env.call_method(self.internal.as_obj(), jni_str!("stopScan"), jni_sig!("()V"), &[])?; + env.call_method( + self.internal.as_obj(), + jni_str!("stopScan"), + jni_sig!("()V"), + &[], + )?; Ok(()) }) } diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index eb3ea6e2..47cd2006 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -1,7 +1,10 @@ pub mod objects; -use ::jni::{Env, EnvUnowned, NativeMethod, jni_str, native_method, objects::{JObject, Reference}}; use ::jni::errors::ThrowRuntimeExAndDefault; +use ::jni::{ + Env, EnvUnowned, NativeMethod, jni_str, native_method, + objects::{JObject, Reference}, +}; use jni::{objects::JString, sys::jboolean}; use std::ffi::c_void; use std::sync::Once; @@ -24,30 +27,32 @@ fn init_inner(env: &mut Env) -> crate::Result<()> { { let adapter_class = env.find_class(jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"))?; - unsafe { env.register_native_methods( - &adapter_class, - &[ - // Can't use native_method! here — JObject maps to Ljava/lang/Object; but the - // Java side declares the parameter as ScanResult. JNI requires exact signature match. - NativeMethod::from_raw_parts( - jni_str!("reportScanResult"), - jni_str!("(Landroid/bluetooth/le/ScanResult;)V"), - adapter_report_scan_result as *mut c_void, - ), - native_method! { - name = "onConnectionStateChanged", - sig = (addr: JString, connected: jboolean) -> (), - fn = adapter_on_connection_state_changed, - }, - ], - )? }; - use objects::*; + unsafe { + env.register_native_methods( + &adapter_class, + &[ + // Can't use native_method! here — JObject maps to Ljava/lang/Object; but the + // Java side declares the parameter as ScanResult. JNI requires exact signature match. + NativeMethod::from_raw_parts( + jni_str!("reportScanResult"), + jni_str!("(Landroid/bluetooth/le/ScanResult;)V"), + adapter_report_scan_result as *mut c_void, + ), + native_method! { + name = "onConnectionStateChanged", + sig = (addr: JString, connected: jboolean) -> (), + fn = adapter_on_connection_state_changed, + }, + ], + )? + }; use super::jni_utils::{ future::{JFuture, JFutureException}, - ops::{JFnAdapter, JFnRunnableImpl, JFnBiFunctionImpl, JFnFunctionImpl}, + ops::{JFnAdapter, JFnBiFunctionImpl, JFnFunctionImpl, JFnRunnableImpl}, stream::{JStream, JStreamPoll}, task::{JPollResult, JWaker}, }; + use objects::*; let loader = jni::objects::LoaderContext::default(); ::lookup_class(env, &loader)?; @@ -71,7 +76,8 @@ fn init_inner(env: &mut Env) -> crate::Result<()> { // FnAdapter native method registration let fn_adapter_class = ::lookup_class(env, &loader)?; - unsafe { env.register_native_methods( + unsafe { + env.register_native_methods( &*fn_adapter_class, &[ NativeMethod::from_raw_parts( @@ -85,8 +91,8 @@ fn init_inner(env: &mut Env) -> crate::Result<()> { super::jni_utils::ops::fn_adapter_close_internal as *mut c_void, ), ], - )? }; - + )? + }; } Ok(()) } diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 3b53cfbe..0f086e5e 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -1,7 +1,6 @@ use crate::droidplug::jni_utils::{future::JFuture, stream::JStream, uuid::JUuid}; use jni::{ - Env, - bind_java_type, + Env, bind_java_type, errors::Result, jni_sig, jni_str, objects::{JObject, JString, Reference}, @@ -52,7 +51,11 @@ bind_java_type! { } impl JPeripheral<'_> { - pub fn create<'local>(env: &mut Env<'local>, adapter: JObject<'local>, addr: BDAddr) -> Result> { + pub fn create<'local>( + env: &mut Env<'local>, + adapter: JObject<'local>, + addr: BDAddr, + ) -> Result> { let addr_jstr = env.new_string(format!("{:X}", addr))?; let class = JPeripheral::lookup_class(env, &Default::default())?; let obj = env.new_object( @@ -66,27 +69,50 @@ impl JPeripheral<'_> { impl<'local> JPeripheral<'local> { pub fn connect(&self, env: &mut Env<'local>) -> Result> { - let raw = env.call_method(self, jni_str!("connect"), - jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[])?.l()?; + let raw = env + .call_method( + self, + jni_str!("connect"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), + &[], + )? + .l()?; env.cast_local::(raw) } pub fn disconnect(&self, env: &mut Env<'local>) -> Result> { - let raw = env.call_method(self, jni_str!("disconnect"), - jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[])?.l()?; + let raw = env + .call_method( + self, + jni_str!("disconnect"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), + &[], + )? + .l()?; env.cast_local::(raw) } pub fn discover_services(&self, env: &mut Env<'local>) -> Result> { - let raw = env.call_method(self, jni_str!("discoverServices"), - jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[])?.l()?; + let raw = env + .call_method( + self, + jni_str!("discoverServices"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), + &[], + )? + .l()?; env.cast_local::(raw) } pub fn read(&self, env: &mut Env<'local>, uuid: &JUuid<'local>) -> Result> { - let raw = env.call_method(self, jni_str!("read"), - jni_sig!("(Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;"), - &[uuid.into()])?.l()?; + let raw = env + .call_method( + self, + jni_str!("read"), + jni_sig!("(Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;"), + &[uuid.into()], + )? + .l()?; env.cast_local::(raw) } @@ -97,9 +123,14 @@ impl<'local> JPeripheral<'local> { data: &JObject<'local>, write_type: jint, ) -> Result> { - let raw = env.call_method(self, jni_str!("write"), - jni_sig!("(Ljava/util/UUID;[BI)Lio/github/gedgygedgy/rust/future/Future;"), - &[uuid.into(), data.into(), write_type.into()])?.l()?; + let raw = env + .call_method( + self, + jni_str!("write"), + jni_sig!("(Ljava/util/UUID;[BI)Lio/github/gedgygedgy/rust/future/Future;"), + &[uuid.into(), data.into(), write_type.into()], + )? + .l()?; env.cast_local::(raw) } @@ -109,15 +140,26 @@ impl<'local> JPeripheral<'local> { uuid: &JUuid<'local>, enable: bool, ) -> Result> { - let raw = env.call_method(self, jni_str!("setCharacteristicNotification"), - jni_sig!("(Ljava/util/UUID;Z)Lio/github/gedgygedgy/rust/future/Future;"), - &[uuid.into(), enable.into()])?.l()?; + let raw = env + .call_method( + self, + jni_str!("setCharacteristicNotification"), + jni_sig!("(Ljava/util/UUID;Z)Lio/github/gedgygedgy/rust/future/Future;"), + &[uuid.into(), enable.into()], + )? + .l()?; env.cast_local::(raw) } pub fn get_notifications(&self, env: &mut Env<'local>) -> Result> { - let raw = env.call_method(self, jni_str!("getNotifications"), - jni_sig!("()Lio/github/gedgygedgy/rust/stream/Stream;"), &[])?.l()?; + let raw = env + .call_method( + self, + jni_str!("getNotifications"), + jni_sig!("()Lio/github/gedgygedgy/rust/stream/Stream;"), + &[], + )? + .l()?; env.cast_local::(raw) } @@ -127,9 +169,16 @@ impl<'local> JPeripheral<'local> { characteristic: &JUuid<'local>, uuid: &JUuid<'local>, ) -> Result> { - let raw = env.call_method(self, jni_str!("readDescriptor"), - jni_sig!("(Ljava/util/UUID;Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;"), - &[characteristic.into(), uuid.into()])?.l()?; + let raw = env + .call_method( + self, + jni_str!("readDescriptor"), + jni_sig!( + "(Ljava/util/UUID;Ljava/util/UUID;)Lio/github/gedgygedgy/rust/future/Future;" + ), + &[characteristic.into(), uuid.into()], + )? + .l()?; env.cast_local::(raw) } @@ -140,15 +189,28 @@ impl<'local> JPeripheral<'local> { uuid: &JUuid<'local>, data: &JObject<'local>, ) -> Result> { - let raw = env.call_method(self, jni_str!("writeDescriptor"), - jni_sig!("(Ljava/util/UUID;Ljava/util/UUID;[B)Lio/github/gedgygedgy/rust/future/Future;"), - &[characteristic.into(), uuid.into(), data.into()])?.l()?; + let raw = env + .call_method( + self, + jni_str!("writeDescriptor"), + jni_sig!( + "(Ljava/util/UUID;Ljava/util/UUID;[B)Lio/github/gedgygedgy/rust/future/Future;" + ), + &[characteristic.into(), uuid.into(), data.into()], + )? + .l()?; env.cast_local::(raw) } pub fn get_device_name(&self, env: &mut Env<'local>) -> Result> { - let obj = env.call_method(self, jni_str!("getDeviceName"), - jni_sig!("()Ljava/lang/String;"), &[])?.l()?; + let obj = env + .call_method( + self, + jni_str!("getDeviceName"), + jni_sig!("()Ljava/lang/String;"), + &[], + )? + .l()?; if obj.is_null() { Ok(None) } else { @@ -159,9 +221,14 @@ impl<'local> JPeripheral<'local> { } pub fn request_mtu(&self, env: &mut Env<'local>, mtu: jint) -> Result> { - let raw = env.call_method(self, jni_str!("requestMtu"), - jni_sig!("(I)Lio/github/gedgygedgy/rust/future/Future;"), - &[mtu.into()])?.l()?; + let raw = env + .call_method( + self, + jni_str!("requestMtu"), + jni_sig!("(I)Lio/github/gedgygedgy/rust/future/Future;"), + &[mtu.into()], + )? + .l()?; env.cast_local::(raw) } @@ -169,8 +236,14 @@ impl<'local> JPeripheral<'local> { &self, env: &mut Env<'local>, ) -> Result> { - let obj = env.call_method(self, jni_str!("getConnectionParameters"), - jni_sig!("()[I"), &[])?.l()?; + let obj = env + .call_method( + self, + jni_str!("getConnectionParameters"), + jni_sig!("()[I"), + &[], + )? + .l()?; if obj.is_null() { return Ok(None); } @@ -189,8 +262,14 @@ impl<'local> JPeripheral<'local> { } pub fn read_remote_rssi(&self, env: &mut Env<'local>) -> Result> { - let raw = env.call_method(self, jni_str!("readRemoteRssi"), - jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), &[])?.l()?; + let raw = env + .call_method( + self, + jni_str!("readRemoteRssi"), + jni_sig!("()Lio/github/gedgygedgy/rust/future/Future;"), + &[], + )? + .l()?; env.cast_local::(raw) } } @@ -208,8 +287,14 @@ impl<'local> JBluetoothGattService<'local> { } pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { - let obj = env.call_method(self, jni_str!("getUuid"), - jni_sig!("()Ljava/util/UUID;"), &[])?.l()?; + let obj = env + .call_method( + self, + jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), + &[], + )? + .l()?; let uuid_obj = env.cast_local::(obj)?; uuid_obj.as_uuid(env) } @@ -218,13 +303,26 @@ impl<'local> JBluetoothGattService<'local> { &self, env: &mut Env<'local>, ) -> Result>> { - let obj = env.call_method(self, jni_str!("getCharacteristics"), - jni_sig!("()Ljava/util/List;"), &[])?.l()?; - let size = env.call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])?.i()?; + let obj = env + .call_method( + self, + jni_str!("getCharacteristics"), + jni_sig!("()Ljava/util/List;"), + &[], + )? + .l()?; + let size = env + .call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])? + .i()?; let mut chr_vec = Vec::with_capacity(size as usize); for i in 0..size { let chr = env - .call_method(&obj, jni_str!("get"), jni_sig!("(I)Ljava/lang/Object;"), &[jni::objects::JValue::from(i)])? + .call_method( + &obj, + jni_str!("get"), + jni_sig!("(I)Ljava/lang/Object;"), + &[jni::objects::JValue::from(i)], + )? .l()?; chr_vec.push(env.cast_local::(chr)?); } @@ -241,8 +339,14 @@ bind_java_type! { impl<'local> JBluetoothGattCharacteristic<'local> { pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { - let obj = env.call_method(self, jni_str!("getUuid"), - jni_sig!("()Ljava/util/UUID;"), &[])?.l()?; + let obj = env + .call_method( + self, + jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), + &[], + )? + .l()?; let uuid_obj = env.cast_local::(obj)?; uuid_obj.as_uuid(env) } @@ -253,8 +357,9 @@ impl<'local> JBluetoothGattCharacteristic<'local> { } pub fn get_value(&self, env: &mut Env<'local>) -> Result> { - let value = env.call_method(self, jni_str!("getValue"), - jni_sig!("()[B"), &[])?.l()?; + let value = env + .call_method(self, jni_str!("getValue"), jni_sig!("()[B"), &[])? + .l()?; let value_arr = unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value_arr) } @@ -263,13 +368,26 @@ impl<'local> JBluetoothGattCharacteristic<'local> { &self, env: &mut Env<'local>, ) -> Result>> { - let obj = env.call_method(self, jni_str!("getDescriptors"), - jni_sig!("()Ljava/util/List;"), &[])?.l()?; - let size = env.call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])?.i()?; + let obj = env + .call_method( + self, + jni_str!("getDescriptors"), + jni_sig!("()Ljava/util/List;"), + &[], + )? + .l()?; + let size = env + .call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])? + .i()?; let mut desc_vec = Vec::with_capacity(size as usize); for i in 0..size { let desc = env - .call_method(&obj, jni_str!("get"), jni_sig!("(I)Ljava/lang/Object;"), &[jni::objects::JValue::from(i)])? + .call_method( + &obj, + jni_str!("get"), + jni_sig!("(I)Ljava/lang/Object;"), + &[jni::objects::JValue::from(i)], + )? .l()?; desc_vec.push(env.cast_local::(desc)?); } @@ -283,8 +401,14 @@ bind_java_type! { impl<'local> JBluetoothGattDescriptor<'local> { pub fn get_uuid(&self, env: &mut Env<'local>) -> Result { - let obj = env.call_method(self, jni_str!("getUuid"), - jni_sig!("()Ljava/util/UUID;"), &[])?.l()?; + let obj = env + .call_method( + self, + jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), + &[], + )? + .l()?; let uuid_obj = env.cast_local::(obj)?; uuid_obj.as_uuid(env) } @@ -312,10 +436,7 @@ impl<'a> JScanFilter<'a> { let uuid_str = env.new_string(uuid.to_string())?; uuids.set_element(env, idx, &uuid_str)?; } - let class = ::lookup_class( - env, - &Default::default(), - )?; + let class = ::lookup_class(env, &Default::default())?; let obj = env.new_object( &*class, jni_sig!("([Ljava/lang/String;)V"), @@ -341,14 +462,25 @@ bind_java_type! { impl<'local> JScanResult<'local> { pub fn get_device(&self, env: &mut Env<'local>) -> Result> { - let obj = env.call_method(self, jni_str!("getDevice"), - jni_sig!("()Landroid/bluetooth/BluetoothDevice;"), &[])?.l()?; + let obj = env + .call_method( + self, + jni_str!("getDevice"), + jni_sig!("()Landroid/bluetooth/BluetoothDevice;"), + &[], + )? + .l()?; env.cast_local::(obj) } pub fn get_scan_record(&self, env: &mut Env<'local>) -> Result> { - env.call_method(self, jni_str!("getScanRecord"), - jni_sig!("()Landroid/bluetooth/le/ScanRecord;"), &[])?.l() + env.call_method( + self, + jni_str!("getScanRecord"), + jni_sig!("()Landroid/bluetooth/le/ScanRecord;"), + &[], + )? + .l() } pub fn to_peripheral_properties( @@ -411,7 +543,12 @@ impl<'local> JScanResult<'local> { let mut service_data = HashMap::new(); if !env.is_same_object(&service_data_obj, JObject::null())? { let entry_set = env - .call_method(&service_data_obj, jni_str!("entrySet"), jni_sig!("()Ljava/util/Set;"), &[])? + .call_method( + &service_data_obj, + jni_str!("entrySet"), + jni_sig!("()Ljava/util/Set;"), + &[], + )? .l()?; let iter_obj = env .call_method( @@ -426,13 +563,28 @@ impl<'local> JScanResult<'local> { .z()? { let entry = env - .call_method(&iter_obj, jni_str!("next"), jni_sig!("()Ljava/lang/Object;"), &[])? + .call_method( + &iter_obj, + jni_str!("next"), + jni_sig!("()Ljava/lang/Object;"), + &[], + )? .l()?; let key = env - .call_method(&entry, jni_str!("getKey"), jni_sig!("()Ljava/lang/Object;"), &[])? + .call_method( + &entry, + jni_str!("getKey"), + jni_sig!("()Ljava/lang/Object;"), + &[], + )? .l()?; let value = env - .call_method(&entry, jni_str!("getValue"), jni_sig!("()Ljava/lang/Object;"), &[])? + .call_method( + &entry, + jni_str!("getValue"), + jni_sig!("()Ljava/lang/Object;"), + &[], + )? .l()?; let parcel_uuid = env.cast_local::(key)?; let juuid = parcel_uuid.get_uuid(env)?; @@ -493,23 +645,43 @@ bind_java_type! { impl<'local> JScanRecord<'local> { pub fn get_device_name(&self, env: &mut Env<'local>) -> Result> { - env.call_method(self, jni_str!("getDeviceName"), - jni_sig!("()Ljava/lang/String;"), &[])?.l() + env.call_method( + self, + jni_str!("getDeviceName"), + jni_sig!("()Ljava/lang/String;"), + &[], + )? + .l() } pub fn get_manufacturer_specific_data(&self, env: &mut Env<'local>) -> Result> { - env.call_method(self, jni_str!("getManufacturerSpecificData"), - jni_sig!("()Landroid/util/SparseArray;"), &[])?.l() + env.call_method( + self, + jni_str!("getManufacturerSpecificData"), + jni_sig!("()Landroid/util/SparseArray;"), + &[], + )? + .l() } pub fn get_service_data(&self, env: &mut Env<'local>) -> Result> { - env.call_method(self, jni_str!("getServiceData"), - jni_sig!("()Ljava/util/Map;"), &[])?.l() + env.call_method( + self, + jni_str!("getServiceData"), + jni_sig!("()Ljava/util/Map;"), + &[], + )? + .l() } pub fn get_service_uuids(&self, env: &mut Env<'local>) -> Result> { - env.call_method(self, jni_str!("getServiceUuids"), - jni_sig!("()Ljava/util/List;"), &[])?.l() + env.call_method( + self, + jni_str!("getServiceUuids"), + jni_sig!("()Ljava/util/List;"), + &[], + )? + .l() } } @@ -528,8 +700,14 @@ bind_java_type! { impl<'local> JParcelUuid<'local> { pub fn get_uuid(&self, env: &mut Env<'local>) -> Result> { - let obj = env.call_method(self, jni_str!("getUuid"), - jni_sig!("()Ljava/util/UUID;"), &[])?.l()?; + let obj = env + .call_method( + self, + jni_str!("getUuid"), + jni_sig!("()Ljava/util/UUID;"), + &[], + )? + .l()?; env.cast_local::(obj) } } diff --git a/src/droidplug/jni_utils/arrays.rs b/src/droidplug/jni_utils/arrays.rs index eb208be1..48682c37 100644 --- a/src/droidplug/jni_utils/arrays.rs +++ b/src/droidplug/jni_utils/arrays.rs @@ -1,12 +1,10 @@ -use jni::{ - Env, - errors::Result, - objects::JByteArray, - sys::jbyte, -}; +use jni::{Env, errors::Result, objects::JByteArray, sys::jbyte}; use std::slice; -pub fn slice_to_byte_array<'local>(env: &mut Env<'local>, slice: &[u8]) -> Result> { +pub fn slice_to_byte_array<'local>( + env: &mut Env<'local>, + slice: &[u8], +) -> Result> { let obj = env.new_byte_array(slice.len())?; let slice = unsafe { &*(slice as *const [u8] as *const [jbyte]) }; obj.set_region(env, 0, slice)?; @@ -38,19 +36,20 @@ mod test { obj.get_region(env, 0, &mut bytes).unwrap(); assert_eq!(bytes, [1, 2, 3, 4, 5]); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] fn test_byte_array_to_vec() { test_utils::with_env(|env| { let obj = env.new_byte_array(5).unwrap(); - obj.set_region(env, 0, &[1, 2, 3, 4, 5]) - .unwrap(); + obj.set_region(env, 0, &[1, 2, 3, 4, 5]).unwrap(); let vec = super::byte_array_to_vec(env, &obj).unwrap(); assert_eq!(vec, vec![1, 2, 3, 4, 5]); Ok(()) - }).unwrap(); + }) + .unwrap(); } } diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index 28afbc88..a650ca44 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -2,7 +2,7 @@ use jni::{ Env, descriptors::Desc, errors::Error, - jni_str, jni_sig, + jni_sig, jni_str, objects::{JClass, JObject, JThrowable}, }; use std::{ @@ -163,10 +163,7 @@ impl<'a> ::std::ops::Deref for JPanicException<'a> { /// Wraps a caught panic payload in a /// `io.github.gedgygedgy.rust.panic.PanicException` and throws it. If a Java /// exception is already pending, it will be added as a suppressed exception. -pub fn throw_panic( - env: &mut Env, - panic: Box, -) -> Result<(), Error> { +pub fn throw_panic(env: &mut Env, panic: Box) -> Result<(), Error> { let old_ex = if env.exception_check() { let ex = env.exception_occurred(); env.exception_clear(); @@ -204,7 +201,13 @@ pub fn throw_unwind( #[cfg(test)] mod test { - use jni::{Env, errors::Error, jni_str, jni_sig, objects::{JObject, JThrowable}, strings::JNIString}; + use jni::{ + Env, + errors::Error, + jni_sig, jni_str, + objects::{JObject, JThrowable}, + strings::JNIString, + }; use super::super::test_utils; use super::try_block; @@ -230,7 +233,9 @@ mod test { } let ex = throw_class.map(|c| { - let obj = env.new_object(JNIString::from(c), jni_sig!("()V"), &[]).unwrap(); + let obj = env + .new_object(JNIString::from(c), jni_sig!("()V"), &[]) + .unwrap(); env.cast_local::(obj).unwrap() }); @@ -299,7 +304,8 @@ mod test { ); assert!(!env.exception_check()); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -317,7 +323,8 @@ mod test { ); assert!(!env.exception_check()); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -335,7 +342,8 @@ mod test { ); assert!(!env.exception_check()); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -344,7 +352,8 @@ mod test { assert_eq!(test_catch(env, None, Ok(0), false).unwrap(), 0); assert!(!env.exception_check()); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -369,7 +378,8 @@ mod test { panic!("No JavaException"); } Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -383,7 +393,8 @@ mod test { panic!("InvalidCtorReturn not found"); } Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -397,13 +408,20 @@ mod test { panic!("JavaException not found"); } Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] fn test_catch_prior_exception() { test_utils::with_env(|env| { - let obj = env.new_object(jni_str!("java/lang/IllegalArgumentException"), jni_sig!("()V"), &[]).unwrap(); + let obj = env + .new_object( + jni_str!("java/lang/IllegalArgumentException"), + jni_sig!("()V"), + &[], + ) + .unwrap(); let ex = env.cast_local::(obj).unwrap(); let _ = env.throw(&ex); @@ -416,7 +434,8 @@ mod test { panic!("JavaException not found"); } Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -441,7 +460,8 @@ mod test { panic!("JavaException not found"); } Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -460,7 +480,8 @@ mod test { panic!("JavaException not found"); } Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -477,7 +498,12 @@ mod test { } let msg_obj = env - .call_method(&*ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[]) + .call_method( + &*ex, + jni_str!("getMessage"), + jni_sig!("()Ljava/lang/String;"), + &[], + ) .unwrap() .l() .unwrap(); @@ -485,7 +511,8 @@ mod test { let chars = msg.mutf8_chars(env).unwrap(); assert_eq!(String::from(chars), STATIC_MSG); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -503,7 +530,12 @@ mod test { } let msg_obj = env - .call_method(&*ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[]) + .call_method( + &*ex, + jni_str!("getMessage"), + jni_sig!("()Ljava/lang/String;"), + &[], + ) .unwrap() .l() .unwrap(); @@ -514,7 +546,8 @@ mod test { let any: Box = ex.take(env).unwrap(); assert_eq!(*any.downcast::().unwrap(), STRING_MSG); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -531,7 +564,12 @@ mod test { } let msg = env - .call_method(&*ex, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[]) + .call_method( + &*ex, + jni_str!("getMessage"), + jni_sig!("()Ljava/lang/String;"), + &[], + ) .unwrap() .l() .unwrap(); @@ -540,7 +578,8 @@ mod test { let any: Box = ex.take(env).unwrap(); assert_eq!(*any.downcast::().unwrap(), 42); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -550,7 +589,8 @@ mod test { assert_eq!(result, 42); assert!(!env.exception_check()); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -563,17 +603,26 @@ mod test { let ex = env.exception_occurred().unwrap(); env.exception_clear(); assert!( - env.is_instance_of(&ex, jni_str!("io/github/gedgygedgy/rust/panic/PanicException")) - .unwrap() + env.is_instance_of( + &ex, + jni_str!("io/github/gedgygedgy/rust/panic/PanicException") + ) + .unwrap() ); let suppressed_list = env - .call_method(&ex, jni_str!("getSuppressed"), jni_sig!("()[Ljava/lang/Throwable;"), &[]) + .call_method( + &ex, + jni_str!("getSuppressed"), + jni_sig!("()[Ljava/lang/Throwable;"), + &[], + ) .unwrap() .l() .unwrap(); - let suppressed_array = - unsafe { jni::objects::JObjectArray::::from_raw(env, suppressed_list.into_raw()) }; + let suppressed_array = unsafe { + jni::objects::JObjectArray::::from_raw(env, suppressed_list.into_raw()) + }; assert_eq!(suppressed_array.len(env).unwrap(), 0); let ex = super::JPanicException::from_env(ex); @@ -581,13 +630,16 @@ mod test { let str = any.downcast::<&str>().unwrap(); assert_eq!(*str, "This is a panic"); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] fn test_throw_unwind_panic_suppress() { test_utils::with_env(|env| { - let obj = env.new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]).unwrap(); + let obj = env + .new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]) + .unwrap(); let old_ex = env.cast_local::(obj).unwrap(); let _ = env.throw(&old_ex); @@ -598,17 +650,26 @@ mod test { let ex = env.exception_occurred().unwrap(); env.exception_clear(); assert!( - env.is_instance_of(&ex, jni_str!("io/github/gedgygedgy/rust/panic/PanicException")) - .unwrap() + env.is_instance_of( + &ex, + jni_str!("io/github/gedgygedgy/rust/panic/PanicException") + ) + .unwrap() ); let suppressed_list = env - .call_method(&ex, jni_str!("getSuppressed"), jni_sig!("()[Ljava/lang/Throwable;"), &[]) + .call_method( + &ex, + jni_str!("getSuppressed"), + jni_sig!("()[Ljava/lang/Throwable;"), + &[], + ) .unwrap() .l() .unwrap(); - let suppressed_array = - unsafe { jni::objects::JObjectArray::::from_raw(env, suppressed_list.into_raw()) }; + let suppressed_array = unsafe { + jni::objects::JObjectArray::::from_raw(env, suppressed_list.into_raw()) + }; assert_eq!(suppressed_array.len(env).unwrap(), 1); let suppressed_ex = suppressed_array.get_element(env, 0).unwrap(); assert!(env.is_same_object(&old_ex, &suppressed_ex).unwrap()); @@ -618,7 +679,8 @@ mod test { let str = any.downcast::<&str>().unwrap(); assert_eq!(*str, "This is a panic"); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -628,6 +690,7 @@ mod test { let ex = super::JPanicException::new(env, Box::new("This is a panic")).unwrap(); ex.resume_unwind(env).unwrap(); Ok(()) - }).unwrap(); + }) + .unwrap(); } } diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 5abcd5f8..3f805158 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -1,6 +1,5 @@ use ::jni::{ - Env, JavaVM, - bind_java_type, + Env, JavaVM, bind_java_type, errors::Result, jni_sig, jni_str, objects::{Global, JObject}, @@ -51,7 +50,10 @@ impl JSendFuture { }) } - fn poll_internal(&self, context: &mut Context<'_>) -> Result>>>> { + fn poll_internal( + &self, + context: &mut Context<'_>, + ) -> Result>>>> { self.vm.attach_current_thread(|env| { let jwaker = super::task::waker(env, context.waker().clone())?; let local = env.new_local_ref(self.internal.as_obj())?; @@ -113,7 +115,11 @@ mod test { assert_eq!(data.value(), false); let future_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), + jni_sig!("()V"), + &[], + ) .unwrap(); let future_local = env.new_local_ref(&future_obj).unwrap(); let jfuture = env.cast_local::(future_local).unwrap(); @@ -131,7 +137,9 @@ mod test { assert_eq!(Arc::strong_count(&data), 3); assert_eq!(data.value(), false); - let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); env.call_method( &future_obj, jni_str!("wake"), @@ -169,7 +177,8 @@ mod test { assert_eq!(data.value(), true); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -179,16 +188,23 @@ mod test { let (future, future_obj_global, obj_global) = test_utils::with_env(|env| { let future_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), + jni_sig!("()V"), + &[], + ) .unwrap(); let future_obj_global = env.new_global_ref(&future_obj).unwrap(); let future_local = env.new_local_ref(&future_obj).unwrap(); let jfuture = env.cast_local::(future_local).unwrap(); let future = JSendFuture::new(env, &jfuture).unwrap(); - let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); let obj_global = env.new_global_ref(&obj).unwrap(); Ok((future, future_obj_global, obj_global)) - }).unwrap(); + }) + .unwrap(); block_on(async { join!( @@ -204,7 +220,8 @@ mod test { ) .unwrap(); Ok(()) - }).unwrap(); + }) + .unwrap(); }, async { let global = future.await.unwrap(); @@ -215,7 +232,8 @@ mod test { let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); assert!(env.is_same_object(&result_obj, &obj_local).unwrap()); Ok(()) - }).unwrap(); + }) + .unwrap(); } ); }); @@ -227,16 +245,23 @@ mod test { let (future, future_obj_global, ex_global) = test_utils::with_env(|env| { let future_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), + jni_sig!("()V"), + &[], + ) .unwrap(); let future_obj_global = env.new_global_ref(&future_obj).unwrap(); let future_local = env.new_local_ref(&future_obj).unwrap(); let jfuture = env.cast_local::(future_local).unwrap(); let future = JSendFuture::new(env, &jfuture).unwrap(); - let ex = env.new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]).unwrap(); + let ex = env + .new_object(jni_str!("java/lang/Exception"), jni_sig!("()V"), &[]) + .unwrap(); let ex_global = env.new_global_ref(&ex).unwrap(); Ok((future, future_obj_global, ex_global)) - }).unwrap(); + }) + .unwrap(); block_on(async { join!( @@ -252,7 +277,8 @@ mod test { ) .unwrap(); Ok(()) - }).unwrap(); + }) + .unwrap(); }, async { use super::super::task::JPollResult; @@ -266,14 +292,20 @@ mod test { let future_ex = env.exception_occurred().unwrap(); env.exception_clear(); let actual_ex = env - .call_method(&future_ex, jni_str!("getCause"), jni_sig!("()Ljava/lang/Throwable;"), &[]) + .call_method( + &future_ex, + jni_str!("getCause"), + jni_sig!("()Ljava/lang/Throwable;"), + &[], + ) .unwrap() .l() .unwrap(); let ex_local = env.new_local_ref(ex_global.as_obj()).unwrap(); assert!(env.is_same_object(&actual_ex, &ex_local).unwrap()); Ok(()) - }).unwrap(); + }) + .unwrap(); } ); }); @@ -286,14 +318,21 @@ mod test { let (future, future_obj_global, obj_global) = test_utils::with_env(|env| { let future_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), jni_sig!("()V"), &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), + jni_sig!("()V"), + &[], + ) .unwrap(); let future_obj_global = env.new_global_ref(&future_obj).unwrap(); let future = JSendFuture::from_env(env, &future_obj).unwrap(); - let obj = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); let obj_global = env.new_global_ref(&obj).unwrap(); Ok((future, future_obj_global, obj_global)) - }).unwrap(); + }) + .unwrap(); block_on(async { join!( @@ -309,7 +348,8 @@ mod test { ) .unwrap(); Ok(()) - }).unwrap(); + }) + .unwrap(); }, async { let global_ref = future.await.unwrap(); @@ -320,7 +360,8 @@ mod test { let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); assert!(env.is_same_object(&result_obj, &obj_local).unwrap()); Ok(()) - }).unwrap(); + }) + .unwrap(); } ); }); diff --git a/src/droidplug/jni_utils/mod.rs b/src/droidplug/jni_utils/mod.rs index 0102d8d8..9611cc88 100644 --- a/src/droidplug/jni_utils/mod.rs +++ b/src/droidplug/jni_utils/mod.rs @@ -8,7 +8,10 @@ pub mod uuid; #[cfg(test)] pub(crate) mod test_utils { - use jni::{Env, JavaVM, NativeMethod, jni_str, jni_sig, objects::{Global, JObject, Reference}}; + use jni::{ + Env, JavaVM, NativeMethod, jni_sig, jni_str, + objects::{Global, JObject, Reference}, + }; use lazy_static::lazy_static; use std::{ cell::Cell, @@ -20,7 +23,7 @@ pub(crate) mod test_utils { fn test_init(env: &mut Env) -> jni::errors::Result<()> { use super::{ future::{JFuture, JFutureException}, - ops::{JFnAdapter, JFnRunnableImpl, JFnBiFunctionImpl, JFnFunctionImpl}, + ops::{JFnAdapter, JFnBiFunctionImpl, JFnFunctionImpl, JFnRunnableImpl}, stream::{JStream, JStreamPoll}, task::{JPollResult, JWaker}, }; @@ -38,7 +41,8 @@ pub(crate) mod test_utils { ::lookup_class(env, &loader)?; let fn_adapter_class = ::lookup_class(env, &loader)?; - unsafe { env.register_native_methods( + unsafe { + env.register_native_methods( &*fn_adapter_class, &[ NativeMethod::from_raw_parts( @@ -52,7 +56,8 @@ pub(crate) mod test_utils { super::ops::fn_adapter_close_internal as *mut c_void, ), ], - )? }; + )? + }; Ok(()) } @@ -134,41 +139,37 @@ pub(crate) mod test_utils { jni_utils_jar.push("libs"); jni_utils_jar.push("btleplug-jni.jar"); - let classpath = format!( - "-Djava.class.path={}", - jni_utils_jar.to_str().unwrap() - ); - let jvm_args = InitArgsBuilder::new() - .option(&classpath) - .build() - .unwrap(); + let classpath = format!("-Djava.class.path={}", jni_utils_jar.to_str().unwrap()); + let jvm_args = InitArgsBuilder::new().option(&classpath).build().unwrap(); let jvm = JavaVM::new(jvm_args).unwrap(); - let class_loader = jvm.attach_current_thread(|env| { - test_init(env).unwrap(); - - let thread = env - .call_static_method( - jni_str!("java/lang/Thread"), - jni_str!("currentThread"), - jni_sig!("()Ljava/lang/Thread;"), - &[], - ) - .unwrap() - .l() - .unwrap(); - let class_loader = env - .call_method( - &thread, - jni_str!("getContextClassLoader"), - jni_sig!("()Ljava/lang/ClassLoader;"), - &[], - ) - .unwrap() - .l() - .unwrap(); - Ok::<_, jni::errors::Error>(env.new_global_ref(class_loader).unwrap()) - }).unwrap(); + let class_loader = jvm + .attach_current_thread(|env| { + test_init(env).unwrap(); + + let thread = env + .call_static_method( + jni_str!("java/lang/Thread"), + jni_str!("currentThread"), + jni_sig!("()Ljava/lang/Thread;"), + &[], + ) + .unwrap() + .l() + .unwrap(); + let class_loader = env + .call_method( + &thread, + jni_str!("getContextClassLoader"), + jni_sig!("()Ljava/lang/ClassLoader;"), + &[], + ) + .unwrap() + .l() + .unwrap(); + Ok::<_, jni::errors::Error>(env.new_global_ref(class_loader).unwrap()) + }) + .unwrap(); GlobalJVM { jvm, class_loader } }; diff --git a/src/droidplug/jni_utils/ops.rs b/src/droidplug/jni_utils/ops.rs index 6e149330..31f2fd5d 100644 --- a/src/droidplug/jni_utils/ops.rs +++ b/src/droidplug/jni_utils/ops.rs @@ -1,11 +1,10 @@ +use ::jni::errors::ThrowRuntimeExAndDefault; use ::jni::{ - Env, EnvUnowned, - bind_java_type, + Env, EnvUnowned, bind_java_type, errors::Result, jni_sig, jni_str, objects::{JObject, Reference}, }; -use ::jni::errors::ThrowRuntimeExAndDefault; use std::sync::{Arc, Mutex}; bind_java_type! { @@ -296,11 +295,7 @@ fn fn_adapter<'local>( > = Arc::from(f); let class = ::lookup_class(env, &Default::default())?; - let obj = env.new_object( - &*class, - jni_sig!("(Z)V"), - &[local.into()], - )?; + let obj = env.new_object(&*class, jni_sig!("(Z)V"), &[local.into()])?; unsafe { env.set_rust_field::<_, _, FnWrapper>(&obj, jni_str!("data"), SendSyncWrapper(arc)) }?; Ok(obj) } @@ -314,21 +309,24 @@ pub(crate) extern "C" fn fn_adapter_call_internal<'local>( ) -> JObject<'local> { use std::panic::{AssertUnwindSafe, catch_unwind}; - env.with_env(|env| -> std::result::Result, jni::errors::Error> { - let arc = - if let Ok(f) = unsafe { env.get_rust_field::<_, _, FnWrapper>(&obj1, jni_str!("data")) } { + env.with_env( + |env| -> std::result::Result, jni::errors::Error> { + let arc = if let Ok(f) = + unsafe { env.get_rust_field::<_, _, FnWrapper>(&obj1, jni_str!("data")) } + { AssertUnwindSafe(f.0.clone()) } else { return Ok(JObject::null()); }; - match catch_unwind(AssertUnwindSafe(|| arc(env, obj1, obj2, arg1, arg2))) { - Ok(result) => Ok(result), - Err(panic) => { - let _ = super::exceptions::throw_panic(env, panic); - Ok(JObject::null()) + match catch_unwind(AssertUnwindSafe(|| arc(env, obj1, obj2, arg1, arg2))) { + Ok(result) => Ok(result), + Err(panic) => { + let _ = super::exceptions::throw_panic(env, panic); + Ok(JObject::null()) + } } - } - }) + }, + ) .resolve::() } diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index 512918d9..44b2c22a 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -1,7 +1,6 @@ use super::task::JPollResult; use ::jni::{ - Env, JavaVM, - bind_java_type, + Env, JavaVM, bind_java_type, errors::Result, jni_sig, jni_str, objects::{Global, JObject}, @@ -18,7 +17,11 @@ bind_java_type! { } impl<'local> JStream<'local> { - pub fn poll_next(&self, env: &mut Env<'local>, waker: &JObject<'local>) -> Result> { + pub fn poll_next( + &self, + env: &mut Env<'local>, + waker: &JObject<'local>, + ) -> Result> { env.call_method( self, jni_str!("pollNext"), @@ -129,7 +132,11 @@ mod test { assert_eq!(data.value(), false); let stream_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) + .new_object( + jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), + jni_sig!("()V"), + &[], + ) .unwrap(); let stream_local = env.new_local_ref(&stream_obj).unwrap(); let jstream = env.cast_local::(stream_local).unwrap(); @@ -143,7 +150,9 @@ mod test { assert_eq!(Arc::strong_count(&data), 3); assert_eq!(data.value(), false); - let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj1 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); env.call_method( &stream_obj, jni_str!("add"), @@ -155,7 +164,9 @@ mod test { assert_eq!(data.value(), true); data.set_value(false); - let obj2 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); + let obj2 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); env.call_method( &stream_obj, jni_str!("add"), @@ -192,7 +203,8 @@ mod test { assert_eq!(Arc::strong_count(&data), 3); assert_eq!(data.value(), false); - env.call_method(&stream_obj, jni_str!("finish"), jni_sig!("()V"), &[]).unwrap(); + env.call_method(&stream_obj, jni_str!("finish"), jni_sig!("()V"), &[]) + .unwrap(); assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), true); data.set_value(false); @@ -206,27 +218,38 @@ mod test { assert_eq!(data.value(), false); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] fn test_jstream_await() { use futures::{executor::block_on, join}; - let (mut stream, stream_obj_global, obj1_global, obj2_global) = test_utils::with_env(|env| { - let stream_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) - .unwrap(); - let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); - let stream_local = env.new_local_ref(&stream_obj).unwrap(); - let jstream = env.cast_local::(stream_local).unwrap(); - let stream = JSendStream::new(env, &jstream).unwrap(); - let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); - let obj1_global = env.new_global_ref(&obj1).unwrap(); - let obj2 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); - let obj2_global = env.new_global_ref(&obj2).unwrap(); - Ok((stream, stream_obj_global, obj1_global, obj2_global)) - }).unwrap(); + let (mut stream, stream_obj_global, obj1_global, obj2_global) = + test_utils::with_env(|env| { + let stream_obj = env + .new_object( + jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), + jni_sig!("()V"), + &[], + ) + .unwrap(); + let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); + let stream_local = env.new_local_ref(&stream_obj).unwrap(); + let jstream = env.cast_local::(stream_local).unwrap(); + let stream = JSendStream::new(env, &jstream).unwrap(); + let obj1 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj1_global = env.new_global_ref(&obj1).unwrap(); + let obj2 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj2_global = env.new_global_ref(&obj2).unwrap(); + Ok((stream, stream_obj_global, obj1_global, obj2_global)) + }) + .unwrap(); block_on(async { join!( @@ -249,9 +272,11 @@ mod test { &[(&o2).into()], ) .unwrap(); - env.call_method(&s, jni_str!("finish"), jni_sig!("()V"), &[]).unwrap(); + env.call_method(&s, jni_str!("finish"), jni_sig!("()V"), &[]) + .unwrap(); Ok(()) - }).unwrap(); + }) + .unwrap(); }, async { use futures::StreamExt; @@ -260,14 +285,16 @@ mod test { let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); assert!(env.is_same_object(g1.as_obj(), &o1).unwrap()); Ok(()) - }).unwrap(); + }) + .unwrap(); let g2 = stream.next().await.unwrap().unwrap(); test_utils::with_env(|env| { let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); assert!(env.is_same_object(g2.as_obj(), &o2).unwrap()); Ok(()) - }).unwrap(); + }) + .unwrap(); assert!(stream.next().await.is_none()); } @@ -279,18 +306,28 @@ mod test { fn test_jsendstream_await() { use futures::{executor::block_on, join}; - let (mut stream, stream_obj_global, obj1_global, obj2_global) = test_utils::with_env(|env| { - let stream_obj = env - .new_object(jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), jni_sig!("()V"), &[]) - .unwrap(); - let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); - let stream = JSendStream::from_env(env, &stream_obj).unwrap(); - let obj1 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); - let obj1_global = env.new_global_ref(&obj1).unwrap(); - let obj2 = env.new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]).unwrap(); - let obj2_global = env.new_global_ref(&obj2).unwrap(); - Ok((stream, stream_obj_global, obj1_global, obj2_global)) - }).unwrap(); + let (mut stream, stream_obj_global, obj1_global, obj2_global) = + test_utils::with_env(|env| { + let stream_obj = env + .new_object( + jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), + jni_sig!("()V"), + &[], + ) + .unwrap(); + let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); + let stream = JSendStream::from_env(env, &stream_obj).unwrap(); + let obj1 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj1_global = env.new_global_ref(&obj1).unwrap(); + let obj2 = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj2_global = env.new_global_ref(&obj2).unwrap(); + Ok((stream, stream_obj_global, obj1_global, obj2_global)) + }) + .unwrap(); block_on(async { join!( @@ -313,9 +350,11 @@ mod test { &[(&o2).into()], ) .unwrap(); - env.call_method(&s, jni_str!("finish"), jni_sig!("()V"), &[]).unwrap(); + env.call_method(&s, jni_str!("finish"), jni_sig!("()V"), &[]) + .unwrap(); Ok(()) - }).unwrap(); + }) + .unwrap(); }, async { use futures::StreamExt; @@ -324,14 +363,16 @@ mod test { let o1 = env.new_local_ref(obj1_global.as_obj()).unwrap(); assert!(env.is_same_object(g1.as_obj(), &o1).unwrap()); Ok(()) - }).unwrap(); + }) + .unwrap(); let g2 = stream.next().await.unwrap().unwrap(); test_utils::with_env(|env| { let o2 = env.new_local_ref(obj2_global.as_obj()).unwrap(); assert!(env.is_same_object(g2.as_obj(), &o2).unwrap()); Ok(()) - }).unwrap(); + }) + .unwrap(); assert!(stream.next().await.is_none()); } diff --git a/src/droidplug/jni_utils/task.rs b/src/droidplug/jni_utils/task.rs index 587dba93..8de5bc7b 100644 --- a/src/droidplug/jni_utils/task.rs +++ b/src/droidplug/jni_utils/task.rs @@ -1,6 +1,5 @@ use ::jni::{ - Env, - bind_java_type, + Env, bind_java_type, errors::Result, jni_sig, objects::{JObject, Reference}, @@ -51,16 +50,19 @@ mod test { assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]).unwrap(); + env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]) + .unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), true); data.set_value(false); - env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]).unwrap(); + env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]) + .unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -78,14 +80,17 @@ mod test { assert_eq!(Arc::strong_count(&data), 2); assert_eq!(data.value(), false); - env.call_method(&jwaker, jni_str!("close"), jni_sig!("()V"), &[]).unwrap(); + env.call_method(&jwaker, jni_str!("close"), jni_sig!("()V"), &[]) + .unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); - env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]).unwrap(); + env.call_method(&jwaker, jni_str!("wake"), jni_sig!("()V"), &[]) + .unwrap(); assert_eq!(Arc::strong_count(&data), 1); assert_eq!(data.value(), false); Ok(()) - }).unwrap(); + }) + .unwrap(); } } diff --git a/src/droidplug/jni_utils/uuid.rs b/src/droidplug/jni_utils/uuid.rs index 840c1538..c67c8e86 100644 --- a/src/droidplug/jni_utils/uuid.rs +++ b/src/droidplug/jni_utils/uuid.rs @@ -1,9 +1,4 @@ -use jni::{ - Env, - bind_java_type, - errors::Result, - sys::jlong, -}; +use jni::{Env, bind_java_type, errors::Result, sys::jlong}; use uuid::Uuid; bind_java_type! { @@ -39,7 +34,7 @@ impl<'local> JUuid<'local> { mod test { use super::super::test_utils; use super::JUuid; - use jni::{jni_str, jni_sig, objects::JObject, sys::jlong}; + use jni::{jni_sig, jni_str, objects::JObject, sys::jlong}; use uuid::Uuid; struct UuidTest { @@ -72,12 +67,22 @@ mod test { let obj: JObject = uuid_obj.into(); let actual_most = env - .call_method(&obj, jni_str!("getMostSignificantBits"), jni_sig!("()J"), &[]) + .call_method( + &obj, + jni_str!("getMostSignificantBits"), + jni_sig!("()J"), + &[], + ) .unwrap() .j() .unwrap(); let actual_least = env - .call_method(&obj, jni_str!("getLeastSignificantBits"), jni_sig!("()J"), &[]) + .call_method( + &obj, + jni_str!("getLeastSignificantBits"), + jni_sig!("()J"), + &[], + ) .unwrap() .j() .unwrap(); @@ -85,7 +90,8 @@ mod test { assert_eq!(actual_least, least); } Ok(()) - }).unwrap(); + }) + .unwrap(); } #[test] @@ -96,13 +102,18 @@ mod test { let least = test.least as jlong; let obj = env - .new_object(jni_str!("java/util/UUID"), jni_sig!("(JJ)V"), &[most.into(), least.into()]) + .new_object( + jni_str!("java/util/UUID"), + jni_sig!("(JJ)V"), + &[most.into(), least.into()], + ) .unwrap(); let uuid_obj = env.cast_local::(obj).unwrap(); assert_eq!(uuid_obj.as_uuid(env).unwrap(), Uuid::from_u128(test.uuid)); } Ok(()) - }).unwrap(); + }) + .unwrap(); } } diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 13361b78..43786b55 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -1,3 +1,7 @@ +use super::jni::{ + jvm, + objects::{JBluetoothGattCharacteristic, JBluetoothGattService, JPeripheral}, +}; use super::jni_utils::{ arrays::byte_array_to_vec, future::{JFuture, JSendFuture}, @@ -29,10 +33,6 @@ use std::{ sync::atomic::{AtomicU16, Ordering}, sync::{Arc, Mutex}, }; -use super::jni::{ - jvm, - objects::{JBluetoothGattCharacteristic, JBluetoothGattService, JPeripheral}, -}; #[cfg_attr( feature = "serde", @@ -60,16 +60,23 @@ fn get_poll_result<'a>( let ex = env.exception_occurred().unwrap(); env.exception_clear(); - use jni::objects::Reference; use super::jni::objects::*; + use jni::objects::Reference; - let future_ex_class = ::lookup_class( - env, &Default::default(), - )?; + let future_ex_class = + ::lookup_class( + env, + &Default::default(), + )?; if env.is_instance_of(&ex, &*future_ex_class)? { let cause = env - .call_method(&ex, jni_str!("getCause"), jni_sig!("()Ljava/lang/Throwable;"), &[])? + .call_method( + &ex, + jni_str!("getCause"), + jni_sig!("()Ljava/lang/Throwable;"), + &[], + )? .l()?; macro_rules! check_exception { @@ -95,7 +102,12 @@ fn get_poll_result<'a>( Err(Error::NoAdapterAvailable) } else if env.is_instance_of(&cause, jni_str!("java/lang/RuntimeException"))? { let msg = env - .call_method(&cause, jni_str!("getMessage"), jni_sig!("()Ljava/lang/String;"), &[])? + .call_method( + &cause, + jni_str!("getMessage"), + jni_sig!("()Ljava/lang/String;"), + &[], + )? .l()?; let jstr = env.cast_local::(msg)?; let msgstr = String::from(jstr.mutf8_chars(env)?); @@ -240,7 +252,9 @@ impl api::Peripheral for Peripheral { let mtu_result_ref = mtu_future.await?; self.with_obj(|env, _obj| -> Result<()> { let mtu_obj = get_poll_result(env, &mtu_result_ref)?; - let mtu_val = env.call_method(&mtu_obj, jni_str!("intValue"), jni_sig!("()I"), &[])?.i()?; + let mtu_val = env + .call_method(&mtu_obj, jni_str!("intValue"), jni_sig!("()I"), &[])? + .i()?; self.mtu.store(mtu_val as u16, Ordering::Relaxed); Ok(()) })?; @@ -272,13 +286,20 @@ impl api::Peripheral for Peripheral { use std::iter::FromIterator; let obj = get_poll_result(env, &result_ref)?; - let size = env.call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])?.i()?; + let size = env + .call_method(&obj, jni_str!("size"), jni_sig!("()I"), &[])? + .i()?; let mut peripheral_services = Vec::new(); let mut peripheral_characteristics = Vec::new(); for i in 0..size { let svc_obj = env - .call_method(&obj, jni_str!("get"), jni_sig!("(I)Ljava/lang/Object;"), &[JValue::from(i)])? + .call_method( + &obj, + jni_str!("get"), + jni_sig!("(I)Ljava/lang/Object;"), + &[JValue::from(i)], + )? .l()?; let service = env.cast_local::(svc_obj)?; let mut characteristics = BTreeSet::::new(); @@ -349,7 +370,8 @@ impl api::Peripheral for Peripheral { let result_ref = future.await?; self.with_obj(|env, _obj| { let bytes_obj = get_poll_result(env, &result_ref)?; - let bytes_arr = unsafe { jni::objects::JByteArray::from_raw(env, bytes_obj.into_raw()) }; + let bytes_arr = + unsafe { jni::objects::JByteArray::from_raw(env, bytes_obj.into_raw()) }; Ok(byte_array_to_vec(env, &bytes_arr)?) }) } @@ -375,29 +397,31 @@ impl api::Peripheral for Peripheral { .map(move |item| match item { Ok(item) => { let vm = jvm()?; - let result: crate::Result<_> = vm.attach_current_thread(|env| -> jni::errors::Result<_> { - let local_obj = env.new_local_ref(item.as_obj())?; - let characteristic = - env.cast_local::(local_obj)?; - let uuid = characteristic.get_uuid(env)?; - let value = characteristic.get_value(env)?; - let service_uuid = shared - .lock() - .ok() - .and_then(|guard| { - guard - .services - .iter() - .find(|s| s.characteristics.iter().any(|c| c.uuid == uuid)) - .map(|s| s.uuid) + let result: crate::Result<_> = vm + .attach_current_thread(|env| -> jni::errors::Result<_> { + let local_obj = env.new_local_ref(item.as_obj())?; + let characteristic = + env.cast_local::(local_obj)?; + let uuid = characteristic.get_uuid(env)?; + let value = characteristic.get_value(env)?; + let service_uuid = shared + .lock() + .ok() + .and_then(|guard| { + guard + .services + .iter() + .find(|s| s.characteristics.iter().any(|c| c.uuid == uuid)) + .map(|s| s.uuid) + }) + .unwrap_or_default(); + Ok(ValueNotification { + uuid, + service_uuid, + value, }) - .unwrap_or_default(); - Ok(ValueNotification { - uuid, - service_uuid, - value, }) - }).map_err(Into::into); + .map_err(Into::into); result } Err(err) => Err(err.into()), @@ -415,7 +439,9 @@ impl api::Peripheral for Peripheral { let result_ref = future.await?; self.with_obj(|env, _obj| { let rssi_obj = get_poll_result(env, &result_ref)?; - let rssi_val = env.call_method(&rssi_obj, jni_str!("intValue"), jni_sig!("()I"), &[])?.i()?; + let rssi_val = env + .call_method(&rssi_obj, jni_str!("intValue"), jni_sig!("()I"), &[])? + .i()?; Ok(rssi_val as i16) }) } @@ -442,7 +468,8 @@ impl api::Peripheral for Peripheral { let result_ref = future.await?; self.with_obj(|env, _obj| { let bytes_obj = get_poll_result(env, &result_ref)?; - let bytes_arr = unsafe { jni::objects::JByteArray::from_raw(env, bytes_obj.into_raw()) }; + let bytes_arr = + unsafe { jni::objects::JByteArray::from_raw(env, bytes_obj.into_raw()) }; Ok(byte_array_to_vec(env, &bytes_arr)?) }) } diff --git a/tests/common/peripheral_finder.rs b/tests/common/peripheral_finder.rs index 9d54f224..9cf1dd79 100644 --- a/tests/common/peripheral_finder.rs +++ b/tests/common/peripheral_finder.rs @@ -41,12 +41,15 @@ pub async fn get_adapter() -> &'static Adapter { .expect("failed to create adapter runtime"); rt.block_on(async { log::info!("adapter thread: creating manager"); - let manager = Manager::new().await.expect("failed to create BLE manager"); + let manager = + Manager::new().await.expect("failed to create BLE manager"); log::info!("adapter thread: getting adapters"); - let adapters = manager.adapters().await.expect("failed to get adapters"); + let adapters = + manager.adapters().await.expect("failed to get adapters"); log::info!("adapter thread: got {} adapters", adapters.len()); std::mem::forget(manager); - let adapter = adapters.into_iter().next().expect("no BLE adapters found"); + let adapter = + adapters.into_iter().next().expect("no BLE adapters found"); log::info!("adapter thread: sending adapter"); tx.send(adapter).ok(); log::info!("adapter thread: blocking forever"); From 1e129e2da9d1d895b35f2c52290d7ec003bced77 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 24 May 2026 22:31:37 -0700 Subject: [PATCH 20/77] fix: resolve clippy warnings breaking CI - bluez/peripheral: fix never_loop, use map entry API, use .values() - bluez/adapter: collapse nested match into outer pattern - droidplug/exceptions: remove redundant closure call, collapse nested ifs - droidplug/ops: suppress unused_unit and type_complexity from macro-generated code - lib.rs: allow dead_code on jni-host-tests module (callers are Android-only) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/bluez/adapter.rs | 13 ++++------ src/bluez/peripheral.rs | 10 +++----- src/droidplug/jni_utils/exceptions.rs | 36 +++++++++++++-------------- src/droidplug/jni_utils/ops.rs | 15 +++++++---- src/lib.rs | 1 + 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/bluez/adapter.rs b/src/bluez/adapter.rs index e8d495b7..6ca8a734 100644 --- a/src/bluez/adapter.rs +++ b/src/bluez/adapter.rs @@ -206,14 +206,11 @@ async fn central_events( }, BluetoothEvent::Adapter { id, - event: adapter_event, - } if id == adapter_id => match adapter_event { - AdapterEvent::Powered { powered } => { - let state = get_central_state(powered); - Some(vec![CentralEvent::StateUpdate(state)]) - } - _ => None, - }, + event: AdapterEvent::Powered { powered }, + } if id == adapter_id => { + let state = get_central_state(powered); + Some(vec![CentralEvent::StateUpdate(state)]) + } _ => None, } } diff --git a/src/bluez/peripheral.rs b/src/bluez/peripheral.rs index 2310aa1b..f974cf00 100644 --- a/src/bluez/peripheral.rs +++ b/src/bluez/peripheral.rs @@ -141,7 +141,7 @@ impl api::Peripheral for Peripheral { fn mtu(&self) -> u16 { let services = self.services.lock().unwrap(); for (_, service) in services.iter() { - for (_, characteristic) in service.characteristics.iter() { + if let Some((_, characteristic)) = service.characteristics.iter().next() { return characteristic.info.mtu.unwrap(); } } @@ -202,9 +202,7 @@ impl api::Peripheral for Peripheral { // This "should" be unique, but of course it's not enforced HashMap::::new(), |mut map, characteristic| { - if !map.contains_key(&characteristic.uuid) { - map.insert(characteristic.uuid, characteristic); - } + map.entry(characteristic.uuid).or_insert(characteristic); map }, ) @@ -391,8 +389,8 @@ fn make_characteristic( uuid: info.uuid, properties: info.flags.into(), descriptors: descriptors - .iter() - .map(|(_, descriptor)| make_descriptor(descriptor, info.uuid, service_uuid)) + .values() + .map(|descriptor| make_descriptor(descriptor, info.uuid, service_uuid)) .collect(), service_uuid, } diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index a650ca44..bd9bbf6a 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -26,13 +26,11 @@ pub fn try_block( block: impl FnOnce(&mut Env) -> Result, ) -> TryCatchResult { TryCatchResult { - try_result: (|| { - if env.exception_check() { - Err(Error::JavaException) - } else { - Ok(block(env)) - } - })(), + try_result: if env.exception_check() { + Err(Error::JavaException) + } else { + Ok(block(env)) + }, catch_result: None, } } @@ -59,18 +57,18 @@ impl TryCatchResult { }, (Ok(Err(Error::JavaException)), None) => { let catch_result = (|| { - if env.exception_check() { - if let Some(ex) = env.exception_occurred() { - env.exception_clear(); - if env.is_instance_of(&ex, class)? { - return block(env, ex).map(|o| Some(o)); - } - // Rethrow — throw() returns Err(JavaException) on success - match env.throw(&ex) { - Err(Error::JavaException) => {} - Err(e) => return Err(e), - Ok(()) => {} - } + if env.exception_check() + && let Some(ex) = env.exception_occurred() + { + env.exception_clear(); + if env.is_instance_of(&ex, class)? { + return block(env, ex).map(|o| Some(o)); + } + // Rethrow — throw() returns Err(JavaException) on success + match env.throw(&ex) { + Err(Error::JavaException) => {} + Err(e) => return Err(e), + Ok(()) => {} } } Ok(None) diff --git a/src/droidplug/jni_utils/ops.rs b/src/droidplug/jni_utils/ops.rs index 31f2fd5d..ba035121 100644 --- a/src/droidplug/jni_utils/ops.rs +++ b/src/droidplug/jni_utils/ops.rs @@ -43,6 +43,7 @@ macro_rules! define_fn_adapter { signature: $closure_name:ident: impl for<'c, 'd> Fn$args:tt -> $ret:ty, closure: $closure:expr, ) => { + #[allow(clippy::unused_unit)] fn $foi<'local>( env: &mut Env<'local>, $closure_name: impl for<'c, 'd> FnOnce$args -> $ret + 'static, @@ -57,6 +58,7 @@ macro_rules! define_fn_adapter { ) } + #[allow(clippy::unused_unit)] pub fn $fo<'local>( env: &mut Env<'local>, f: impl for<'c, 'd> FnOnce$args -> $ret + Send + 'static, @@ -64,7 +66,7 @@ macro_rules! define_fn_adapter { $foi(env, f, false) } - #[allow(dead_code)] + #[allow(dead_code, clippy::unused_unit)] pub fn $fol<'local>( env: &mut Env<'local>, f: impl for<'c, 'd> FnOnce$args -> $ret + 'static, @@ -72,6 +74,7 @@ macro_rules! define_fn_adapter { $foi(env, f, true) } + #[allow(clippy::unused_unit)] fn $fmi<'local>( env: &mut Env<'local>, mut $closure_name: impl for<'c, 'd> FnMut$args -> $ret + 'static, @@ -86,7 +89,7 @@ macro_rules! define_fn_adapter { ) } - #[allow(dead_code)] + #[allow(dead_code, clippy::unused_unit)] pub fn $fm<'local>( env: &mut Env<'local>, f: impl for<'c, 'd> FnMut$args -> $ret + Send + 'static, @@ -94,7 +97,7 @@ macro_rules! define_fn_adapter { $fmi(env, f, false) } - #[allow(dead_code)] + #[allow(dead_code, clippy::unused_unit)] pub fn $fml<'local>( env: &mut Env<'local>, f: impl for<'c, 'd> FnMut$args -> $ret + 'static, @@ -102,6 +105,7 @@ macro_rules! define_fn_adapter { $fmi(env, f, true) } + #[allow(clippy::unused_unit)] fn $fi<'local>( env: &mut Env<'local>, $closure_name: impl for<'c, 'd> Fn$args -> $ret + 'static, @@ -116,7 +120,7 @@ macro_rules! define_fn_adapter { ) } - #[allow(dead_code)] + #[allow(dead_code, clippy::unused_unit)] pub fn $f<'local>( env: &mut Env<'local>, f: impl for<'c, 'd> Fn$args -> $ret + Send + Sync + 'static, @@ -124,7 +128,7 @@ macro_rules! define_fn_adapter { $fi(env, f, false) } - #[allow(dead_code)] + #[allow(dead_code, clippy::unused_unit)] pub fn $fl<'local>( env: &mut Env<'local>, f: impl for<'c, 'd> Fn$args -> $ret + 'static, @@ -272,6 +276,7 @@ fn fn_mut_adapter<'local>( ) } +#[allow(clippy::type_complexity)] fn fn_adapter<'local>( env: &mut Env<'local>, f: impl for<'c, 'd> Fn( diff --git a/src/lib.rs b/src/lib.rs index e2181590..dd7dc4f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,6 +99,7 @@ mod corebluetooth; #[cfg(target_os = "android")] mod droidplug; #[cfg(all(not(target_os = "android"), feature = "jni-host-tests"))] +#[allow(dead_code)] mod droidplug { mod jni_utils; } From a332913ff53e004fdf67dc6236663955025d1593 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 14:33:16 -0700 Subject: [PATCH 21/77] fix: report droidplug init failure to all callers instead of only the first --- src/droidplug/jni/mod.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index 47cd2006..f812c681 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -7,18 +7,21 @@ use ::jni::{ }; use jni::{objects::JString, sys::jboolean}; use std::ffi::c_void; -use std::sync::Once; +use std::sync::OnceLock; -static INIT: Once = Once::new(); +static INIT: OnceLock<()> = OnceLock::new(); pub fn init(env: &mut Env) -> crate::Result<()> { - let mut init_result: crate::Result<()> = Ok(()); - INIT.call_once(|| { - if let Err(e) = init_inner(env) { - init_result = Err(e); + match INIT.get() { + Some(()) => Ok(()), + None => { + let result = init_inner(env); + if result.is_ok() { + let _ = INIT.set(()); + } + result } - }); - init_result + } } fn init_inner(env: &mut Env) -> crate::Result<()> { From b5278e3f87e0d4ded28d273e146b8e2f00ae907e Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 14:33:16 -0700 Subject: [PATCH 22/77] fix: preserve Java exception details on unknown-exception error paths --- src/droidplug/adapter.rs | 5 +++-- src/droidplug/jni_utils/exceptions.rs | 18 +++++++++++++++++- src/droidplug/peripheral.rs | 20 +++++++++++++++----- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index 3ed016e7..91ecaa66 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -3,6 +3,7 @@ use super::{ jvm, objects::{JScanFilter, JScanResult}, }, + jni_utils::exceptions::throwable_to_string, peripheral::{Peripheral, PeripheralId}, }; use crate::{ @@ -164,8 +165,8 @@ impl Central for Adapter { let msgstr = String::from(jstr.mutf8_chars(env)?); Err(Error::RuntimeError(msgstr)) } else { - let _ = env.throw(&ex); - Err(jni::errors::Error::JavaException.into()) + let desc = throwable_to_string(env, &ex)?; + Err(Error::RuntimeError(format!("Java exception: {}", desc))) } } Err(e) => Err(e.into()), diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index bd9bbf6a..4b001d0c 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -3,7 +3,7 @@ use jni::{ descriptors::Desc, errors::Error, jni_sig, jni_str, - objects::{JClass, JObject, JThrowable}, + objects::{JClass, JObject, JString, JThrowable}, }; use std::{ any::Any, @@ -11,6 +11,22 @@ use std::{ sync::MutexGuard, }; +pub(crate) fn throwable_to_string( + env: &mut Env, + throwable: &JThrowable, +) -> jni::errors::Result { + let msg = env + .call_method( + throwable, + jni_str!("toString"), + jni_sig!("()Ljava/lang/String;"), + &[], + )? + .l()?; + let jstr = env.cast_local::(msg)?; + Ok(String::from(jstr.mutf8_chars(env)?)) +} + /// Result from [`try_block`]. This object can be chained into /// [`catch`](TryCatchResult::catch) calls to catch exceptions. pub struct TryCatchResult { diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 43786b55..20e401d8 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -4,6 +4,7 @@ use super::jni::{ }; use super::jni_utils::{ arrays::byte_array_to_vec, + exceptions::throwable_to_string, future::{JFuture, JSendFuture}, stream::JSendStream, task::JPollResult, @@ -20,7 +21,7 @@ use async_trait::async_trait; use futures::stream::Stream; use jni::{ Env, jni_sig, jni_str, - objects::{Global, JObject, JString, JValue}, + objects::{Global, JObject, JString, JThrowable, JValue}, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -113,12 +114,21 @@ fn get_poll_result<'a>( let msgstr = String::from(jstr.mutf8_chars(env)?); Err(Error::RuntimeError(msgstr)) } else { - let _ = env.throw(&ex); - Err(jni::errors::Error::JavaException.into()) + let cause = if cause.is_null() { + None + } else { + Some(env.cast_local::(cause)?) + }; + let desc = if let Some(cause) = &cause { + throwable_to_string(env, cause)? + } else { + throwable_to_string(env, &ex)? + }; + Err(Error::RuntimeError(format!("Java exception: {}", desc))) } } else { - let _ = env.throw(&ex); - Err(jni::errors::Error::JavaException.into()) + let desc = throwable_to_string(env, &ex)?; + Err(Error::RuntimeError(format!("Java exception: {}", desc))) } } Err(e) => Err(e.into()), From 52f64f693ff8a50ae6b6aa8904ac350301649375 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 17:16:54 -0700 Subject: [PATCH 23/77] Add local adapter address API Replaces #433. --- CHANGELOG.md | 1 + Cargo.toml | 2 +- README.md | 3 ++ src/api/bdaddr.rs | 5 +++ src/api/mod.rs | 22 +++++++++++++ src/bluez/adapter.rs | 12 ++++++- src/corebluetooth/adapter.rs | 7 +++- src/droidplug/adapter.rs | 5 +++ src/winrtble/adapter.rs | 20 +++++++++-- src/winrtble/manager.rs | 22 +++++++++---- tests/CLAUDE.md | 4 ++- tests/android/rust/src/lib.rs | 5 +++ .../btleplug/test/BleIntegrationTest.kt | 3 ++ .../btleplug/test/NativeTests.kt | 3 ++ tests/common/test_cases.rs | 33 +++++++++++++++++++ tests/documentation.rs | 17 ++++++++++ tests/test_adapter_address.rs | 7 ++++ 17 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 tests/documentation.rs create mode 100644 tests/test_adapter_address.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c5b455f..b54cf760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Add `clear_peripherals()` to `Central` trait - Thanks danielstuart14! - Add `adapter_state()` to `Central` trait for querying Bluetooth on/off state +- Add optional `adapter_address()` to `Central` for platforms exposing a local adapter Bluetooth address; unsupported platforms return `Ok(None)` without breaking custom implementations - Add `add_peripheral()` to `Central` trait for adding a device by address without scanning (Android) - Add `advertisement_name` field to `PeripheralProperties` - Thanks szymonlesisz! diff --git a/Cargo.toml b/Cargo.toml index 3bbb65e3..43f8ec68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,7 @@ objc2-core-bluetooth = { version = "0.2.2", default-features = false, features = ] } [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.62", features = ["Devices_Bluetooth", "Devices_Bluetooth_GenericAttributeProfile", "Devices_Bluetooth_Advertisement", "Devices_Radios", "Foundation_Collections", "Foundation", "Storage_Streams"] } +windows = { version = "0.62", features = ["Devices_Bluetooth", "Devices_Enumeration", "Devices_Bluetooth_GenericAttributeProfile", "Devices_Bluetooth_Advertisement", "Devices_Radios", "Foundation_Collections", "Foundation", "Storage_Streams"] } windows-future = "0.3.2" [dev-dependencies] diff --git a/README.md b/README.md index 5abfac2a..ee85a81d 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ support. | └ Discover Manufacturer Data | X | X | X | X | | └ Discover Service Data | X | X | X | X | | └ Discover MAC address | X | | X | X | +| Retrieve local adapter address | X | | X | | | GATT Server Connect | X | X | X | X | | GATT Server Connect Event | X | X | X | X | | GATT Server Disconnect | X | X | X | X | @@ -74,6 +75,8 @@ support. #### Scan Filtering on Linux (BlueZ) +The `Central::adapter_address()` API retrieves a local Bluetooth adapter address only where the platform exposes one (currently Linux and Windows). CoreBluetooth and ordinary Android applications return `Ok(None)` because their public APIs provide opaque or privacy-restricted adapter identities. This address is distinct from a discovered peripheral address; `PeripheralId` remains the portable identity for adapters' peripherals. + The `ScanFilter` passed to `start_scan()` behaves differently on Linux than other platforms. btleplug forwards service UUID filters to BlueZ, but BlueZ [merges discovery filters across all D-Bus clients](https://github.com/bluez/bluez/blob/290f9973c9069f293367284e95fd338a221ab90d/doc/org.bluez.Adapter.rst?plain=1#L171-L173). diff --git a/src/api/bdaddr.rs b/src/api/bdaddr.rs index aef1ce3e..fcb5651c 100644 --- a/src/api/bdaddr.rs +++ b/src/api/bdaddr.rs @@ -476,6 +476,11 @@ mod tests { ); } + #[test] + fn zero_u64_to_addr_is_zero_sentinel() { + assert_eq!(BDAddr::try_from(0), Ok(BDAddr::default())); + } + #[test] fn addr_to_u64() { let addr_as_hex: u64 = ADDR.into(); diff --git a/src/api/mod.rs b/src/api/mod.rs index 5986714e..ed5eccd1 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -474,6 +474,28 @@ pub trait Central: Send + Sync + Clone { /// be useful for debug logs. async fn adapter_info(&self) -> Result; + /// Retrieve the Bluetooth address exposed by the local adapter, when available. + /// + /// `Ok(Some(address))` means the platform exposed a usable adapter Bluetooth address. + /// `Ok(None)` means the platform or its public API does not expose one. `Err` means + /// retrieving the adapter metadata failed operationally. This value is optional and is + /// not the portable adapter identity: callers should continue using the platform adapter + /// handle and [`PeripheralId`] for identity and lookup. + /// + /// ```no_run + /// # use btleplug::api::Central as _; + /// # async fn example(adapter: impl btleplug::api::Central) { + /// match adapter.adapter_address().await { + /// Ok(Some(address)) => println!("adapter address: {address}"), + /// Ok(None) => println!("adapter address is unavailable on this platform"), + /// Err(error) => eprintln!("could not query adapter address: {error}"), + /// } + /// # } + /// ``` + async fn adapter_address(&self) -> Result> { + Ok(None) + } + /// Get information about the Bluetooth adapter state. async fn adapter_state(&self) -> Result; } diff --git a/src/bluez/adapter.rs b/src/bluez/adapter.rs index 6ca8a734..f4f40ec9 100644 --- a/src/bluez/adapter.rs +++ b/src/bluez/adapter.rs @@ -1,5 +1,5 @@ use super::peripheral::{Peripheral, PeripheralId}; -use crate::api::{Central, CentralEvent, CentralState, ScanFilter}; +use crate::api::{BDAddr, Central, CentralEvent, CentralState, ScanFilter}; use crate::{Error, Result}; use async_trait::async_trait; use bluez_async::{ @@ -126,6 +126,16 @@ impl Central for Adapter { Ok(format!("{} ({})", adapter_info.id, adapter_info.modalias)) } + async fn adapter_address(&self) -> Result> { + let address: BDAddr = self + .session + .get_adapter_info(&self.adapter) + .await? + .mac_address + .into(); + Ok((address != BDAddr::default()).then_some(address)) + } + async fn adapter_state(&self) -> Result { let mut powered = false; if let Ok(info) = self.session.get_adapter_info(&self.adapter).await { diff --git a/src/corebluetooth/adapter.rs b/src/corebluetooth/adapter.rs index 0c489a28..a2a462fd 100644 --- a/src/corebluetooth/adapter.rs +++ b/src/corebluetooth/adapter.rs @@ -3,7 +3,7 @@ use super::internal::{ run_corebluetooth_thread, }; use super::peripheral::{Peripheral, PeripheralId}; -use crate::api::{Central, CentralEvent, CentralState, ScanFilter}; +use crate::api::{BDAddr, Central, CentralEvent, CentralState, ScanFilter}; use crate::common::adapter_manager::AdapterManager; use crate::{Error, Result}; use async_trait::async_trait; @@ -148,6 +148,11 @@ impl Central for Adapter { Ok("CoreBluetooth".to_string()) } + async fn adapter_address(&self) -> Result> { + // CoreBluetooth exposes opaque UUID identities, not controller addresses. + Ok(None) + } + async fn adapter_state(&self) -> Result { let fut = CoreBluetoothReplyFuture::default(); self.sender diff --git a/src/droidplug/adapter.rs b/src/droidplug/adapter.rs index 91ecaa66..36d49e61 100644 --- a/src/droidplug/adapter.rs +++ b/src/droidplug/adapter.rs @@ -205,6 +205,11 @@ impl Central for Adapter { Ok(()) } + async fn adapter_address(&self) -> Result> { + // Ordinary Android applications cannot access the local factory address. + Ok(None) + } + async fn adapter_state(&self) -> Result { Ok(CentralState::Unknown) } diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index c4cba39d..e1c0b7c1 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -24,7 +24,10 @@ use std::fmt::{self, Debug, Formatter}; use std::pin::Pin; use std::sync::{Arc, Mutex}; use windows::{ - Devices::Radios::{Radio, RadioState}, + Devices::{ + Bluetooth::BluetoothAdapter, + Radios::{Radio, RadioState}, + }, Foundation::TypedEventHandler, }; @@ -34,6 +37,7 @@ pub struct Adapter { watcher: Arc>, manager: Arc>, radio: Radio, + bluetooth_adapter: BluetoothAdapter, } // https://github.com/microsoft/windows-rs/blob/master/crates/libs/windows/src/Windows/Devices/Radios/mod.rs @@ -47,7 +51,7 @@ fn get_central_state(radio: &Radio) -> CentralState { } impl Adapter { - pub(crate) fn new(radio: Radio) -> Result { + pub(crate) fn new(bluetooth_adapter: BluetoothAdapter, radio: Radio) -> Result { let watcher = Arc::new(Mutex::new(BLEWatcher::new()?)); let manager = Arc::new(AdapterManager::default()); @@ -66,6 +70,7 @@ impl Adapter { watcher, manager, radio, + bluetooth_adapter, }) } } @@ -138,6 +143,17 @@ impl Central for Adapter { Ok("WinRT".to_string()) } + async fn adapter_address(&self) -> Result> { + let bluetooth_address = self.bluetooth_adapter.BluetoothAddress().map_err(|error| { + Error::Other(format!("Could not get Bluetooth adapter address: {error:?}").into()) + })?; + if bluetooth_address == 0 { + return Ok(None); + } + let address: BDAddr = bluetooth_address.try_into()?; + Ok(Some(address)) + } + async fn adapter_state(&self) -> Result { Ok(get_central_state(&self.radio)) } diff --git a/src/winrtble/manager.rs b/src/winrtble/manager.rs index b1230208..8198b0a7 100644 --- a/src/winrtble/manager.rs +++ b/src/winrtble/manager.rs @@ -15,7 +15,7 @@ use super::adapter::Adapter; use crate::{Result, api}; use async_trait::async_trait; use std::future::IntoFuture; -use windows::Devices::Radios::{Radio, RadioKind}; +use windows::Devices::{Bluetooth::BluetoothAdapter, Enumeration::DeviceInformation}; /// Implementation of [api::Manager](crate::api::Manager). #[derive(Clone, Debug)] @@ -32,11 +32,19 @@ impl api::Manager for Manager { type Adapter = Adapter; async fn adapters(&self) -> Result> { - let radios = Radio::GetRadiosAsync()?.into_future().await?; - radios - .into_iter() - .filter(|radio| radio.Kind() == Ok(RadioKind::Bluetooth)) - .map(|radio| Adapter::new(radio)) - .collect() + let selector = BluetoothAdapter::GetDeviceSelector()?; + let devices = DeviceInformation::FindAllAsyncAqsFilter(&selector)? + .into_future() + .await?; + let mut adapters = Vec::new(); + for device in devices { + let device_id = device.Id()?; + let bluetooth_adapter = BluetoothAdapter::FromIdAsync(&device_id)? + .into_future() + .await?; + let radio = bluetooth_adapter.GetRadioAsync()?.into_future().await?; + adapters.push(Adapter::new(bluetooth_adapter, radio)?); + } + Ok(adapters) } } diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md index 9ab2e8ee..b04aae87 100644 --- a/tests/CLAUDE.md +++ b/tests/CLAUDE.md @@ -33,7 +33,8 @@ Each test is its own file (and therefore its own binary), ensuring process isola ## Contracts -- Every test file uses `find_and_connect()` from `peripheral_finder.rs` to get a connected peripheral with services discovered. +- Every peripheral-backed test file uses `find_and_connect()` from `peripheral_finder.rs` to get a connected peripheral with services discovered. +- Adapter-only scenarios may use `get_adapter()` without `find_and_connect()`; they still require local adapter hardware but do not require the btleplug test peripheral. - Tests that mutate peripheral state must call `reset_peripheral()` in setup to ensure clean state. - Control commands are sent via the Control Point characteristic (UUID `00000101-...`) using `send_control_command()`. - The env var `BTLEPLUG_TEST_PERIPHERAL` overrides the default peripheral name (`btleplug-test`). @@ -51,3 +52,4 @@ Each test is its own file (and therefore its own binary), ensuring process isola - Tests must not depend on execution order; each test connects independently. - The scan timeout is 10 seconds (hardcoded in `peripheral_finder.rs`). - When adding a new test, also add the corresponding JNI export in `android/rust/src/lib.rs`, native declaration in `NativeTests.kt`, and `@Test` in `BleIntegrationTest.kt`. +- Adapter-only tests require local adapter hardware but do not require the btleplug test peripheral; their desktop assertions are target-specific because CoreBluetooth and ordinary Android intentionally return `Ok(None)`. diff --git a/tests/android/rust/src/lib.rs b/tests/android/rust/src/lib.rs index 3378f7ea..b7360838 100644 --- a/tests/android/rust/src/lib.rs +++ b/tests/android/rust/src/lib.rs @@ -101,6 +101,11 @@ pub extern "system" fn Java_com_nonpolynomial_btleplug_test_NativeTests_initBtle } // ── Test JNI exports ──────────────────────────────────────────────── + +jni_test!( + Java_com_nonpolynomial_btleplug_test_NativeTests_testAdapterAddress, + test_cases::test_adapter_address +); // // Each function follows the JNI naming convention: // Java_com_nonpolynomial_btleplug_test_NativeTests_ diff --git a/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt b/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt index ccb5dd7f..fecbca7a 100644 --- a/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt +++ b/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt @@ -62,6 +62,9 @@ class BleIntegrationTest { } } + // ── Adapter capabilities ──────────────────────────────────────── + @Test fun testAdapterAddress() = NativeTests.testAdapterAddress() + // ── Discovery ─────────────────────────────────────────────────── @Test fun testDiscoverPeripheralByName() = NativeTests.testDiscoverPeripheralByName() @Test fun testDiscoverServices() = NativeTests.testDiscoverServices() diff --git a/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt b/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt index ab43ee4e..2b174886 100644 --- a/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt +++ b/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt @@ -4,6 +4,9 @@ package com.nonpolynomial.btleplug.test object NativeTests { external fun initBtleplug() + // Adapter capabilities + external fun testAdapterAddress() + // Discovery external fun testDiscoverPeripheralByName() external fun testDiscoverServices() diff --git a/tests/common/test_cases.rs b/tests/common/test_cases.rs index 5915f5e4..4791c312 100644 --- a/tests/common/test_cases.rs +++ b/tests/common/test_cases.rs @@ -8,6 +8,39 @@ use btleplug::api::Peripheral as _; use super::gatt_uuids; use super::peripheral_finder; +// ── Adapter capabilities ───────────────────────────────────────────── + +pub async fn test_adapter_address() { + use btleplug::api::{BDAddr, Central}; + + let adapter = peripheral_finder::get_adapter().await; + let result = adapter + .adapter_address() + .await + .expect("Failed to get adapter address"); + match &result { + Some(address) => assert_ne!( + *address, + BDAddr::default(), + "adapter address must be nonzero" + ), + None => { + #[cfg(any(target_os = "linux", target_os = "windows"))] + panic!("adapter address is unavailable on a supported desktop platform"); + #[cfg(any(target_vendor = "apple", target_os = "android"))] + return; + } + } + + #[cfg(target_os = "linux")] + if let Ok(expected) = std::env::var("BTLEPLUG_TEST_ADAPTER_ADDRESS") { + let expected = expected + .parse() + .expect("invalid BTLEPLUG_TEST_ADAPTER_ADDRESS"); + assert_eq!(Some(expected), result); + } +} + // ── Discovery ─────────────────────────────────────────────────────── pub async fn test_discover_peripheral_by_name() { diff --git a/tests/documentation.rs b/tests/documentation.rs new file mode 100644 index 00000000..4bc6e2e3 --- /dev/null +++ b/tests/documentation.rs @@ -0,0 +1,17 @@ +#[test] +fn readme_distinguishes_adapter_and_peripheral_addresses() { + let readme = include_str!("../README.md"); + assert!(readme.contains("Retrieve local adapter address")); + assert!(readme.contains( + "| Retrieve local adapter address | X | | X | |" + )); + assert!(readme.contains("Discover MAC address")); + assert!(readme.contains("PeripheralId")); + assert!(readme.contains("distinct from a discovered peripheral address")); +} + +#[test] +fn central_adapter_address_default_is_source_compatible() { + fn assert_default() {} + assert_default::(); +} diff --git a/tests/test_adapter_address.rs b/tests/test_adapter_address.rs new file mode 100644 index 00000000..f027c4df --- /dev/null +++ b/tests/test_adapter_address.rs @@ -0,0 +1,7 @@ +mod common; + +#[tokio::test] +#[ignore = "requires a local Bluetooth adapter"] +async fn test_adapter_address() { + common::test_cases::test_adapter_address().await; +} From 81ceb823c5ce9cf14f38de7ffaff1b976312ea13 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 21:10:07 -0700 Subject: [PATCH 24/77] finish peripheral retrieval on dev Complete the retrieval implementation introduced by PR #448, carrying forward reusable mechanics from PR #437 while preserving dev's newer adapter-address and Android infrastructure changes. --- src/api/mod.rs | 174 +++++++++++++++++- src/bluez/adapter.rs | 36 +++- src/corebluetooth/adapter.rs | 99 +++++++++- src/corebluetooth/internal.rs | 101 +++++++++- src/winrtble/adapter.rs | 173 ++++++++++++++++- src/winrtble/peripheral.rs | 6 + tests/android/rust/src/lib.rs | 4 + .../btleplug/test/BleIntegrationTest.kt | 3 + .../btleplug/test/NativeTests.kt | 3 + tests/common/test_cases.rs | 38 ++++ ...etrieve_connected_peripheral_by_service.rs | 7 + 11 files changed, 625 insertions(+), 19 deletions(-) create mode 100644 tests/test_retrieve_connected_peripheral_by_service.rs diff --git a/src/api/mod.rs b/src/api/mod.rs index ed5eccd1..2c2a2945 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -33,8 +33,9 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "serde")] use serde_cr as serde; use std::{ - collections::{BTreeSet, HashMap}, + collections::{BTreeSet, HashMap, HashSet}, fmt::{self, Debug, Display, Formatter}, + hash::Hash, pin::Pin, time::Duration, }; @@ -217,6 +218,86 @@ pub struct ScanFilter { pub services: Vec, } +/// Selects peripherals for [`Central::retrieve_peripherals`]. +/// +/// `None` leaves a selector unspecified; an explicitly empty selector matches nothing. Values +/// within a selector are OR'ed, while the identifier and service selectors are combined as a +/// union. Returned peripherals retain backend order and are deduplicated by identifier. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RetrievePeripheralsOptions { + /// Known peripheral identifiers to retrieve. + pub identifiers: Option>, + /// Service UUIDs used to retrieve connected peripherals. + pub services: Option>, +} + +impl Default for RetrievePeripheralsOptions { + fn default() -> Self { + Self { + identifiers: None, + services: None, + } + } +} + +/// Returns whether a candidate identifier is included in an identifier selector. +pub(crate) fn matches_identifier(candidate: &T, requested: &[T]) -> bool { + requested.iter().any(|requested| requested == candidate) +} + +/// Returns whether a candidate service set contains one of the requested services. +pub(crate) fn matches_service(candidate_services: &[Uuid], requested: &[Uuid]) -> bool { + requested + .iter() + .any(|requested| candidate_services.contains(requested)) +} + +/// Returns whether a candidate matches either supplied selector. +/// +/// A selector is considered supplied even when empty; in that case it matches nothing. +pub(crate) fn matches_retrieval_selectors( + candidate_id: &PeripheralId, + candidate_services: &[Uuid], + options: &RetrievePeripheralsOptions, +) -> bool { + let id_match = options + .identifiers + .as_deref() + .is_some_and(|requested| matches_identifier(candidate_id, requested)); + let service_match = options + .services + .as_deref() + .is_some_and(|requested| matches_service(candidate_services, requested)); + + if options.identifiers.is_none() && options.services.is_none() { + true + } else { + id_match || service_match + } +} + +/// Merges retrieved peripherals while preserving the first occurrence of each identifier. +pub(crate) fn merge_retrieved_peripherals( + peripherals: impl IntoIterator, + id: F, +) -> Vec

+where + K: Eq + Hash, + F: Fn(&P) -> K, +{ + let mut seen = HashSet::new(); + peripherals + .into_iter() + .filter(|peripheral| seen.insert(id(peripheral))) + .collect() +} + +fn unsupported_retrieve_peripherals

() -> Result> { + Err(crate::Error::NotSupported( + "retrieve_peripherals".to_string(), + )) +} + /// Current BLE connection parameters as reported by the OS. #[derive(Debug, Clone, Copy, PartialEq)] pub struct ConnectionParameters { @@ -457,6 +538,22 @@ pub trait Central: Send + Sync + Clone { /// may contain peripherals that are no longer available. async fn peripherals(&self) -> Result>; + /// Retrieves peripherals from the backend's connected-device or known-device source. + /// + /// Selectors are combined as a union: a peripheral is returned when its identifier matches + /// any requested identifier or its backend-reported services contain any requested service. + /// Results are in backend order and deduplicated by [`Peripheral::id`]. An explicitly empty + /// selector matches nothing. Backends without a retrieval source return + /// [`Error::NotSupported`](crate::Error::NotSupported) with `"retrieve_peripherals"`. + async fn retrieve_peripherals( + &self, + _options: RetrievePeripheralsOptions, + ) -> Result> { + Err(crate::Error::NotSupported( + "retrieve_peripherals".to_string(), + )) + } + /// Returns a particular [`Peripheral`] by its address if it has been discovered. async fn peripheral(&self, id: &PeripheralId) -> Result; @@ -500,7 +597,80 @@ pub trait Central: Send + Sync + Clone { async fn adapter_state(&self) -> Result; } -/// The Manager is the entry point to the library, providing access to all the Bluetooth adapters on +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn retrieve_options_are_publicly_constructible() { + let options = RetrievePeripheralsOptions { + identifiers: Some(Vec::new()), + services: Some(vec![Uuid::nil()]), + }; + assert_eq!(options.services, Some(vec![Uuid::nil()])); + assert_eq!(options.identifiers, Some(Vec::new())); + } + + #[test] + fn retrieve_options_default_is_explicit() { + assert_eq!(RetrievePeripheralsOptions::default().identifiers, None); + assert_eq!(RetrievePeripheralsOptions::default().services, None); + } + + #[test] + fn retrieve_empty_identifier_selector_matches_nothing() { + assert!(!matches_identifier(&1_u8, &[])); + } + + #[test] + fn retrieve_empty_service_selector_matches_nothing() { + assert!(!matches_service(&[Uuid::nil()], &[])); + } + + #[test] + fn retrieve_selector_matching_uses_any_value() { + assert!(matches_identifier(&2_u8, &[1, 2, 3])); + assert!(matches_service( + &[Uuid::nil()], + &[Uuid::from_u128(1), Uuid::nil()], + )); + } + + #[test] + fn retrieve_unknown_identifiers_are_omitted() { + let requested = [1_u8, 2_u8]; + assert!(!matches_identifier(&3, &requested)); + } + + #[test] + fn retrieve_order_is_preserved() { + let merged = merge_retrieved_peripherals([3_u8, 1, 2], |value| *value); + assert_eq!(merged, vec![3, 1, 2]); + } + + #[test] + fn retrieve_combined_selectors_use_union() { + assert!(matches_service(&[Uuid::nil()], &[Uuid::nil()])); + assert!(!matches_service(&[], &[Uuid::nil()])); + } + + #[test] + fn retrieve_results_are_deduplicated() { + let merged = merge_retrieved_peripherals([1_u8, 2, 1, 3, 2], |value| *value); + assert_eq!(merged, vec![1, 2, 3]); + } + + #[test] + fn retrieve_peripherals_default_is_not_supported() { + let error = unsupported_retrieve_peripherals::().unwrap_err(); + assert!(matches!( + error, + crate::Error::NotSupported(operation) if operation == "retrieve_peripherals" + )); + } +} + +/// The Manager is the entry point for the library, providing access to all Bluetooth adapters on /// the system. You can obtain an instance from [`platform::Manager::new()`](crate::platform::Manager::new). /// /// ## Usage diff --git a/src/bluez/adapter.rs b/src/bluez/adapter.rs index f4f40ec9..309e28f3 100644 --- a/src/bluez/adapter.rs +++ b/src/bluez/adapter.rs @@ -1,5 +1,7 @@ use super::peripheral::{Peripheral, PeripheralId}; -use crate::api::{BDAddr, Central, CentralEvent, CentralState, ScanFilter}; +use crate::api::{ + self, BDAddr, Central, CentralEvent, CentralState, RetrievePeripheralsOptions, ScanFilter, +}; use crate::{Error, Result}; use async_trait::async_trait; use bluez_async::{ @@ -29,6 +31,22 @@ fn get_central_state(powered: bool) -> CentralState { } } +fn matches_retrieval_options( + candidate_id: &bluez_async::DeviceId, + candidate_services: &[uuid::Uuid], + connected: bool, + options: &RetrievePeripheralsOptions, +) -> bool { + let candidate_id = PeripheralId(candidate_id.clone()); + let service_match = connected && options.services.is_some(); + let services = if service_match { + candidate_services + } else { + &[] + }; + api::matches_retrieval_selectors(&candidate_id, services, options) +} + #[async_trait] impl Central for Adapter { type Peripheral = Peripheral; @@ -99,6 +117,22 @@ impl Central for Adapter { .collect()) } + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result> { + let devices = self.session.get_devices_on_adapter(&self.adapter).await?; + let devices = devices.into_iter().filter(|device| { + matches_retrieval_options(&device.id, &device.services, device.connected, &options) + }); + let devices = api::merge_retrieved_peripherals(devices, |device| device.id.clone()); + + Ok(devices + .into_iter() + .map(|device| Peripheral::new(self.session.clone(), device)) + .collect()) + } + async fn peripheral(&self, id: &PeripheralId) -> Result { let device = self.session.get_device_info(&id.0).await.map_err(|e| { if let BluetoothError::DbusError(_) = e { diff --git a/src/corebluetooth/adapter.rs b/src/corebluetooth/adapter.rs index a2a462fd..fb65e8a1 100644 --- a/src/corebluetooth/adapter.rs +++ b/src/corebluetooth/adapter.rs @@ -3,7 +3,10 @@ use super::internal::{ run_corebluetooth_thread, }; use super::peripheral::{Peripheral, PeripheralId}; -use crate::api::{BDAddr, Central, CentralEvent, CentralState, ScanFilter}; +use crate::api::{ + BDAddr, Central, CentralEvent, CentralState, Peripheral as PeripheralTrait, + RetrievePeripheralsOptions, ScanFilter, +}; use crate::common::adapter_manager::AdapterManager; use crate::{Error, Result}; use async_trait::async_trait; @@ -12,6 +15,7 @@ use futures::sink::SinkExt; use futures::stream::{Stream, StreamExt}; use log::*; use objc2_core_bluetooth::CBManagerState; +use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; use tokio::task; @@ -53,6 +57,7 @@ impl Adapter { let manager_clone = manager.clone(); let adapter_sender_clone = adapter_sender.clone(); task::spawn(async move { + let mut handles = HashMap::new(); while let Some(msg) = receiver.next().await { match msg { CoreBluetoothEvent::DeviceDiscovered { @@ -61,15 +66,58 @@ impl Adapter { advertisement_name, event_receiver, } => { - manager_clone.add_peripheral(Peripheral::new( - uuid, - local_name, - advertisement_name, - Arc::downgrade(&manager_clone), - event_receiver, - adapter_sender_clone.clone(), - )); - manager_clone.emit(CentralEvent::DeviceDiscovered(uuid.into())); + if manager_clone.peripheral(&uuid.into()).is_none() { + let peripheral = Peripheral::new( + uuid, + local_name, + advertisement_name, + Arc::downgrade(&manager_clone), + event_receiver, + adapter_sender_clone.clone(), + ); + handles.insert(peripheral.id(), peripheral.clone()); + manager_clone.add_peripheral(peripheral); + manager_clone.emit(CentralEvent::DeviceDiscovered(uuid.into())); + } + } + CoreBluetoothEvent::RetrievedPeripherals { + peripherals, + future, + } => { + let mut result = Vec::with_capacity(peripherals.len()); + for retrieved in peripherals { + let id = retrieved.uuid.into(); + let peripheral = if let Some(peripheral) = handles.get(&id).cloned() { + peripheral.update_name( + retrieved.local_name.clone(), + retrieved.advertisement_name.clone(), + ); + peripheral + } else if let Some(event_receiver) = retrieved.event_receiver { + let peripheral = Peripheral::new( + retrieved.uuid, + retrieved.local_name, + retrieved.advertisement_name, + Arc::downgrade(&manager_clone), + event_receiver, + adapter_sender_clone.clone(), + ); + handles.insert(id.clone(), peripheral.clone()); + peripheral + } else { + continue; + }; + + if manager_clone.peripheral(&id).is_none() { + manager_clone.add_peripheral(peripheral.clone()); + manager_clone.emit(CentralEvent::DeviceDiscovered(id)); + } + result.push(peripheral); + } + future + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Peripherals(result)); } CoreBluetoothEvent::DeviceUpdated { uuid, @@ -83,6 +131,7 @@ impl Adapter { } } CoreBluetoothEvent::DeviceDisconnected { uuid } => { + handles.remove(&uuid.into()); manager_clone.emit(CentralEvent::DeviceDisconnected(uuid.into())); } CoreBluetoothEvent::DidUpdateState { state } => { @@ -128,6 +177,36 @@ impl Central for Adapter { Ok(self.manager.peripherals()) } + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result> { + if options.identifiers.is_none() && options.services.is_none() { + return Err(Error::NotSupported("retrieve_peripherals".to_string())); + } + if options.identifiers.as_ref().is_some_and(Vec::is_empty) + && options.services.as_ref().is_none_or(Vec::is_empty) + { + return Ok(Vec::new()); + } + let fut = CoreBluetoothReplyFuture::default(); + self.sender + .to_owned() + .send(CoreBluetoothMessage::RetrievePeripherals { + options, + future: fut.get_state_clone(), + }) + .await?; + match fut.await { + CoreBluetoothReply::Peripherals(peripherals) => Ok(peripherals), + CoreBluetoothReply::Err(msg) => Err(Error::RuntimeError(msg)), + CoreBluetoothReply::Ok => Ok(Vec::new()), + _ => Err(Error::RuntimeError( + "Unexpected CoreBluetooth retrieval reply".to_string(), + )), + } + } + async fn peripheral(&self, id: &PeripheralId) -> Result { self.manager.peripheral(id).ok_or(Error::DeviceNotFound) } diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index c0f42aa6..a691fb3c 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -12,13 +12,17 @@ use super::{ central_delegate::{CentralDelegate, CentralDelegateEvent}, ffi, future::{BtlePlugFuture, BtlePlugFutureStateShared}, + peripheral::Peripheral, utils::{ core_bluetooth::{cbuuid_to_uuid, uuid_to_cbuuid}, nsuuid_to_uuid, }, }; use crate::Error; -use crate::api::{CharPropFlags, Characteristic, Descriptor, ScanFilter, Service, WriteType}; +use crate::api::{ + CharPropFlags, Characteristic, Descriptor, RetrievePeripheralsOptions, ScanFilter, Service, + WriteType, +}; use futures::channel::mpsc::{self, Receiver, Sender}; use futures::select; use futures::sink::SinkExt; @@ -31,7 +35,7 @@ use objc2_core_bluetooth::{ CBCharacteristicProperties, CBCharacteristicWriteType, CBDescriptor, CBManager, CBManagerAuthorization, CBManagerState, CBPeripheral, CBPeripheralState, CBService, CBUUID, }; -use objc2_foundation::{NSArray, NSData, NSMutableDictionary, NSNumber}; +use objc2_foundation::{NSArray, NSData, NSMutableDictionary, NSNumber, NSUUID}; use std::{ collections::{BTreeSet, HashMap, VecDeque}, ffi::CString, @@ -162,6 +166,7 @@ pub enum CoreBluetoothReply { ServicesDiscovered(BTreeSet), State(CBPeripheralState), Ok, + Peripherals(Vec), Err(String), } @@ -499,6 +504,18 @@ pub enum CoreBluetoothMessage { peripheral_uuid: Uuid, future: CoreBluetoothReplyStateShared, }, + RetrievePeripherals { + options: RetrievePeripheralsOptions, + future: CoreBluetoothReplyStateShared, + }, +} + +#[derive(Debug)] +pub struct RetrievedPeripheral { + pub uuid: Uuid, + pub local_name: Option, + pub advertisement_name: Option, + pub event_receiver: Option>, } #[derive(Debug)] @@ -512,6 +529,10 @@ pub enum CoreBluetoothEvent { advertisement_name: Option, event_receiver: Receiver, }, + RetrievedPeripherals { + peripherals: Vec, + future: CoreBluetoothReplyStateShared, + }, DeviceUpdated { uuid: Uuid, local_name: Option, @@ -1297,6 +1318,79 @@ impl CoreBluetoothInternal { } } + async fn retrieve_peripherals( + &mut self, + options: RetrievePeripheralsOptions, + future: CoreBluetoothReplyStateShared, + ) { + if options.identifiers.is_none() && options.services.is_none() { + future.lock().unwrap().set_reply(CoreBluetoothReply::Err( + "retrieve_peripherals requires an identifier or service selector".to_string(), + )); + return; + } + let mut retrieved = Vec::new(); + if let Some(services) = options.services.filter(|services| !services.is_empty()) { + let services = NSArray::from_vec(services.into_iter().map(uuid_to_cbuuid).collect()); + retrieved.extend(unsafe { + self.manager + .retrieveConnectedPeripheralsWithServices(&services) + }); + } + if let Some(identifiers) = options + .identifiers + .filter(|identifiers| !identifiers.is_empty()) + { + let identifiers = NSArray::from_vec( + identifiers + .into_iter() + .map(|id| { + NSUUID::from_string(&objc2_foundation::NSString::from_str(&id.to_string())) + .unwrap() + }) + .collect(), + ); + retrieved.extend(unsafe { + self.manager + .retrievePeripheralsWithIdentifiers(&identifiers) + }); + } + let mut peripherals = Vec::new(); + for peripheral in retrieved { + let identifier = unsafe { peripheral.identifier() }; + let uuid = nsuuid_to_uuid(&identifier); + if peripherals + .iter() + .any(|retrieved: &RetrievedPeripheral| retrieved.uuid == uuid) + { + continue; + } + + let peripheral_name = unsafe { peripheral.name() }; + let local_name = peripheral_name.map(|name| name.to_string()); + let event_receiver = if let Some(existing) = self.peripherals.get_mut(&uuid) { + existing.peripheral = peripheral; + None + } else { + let (event_sender, event_receiver) = mpsc::channel(256); + self.peripherals + .insert(uuid, PeripheralInternal::new(peripheral, event_sender)); + Some(event_receiver) + }; + peripherals.push(RetrievedPeripheral { + uuid, + local_name, + advertisement_name: None, + event_receiver, + }); + } + self.dispatch_event(CoreBluetoothEvent::RetrievedPeripherals { + peripherals, + future, + }) + .await; + } + async fn wait_for_message(&mut self) { select! { delegate_msg = self.delegate_receiver.select_next_some() => { @@ -1438,6 +1532,9 @@ impl CoreBluetoothInternal { CoreBluetoothMessage::ReadRssi{peripheral_uuid, future} => { self.read_rssi(peripheral_uuid, future) } + CoreBluetoothMessage::RetrievePeripherals { options, future } => { + self.retrieve_peripherals(options, future).await + } }; } } diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index e1c0b7c1..48f9ca12 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -14,18 +14,25 @@ use super::{ble::watcher::BLEWatcher, peripheral::Peripheral, peripheral::PeripheralId}; use crate::{ Error, Result, - api::{BDAddr, Central, CentralEvent, CentralState, ScanFilter}, + api::{ + self, BDAddr, Central, CentralEvent, CentralState, RetrievePeripheralsOptions, ScanFilter, + }, common::adapter_manager::AdapterManager, }; use async_trait::async_trait; use futures::stream::Stream; -use std::convert::TryInto; +use std::convert::TryFrom; use std::fmt::{self, Debug, Formatter}; +use std::future::IntoFuture; use std::pin::Pin; use std::sync::{Arc, Mutex}; use windows::{ Devices::{ - Bluetooth::BluetoothAdapter, + Bluetooth::{ + BluetoothAdapter, BluetoothCacheMode, BluetoothLEDevice, + GenericAttributeProfile::GattCommunicationStatus, + }, + Enumeration::DeviceInformation, Radios::{Radio, RadioState}, }, Foundation::TypedEventHandler, @@ -41,6 +48,14 @@ pub struct Adapter { } // https://github.com/microsoft/windows-rs/blob/master/crates/libs/windows/src/Windows/Devices/Radios/mod.rs +fn winrt_error(error: E) -> Error { + Error::Other(format!("{error:?}").into()) +} + +fn checked_address(value: u64) -> Result { + BDAddr::try_from(value).map_err(Error::from) +} + fn get_central_state(radio: &Radio) -> CentralState { let state = radio.State().unwrap_or(RadioState::Unknown); match state { @@ -75,6 +90,49 @@ impl Adapter { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn checked_address_rejects_values_outside_six_bytes() { + assert!(checked_address(0x11_22_33_44_55_66_77).is_err()); + } + + #[test] + fn checked_address_preserves_windows_address_order() { + assert_eq!( + checked_address(0x11_22_33_44_55_66).unwrap().into_inner(), + [0x11, 0x22, 0x33, 0x44, 0x55, 0x66] + ); + } + + #[test] + fn retrieve_selector_union_matches_identifier_or_service() { + let id = PeripheralId::from(BDAddr::from([1, 2, 3, 4, 5, 6])); + let options = RetrievePeripheralsOptions { + identifiers: Some(vec![id.clone()]), + services: Some(vec![uuid::Uuid::nil()]), + }; + assert!(api::matches_retrieval_selectors(&id, &[], &options)); + assert!(api::matches_retrieval_selectors( + &PeripheralId::from(BDAddr::from([6, 5, 4, 3, 2, 1])), + &[uuid::Uuid::nil()], + &options + )); + } + + #[test] + fn retrieve_selector_empty_values_match_nothing() { + let id = PeripheralId::from(BDAddr::from([1, 2, 3, 4, 5, 6])); + let options = RetrievePeripheralsOptions { + identifiers: Some(vec![]), + services: None, + }; + assert!(!api::matches_retrieval_selectors(&id, &[], &options)); + } +} + impl Debug for Adapter { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.debug_struct("Adapter") @@ -98,7 +156,7 @@ impl Central for Adapter { filter, Box::new(move |args| { let bluetooth_address = args.BluetoothAddress()?; - let address: BDAddr = bluetooth_address.try_into().unwrap(); + let address = checked_address(bluetooth_address)?; if let Some(mut entry) = manager.peripheral_mut(&address.into()) { entry.value_mut().update_properties(args); manager.emit(CentralEvent::DeviceUpdated(address.into())); @@ -123,6 +181,113 @@ impl Central for Adapter { Ok(self.manager.peripherals()) } + /// Retrieves connected BLE devices from the Windows device enumeration service. + /// + /// WinRT's connected-device selector is system-wide and cannot be restricted to + /// this `Radio`; callers must treat results as belonging to the Windows BLE + /// subsystem rather than to one physical adapter when multiple radios exist. + async fn retrieve_peripherals( + &self, + options: RetrievePeripheralsOptions, + ) -> Result> { + // Identifier-only retrieval must not use the connected-device selector: it is + // intentionally independent of enumeration, and preserves the requested ID order. + if options.identifiers.is_some() && options.services.is_none() { + let mut result = Vec::new(); + for requested_id in options.identifiers.as_deref().unwrap_or_default() { + let async_operation = match BluetoothLEDevice::FromBluetoothAddressAsync( + requested_id.address().into(), + ) { + Ok(async_operation) => async_operation, + // Unknown cached IDs are omitted, not errors. + Err(_) => continue, + }; + let device = match async_operation.into_future().await { + Ok(device) => device, + // Disconnected cached IDs are omitted, not errors. + Err(_) => continue, + }; + if device.ConnectionStatus().map_err(winrt_error)? + != windows::Devices::Bluetooth::BluetoothConnectionStatus::Connected + { + continue; + } + let address = checked_address(device.BluetoothAddress().map_err(winrt_error)?)?; + let peripheral = self + .manager + .peripheral(&PeripheralId::from(address)) + .unwrap_or_else(|| { + let peripheral = Peripheral::new(Arc::downgrade(&self.manager), address); + self.manager.add_peripheral(peripheral.clone()); + peripheral + }); + result.push(peripheral); + } + return Ok(api::merge_retrieved_peripherals(result, |peripheral| { + crate::api::Peripheral::id(peripheral) + })); + } + + // Service and combined retrieval use WinRT's connected-device enumeration. + let selector = BluetoothLEDevice::GetDeviceSelectorFromConnectionStatus( + windows::Devices::Bluetooth::BluetoothConnectionStatus::Connected, + ) + .map_err(winrt_error)?; + let devices = DeviceInformation::FindAllAsyncAqsFilter(&selector) + .map_err(winrt_error)? + .into_future() + .await + .map_err(winrt_error)?; + let mut result = Vec::new(); + + for info in devices { + let id = info.Id().map_err(winrt_error)?; + let device = BluetoothLEDevice::FromIdAsync(&id) + .map_err(winrt_error)? + .into_future() + .await + .map_err(winrt_error)?; + let address = checked_address(device.BluetoothAddress().map_err(winrt_error)?)?; + let candidate_id = PeripheralId::from(address); + + let service_result = device + .GetGattServicesWithCacheModeAsync(BluetoothCacheMode::Cached) + .map_err(winrt_error)? + .into_future() + .await + .map_err(winrt_error)?; + let service_uuids = if service_result.Status().map_err(winrt_error)? + == GattCommunicationStatus::Success + { + service_result + .Services() + .map_err(winrt_error)? + .into_iter() + .map(|service| { + service + .Uuid() + .map(|uuid| crate::winrtble::utils::to_uuid(&uuid)) + }) + .collect::>>() + .map_err(winrt_error)? + } else { + Vec::new() + }; + if !api::matches_retrieval_selectors(&candidate_id, &service_uuids, &options) { + continue; + } + let peripheral = self.manager.peripheral(&candidate_id).unwrap_or_else(|| { + let peripheral = Peripheral::new(Arc::downgrade(&self.manager), address); + self.manager.add_peripheral(peripheral.clone()); + peripheral + }); + result.push(peripheral); + } + Ok(api::merge_retrieved_peripherals(result, |peripheral| { + crate::api::Peripheral::id(peripheral) + })) + } + async fn peripheral(&self, id: &PeripheralId) -> Result { self.manager.peripheral(id).ok_or(Error::DeviceNotFound) } diff --git a/src/winrtble/peripheral.rs b/src/winrtble/peripheral.rs index 7924d678..22796fd2 100644 --- a/src/winrtble/peripheral.rs +++ b/src/winrtble/peripheral.rs @@ -57,6 +57,12 @@ use windows::core::GUID; #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct PeripheralId(BDAddr); +impl PeripheralId { + pub(crate) fn address(&self) -> BDAddr { + self.0 + } +} + impl Display for PeripheralId { fn fmt(&self, f: &mut Formatter) -> fmt::Result { Display::fmt(&self.0, f) diff --git a/tests/android/rust/src/lib.rs b/tests/android/rust/src/lib.rs index b7360838..c2875787 100644 --- a/tests/android/rust/src/lib.rs +++ b/tests/android/rust/src/lib.rs @@ -147,6 +147,10 @@ jni_test!( Java_com_nonpolynomial_btleplug_test_NativeTests_testAdvertisementServices, test_cases::test_advertisement_services ); +jni_test!( + Java_com_nonpolynomial_btleplug_test_NativeTests_testRetrievePeripheralsNotSupported, + test_cases::test_retrieve_peripherals_not_supported +); jni_test!( Java_com_nonpolynomial_btleplug_test_NativeTests_testConnectAndDisconnect, test_cases::test_connect_and_disconnect diff --git a/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt b/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt index fecbca7a..a8656e6d 100644 --- a/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt +++ b/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt @@ -73,6 +73,9 @@ class BleIntegrationTest { @Test fun testAdvertisementManufacturerData() = NativeTests.testAdvertisementManufacturerData() @Test fun testAdvertisementServices() = NativeTests.testAdvertisementServices() + // ── Retrieval ──────────────────────────────────────────────────── + @Test fun testRetrievePeripheralsNotSupported() = NativeTests.testRetrievePeripheralsNotSupported() + // ── Connection ────────────────────────────────────────────────── @Test fun testConnectAndDisconnect() = NativeTests.testConnectAndDisconnect() @Test fun testReconnectAfterDisconnect() = NativeTests.testReconnectAfterDisconnect() diff --git a/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt b/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt index 2b174886..2a9448d7 100644 --- a/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt +++ b/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt @@ -15,6 +15,9 @@ object NativeTests { external fun testAdvertisementManufacturerData() external fun testAdvertisementServices() + // Retrieval + external fun testRetrievePeripheralsNotSupported() + // Connection external fun testConnectAndDisconnect() external fun testReconnectAfterDisconnect() diff --git a/tests/common/test_cases.rs b/tests/common/test_cases.rs index 4791c312..a51d31fb 100644 --- a/tests/common/test_cases.rs +++ b/tests/common/test_cases.rs @@ -192,6 +192,44 @@ pub async fn test_advertisement_services() { ); } +// ── Retrieval ────────────────────────────────────────────────────── + +pub async fn test_retrieve_peripherals_not_supported() { + use btleplug::api::{Central, RetrievePeripheralsOptions}; + + let adapter = peripheral_finder::get_adapter().await; + let error = adapter + .retrieve_peripherals(RetrievePeripheralsOptions::default()) + .await + .expect_err("retrieval without selectors should not be supported on Android"); + assert!(matches!( + error, + btleplug::Error::NotSupported(operation) if operation == "retrieve_peripherals" + )); +} + +pub async fn test_retrieve_connected_peripheral_by_service() { + use btleplug::api::{Central, RetrievePeripheralsOptions}; + + let adapter = peripheral_finder::get_adapter().await; + let expected = peripheral_finder::find_and_connect().await; + let expected_id = expected.id(); + let retrieved = adapter + .retrieve_peripherals(RetrievePeripheralsOptions { + identifiers: None, + services: Some(vec![gatt_uuids::CONTROL_SERVICE]), + }) + .await + .expect("retrieval by service should be supported on desktop backends"); + assert!( + retrieved + .iter() + .any(|peripheral| peripheral.id() == expected_id), + "connected test peripheral was not returned by service retrieval" + ); + expected.disconnect().await.unwrap(); +} + // ── Connection ────────────────────────────────────────────────────── pub async fn test_connect_and_disconnect() { diff --git a/tests/test_retrieve_connected_peripheral_by_service.rs b/tests/test_retrieve_connected_peripheral_by_service.rs new file mode 100644 index 00000000..e5d3db11 --- /dev/null +++ b/tests/test_retrieve_connected_peripheral_by_service.rs @@ -0,0 +1,7 @@ +mod common; + +#[tokio::test] +#[ignore = "requires BLE test peripheral"] +async fn test_retrieve_connected_peripheral_by_service() { + common::test_cases::test_retrieve_connected_peripheral_by_service().await; +} From 6d9946a66ff661cf5e4e22a986b2c6c8f4f048eb Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 21:21:15 -0700 Subject: [PATCH 25/77] chore: Clippy fix run --- src/api/mod.rs | 9 +- src/corebluetooth/central_delegate.rs | 2 +- src/corebluetooth/internal.rs | 131 ++++++++++---------------- src/corebluetooth/peripheral.rs | 2 +- 4 files changed, 55 insertions(+), 89 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 2c2a2945..e6995a53 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -224,6 +224,7 @@ pub struct ScanFilter { /// within a selector are OR'ed, while the identifier and service selectors are combined as a /// union. Returned peripherals retain backend order and are deduplicated by identifier. #[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Default)] pub struct RetrievePeripheralsOptions { /// Known peripheral identifiers to retrieve. pub identifiers: Option>, @@ -231,14 +232,6 @@ pub struct RetrievePeripheralsOptions { pub services: Option>, } -impl Default for RetrievePeripheralsOptions { - fn default() -> Self { - Self { - identifiers: None, - services: None, - } - } -} /// Returns whether a candidate identifier is included in an identifier selector. pub(crate) fn matches_identifier(candidate: &T, requested: &[T]) -> bool { diff --git a/src/corebluetooth/central_delegate.rs b/src/corebluetooth/central_delegate.rs index eb626c2e..73498b91 100644 --- a/src/corebluetooth/central_delegate.rs +++ b/src/corebluetooth/central_delegate.rs @@ -789,7 +789,7 @@ declare_class!( service_uuid, characteristic_uuid, descriptor_uuid, - data: get_descriptor_value(&descriptor), + data: get_descriptor_value(descriptor), }); // Notify BluetoothGATTCharacteristic::read_value that read was successful. } diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index a691fb3c..6a3c0f98 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -94,7 +94,7 @@ impl Debug for CharacteristicInternal { impl CharacteristicInternal { pub fn new(characteristic: Retained) -> Self { - let properties = CharacteristicInternal::form_flags(&*characteristic); + let properties = CharacteristicInternal::form_flags(&characteristic); let raw_uuid = unsafe { characteristic.UUID() }; let uuid = cbuuid_to_uuid(&raw_uuid); let descriptors_arr = unsafe { characteristic.descriptors() }; @@ -256,7 +256,7 @@ impl PeripheralInternal { // in-flight future state and already-discovered descriptors to // avoid dropping pending operations during late re-discovery // events (see issue #167). - existing.properties = CharacteristicInternal::form_flags(&*cb_characteristic); + existing.properties = CharacteristicInternal::form_flags(&cb_characteristic); existing.characteristic = cb_characteristic; } else { service.characteristics.insert( @@ -393,10 +393,10 @@ impl PeripheralInternal { } = characteristic; let futures = read_future_state - .into_iter() - .chain(write_future_state.into_iter()) - .chain(subscribe_future_state.into_iter()) - .chain(unsubscribe_future_state.into_iter()); + .iter() + .chain(write_future_state) + .chain(subscribe_future_state) + .chain(unsubscribe_future_state); for state in futures { state.lock().unwrap().set_reply(error.clone()); } @@ -589,8 +589,8 @@ impl CoreBluetoothInternal { "Got manufacturer data advertisement! {}: {:?}", manufacturer_id, manufacturer_data ); - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { - if let Err(e) = p + if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) + && let Err(e) = p .event_sender .send(PeripheralEventInternal::ManufacturerData( manufacturer_id, @@ -601,7 +601,6 @@ impl CoreBluetoothInternal { { error!("Error sending notification event: {}", e); } - } } async fn on_service_data( @@ -611,28 +610,26 @@ impl CoreBluetoothInternal { rssi: i16, ) { trace!("Got service data advertisement! {:?}", service_data); - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { - if let Err(e) = p + if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) + && let Err(e) = p .event_sender .send(PeripheralEventInternal::ServiceData(service_data, rssi)) .await { error!("Error sending notification event: {}", e); } - } } async fn on_services(&mut self, peripheral_uuid: Uuid, services: Vec, rssi: i16) { trace!("Got service advertisement! {:?}", services); - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { - if let Err(e) = p + if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) + && let Err(e) = p .event_sender .send(PeripheralEventInternal::Services(services, rssi)) .await { error!("Error sending notification event: {}", e); } - } } async fn on_services_modified(&mut self, peripheral_uuid: Uuid) { @@ -664,20 +661,10 @@ impl CoreBluetoothInternal { .map(|n| n.to_string()) .or(advertisement_name.clone()); - if self.peripherals.contains_key(&uuid) { - if local_name.is_some() || advertisement_name.is_some() { - self.dispatch_event(CoreBluetoothEvent::DeviceUpdated { - uuid, - local_name, - advertisement_name, - }) - .await; - } - } else { + if let std::collections::hash_map::Entry::Vacant(e) = self.peripherals.entry(uuid) { // Create our channels let (event_sender, event_receiver) = mpsc::channel(256); - self.peripherals - .insert(uuid, PeripheralInternal::new(peripheral, event_sender)); + e.insert(PeripheralInternal::new(peripheral, event_sender)); self.dispatch_event(CoreBluetoothEvent::DeviceDiscovered { uuid, local_name, @@ -685,6 +672,15 @@ impl CoreBluetoothInternal { event_receiver, }) .await; + } else { + if local_name.is_some() || advertisement_name.is_some() { + self.dispatch_event(CoreBluetoothEvent::DeviceUpdated { + uuid, + local_name, + advertisement_name, + }) + .await; + } } } @@ -908,9 +904,9 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, data: Vec, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Some(service) = peripheral.services.get_mut(&service_uuid) + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) { trace!("Got read event!"); @@ -940,8 +936,6 @@ impl CoreBluetoothInternal { error!("Error sending notification event: {}", e); } } - } - } } fn on_characteristic_written( @@ -1003,9 +997,9 @@ impl CoreBluetoothInternal { kind: WriteType, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Some(service) = peripheral.services.get_mut(&service_uuid) + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) { trace!("Writing value! With kind {:?}", kind); match kind { @@ -1043,8 +1037,6 @@ impl CoreBluetoothInternal { } } } - } - } } fn drain_write_without_response_queue(&mut self, peripheral_uuid: Uuid) { @@ -1099,9 +1091,9 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Some(service) = peripheral.services.get_mut(&service_uuid) + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) { trace!("Reading value!"); unsafe { @@ -1111,8 +1103,6 @@ impl CoreBluetoothInternal { } characteristic.read_future_state.push_front(fut); } - } - } } fn subscribe( @@ -1122,9 +1112,9 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Some(service) = peripheral.services.get_mut(&service_uuid) + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) { trace!("Setting subscribe!"); unsafe { @@ -1134,8 +1124,6 @@ impl CoreBluetoothInternal { } characteristic.subscribe_future_state.push_front(fut); } - } - } } fn unsubscribe( @@ -1145,9 +1133,9 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Some(service) = peripheral.services.get_mut(&service_uuid) + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) { trace!("Setting subscribe!"); unsafe { @@ -1158,8 +1146,6 @@ impl CoreBluetoothInternal { } characteristic.unsubscribe_future_state.push_front(fut); } - } - } } fn write_descriptor_value( @@ -1171,11 +1157,10 @@ impl CoreBluetoothInternal { data: Vec, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - if let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Some(service) = peripheral.services.get_mut(&service_uuid) + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { trace!("Writing descriptor value!"); unsafe { peripheral.peripheral.writeValue_forDescriptor( @@ -1185,9 +1170,6 @@ impl CoreBluetoothInternal { } descriptor.write_future_state.push_front(fut); } - } - } - } } fn read_descriptor_value( @@ -1198,11 +1180,10 @@ impl CoreBluetoothInternal { descriptor_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - if let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Some(service) = peripheral.services.get_mut(&service_uuid) + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { trace!("Reading descriptor value!"); unsafe { peripheral @@ -1211,9 +1192,6 @@ impl CoreBluetoothInternal { } descriptor.read_future_state.push_front(fut); } - } - } - } } fn read_rssi(&mut self, peripheral_uuid: Uuid, fut: CoreBluetoothReplyStateShared) { @@ -1247,15 +1225,14 @@ impl CoreBluetoothInternal { } async fn on_tx_power_level(&mut self, peripheral_uuid: Uuid, tx_power_level: i16) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Err(e) = peripheral + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Err(e) = peripheral .event_sender .send(PeripheralEventInternal::TxPowerLevel(tx_power_level)) .await { error!("Error sending tx_power_level event: {}", e); } - } } async fn on_descriptor_read( @@ -1266,11 +1243,10 @@ impl CoreBluetoothInternal { descriptor_uuid: Uuid, data: Vec, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { - if let Some(service) = peripheral.services.get_mut(&service_uuid) { - if let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - if let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { + if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) + && let Some(service) = peripheral.services.get_mut(&service_uuid) + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { trace!("Got read event!"); let mut data_clone = Vec::new(); @@ -1284,9 +1260,6 @@ impl CoreBluetoothInternal { .set_reply(CoreBluetoothReply::ReadResult(data_clone)); } } - } - } - } } fn on_descriptor_written( diff --git a/src/corebluetooth/peripheral.rs b/src/corebluetooth/peripheral.rs index 229daaa6..e7377990 100644 --- a/src/corebluetooth/peripheral.rs +++ b/src/corebluetooth/peripheral.rs @@ -193,7 +193,7 @@ impl Peripheral { } } }); - Self { shared: shared } + Self { shared } } pub(super) fn update_name( From f66da8dc906e31d41f6171435b08273722078c1b Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 21:25:13 -0700 Subject: [PATCH 26/77] chore: clean retrieval lint warnings Document platform-gated retrieval helpers, limit the unsupported helper to tests, and fix the CoreBluetooth lazy-doc continuation warning. Full strict Clippy remains blocked by unrelated pre-existing lints in corebluetooth/internal.rs. --- src/api/mod.rs | 5 +++++ src/corebluetooth/future.rs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index e6995a53..0461f9b0 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -234,11 +234,13 @@ pub struct RetrievePeripheralsOptions { /// Returns whether a candidate identifier is included in an identifier selector. +#[allow(dead_code)] // Used by platform-gated backend implementations. pub(crate) fn matches_identifier(candidate: &T, requested: &[T]) -> bool { requested.iter().any(|requested| requested == candidate) } /// Returns whether a candidate service set contains one of the requested services. +#[allow(dead_code)] // Used by platform-gated backend implementations. pub(crate) fn matches_service(candidate_services: &[Uuid], requested: &[Uuid]) -> bool { requested .iter() @@ -248,6 +250,7 @@ pub(crate) fn matches_service(candidate_services: &[Uuid], requested: &[Uuid]) - /// Returns whether a candidate matches either supplied selector. /// /// A selector is considered supplied even when empty; in that case it matches nothing. +#[allow(dead_code)] // Used by platform-gated backend implementations. pub(crate) fn matches_retrieval_selectors( candidate_id: &PeripheralId, candidate_services: &[Uuid], @@ -270,6 +273,7 @@ pub(crate) fn matches_retrieval_selectors( } /// Merges retrieved peripherals while preserving the first occurrence of each identifier. +#[allow(dead_code)] // Used by platform-gated backend implementations. pub(crate) fn merge_retrieved_peripherals( peripherals: impl IntoIterator, id: F, @@ -285,6 +289,7 @@ where .collect() } +#[cfg(test)] fn unsupported_retrieve_peripherals

() -> Result> { Err(crate::Error::NotSupported( "retrieve_peripherals".to_string(), diff --git a/src/corebluetooth/future.rs b/src/corebluetooth/future.rs index 5757121e..2228e42d 100644 --- a/src/corebluetooth/future.rs +++ b/src/corebluetooth/future.rs @@ -36,7 +36,7 @@ impl BtlePlugFutureState { /// # Parameters /// /// - `msg`: Message to set as reply, which will be returned by the - /// corresponding future. + /// corresponding future. pub fn set_reply(&mut self, reply: T) { if self.reply_msg.is_some() { // TODO Can we stop multiple calls to set_reply_msg at compile time? From a3b5d5382af131edd69d0d6d4f518c1c9040cd2a Mon Sep 17 00:00:00 2001 From: Mika Tammi Date: Wed, 18 Mar 2026 22:33:18 +0200 Subject: [PATCH 27/77] fix(corebluetooth): send descriptor discovery event even on error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS, `peripheral:didDiscoverDescriptorsForCharacteristic:error:` only sent the `DiscoveredCharacteristicDescriptors` event when error was nil. If CoreBluetooth reported an error for any characteristic's descriptors (e.g. "The specified UUID is not allowed for this operation"), the event was never emitted and `discover_services()` would hang indefinitely. This moves the event send outside the error check so it always fires — with an empty descriptor map on error — allowing service discovery to complete. A warning is now logged when descriptor discovery fails. This matches the Windows behaviour where `BLEDevice::get_characteristic_descriptors` uses `unwrap_or` to return an empty list on error (PR #362). Co-Authored-By: Claude Opus 4.6 Signed-off-by: Mika Tammi --- src/corebluetooth/central_delegate.rs | 40 ++++++++++++++++----------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/corebluetooth/central_delegate.rs b/src/corebluetooth/central_delegate.rs index 73498b91..84c7f7de 100644 --- a/src/corebluetooth/central_delegate.rs +++ b/src/corebluetooth/central_delegate.rs @@ -20,7 +20,7 @@ use super::utils::nsstring_to_string; use super::utils::{core_bluetooth::cbuuid_to_uuid, nsuuid_to_uuid}; use futures::channel::mpsc::Sender; use futures::sink::SinkExt; -use log::{error, trace}; +use log::{error, trace, warn}; use objc2::runtime::{AnyObject, ProtocolObject}; use objc2::{ClassType, DeclaredClass, declare_class, msg_send_id, mutability, rc::Retained}; use objc2_core_bluetooth::{ @@ -622,29 +622,37 @@ declare_class!( characteristic_debug(characteristic), localized_description(error) ); + // Always send the event, even on error, so that the characteristic + // is marked as discovered and discover_services() can complete. + let mut descriptors = HashMap::new(); + if error.is_some() { + warn!( + "Error discovering descriptors for characteristic {}, continuing with empty descriptors: {}", + characteristic_debug(characteristic), + localized_description(error) + ); + } if error.is_none() { - let mut descriptors = HashMap::new(); let descs = unsafe { characteristic.descriptors() }.unwrap_or_default(); for d in descs { - // Create the map entry we'll need to export. let raw_uuid = unsafe { d.UUID() }; let uuid = cbuuid_to_uuid(&raw_uuid); descriptors.insert(uuid, d); } - let id = unsafe { peripheral.identifier() }; - let peripheral_uuid = nsuuid_to_uuid(&id); - let service = unsafe { characteristic.service() }.unwrap(); - let raw_service_uuid = unsafe { service.UUID() }; - let service_uuid = cbuuid_to_uuid(&raw_service_uuid); - let raw_char_uuid = unsafe { characteristic.UUID() }; - let characteristic_uuid = cbuuid_to_uuid(&raw_char_uuid); - self.send_event(CentralDelegateEvent::DiscoveredCharacteristicDescriptors { - peripheral_uuid, - service_uuid, - characteristic_uuid, - descriptors, - }); } + let id = unsafe { peripheral.identifier() }; + let peripheral_uuid = nsuuid_to_uuid(&id); + let service = unsafe { characteristic.service() }.unwrap(); + let raw_service_uuid = unsafe { service.UUID() }; + let service_uuid = cbuuid_to_uuid(&raw_service_uuid); + let raw_char_uuid = unsafe { characteristic.UUID() }; + let characteristic_uuid = cbuuid_to_uuid(&raw_char_uuid); + self.send_event(CentralDelegateEvent::DiscoveredCharacteristicDescriptors { + peripheral_uuid, + service_uuid, + characteristic_uuid, + descriptors, + }); } #[method(peripheral:didUpdateValueForCharacteristic:error:)] From 1cbfdfdd77b79c04a6fd3477df638bb1e6effd6c Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 21:39:02 -0700 Subject: [PATCH 28/77] test(corebluetooth): cover descriptor discovery errors --- src/corebluetooth/central_delegate.rs | 12 +- src/corebluetooth/internal.rs | 151 ++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 3 deletions(-) diff --git a/src/corebluetooth/central_delegate.rs b/src/corebluetooth/central_delegate.rs index 84c7f7de..837d3467 100644 --- a/src/corebluetooth/central_delegate.rs +++ b/src/corebluetooth/central_delegate.rs @@ -622,8 +622,8 @@ declare_class!( characteristic_debug(characteristic), localized_description(error) ); - // Always send the event, even on error, so that the characteristic - // is marked as discovered and discover_services() can complete. + // Send the event even on error when the characteristic is associated + // with a service, so discover_services() can complete. let mut descriptors = HashMap::new(); if error.is_some() { warn!( @@ -642,7 +642,13 @@ declare_class!( } let id = unsafe { peripheral.identifier() }; let peripheral_uuid = nsuuid_to_uuid(&id); - let service = unsafe { characteristic.service() }.unwrap(); + let Some(service) = (unsafe { characteristic.service() }) else { + warn!( + "Descriptor discovery completed for characteristic {} without an associated service", + characteristic_debug(characteristic) + ); + return; + }; let raw_service_uuid = unsafe { service.UUID() }; let service_uuid = cbuuid_to_uuid(&raw_service_uuid); let raw_char_uuid = unsafe { characteristic.UUID() }; diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 6a3c0f98..97fdfea2 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -1567,6 +1567,157 @@ impl Drop for CoreBluetoothInternal { } } +#[cfg(test)] +mod tests { + use super::*; + use futures::StreamExt; + use objc2::{DeclaredClass, declare_class, mutability}; + use objc2_core_bluetooth::{ + CBAttributePermissions, CBMutableCharacteristic, CBMutableService, CBPeripheralDelegate, + }; + use objc2_foundation::{NSError, NSObjectProtocol, NSString, ns_string}; + use std::time::Duration; + + declare_class!( + struct TestPeripheral; + + unsafe impl ClassType for TestPeripheral { + type Super = CBPeripheral; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "BtlePlugTestPeripheral"; + } + + impl DeclaredClass for TestPeripheral { + type Ivars = Retained; + } + + unsafe impl NSObjectProtocol for TestPeripheral {} + + unsafe impl TestPeripheral { + #[method_id(identifier)] + fn identifier(&self) -> Retained { + self.ivars().clone() + } + + #[method_id(name)] + fn name(&self) -> Option> { + None + } + } + ); + + impl TestPeripheral { + fn new(identifier: Retained) -> Retained { + let this = Self::alloc().set_ivars(identifier); + unsafe { msg_send_id![super(this), init] } + } + } + + #[tokio::test] + async fn descriptor_discovery_error_completes_service_discovery_without_descriptors() { + let peripheral_uuid = Uuid::from_u128(0x12345678_1234_5678_1234_567812345678); + let peripheral_uuid_string = NSString::from_str(&peripheral_uuid.to_string()); + let peripheral_identifier = + NSUUID::initWithUUIDString(NSUUID::alloc(), &peripheral_uuid_string) + .expect("valid peripheral UUID"); + let peripheral = TestPeripheral::new(peripheral_identifier); + let service_uuid = Uuid::from_u128(0x0000180f_0000_1000_8000_00805f9b34fb); + let characteristic_uuid = Uuid::from_u128(0x00002a19_0000_1000_8000_00805f9b34fb); + let service_cbuuid = uuid_to_cbuuid(service_uuid); + let characteristic_cbuuid = uuid_to_cbuuid(characteristic_uuid); + let characteristic = unsafe { + CBMutableCharacteristic::initWithType_properties_value_permissions( + CBMutableCharacteristic::alloc(), + &characteristic_cbuuid, + CBCharacteristicProperties::CBCharacteristicPropertyRead, + None, + CBAttributePermissions::Readable, + ) + }; + let service = unsafe { + CBMutableService::initWithType_primary(CBMutableService::alloc(), &service_cbuuid, true) + }; + let characteristic: Retained = Retained::into_super(characteristic); + let characteristics = NSArray::from_vec(vec![characteristic.clone()]); + unsafe { service.setCharacteristics(Some(&characteristics)) }; + let service: Retained = Retained::into_super(service); + + let (event_sender, _) = mpsc::channel(1); + let mut internal = + PeripheralInternal::new(Retained::into_super(peripheral.clone()), event_sender); + internal.services.insert( + service_uuid, + ServiceInternal { + cbservice: service, + characteristics: HashMap::from([( + characteristic_uuid, + CharacteristicInternal::new(characteristic.clone()), + )]), + discovered: false, + }, + ); + let discovery = CoreBluetoothReplyFuture::default(); + internal.services_discovered_future_state = Some(discovery.get_state_clone()); + + let (delegate_sender, mut delegate_receiver) = mpsc::channel(1); + let delegate = CentralDelegate::new(delegate_sender); + let error = NSError::new(1, ns_string!("BtlePlugCoreBluetoothTests")); + unsafe { + delegate.peripheral_didDiscoverDescriptorsForCharacteristic_error( + &peripheral, + &characteristic, + Some(&error), + ); + } + + let event = tokio::time::timeout(Duration::from_secs(1), delegate_receiver.next()) + .await + .expect("descriptor error callback did not emit an event") + .expect("delegate event channel closed"); + let CentralDelegateEvent::DiscoveredCharacteristicDescriptors { + peripheral_uuid: event_peripheral_uuid, + service_uuid: event_service_uuid, + characteristic_uuid: event_characteristic_uuid, + descriptors, + } = event + else { + panic!("unexpected delegate event: {event:?}"); + }; + assert_eq!(event_peripheral_uuid, peripheral_uuid); + assert_eq!(event_service_uuid, service_uuid); + assert_eq!(event_characteristic_uuid, characteristic_uuid); + assert!(descriptors.is_empty()); + + internal.set_characteristic_descriptors( + event_service_uuid, + event_characteristic_uuid, + descriptors, + ); + let reply = tokio::time::timeout(Duration::from_secs(1), discovery) + .await + .expect("service discovery remained pending after descriptor error"); + let CoreBluetoothReply::ServicesDiscovered(services) = reply else { + panic!("unexpected discovery reply: {reply:?}"); + }; + let characteristic = services + .iter() + .find(|service| service.uuid == service_uuid) + .and_then(|service| { + service + .characteristics + .iter() + .find(|characteristic| characteristic.uuid == characteristic_uuid) + }) + .expect("discovered characteristic"); + assert!(characteristic.descriptors.is_empty()); + + // CBPeripheral has no public initializer suitable for tests, so this + // subclass must not run CoreBluetooth's private destruction path. + std::mem::forget(internal); + std::mem::forget(peripheral); + } +} + pub fn run_corebluetooth_thread( event_sender: Sender, ) -> Result, Error> { From 26ac1d11c4c740e91f9166787719ca2797998c24 Mon Sep 17 00:00:00 2001 From: Joseph Birks Date: Sat, 28 Mar 2026 22:22:43 +0000 Subject: [PATCH 29/77] Fix clear_peripherals on CoreBluetooth to clear internal map clear_peripherals() only cleared the AdapterManager's public DashMap but not CoreBluetoothInternal's private HashMap. After clearing, re-discovered peripherals were treated as DeviceUpdated (already known internally) instead of DeviceDiscovered, so they were never re-added to the public map. Send a ClearPeripherals message to the CoreBluetooth thread so both maps are cleared in sync. Windows and Android are unaffected as they only have the single AdapterManager map. --- src/corebluetooth/adapter.rs | 5 +++++ src/corebluetooth/internal.rs | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/corebluetooth/adapter.rs b/src/corebluetooth/adapter.rs index fb65e8a1..23c7bbb6 100644 --- a/src/corebluetooth/adapter.rs +++ b/src/corebluetooth/adapter.rs @@ -219,6 +219,11 @@ impl Central for Adapter { async fn clear_peripherals(&self) -> Result<()> { self.manager.clear_peripherals(); + self.sender + .to_owned() + .send(CoreBluetoothMessage::ClearPeripherals) + .await + .map_err(|e| Error::Other(Box::new(e)))?; Ok(()) } diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 97fdfea2..933741ad 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -508,6 +508,7 @@ pub enum CoreBluetoothMessage { options: RetrievePeripheralsOptions, future: CoreBluetoothReplyStateShared, }, + ClearPeripherals, } #[derive(Debug)] @@ -1508,6 +1509,9 @@ impl CoreBluetoothInternal { CoreBluetoothMessage::RetrievePeripherals { options, future } => { self.retrieve_peripherals(options, future).await } + CoreBluetoothMessage::ClearPeripherals => { + self.peripherals.clear(); + } }; } } From 706acefcdde3a1aa866ffe57ea2dcc4584bbe44a Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 22:20:03 -0700 Subject: [PATCH 30/77] fix(corebluetooth): synchronize peripheral clearing Acknowledge clear_peripherals only after the CoreBluetooth worker has cleared its private map and the adapter event loop has processed every earlier event and cleared its public state. This event-channel fence prevents stale queued discovery events from leaving the maps divergent. Add a macOS integration regression covering discover, clear, rescan, and fresh discovery emission. --- src/corebluetooth/adapter.rs | 18 ++++- src/corebluetooth/internal.rs | 11 +++- tests/common/test_cases.rs | 65 +++++++++++++++++++ ...st_clear_peripherals_rediscovers_device.rs | 8 +++ 4 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 tests/test_clear_peripherals_rediscovers_device.rs diff --git a/src/corebluetooth/adapter.rs b/src/corebluetooth/adapter.rs index 23c7bbb6..51b01119 100644 --- a/src/corebluetooth/adapter.rs +++ b/src/corebluetooth/adapter.rs @@ -134,6 +134,11 @@ impl Adapter { handles.remove(&uuid.into()); manager_clone.emit(CentralEvent::DeviceDisconnected(uuid.into())); } + CoreBluetoothEvent::PeripheralsCleared { future } => { + manager_clone.clear_peripherals(); + handles.clear(); + future.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + } CoreBluetoothEvent::DidUpdateState { state } => { let central_state = get_central_state(state); manager_clone.emit(CentralEvent::StateUpdate(central_state)); @@ -218,13 +223,20 @@ impl Central for Adapter { } async fn clear_peripherals(&self) -> Result<()> { - self.manager.clear_peripherals(); + let fut = CoreBluetoothReplyFuture::default(); self.sender .to_owned() - .send(CoreBluetoothMessage::ClearPeripherals) + .send(CoreBluetoothMessage::ClearPeripherals { + future: fut.get_state_clone(), + }) .await .map_err(|e| Error::Other(Box::new(e)))?; - Ok(()) + match fut.await { + CoreBluetoothReply::Ok => Ok(()), + _ => Err(Error::RuntimeError( + "Unexpected CoreBluetooth clear reply".to_string(), + )), + } } async fn adapter_info(&self) -> Result { diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 933741ad..67f6ea7b 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -508,7 +508,9 @@ pub enum CoreBluetoothMessage { options: RetrievePeripheralsOptions, future: CoreBluetoothReplyStateShared, }, - ClearPeripherals, + ClearPeripherals { + future: CoreBluetoothReplyStateShared, + }, } #[derive(Debug)] @@ -542,6 +544,9 @@ pub enum CoreBluetoothEvent { DeviceDisconnected { uuid: Uuid, }, + PeripheralsCleared { + future: CoreBluetoothReplyStateShared, + }, } impl CoreBluetoothInternal { @@ -1509,8 +1514,10 @@ impl CoreBluetoothInternal { CoreBluetoothMessage::RetrievePeripherals { options, future } => { self.retrieve_peripherals(options, future).await } - CoreBluetoothMessage::ClearPeripherals => { + CoreBluetoothMessage::ClearPeripherals { future } => { self.peripherals.clear(); + self.dispatch_event(CoreBluetoothEvent::PeripheralsCleared { future }) + .await; } }; } diff --git a/tests/common/test_cases.rs b/tests/common/test_cases.rs index a51d31fb..2a897aa0 100644 --- a/tests/common/test_cases.rs +++ b/tests/common/test_cases.rs @@ -66,6 +66,71 @@ pub async fn test_discover_peripheral_by_name() { peripheral.disconnect().await.unwrap(); } +#[cfg(target_os = "macos")] +pub async fn test_clear_peripherals_rediscovers_device() { + use btleplug::api::{Central, CentralEvent, ScanFilter}; + use futures::StreamExt; + use std::time::Duration; + use tokio::time; + + let adapter = peripheral_finder::get_adapter().await; + let peripheral_name = std::env::var("BTLEPLUG_TEST_PERIPHERAL") + .unwrap_or_else(|_| gatt_uuids::TEST_PERIPHERAL_NAME.to_string()); + let mut events = adapter.events().await.unwrap(); + + adapter.start_scan(ScanFilter::default()).await.unwrap(); + let peripheral = time::timeout(Duration::from_secs(15), async { + loop { + for peripheral in adapter.peripherals().await.unwrap() { + if peripheral + .properties() + .await + .unwrap() + .is_some_and(|properties| { + properties.local_name.as_deref() == Some(&peripheral_name) + }) + { + return peripheral; + } + } + let _ = events.next().await; + } + }) + .await + .expect("timed out waiting for initial peripheral discovery"); + let peripheral_id = peripheral.id(); + + adapter.stop_scan().await.unwrap(); + adapter.clear_peripherals().await.unwrap(); + assert!( + adapter.peripherals().await.unwrap().is_empty(), + "clear_peripherals returned before the public map was cleared" + ); + + adapter.start_scan(ScanFilter::default()).await.unwrap(); + time::timeout(Duration::from_secs(15), async { + loop { + if matches!(events.next().await, Some(CentralEvent::DeviceDiscovered(id)) if id == peripheral_id) + { + break; + } + } + }) + .await + .expect("timed out waiting for rediscovery after clear_peripherals"); + adapter.stop_scan().await.unwrap(); + + assert!( + adapter + .peripherals() + .await + .unwrap() + .iter() + .any(|peripheral| peripheral.id() == peripheral_id), + "rediscovered peripheral was not restored to the public map" + ); +} + pub async fn test_discover_services() { let peripheral = peripheral_finder::find_and_connect().await; let services = peripheral.services(); diff --git a/tests/test_clear_peripherals_rediscovers_device.rs b/tests/test_clear_peripherals_rediscovers_device.rs new file mode 100644 index 00000000..e4706d85 --- /dev/null +++ b/tests/test_clear_peripherals_rediscovers_device.rs @@ -0,0 +1,8 @@ +mod common; + +#[cfg(target_os = "macos")] +#[tokio::test] +#[ignore = "requires BLE test peripheral"] +async fn test_clear_peripherals_rediscovers_device() { + common::test_cases::test_clear_peripherals_rediscovers_device().await; +} From 4fae93218b84771e3f7c166d105073cd4d2afc73 Mon Sep 17 00:00:00 2001 From: Yongle Date: Tue, 14 Apr 2026 20:18:49 +0800 Subject: [PATCH 31/77] fix(common): preserve connected peripherals during clear_peripherals Stopping a scan or calling clear_peripherals() currently evicts active connections from the internal state map. This fixes cross-platform consistency by ensuring connected devices are always retained (matching the established behavior pattern of macOS CoreBluetooth and Linux BlueZ). --- src/common/adapter_manager.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/common/adapter_manager.rs b/src/common/adapter_manager.rs index 7da91d66..e5a09572 100644 --- a/src/common/adapter_manager.rs +++ b/src/common/adapter_manager.rs @@ -67,7 +67,15 @@ where } pub fn clear_peripherals(&self) { - self.peripherals.clear(); + // Retain peripherals that are currently connected. + // This ensures that stopping a scan or clearing discovered devices + // does not evict active connections — matching CoreBluetooth/BlueZ behavior. + self.peripherals.retain(|_id, peripheral| { + // is_connected() is async in the trait, but all platform implementations + // use an AtomicBool internally, so block_on is safe here. + // We use a short-lived runtime to avoid panics if already in an async context. + futures::executor::block_on(peripheral.is_connected()).unwrap_or(false) + }); } pub fn peripherals(&self) -> Vec { From 5211ba281624e7df36ba6a36c7cf47457eade4e5 Mon Sep 17 00:00:00 2001 From: Yongle Date: Tue, 14 Apr 2026 20:18:58 +0800 Subject: [PATCH 32/77] fix(winrt): cache matched addresses in watcher to prevent dropping ScanResponses The Windows WinRT BLE watcher previously dropped all ScanResponse packets when a UUID filter was applied, because ScanResponses lack the service UUIDs present in the main advertisement. This introduces a lightweight Address cache within the watcher closure, allowing subsequent ScanResponse packets to pass through the filter if the device previously advertised the requested UUID. --- src/winrtble/ble/watcher.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/winrtble/ble/watcher.rs b/src/winrtble/ble/watcher.rs index 59881ccb..98d78b7d 100644 --- a/src/winrtble/ble/watcher.rs +++ b/src/winrtble/ble/watcher.rs @@ -51,6 +51,8 @@ impl BLEWatcher { // Pre-convert the filter UUIDs once so the handler closure is cheap. let filter_guids: Vec = services.iter().map(utils::to_guid).collect(); + + let matching_devices = std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())); let handler: TypedEventHandler< BluetoothLEAdvertisementWatcher, @@ -60,18 +62,27 @@ impl BLEWatcher { if let Ok(args) = args.ok() { // Software service-UUID filter. if !filter_guids.is_empty() { + let address = args.BluetoothAddress().unwrap_or(0); + let mut is_match = false; + if let Ok(ad) = args.Advertisement() { if let Ok(ad_uuids) = ad.ServiceUuids() { let count = ad_uuids.Size().unwrap_or(0); - let advertised: Vec = - (0..count).filter_map(|i| ad_uuids.GetAt(i).ok()).collect(); - let all_present = - filter_guids.iter().all(|g| advertised.contains(g)); - if !all_present { - return Ok(()); + if count > 0 { + let advertised: Vec = + (0..count).filter_map(|i| ad_uuids.GetAt(i).ok()).collect(); + is_match = filter_guids.iter().all(|g| advertised.contains(g)); } } } + + let mut cache = matching_devices.lock().unwrap(); + if is_match { + cache.insert(address); + } else if !cache.contains(&address) { + // If the current packet doesn't have the UUID and we haven't seen it before, drop it. + return Ok(()); + } } on_received(args)?; } From bb70abdcbff5fb1e6e32a168ae8e2fdac7724efc Mon Sep 17 00:00:00 2001 From: Yongle Date: Tue, 14 Apr 2026 20:19:06 +0800 Subject: [PATCH 33/77] fix(cross-platform): enforce COMPLETE_LOCAL_NAME priority over SHORT_LOCAL_NAME Windows (WinRT): - Parse SHORT_LOCAL_NAME (0x08) and COMPLETE_LOCAL_NAME (0x09) from DataSections - Prevent short names from overwriting complete names across split packets macOS (CoreBluetooth): - Prefer advertisement_name (scan response) over peripheral.name() (GAP cache), as GAP names are frequently truncated short names - Add length protections in update_name() to prevent shorter names from overwriting longer complete names in successive updates Android: - Restore upstream MTU auto-negotiation compatibility --- src/corebluetooth/internal.rs | 8 ++++--- src/corebluetooth/peripheral.rs | 12 +++++++++- src/winrtble/mod.rs | 2 ++ src/winrtble/peripheral.rs | 41 ++++++++++++++++++++++++++++++++- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 67f6ea7b..0c8d3a2e 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -663,9 +663,11 @@ impl CoreBluetoothInternal { let id = unsafe { peripheral.identifier() }; let uuid = nsuuid_to_uuid(&id); let peripheral_name = unsafe { peripheral.name() }; - let local_name = peripheral_name - .map(|n| n.to_string()) - .or(advertisement_name.clone()); + // Prefer advertisement_name (from scan response, usually COMPLETE_LOCAL_NAME) + // over peripheral.name() (GAP cache, often the truncated SHORT_LOCAL_NAME) + let local_name = advertisement_name + .clone() + .or_else(|| peripheral_name.map(|n| n.to_string())); if let std::collections::hash_map::Entry::Vacant(e) = self.peripherals.entry(uuid) { // Create our channels diff --git a/src/corebluetooth/peripheral.rs b/src/corebluetooth/peripheral.rs index e7377990..66b13a10 100644 --- a/src/corebluetooth/peripheral.rs +++ b/src/corebluetooth/peripheral.rs @@ -202,7 +202,17 @@ impl Peripheral { advertisement_name: Option, ) { if let Ok(mut props) = self.shared.properties.lock() { - props.local_name = local_name; + // Only update local_name if new value is present and is at least as informative + // (i.e. don't let a shorter GAP name overwrite a longer advertisement name) + if let Some(ref new_name) = local_name { + let should_update = match &props.local_name { + None => true, + Some(old) => new_name.len() >= old.len(), + }; + if should_update { + props.local_name = local_name; + } + } props.advertisement_name = advertisement_name; } } diff --git a/src/winrtble/mod.rs b/src/winrtble/mod.rs index 2d9920a9..39ebf434 100644 --- a/src/winrtble/mod.rs +++ b/src/winrtble/mod.rs @@ -22,4 +22,6 @@ mod advertisement_data_type { pub const SERVICE_DATA_16_BIT_UUID: u8 = 0x16; pub const SERVICE_DATA_32_BIT_UUID: u8 = 0x20; pub const SERVICE_DATA_128_BIT_UUID: u8 = 0x21; + pub const SHORT_LOCAL_NAME: u8 = 0x08; + pub const COMPLETE_LOCAL_NAME: u8 = 0x09; } diff --git a/src/winrtble/peripheral.rs b/src/winrtble/peripheral.rs index 22796fd2..2fa570d8 100644 --- a/src/winrtble/peripheral.rs +++ b/src/winrtble/peripheral.rs @@ -191,17 +191,56 @@ impl Peripheral { if let Ok(data_sections) = advertisement.DataSections() { // See if we have any advertised service data before taking a lock to update... let mut found_service_data = false; + let mut manual_local_name: Option = None; + let mut has_complete_name = false; for section in &data_sections { match section.DataType().unwrap() { advertisement_data_type::SERVICE_DATA_16_BIT_UUID | advertisement_data_type::SERVICE_DATA_32_BIT_UUID | advertisement_data_type::SERVICE_DATA_128_BIT_UUID => { found_service_data = true; - break; + } + advertisement_data_type::COMPLETE_LOCAL_NAME => { + let data = utils::to_vec(§ion.Data().unwrap()); + if let Ok(name) = String::from_utf8(data) { + let name = name.trim_end_matches('\0').trim().to_string(); + if !name.is_empty() { + manual_local_name = Some(name); + has_complete_name = true; + } + } + } + advertisement_data_type::SHORT_LOCAL_NAME => { + // Only use SHORT_LOCAL_NAME if we haven't already found a COMPLETE_LOCAL_NAME + if !has_complete_name { + let data = utils::to_vec(§ion.Data().unwrap()); + if let Ok(name) = String::from_utf8(data) { + let name = name.trim_end_matches('\0').trim().to_string(); + if !name.is_empty() { + manual_local_name = Some(name); + } + } + } } _ => {} } } + + if let Some(name) = manual_local_name { + if !name.is_empty() { + let existing = self.shared.local_name.read().unwrap().clone(); + // Only update if: (1) no existing name, or (2) this is a COMPLETE_LOCAL_NAME, + // or (3) new name is longer (prevents SHORT from overwriting COMPLETE across packets) + let should_update = match &existing { + None => true, + Some(old) => has_complete_name || name.len() > old.len(), + }; + if should_update { + *self.shared.local_name.write().unwrap() = Some(name.clone()); + self.emit_event(CentralEvent::DeviceUpdated(self.shared.address.into())); + } + } + } if found_service_data { let mut service_data_guard = self.shared.latest_service_data.write().unwrap(); From cb32fb007597557a420417e696ad9f4cbad3a393 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 22:31:01 -0700 Subject: [PATCH 34/77] revert: avoid blocking while clearing peripherals The public contract requires callers to disconnect peripherals before clearing them. Checking async backend state while holding DashMap retain locks can deadlock CoreBluetooth and enter JNI on Android, so keep the existing synchronous clear semantics. --- src/common/adapter_manager.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/common/adapter_manager.rs b/src/common/adapter_manager.rs index e5a09572..7da91d66 100644 --- a/src/common/adapter_manager.rs +++ b/src/common/adapter_manager.rs @@ -67,15 +67,7 @@ where } pub fn clear_peripherals(&self) { - // Retain peripherals that are currently connected. - // This ensures that stopping a scan or clearing discovered devices - // does not evict active connections — matching CoreBluetooth/BlueZ behavior. - self.peripherals.retain(|_id, peripheral| { - // is_connected() is async in the trait, but all platform implementations - // use an AtomicBool internally, so block_on is safe here. - // We use a short-lived runtime to avoid panics if already in an async context. - futures::executor::block_on(peripheral.is_connected()).unwrap_or(false) - }); + self.peripherals.clear(); } pub fn peripherals(&self) -> Vec { From bc0ee89ee69ac1d7d9ac1759f3642d6a79c8aa60 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 22:34:43 -0700 Subject: [PATCH 35/77] fix(corebluetooth): preserve advertisement name precedence Treat the advertisement local name as authoritative over the cached GAP name, and keep the last advertisement name when later callbacks omit it. Add focused merge-state tests. --- src/corebluetooth/peripheral.rs | 92 ++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/src/corebluetooth/peripheral.rs b/src/corebluetooth/peripheral.rs index 66b13a10..3b46fcc0 100644 --- a/src/corebluetooth/peripheral.rs +++ b/src/corebluetooth/peripheral.rs @@ -202,22 +202,37 @@ impl Peripheral { advertisement_name: Option, ) { if let Ok(mut props) = self.shared.properties.lock() { - // Only update local_name if new value is present and is at least as informative - // (i.e. don't let a shorter GAP name overwrite a longer advertisement name) - if let Some(ref new_name) = local_name { - let should_update = match &props.local_name { - None => true, - Some(old) => new_name.len() >= old.len(), - }; - if should_update { - props.local_name = local_name; - } - } - props.advertisement_name = advertisement_name; + let PeripheralProperties { + local_name: current_local_name, + advertisement_name: current_advertisement_name, + .. + } = &mut *props; + merge_names( + current_local_name, + current_advertisement_name, + local_name, + advertisement_name, + ); } } } +fn merge_names( + local_name: &mut Option, + advertisement_name: &mut Option, + new_local_name: Option, + new_advertisement_name: Option, +) { + if let Some(name) = new_advertisement_name { + *local_name = Some(name.clone()); + *advertisement_name = Some(name); + } else if advertisement_name.is_none() + && let Some(name) = new_local_name + { + *local_name = Some(name); + } +} + impl Display for Peripheral { fn fmt(&self, f: &mut Formatter) -> fmt::Result { // let connected = if self.is_connected() { " connected" } else { "" }; @@ -228,6 +243,59 @@ impl Display for Peripheral { } } +#[cfg(test)] +mod tests { + use super::merge_names; + + #[test] + fn advertisement_name_takes_precedence_over_gap_name() { + let mut local_name = Some("Longer GAP name".to_string()); + let mut advertisement_name = None; + + merge_names( + &mut local_name, + &mut advertisement_name, + Some("Short GAP".to_string()), + Some("Complete".to_string()), + ); + + assert_eq!(local_name.as_deref(), Some("Complete")); + assert_eq!(advertisement_name.as_deref(), Some("Complete")); + } + + #[test] + fn absent_advertisement_does_not_erase_or_override_it() { + let mut local_name = Some("Complete".to_string()); + let mut advertisement_name = Some("Complete".to_string()); + + merge_names( + &mut local_name, + &mut advertisement_name, + Some("Different GAP name".to_string()), + None, + ); + + assert_eq!(local_name.as_deref(), Some("Complete")); + assert_eq!(advertisement_name.as_deref(), Some("Complete")); + } + + #[test] + fn gap_name_is_used_until_an_advertisement_name_arrives() { + let mut local_name = None; + let mut advertisement_name = None; + + merge_names( + &mut local_name, + &mut advertisement_name, + Some("GAP name".to_string()), + None, + ); + + assert_eq!(local_name.as_deref(), Some("GAP name")); + assert_eq!(advertisement_name, None); + } +} + impl Debug for Peripheral { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.debug_struct("Peripheral") From 02a04bd8e31892e3130e5cab5699f43761c2a68d Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 22:40:39 -0700 Subject: [PATCH 36/77] fix(winrt): scope scan-response matching to active scans Remove stale watcher callbacks between scans, use any-service filter semantics, and accept UUID-less packets only for scan responses matched during the active scan. Bound the address cache and preserve explicit complete-name provenance without duplicate lifecycle events. --- src/winrtble/adapter.rs | 4 +- src/winrtble/ble/watcher.rs | 78 ++++++++++++++++++---- src/winrtble/peripheral.rs | 126 ++++++++++++++++++++++-------------- 3 files changed, 148 insertions(+), 60 deletions(-) diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index 48f9ca12..cf7ec7c9 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -150,7 +150,7 @@ impl Central for Adapter { } async fn start_scan(&self, filter: ScanFilter) -> Result<()> { - let watcher = self.watcher.lock().map_err(Into::::into)?; + let mut watcher = self.watcher.lock().map_err(Into::::into)?; let manager = self.manager.clone(); watcher.start( filter, @@ -172,7 +172,7 @@ impl Central for Adapter { } async fn stop_scan(&self) -> Result<()> { - let watcher = self.watcher.lock().map_err(Into::::into)?; + let mut watcher = self.watcher.lock().map_err(Into::::into)?; watcher.stop()?; Ok(()) } diff --git a/src/winrtble/ble/watcher.rs b/src/winrtble/ble/watcher.rs index 98d78b7d..7fe6a106 100644 --- a/src/winrtble/ble/watcher.rs +++ b/src/winrtble/ble/watcher.rs @@ -12,14 +12,18 @@ // Copyright (c) 2014 The Rust Project Developers use crate::{Error, Result, api::ScanFilter, winrtble::utils}; +use std::{collections::HashSet, sync::Mutex}; use windows::{Devices::Bluetooth::Advertisement::*, Foundation::TypedEventHandler, core::Ref}; +const MATCH_CACHE_CAPACITY: usize = 1024; + pub type AdvertisementEventHandler = Box windows::core::Result<()> + Send>; #[derive(Debug)] pub struct BLEWatcher { watcher: BluetoothLEAdvertisementWatcher, + received_token: Option, } impl From for Error { @@ -28,14 +32,39 @@ impl From for Error { } } +#[derive(Default)] +struct MatchCache { + addresses: HashSet, +} + +impl MatchCache { + fn record(&mut self, address: u64) { + if self.addresses.len() < MATCH_CACHE_CAPACITY || self.addresses.contains(&address) { + self.addresses.insert(address); + } + } + + fn contains(&self, address: u64) -> bool { + self.addresses.contains(&address) + } +} + impl BLEWatcher { pub fn new() -> Result { let ad = BluetoothLEAdvertisementFilter::new()?; let watcher = BluetoothLEAdvertisementWatcher::Create(&ad)?; - Ok(BLEWatcher { watcher }) + Ok(BLEWatcher { + watcher, + received_token: None, + }) } - pub fn start(&self, filter: ScanFilter, on_received: AdvertisementEventHandler) -> Result<()> { + pub fn start( + &mut self, + filter: ScanFilter, + on_received: AdvertisementEventHandler, + ) -> Result<()> { + self.remove_received_handler()?; let ScanFilter { services } = filter; // Clear any OS-level service UUID filter from a previous scan. @@ -51,8 +80,7 @@ impl BLEWatcher { // Pre-convert the filter UUIDs once so the handler closure is cheap. let filter_guids: Vec = services.iter().map(utils::to_guid).collect(); - - let matching_devices = std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())); + let matching_devices = Mutex::new(MatchCache::default()); let handler: TypedEventHandler< BluetoothLEAdvertisementWatcher, @@ -64,23 +92,26 @@ impl BLEWatcher { if !filter_guids.is_empty() { let address = args.BluetoothAddress().unwrap_or(0); let mut is_match = false; - + if let Ok(ad) = args.Advertisement() { if let Ok(ad_uuids) = ad.ServiceUuids() { let count = ad_uuids.Size().unwrap_or(0); if count > 0 { let advertised: Vec = (0..count).filter_map(|i| ad_uuids.GetAt(i).ok()).collect(); - is_match = filter_guids.iter().all(|g| advertised.contains(g)); + is_match = filter_guids.iter().any(|g| advertised.contains(g)); } } } let mut cache = matching_devices.lock().unwrap(); if is_match { - cache.insert(address); - } else if !cache.contains(&address) { - // If the current packet doesn't have the UUID and we haven't seen it before, drop it. + cache.record(address); + } else if !matches!( + args.AdvertisementType(), + Ok(BluetoothLEAdvertisementType::ScanResponse) + ) || !cache.contains(address) + { return Ok(()); } } @@ -90,13 +121,38 @@ impl BLEWatcher { }, ); - self.watcher.Received(&handler)?; + self.received_token = Some(self.watcher.Received(&handler)?); self.watcher.Start()?; Ok(()) } - pub fn stop(&self) -> Result<()> { + pub fn stop(&mut self) -> Result<()> { self.watcher.Stop()?; + self.remove_received_handler() + } + + fn remove_received_handler(&mut self) -> Result<()> { + if let Some(token) = self.received_token.take() { + self.watcher.RemoveReceived(token)?; + } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::{MATCH_CACHE_CAPACITY, MatchCache}; + + #[test] + fn match_cache_is_bounded() { + let mut cache = MatchCache::default(); + for address in 0..MATCH_CACHE_CAPACITY as u64 { + cache.record(address); + } + cache.record(MATCH_CACHE_CAPACITY as u64); + + assert_eq!(cache.addresses.len(), MATCH_CACHE_CAPACITY); + assert!(cache.contains(0)); + assert!(!cache.contains(MATCH_CACHE_CAPACITY as u64)); + } +} diff --git a/src/winrtble/peripheral.rs b/src/winrtble/peripheral.rs index 2fa570d8..d343352e 100644 --- a/src/winrtble/peripheral.rs +++ b/src/winrtble/peripheral.rs @@ -87,6 +87,7 @@ struct Shared { // Mutable, advertised, state... address_type: RwLock>, local_name: RwLock>, + has_complete_local_name: AtomicBool, advertisement_name: RwLock>, last_tx_power_level: RwLock>, // XXX: would be nice to avoid lock here! last_rssi: RwLock>, // XXX: would be nice to avoid lock here! @@ -96,6 +97,23 @@ struct Shared { class: RwLock>, } +struct AdvertisedName { + value: String, + is_complete: bool, +} + +fn parse_advertised_name(data: &[u8], is_complete: bool) -> Option { + let value = std::str::from_utf8(data) + .ok()? + .trim_end_matches('\0') + .to_string(); + (!value.is_empty()).then_some(AdvertisedName { value, is_complete }) +} + +fn should_accept_name(has_complete_name: bool, new_name_is_complete: bool) -> bool { + !has_complete_name || new_name_is_complete +} + impl Peripheral { pub(crate) fn new(adapter: Weak>, address: BDAddr) -> Self { let (broadcast_sender, _) = broadcast::channel(16); @@ -110,6 +128,7 @@ impl Peripheral { notifications_channel: broadcast_sender, address_type: RwLock::new(None), local_name: RwLock::new(None), + has_complete_local_name: AtomicBool::new(false), advertisement_name: RwLock::new(None), last_tx_power_level: RwLock::new(None), last_rssi: RwLock::new(None), @@ -149,20 +168,13 @@ impl Peripheral { let advertisement = args.Advertisement().unwrap(); // Advertisements are cumulative: set/replace data only if it's set - if let Ok(name) = advertisement.LocalName() { - if !name.is_empty() { - let name_str = name.to_string(); - let mut adv_name_guard = self.shared.advertisement_name.write().unwrap(); - *adv_name_guard = Some(name_str.clone()); - drop(adv_name_guard); - // Also use as local_name fallback if we don't have one yet - let local_name_guard = self.shared.local_name.read().unwrap(); - if local_name_guard.is_none() { - drop(local_name_guard); - let mut local_name_guard = self.shared.local_name.write().unwrap(); - *local_name_guard = Some(name_str); - } - } + let projected_local_name = advertisement + .LocalName() + .ok() + .map(|name| name.to_string()) + .filter(|name| !name.is_empty()); + if let Some(name) = &projected_local_name { + *self.shared.advertisement_name.write().unwrap() = Some(name.clone()); } if let Ok(manufacturer_data) = advertisement.ManufacturerData() { if manufacturer_data.Size().unwrap() > 0 { @@ -191,8 +203,7 @@ impl Peripheral { if let Ok(data_sections) = advertisement.DataSections() { // See if we have any advertised service data before taking a lock to update... let mut found_service_data = false; - let mut manual_local_name: Option = None; - let mut has_complete_name = false; + let mut advertised_name = None; for section in &data_sections { match section.DataType().unwrap() { advertisement_data_type::SERVICE_DATA_16_BIT_UUID @@ -201,44 +212,38 @@ impl Peripheral { found_service_data = true; } advertisement_data_type::COMPLETE_LOCAL_NAME => { - let data = utils::to_vec(§ion.Data().unwrap()); - if let Ok(name) = String::from_utf8(data) { - let name = name.trim_end_matches('\0').trim().to_string(); - if !name.is_empty() { - manual_local_name = Some(name); - has_complete_name = true; - } + if let Some(name) = + parse_advertised_name(&utils::to_vec(§ion.Data().unwrap()), true) + { + advertised_name = Some(name); } } - advertisement_data_type::SHORT_LOCAL_NAME => { - // Only use SHORT_LOCAL_NAME if we haven't already found a COMPLETE_LOCAL_NAME - if !has_complete_name { - let data = utils::to_vec(§ion.Data().unwrap()); - if let Ok(name) = String::from_utf8(data) { - let name = name.trim_end_matches('\0').trim().to_string(); - if !name.is_empty() { - manual_local_name = Some(name); - } - } - } + advertisement_data_type::SHORT_LOCAL_NAME if advertised_name.is_none() => { + advertised_name = + parse_advertised_name(&utils::to_vec(§ion.Data().unwrap()), false); } _ => {} } } - if let Some(name) = manual_local_name { - if !name.is_empty() { - let existing = self.shared.local_name.read().unwrap().clone(); - // Only update if: (1) no existing name, or (2) this is a COMPLETE_LOCAL_NAME, - // or (3) new name is longer (prevents SHORT from overwriting COMPLETE across packets) - let should_update = match &existing { - None => true, - Some(old) => has_complete_name || name.len() > old.len(), - }; - if should_update { - *self.shared.local_name.write().unwrap() = Some(name.clone()); - self.emit_event(CentralEvent::DeviceUpdated(self.shared.address.into())); - } + let has_complete_name = self.shared.has_complete_local_name.load(Ordering::Relaxed); + let name = advertised_name.or_else(|| { + (!has_complete_name) + .then(|| projected_local_name.clone()) + .flatten() + .map(|value| AdvertisedName { + value, + is_complete: false, + }) + }); + if let Some(name) = name + && should_accept_name(has_complete_name, name.is_complete) + { + *self.shared.local_name.write().unwrap() = Some(name.value); + if name.is_complete { + self.shared + .has_complete_local_name + .store(true, Ordering::Relaxed); } } if found_service_data { @@ -359,6 +364,33 @@ impl Peripheral { } } +#[cfg(test)] +mod tests { + use super::{parse_advertised_name, should_accept_name}; + + #[test] + fn advertised_name_removes_only_nul_padding() { + let name = parse_advertised_name(b" Device Name \0\0", true).unwrap(); + + assert_eq!(name.value, " Device Name "); + assert!(name.is_complete); + } + + #[test] + fn advertised_name_rejects_empty_and_invalid_utf8() { + assert!(parse_advertised_name(b"\0\0", false).is_none()); + assert!(parse_advertised_name(&[0xff], true).is_none()); + } + + #[test] + fn complete_name_cannot_be_replaced_by_short_name() { + assert!(!should_accept_name(true, false)); + assert!(should_accept_name(true, true)); + assert!(should_accept_name(false, false)); + assert!(should_accept_name(false, true)); + } +} + impl Display for Peripheral { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let connected = if self.shared.connected.load(Ordering::Relaxed) { From b2bd989775ebf5d49f08c35a0d87fb9b8f3ff1c3 Mon Sep 17 00:00:00 2001 From: Ivan P <25914822+userepo@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:08:53 -0500 Subject: [PATCH 37/77] winrtble: also receive on the Coded PHY where supported Extended advertisements are already enabled unconditionally via SetAllowExtendedAdvertisements with the error ignored; enable UseCodedPhy the same way, so coded-primary (long range) BLE 5 advertisements are received on adapters that support them. On systems without Coded PHY support the call fails and is ignored, leaving behavior unchanged. --- src/winrtble/ble/watcher.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/winrtble/ble/watcher.rs b/src/winrtble/ble/watcher.rs index 7fe6a106..1f09cb44 100644 --- a/src/winrtble/ble/watcher.rs +++ b/src/winrtble/ble/watcher.rs @@ -77,6 +77,11 @@ impl BLEWatcher { self.watcher .SetScanningMode(BluetoothLEScanningMode::Active)?; let _ = self.watcher.SetAllowExtendedAdvertisements(true); + // Also receive on the Coded (long-range) PHY where the adapter and + // OS support it. Only takes effect alongside extended advertisements + // (above); the error is ignored the same way, so systems without + // Coded PHY support behave exactly as before. + let _ = self.watcher.SetUseCodedPhy(true); // Pre-convert the filter UUIDs once so the handler closure is cheap. let filter_guids: Vec = services.iter().map(utils::to_guid).collect(); From 21fe42bdc71f721713fa9f0d12e25aaf8749d6de Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 22:54:08 -0700 Subject: [PATCH 38/77] chore: Run rustfmt --- src/api/mod.rs | 4 +- src/corebluetooth/internal.rs | 285 +++++++++++++++++----------------- 2 files changed, 144 insertions(+), 145 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 0461f9b0..792d9187 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -223,8 +223,7 @@ pub struct ScanFilter { /// `None` leaves a selector unspecified; an explicitly empty selector matches nothing. Values /// within a selector are OR'ed, while the identifier and service selectors are combined as a /// union. Returned peripherals retain backend order and are deduplicated by identifier. -#[derive(Clone, Debug, Eq, PartialEq)] -#[derive(Default)] +#[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct RetrievePeripheralsOptions { /// Known peripheral identifiers to retrieve. pub identifiers: Option>, @@ -232,7 +231,6 @@ pub struct RetrievePeripheralsOptions { pub services: Option>, } - /// Returns whether a candidate identifier is included in an identifier selector. #[allow(dead_code)] // Used by platform-gated backend implementations. pub(crate) fn matches_identifier(candidate: &T, requested: &[T]) -> bool { diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 0c8d3a2e..f5a99b8d 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -604,9 +604,9 @@ impl CoreBluetoothInternal { rssi, )) .await - { - error!("Error sending notification event: {}", e); - } + { + error!("Error sending notification event: {}", e); + } } async fn on_service_data( @@ -621,9 +621,9 @@ impl CoreBluetoothInternal { .event_sender .send(PeripheralEventInternal::ServiceData(service_data, rssi)) .await - { - error!("Error sending notification event: {}", e); - } + { + error!("Error sending notification event: {}", e); + } } async fn on_services(&mut self, peripheral_uuid: Uuid, services: Vec, rssi: i16) { @@ -633,9 +633,9 @@ impl CoreBluetoothInternal { .event_sender .send(PeripheralEventInternal::Services(services, rssi)) .await - { - error!("Error sending notification event: {}", e); - } + { + error!("Error sending notification event: {}", e); + } } async fn on_services_modified(&mut self, peripheral_uuid: Uuid) { @@ -914,36 +914,36 @@ impl CoreBluetoothInternal { ) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - trace!("Got read event!"); + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + { + trace!("Got read event!"); - let mut data_clone = Vec::new(); - for byte in data.iter() { - data_clone.push(*byte); - } - // Reads and notifications both return the same callback. If - // we're trying to do a read, we'll have a future we can - // fulfill. Otherwise, just treat the returned value as a - // notification and use the event system. - if !characteristic.read_future_state.is_empty() { - let state = characteristic.read_future_state.pop_back().unwrap(); - state - .lock() - .unwrap() - .set_reply(CoreBluetoothReply::ReadResult(data_clone)); - } else if let Err(e) = peripheral - .event_sender - .send(PeripheralEventInternal::Notification( - characteristic_uuid, - service_uuid, - data, - )) - .await - { - error!("Error sending notification event: {}", e); - } - } + let mut data_clone = Vec::new(); + for byte in data.iter() { + data_clone.push(*byte); + } + // Reads and notifications both return the same callback. If + // we're trying to do a read, we'll have a future we can + // fulfill. Otherwise, just treat the returned value as a + // notification and use the event system. + if !characteristic.read_future_state.is_empty() { + let state = characteristic.read_future_state.pop_back().unwrap(); + state + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::ReadResult(data_clone)); + } else if let Err(e) = peripheral + .event_sender + .send(PeripheralEventInternal::Notification( + characteristic_uuid, + service_uuid, + data, + )) + .await + { + error!("Error sending notification event: {}", e); + } + } } fn on_characteristic_written( @@ -1007,44 +1007,44 @@ impl CoreBluetoothInternal { ) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - trace!("Writing value! With kind {:?}", kind); - match kind { - WriteType::WithoutResponse => { - if unsafe { peripheral.peripheral.canSendWriteWithoutResponse() } { - unsafe { - peripheral.peripheral.writeValue_forCharacteristic_type( - &NSData::from_vec(data), - &characteristic.characteristic, - CBCharacteristicWriteType::CBCharacteristicWriteWithoutResponse, - ); - } - fut.lock().unwrap().set_reply(CoreBluetoothReply::Ok); - } else { - trace!("Queueing write-without-response (peripheral not ready)"); - peripheral.write_without_response_queue.push_back( - PendingWriteWithoutResponse { - service_uuid, - characteristic_uuid, - data, - fut, - }, - ); - } - } - WriteType::WithResponse => { - unsafe { - peripheral.peripheral.writeValue_forCharacteristic_type( - &NSData::from_vec(data), - &characteristic.characteristic, - CBCharacteristicWriteType::CBCharacteristicWriteWithResponse, - ); - } - characteristic.write_future_state.push_front(fut); + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + { + trace!("Writing value! With kind {:?}", kind); + match kind { + WriteType::WithoutResponse => { + if unsafe { peripheral.peripheral.canSendWriteWithoutResponse() } { + unsafe { + peripheral.peripheral.writeValue_forCharacteristic_type( + &NSData::from_vec(data), + &characteristic.characteristic, + CBCharacteristicWriteType::CBCharacteristicWriteWithoutResponse, + ); } + fut.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + } else { + trace!("Queueing write-without-response (peripheral not ready)"); + peripheral.write_without_response_queue.push_back( + PendingWriteWithoutResponse { + service_uuid, + characteristic_uuid, + data, + fut, + }, + ); } } + WriteType::WithResponse => { + unsafe { + peripheral.peripheral.writeValue_forCharacteristic_type( + &NSData::from_vec(data), + &characteristic.characteristic, + CBCharacteristicWriteType::CBCharacteristicWriteWithResponse, + ); + } + characteristic.write_future_state.push_front(fut); + } + } + } } fn drain_write_without_response_queue(&mut self, peripheral_uuid: Uuid) { @@ -1101,16 +1101,16 @@ impl CoreBluetoothInternal { ) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - trace!("Reading value!"); - unsafe { - peripheral - .peripheral - .readValueForCharacteristic(&characteristic.characteristic); - } - characteristic.read_future_state.push_front(fut); - } + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + { + trace!("Reading value!"); + unsafe { + peripheral + .peripheral + .readValueForCharacteristic(&characteristic.characteristic); + } + characteristic.read_future_state.push_front(fut); + } } fn subscribe( @@ -1122,16 +1122,16 @@ impl CoreBluetoothInternal { ) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - trace!("Setting subscribe!"); - unsafe { - peripheral - .peripheral - .setNotifyValue_forCharacteristic(true, &characteristic.characteristic); - } - characteristic.subscribe_future_state.push_front(fut); - } + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + { + trace!("Setting subscribe!"); + unsafe { + peripheral + .peripheral + .setNotifyValue_forCharacteristic(true, &characteristic.characteristic); + } + characteristic.subscribe_future_state.push_front(fut); + } } fn unsubscribe( @@ -1143,17 +1143,16 @@ impl CoreBluetoothInternal { ) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - { - trace!("Setting subscribe!"); - unsafe { - peripheral.peripheral.setNotifyValue_forCharacteristic( - false, - &characteristic.characteristic, - ); - } - characteristic.unsubscribe_future_state.push_front(fut); - } + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + { + trace!("Setting subscribe!"); + unsafe { + peripheral + .peripheral + .setNotifyValue_forCharacteristic(false, &characteristic.characteristic); + } + characteristic.unsubscribe_future_state.push_front(fut); + } } fn write_descriptor_value( @@ -1167,17 +1166,17 @@ impl CoreBluetoothInternal { ) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { - trace!("Writing descriptor value!"); - unsafe { - peripheral.peripheral.writeValue_forDescriptor( - &NSData::from_vec(data), - &descriptor.descriptor, - ); - } - descriptor.write_future_state.push_front(fut); - } + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) + { + trace!("Writing descriptor value!"); + unsafe { + peripheral + .peripheral + .writeValue_forDescriptor(&NSData::from_vec(data), &descriptor.descriptor); + } + descriptor.write_future_state.push_front(fut); + } } fn read_descriptor_value( @@ -1190,16 +1189,17 @@ impl CoreBluetoothInternal { ) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { - trace!("Reading descriptor value!"); - unsafe { - peripheral - .peripheral - .readValueForDescriptor(&descriptor.descriptor); - } - descriptor.read_future_state.push_front(fut); - } + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) + { + trace!("Reading descriptor value!"); + unsafe { + peripheral + .peripheral + .readValueForDescriptor(&descriptor.descriptor); + } + descriptor.read_future_state.push_front(fut); + } } fn read_rssi(&mut self, peripheral_uuid: Uuid, fut: CoreBluetoothReplyStateShared) { @@ -1238,9 +1238,9 @@ impl CoreBluetoothInternal { .event_sender .send(PeripheralEventInternal::TxPowerLevel(tx_power_level)) .await - { - error!("Error sending tx_power_level event: {}", e); - } + { + error!("Error sending tx_power_level event: {}", e); + } } async fn on_descriptor_read( @@ -1253,21 +1253,22 @@ impl CoreBluetoothInternal { ) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { - trace!("Got read event!"); + && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) + { + trace!("Got read event!"); - let mut data_clone = Vec::new(); - for byte in data.iter() { - data_clone.push(*byte); - } - if let Some(state) = descriptor.read_future_state.pop_back() { - state - .lock() - .unwrap() - .set_reply(CoreBluetoothReply::ReadResult(data_clone)); - } - } + let mut data_clone = Vec::new(); + for byte in data.iter() { + data_clone.push(*byte); + } + if let Some(state) = descriptor.read_future_state.pop_back() { + state + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::ReadResult(data_clone)); + } + } } fn on_descriptor_written( From c9b7576e624073eaf32eb7e4bfecefac325769e0 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 23:04:51 -0700 Subject: [PATCH 39/77] fix(winrt): avoid non-Send iterators across awaits --- src/winrtble/adapter.rs | 11 +++++++++-- src/winrtble/manager.rs | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index cf7ec7c9..29c107f2 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -156,7 +156,12 @@ impl Central for Adapter { filter, Box::new(move |args| { let bluetooth_address = args.BluetoothAddress()?; - let address = checked_address(bluetooth_address)?; + let address = checked_address(bluetooth_address).map_err(|error| { + windows::core::Error::new( + windows::core::HRESULT::from_win32(87), + error.to_string(), + ) + })?; if let Some(mut entry) = manager.peripheral_mut(&address.into()) { entry.value_mut().update_properties(args); manager.emit(CentralEvent::DeviceUpdated(address.into())); @@ -237,7 +242,9 @@ impl Central for Adapter { .map_err(winrt_error)? .into_future() .await - .map_err(winrt_error)?; + .map_err(winrt_error)? + .into_iter() + .collect::>(); let mut result = Vec::new(); for info in devices { diff --git a/src/winrtble/manager.rs b/src/winrtble/manager.rs index 8198b0a7..e9417707 100644 --- a/src/winrtble/manager.rs +++ b/src/winrtble/manager.rs @@ -35,7 +35,9 @@ impl api::Manager for Manager { let selector = BluetoothAdapter::GetDeviceSelector()?; let devices = DeviceInformation::FindAllAsyncAqsFilter(&selector)? .into_future() - .await?; + .await? + .into_iter() + .collect::>(); let mut adapters = Vec::new(); for device in devices { let device_id = device.Id()?; From be416373101767bd4e17d8c179545306d281c472 Mon Sep 17 00:00:00 2001 From: tanarchytan Date: Wed, 15 Jul 2026 12:03:20 +0200 Subject: [PATCH 40/77] feat(winrtble): implement add_peripheral (connect by address) Was a NotSupported stub on Windows. Implement it the same way the scanner registers a discovered device (create a Peripheral from the address and add it to the manager), matching the Android backend, so a bonded or already connected device can be reached by address without an advertisement. Also adds the symmetric From for BDAddr. --- src/winrtble/adapter.rs | 13 +++++++++---- src/winrtble/peripheral.rs | 6 ++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index 29c107f2..dcfdcbd5 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -299,10 +299,15 @@ impl Central for Adapter { self.manager.peripheral(id).ok_or(Error::DeviceNotFound) } - async fn add_peripheral(&self, _address: &PeripheralId) -> Result { - Err(Error::NotSupported( - "Can't add a Peripheral from a BDAddr".to_string(), - )) + async fn add_peripheral(&self, id: &PeripheralId) -> Result { + if let Some(peripheral) = self.manager.peripheral(id) { + return Ok(peripheral); + } + // Create a peripheral straight from its address so a device the OS already knows (bonded or + // connected to another central) can be reached without waiting for an advertisement. + let peripheral = Peripheral::new(Arc::downgrade(&self.manager), id.clone().into()); + self.manager.add_peripheral(peripheral.clone()); + Ok(peripheral) } async fn clear_peripherals(&self) -> Result<()> { diff --git a/src/winrtble/peripheral.rs b/src/winrtble/peripheral.rs index d343352e..6d558bbf 100644 --- a/src/winrtble/peripheral.rs +++ b/src/winrtble/peripheral.rs @@ -748,3 +748,9 @@ impl From for PeripheralId { PeripheralId(address) } } + +impl From for BDAddr { + fn from(id: PeripheralId) -> Self { + id.0 + } +} From 40279f1e60e07b10a4d55b3c513898eb18b04443 Mon Sep 17 00:00:00 2001 From: Chris Morgan Date: Thu, 16 Jul 2026 08:21:05 -0400 Subject: [PATCH 41/77] corebluetooth - store negotiated MTU after service discovery CoreBluetooth's maximum write length is not reliably negotiated in the initial connection callback. Infer and publish the ATT MTU after service discovery, while resetting the exposed MTU to the default for each new connection. Rename the Rust integration test and its Android JNI/Kotlin entry points from "after connection" to "after service discovery". The shared helper discovers services before reading the MTU, so the old name overstated connect-only coverage; the JNI export and Kotlin declarations must match the renamed shared test. --- src/corebluetooth/internal.rs | 31 +++++++++++++++++-- src/corebluetooth/peripheral.rs | 8 ++++- tests/android/rust/src/lib.rs | 4 +-- .../btleplug/test/BleIntegrationTest.kt | 2 +- .../btleplug/test/NativeTests.kt | 2 +- tests/common/test_cases.rs | 2 +- tests/test_mtu_after_connection.rs | 7 ----- tests/test_mtu_after_service_discovery.rs | 7 +++++ 8 files changed, 48 insertions(+), 15 deletions(-) delete mode 100644 tests/test_mtu_after_connection.rs create mode 100644 tests/test_mtu_after_service_discovery.rs diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index f5a99b8d..63a0776e 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -46,6 +46,21 @@ use std::{ use tokio::runtime; use uuid::Uuid; +/// ATT Write Command PDUs reserve one byte for the opcode and two bytes for +/// the attribute handle (Bluetooth Core Specification, Vol 3, Part F, 3.4.5.3). +const ATT_WRITE_COMMAND_HEADER_LEN: usize = 3; + +fn maximum_write_value_length_to_att_mtu(maximum_write_value_length: usize) -> Result { + maximum_write_value_length + .checked_add(ATT_WRITE_COMMAND_HEADER_LEN) + .and_then(|mtu| u16::try_from(mtu).ok()) + .ok_or_else(|| { + format!( + "CoreBluetooth maximum write value length {maximum_write_value_length} cannot be represented as a u16 ATT MTU" + ) + }) +} + struct DescriptorInternal { pub descriptor: Retained, pub uuid: Uuid, @@ -163,7 +178,7 @@ pub enum CoreBluetoothReply { ReadResult(Vec), ReadRssi(i16), Connected, - ServicesDiscovered(BTreeSet), + ServicesDiscovered(BTreeSet, u16), State(CBPeripheralState), Ok, Peripherals(Vec), @@ -351,12 +366,24 @@ impl PeripheralInternal { .collect(), }) .collect(); + // CoreBluetooth exposes the maximum characteristic value length for + // a write, not the ATT MTU. Sample it after discovery, then account + // for the ATT Write Command header to infer the full ATT MTU. + let maximum_write_value_length = unsafe { + self.peripheral.maximumWriteValueLengthForType( + CBCharacteristicWriteType::CBCharacteristicWriteWithoutResponse, + ) + }; + let reply = match maximum_write_value_length_to_att_mtu(maximum_write_value_length) { + Ok(mtu) => CoreBluetoothReply::ServicesDiscovered(services, mtu), + Err(error) => CoreBluetoothReply::Err(error), + }; self.services_discovered_future_state .take() .unwrap() .lock() .unwrap() - .set_reply(CoreBluetoothReply::ServicesDiscovered(services)); + .set_reply(reply); } } diff --git a/src/corebluetooth/peripheral.rs b/src/corebluetooth/peripheral.rs index 3b46fcc0..cddb8fbf 100644 --- a/src/corebluetooth/peripheral.rs +++ b/src/corebluetooth/peripheral.rs @@ -366,6 +366,9 @@ impl api::Peripheral for Peripheral { .await?; match fut.await { CoreBluetoothReply::Connected => { + self.shared + .mtu + .store(api::DEFAULT_MTU_SIZE, std::sync::atomic::Ordering::Relaxed); self.shared .emit_event(CentralEvent::DeviceConnected(self.shared.uuid.into())); } @@ -408,8 +411,11 @@ impl api::Peripheral for Peripheral { }) .await?; match fut.await { - CoreBluetoothReply::ServicesDiscovered(services) => { + CoreBluetoothReply::ServicesDiscovered(services, mtu) => { *(self.shared.services.lock().map_err(Into::::into)?) = services; + self.shared + .mtu + .store(mtu, std::sync::atomic::Ordering::Relaxed); return Ok(()); } CoreBluetoothReply::Err(msg) => return Err(Error::RuntimeError(msg)), diff --git a/tests/android/rust/src/lib.rs b/tests/android/rust/src/lib.rs index c2875787..f6781c4f 100644 --- a/tests/android/rust/src/lib.rs +++ b/tests/android/rust/src/lib.rs @@ -224,8 +224,8 @@ jni_test!( test_cases::test_descriptor_discovery ); jni_test!( - Java_com_nonpolynomial_btleplug_test_NativeTests_testMtuAfterConnection, - test_cases::test_mtu_after_connection + Java_com_nonpolynomial_btleplug_test_NativeTests_testMtuAfterServiceDiscovery, + test_cases::test_mtu_after_service_discovery ); jni_test!( Java_com_nonpolynomial_btleplug_test_NativeTests_testReadRssi, diff --git a/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt b/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt index a8656e6d..87f1eb65 100644 --- a/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt +++ b/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt @@ -103,7 +103,7 @@ class BleIntegrationTest { @Test fun testDescriptorDiscovery() = NativeTests.testDescriptorDiscovery() // ── Device Info ───────────────────────────────────────────────── - @Test fun testMtuAfterConnection() = NativeTests.testMtuAfterConnection() + @Test fun testMtuAfterServiceDiscovery() = NativeTests.testMtuAfterServiceDiscovery() @Test fun testReadRssi() = NativeTests.testReadRssi() @Test fun testPropertiesContainPeripheralInfo() = NativeTests.testPropertiesContainPeripheralInfo() @Test fun testConnectionParameters() = NativeTests.testConnectionParameters() diff --git a/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt b/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt index 2a9448d7..8c0b8815 100644 --- a/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt +++ b/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt @@ -45,7 +45,7 @@ object NativeTests { external fun testDescriptorDiscovery() // Device Info - external fun testMtuAfterConnection() + external fun testMtuAfterServiceDiscovery() external fun testReadRssi() external fun testPropertiesContainPeripheralInfo() external fun testConnectionParameters() diff --git a/tests/common/test_cases.rs b/tests/common/test_cases.rs index 2a897aa0..72c2edd2 100644 --- a/tests/common/test_cases.rs +++ b/tests/common/test_cases.rs @@ -717,7 +717,7 @@ pub async fn test_descriptor_discovery() { // ── Device Info ───────────────────────────────────────────────────── -pub async fn test_mtu_after_connection() { +pub async fn test_mtu_after_service_discovery() { let peripheral = peripheral_finder::find_and_connect().await; let mtu = peripheral.mtu(); assert!( diff --git a/tests/test_mtu_after_connection.rs b/tests/test_mtu_after_connection.rs deleted file mode 100644 index 50f549b1..00000000 --- a/tests/test_mtu_after_connection.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod common; - -#[tokio::test] -#[ignore = "requires BLE test peripheral"] -async fn test_mtu_after_connection() { - common::test_cases::test_mtu_after_connection().await; -} diff --git a/tests/test_mtu_after_service_discovery.rs b/tests/test_mtu_after_service_discovery.rs new file mode 100644 index 00000000..603ea1eb --- /dev/null +++ b/tests/test_mtu_after_service_discovery.rs @@ -0,0 +1,7 @@ +mod common; + +#[tokio::test] +#[ignore = "requires BLE test peripheral"] +async fn test_mtu_after_service_discovery() { + common::test_cases::test_mtu_after_service_discovery().await; +} From 4fc1ce2ba5f1816eb1746a71f798afe6e1fb098e Mon Sep 17 00:00:00 2001 From: Chris Morgan Date: Thu, 16 Jul 2026 08:22:29 -0400 Subject: [PATCH 42/77] tests - require negotiated CoreBluetooth MTU --- tests/common/test_cases.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/common/test_cases.rs b/tests/common/test_cases.rs index 72c2edd2..f76680cd 100644 --- a/tests/common/test_cases.rs +++ b/tests/common/test_cases.rs @@ -720,6 +720,15 @@ pub async fn test_descriptor_discovery() { pub async fn test_mtu_after_service_discovery() { let peripheral = peripheral_finder::find_and_connect().await; let mtu = peripheral.mtu(); + + #[cfg(any(target_os = "macos", target_os = "ios"))] + assert!( + mtu > 23, + "CoreBluetooth MTU should be negotiated above the default, got {}", + mtu + ); + + #[cfg(not(any(target_os = "macos", target_os = "ios")))] assert!( mtu >= 23, "MTU should be at least 23 (default), got {}", From d2170d910cb0a9b7d1bf98ff449a2bbdc11402ff Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 23:30:48 -0700 Subject: [PATCH 43/77] fix(corebluetooth): validate inferred MTU --- src/corebluetooth/internal.rs | 39 ++++++++++++++++++++++++++++++++++- tests/common/test_cases.rs | 8 ------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 63a0776e..83098820 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -51,6 +51,10 @@ use uuid::Uuid; const ATT_WRITE_COMMAND_HEADER_LEN: usize = 3; fn maximum_write_value_length_to_att_mtu(maximum_write_value_length: usize) -> Result { + if maximum_write_value_length == 0 { + return Ok(crate::api::DEFAULT_MTU_SIZE); + } + maximum_write_value_length .checked_add(ATT_WRITE_COMMAND_HEADER_LEN) .and_then(|mtu| u16::try_from(mtu).ok()) @@ -1644,6 +1648,14 @@ mod tests { fn name(&self) -> Option> { None } + + #[method(maximumWriteValueLengthForType:)] + fn maximum_write_value_length_for_type( + &self, + _write_type: CBCharacteristicWriteType, + ) -> usize { + 0 + } } ); @@ -1654,6 +1666,30 @@ mod tests { } } + #[test] + fn maximum_write_value_length_is_converted_to_att_mtu() { + assert_eq!(maximum_write_value_length_to_att_mtu(20), Ok(23)); + assert_eq!(maximum_write_value_length_to_att_mtu(512), Ok(515)); + assert_eq!( + maximum_write_value_length_to_att_mtu(u16::MAX as usize - 3), + Ok(u16::MAX) + ); + } + + #[test] + fn zero_maximum_write_value_length_uses_default_mtu() { + assert_eq!( + maximum_write_value_length_to_att_mtu(0), + Ok(crate::api::DEFAULT_MTU_SIZE) + ); + } + + #[test] + fn unrepresentable_maximum_write_value_length_is_rejected() { + assert!(maximum_write_value_length_to_att_mtu(u16::MAX as usize).is_err()); + assert!(maximum_write_value_length_to_att_mtu(usize::MAX).is_err()); + } + #[tokio::test] async fn descriptor_discovery_error_completes_service_discovery_without_descriptors() { let peripheral_uuid = Uuid::from_u128(0x12345678_1234_5678_1234_567812345678); @@ -1737,9 +1773,10 @@ mod tests { let reply = tokio::time::timeout(Duration::from_secs(1), discovery) .await .expect("service discovery remained pending after descriptor error"); - let CoreBluetoothReply::ServicesDiscovered(services) = reply else { + let CoreBluetoothReply::ServicesDiscovered(services, mtu) = reply else { panic!("unexpected discovery reply: {reply:?}"); }; + assert_eq!(mtu, crate::api::DEFAULT_MTU_SIZE); let characteristic = services .iter() .find(|service| service.uuid == service_uuid) diff --git a/tests/common/test_cases.rs b/tests/common/test_cases.rs index f76680cd..4a53104d 100644 --- a/tests/common/test_cases.rs +++ b/tests/common/test_cases.rs @@ -721,14 +721,6 @@ pub async fn test_mtu_after_service_discovery() { let peripheral = peripheral_finder::find_and_connect().await; let mtu = peripheral.mtu(); - #[cfg(any(target_os = "macos", target_os = "ios"))] - assert!( - mtu > 23, - "CoreBluetooth MTU should be negotiated above the default, got {}", - mtu - ); - - #[cfg(not(any(target_os = "macos", target_os = "ios")))] assert!( mtu >= 23, "MTU should be at least 23 (default), got {}", From b1a1e80ff0c65ae20b568e322a77fad54b0d4b82 Mon Sep 17 00:00:00 2001 From: Thaddeus Ternes Date: Mon, 27 Jul 2026 13:59:26 -0500 Subject: [PATCH 44/77] Raise droidplug's minSdk to 24 to match the API it uses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vendored jni-utils interfaces `FnFunction` and `FnBiFunction` extend `java.util.function.Function` and `java.util.function.BiFunction`, both introduced in API 24, while the library declared `minSdk 23`. Lint flags both as errors, so `./gradlew build` fails on a clean checkout; the declared floor also misrepresents what the library can actually run on. `FnBiFunctionImpl` is registered in the class cache by `platform::init`, so on an API 23 device without core library desugaring resolving it can fail at runtime rather than at build time. Raise the floor to the API level the code requires. The alternative — keeping minSdk 23 and enabling core library desugaring — pushes the same requirement onto every consumer, since desugaring must be enabled in the consuming application too. --- src/droidplug/java/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/droidplug/java/build.gradle b/src/droidplug/java/build.gradle index d005720a..7793f39f 100644 --- a/src/droidplug/java/build.gradle +++ b/src/droidplug/java/build.gradle @@ -12,7 +12,7 @@ android { compileSdk 34 defaultConfig { - minSdk 23 + minSdk 24 versionCode 1 versionName '0.7.3' } From 5a83163b3c1b80e7629b63e67bd733b41096c26a Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 23:37:36 -0700 Subject: [PATCH 45/77] test(android): raise instrumentation app minSdk to 24 --- tests/android/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/android/build.gradle.kts b/tests/android/build.gradle.kts index 0e1d3a78..3b8cc18f 100644 --- a/tests/android/build.gradle.kts +++ b/tests/android/build.gradle.kts @@ -15,7 +15,7 @@ android { defaultConfig { applicationId = "com.nonpolynomial.btleplug.test" - minSdk = 23 + minSdk = 24 targetSdk = 34 versionCode = 1 versionName = "1.0" From 32180be24b4d1c703245c0ce139994ade933089a Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 23:38:43 -0700 Subject: [PATCH 46/77] ci(android): build Gradle artifacts and document API floor --- .github/workflows/rust.yml | 15 ++++++++++++++- README.md | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a81ed291..247d89cd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -44,7 +44,7 @@ jobs: if: ${{ runner.os == 'Linux' }} run: | sudo apt-get update - sudo apt-get install libdbus-1-dev + sudo apt-get install libdbus-1-dev openjdk-17-jdk - uses: actions/setup-java@v2 if: ${{ matrix.target == 'android' }} with: @@ -71,9 +71,22 @@ jobs: run: cargo check --all --bins --examples --no-default-features - name: Check with all features run: cargo check --all --bins --examples --all-features + - name: Run JNI host tests + if: ${{ matrix.target == 'linux' || matrix.target == 'macos' }} + run: ./scripts/run-jni-tests.sh - name: Run tests if: ${{ matrix.target != 'android' }} run: cargo test --all + - name: Build Android library and test app + if: ${{ matrix.target == 'android' }} + run: | + ./scripts/build-java.sh + cp src/droidplug/java/gradlew tests/android/ + cp -r src/droidplug/java/gradle tests/android/ + printf 'sdk.dir=%s\n' "$ANDROID_HOME" > tests/android/local.properties + cd tests/android + chmod +x gradlew + ./gradlew assembleDebug assembleAndroidTest - name: Run clippy uses: actions-rs/clippy-check@v1 with: diff --git a/README.md b/README.md index ee85a81d..d43b0c82 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,12 @@ Privacy_ → _Privacy_ → _Bluetooth_, clicking the '+' button, and selecting Due to requiring a hybrid Rust/Java build, btleplug for Android requires a somewhat complicated setup. +The Android library and its test application require Android API 24 (Android 7.0) or newer. This is +because the JNI support classes use Java functional interfaces introduced in API 24. Applications +that need to support API 23 must use a compatible btleplug release or provide a redesigned JNI +interface; core library desugaring is not enabled by btleplug and would also need to be configured +by the consuming application. + There is now a build script at `./scripts/build-java.sh` for building the java portion of the library on linux or macOS. This can also be used as a guide for manual building if need be. If your app uses Proguard/R8 with `minifyEnabled true`, you must add keep rules for btleplug's From 07b2b18322c669375f959f989b9acd9162be065b Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 23:39:53 -0700 Subject: [PATCH 47/77] ci: provision Java for JNI host tests --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 247d89cd..73b83b3e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,7 +46,7 @@ jobs: sudo apt-get update sudo apt-get install libdbus-1-dev openjdk-17-jdk - uses: actions/setup-java@v2 - if: ${{ matrix.target == 'android' }} + if: ${{ matrix.target == 'android' || matrix.target == 'linux' || matrix.target == 'macos' }} with: distribution: 'zulu' java-version: '17' From e2917ce4842d0d3c790f908b0c8512a0f7097af4 Mon Sep 17 00:00:00 2001 From: liumingzhu Date: Wed, 29 Jul 2026 00:55:57 -0700 Subject: [PATCH 48/77] feat: expose GAP appearance in peripheral properties --- CHANGELOG.md | 13 ++++ src/advertisement.rs | 115 ++++++++++++++++++++++++++++++++ src/api/mod.rs | 31 +++++++++ src/bluez/peripheral.rs | 1 + src/corebluetooth/peripheral.rs | 1 + src/droidplug/jni/objects.rs | 17 +++++ src/lib.rs | 2 + src/winrtble/peripheral.rs | 23 ++++++- 8 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 src/advertisement.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b54cf760..78e0a98d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# Unreleased + +## Features + +- Add `appearance` to `PeripheralProperties`, populated from GAP Appearance + advertising data on Windows, Linux, and Android. CoreBluetooth does not expose + this advertising field, so it remains `None` on Apple platforms. + +## Breaking Changes + +- **`PeripheralProperties` struct literals**: The new `appearance` field must be + initialized by callers that construct this public struct directly. + # 0.12.0 (2026-03-08) ## Features diff --git a/src/advertisement.rs b/src/advertisement.rs new file mode 100644 index 00000000..d2fb3612 --- /dev/null +++ b/src/advertisement.rs @@ -0,0 +1,115 @@ +// btleplug Source Code File +// +// Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. +// +// Licensed under the BSD 3-Clause license. See LICENSE file in the project root +// for full license information. + +/// GAP Appearance advertising data type. +pub(crate) const APPEARANCE_DATA_TYPE: u8 = 0x19; + +/// Parse the payload of a GAP Appearance advertising data section. +pub(crate) fn parse_appearance(data: &[u8]) -> Option { + let bytes: [u8; 2] = data.try_into().ok()?; + Some(u16::from_le_bytes(bytes)) +} + +/// Parse GAP Appearance from a length-prefixed Bluetooth LE advertising record. +#[cfg(any(target_os = "android", test))] +pub(crate) fn parse_appearance_from_advertisement(data: &[u8]) -> Option { + let mut offset = 0; + + while let Some(&length) = data.get(offset) { + offset += 1; + + if length == 0 { + break; + } + + let end = offset.checked_add(usize::from(length))?; + let section = data.get(offset..end)?; + let (&data_type, payload) = section.split_first()?; + + if data_type == APPEARANCE_DATA_TYPE { + if let Some(appearance) = parse_appearance(payload) { + return Some(appearance); + } + } + + offset = end; + } + + None +} + +#[cfg(test)] +mod tests { + use super::{APPEARANCE_DATA_TYPE, parse_appearance, parse_appearance_from_advertisement}; + + #[test] + fn parses_appearance_payload_as_little_endian() { + assert_eq!(parse_appearance(&[0x80, 0x04]), Some(0x0480)); + assert_eq!(parse_appearance(&[0x00, 0x00]), Some(0x0000)); + } + + #[test] + fn rejects_appearance_payloads_that_are_not_exactly_two_bytes() { + assert_eq!(parse_appearance(&[]), None); + assert_eq!(parse_appearance(&[0x80]), None); + assert_eq!(parse_appearance(&[0x80, 0x04, 0x00]), None); + } + + #[test] + fn finds_appearance_among_other_advertising_sections() { + let advertisement = [ + 2, + 0x01, + 0x06, + 3, + APPEARANCE_DATA_TYPE, + 0x80, + 0x04, + 2, + 0x0a, + 0xf8, + ]; + + assert_eq!( + parse_appearance_from_advertisement(&advertisement), + Some(0x0480) + ); + } + + #[test] + fn skips_invalid_appearance_section_and_accepts_a_later_valid_one() { + let advertisement = [ + 2, + APPEARANCE_DATA_TYPE, + 0x80, + 3, + APPEARANCE_DATA_TYPE, + 0x40, + 0x03, + ]; + + assert_eq!( + parse_appearance_from_advertisement(&advertisement), + Some(0x0340) + ); + } + + #[test] + fn handles_missing_and_malformed_advertising_sections() { + assert_eq!(parse_appearance_from_advertisement(&[]), None); + assert_eq!(parse_appearance_from_advertisement(&[0]), None); + assert_eq!(parse_appearance_from_advertisement(&[2, 0x01, 0x06]), None); + assert_eq!( + parse_appearance_from_advertisement(&[3, APPEARANCE_DATA_TYPE, 0x80,]), + None + ); + assert_eq!( + parse_appearance_from_advertisement(&[0, 3, APPEARANCE_DATA_TYPE, 0x80, 0x04,]), + None + ); + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 792d9187..985a9bf7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -190,6 +190,9 @@ pub struct PeripheralProperties { pub local_name: Option, /// The advertisement name. May be different than local_name. pub advertisement_name: Option, + /// The GAP appearance reported by this peripheral. + #[cfg_attr(feature = "serde", serde(default))] + pub appearance: Option, /// The transmission power level for the device pub tx_power_level: Option, /// The most recent Received Signal Strength Indicator for the device @@ -692,3 +695,31 @@ pub trait Manager { /// Get a list of all Bluetooth adapters on the system. Each adapter implements [`Central`]. async fn adapters(&self) -> Result>; } + +#[cfg(all(test, feature = "serde"))] +mod tests { + use super::PeripheralProperties; + + #[test] + fn peripheral_properties_round_trip_appearance() { + let properties = PeripheralProperties { + appearance: Some(0x0340), + ..PeripheralProperties::default() + }; + + let value = serde_json::to_value(&properties).unwrap(); + assert_eq!(value["appearance"], 0x0340); + + let decoded: PeripheralProperties = serde_json::from_value(value).unwrap(); + assert_eq!(decoded.appearance, Some(0x0340)); + } + + #[test] + fn peripheral_properties_missing_appearance_defaults_to_none() { + let mut value = serde_json::to_value(PeripheralProperties::default()).unwrap(); + value.as_object_mut().unwrap().remove("appearance").unwrap(); + + let properties: PeripheralProperties = serde_json::from_value(value).unwrap(); + assert_eq!(properties.appearance, None); + } +} diff --git a/src/bluez/peripheral.rs b/src/bluez/peripheral.rs index f974cf00..fba663a0 100644 --- a/src/bluez/peripheral.rs +++ b/src/bluez/peripheral.rs @@ -156,6 +156,7 @@ impl api::Peripheral for Peripheral { address_type: Some(device_info.address_type.into()), local_name: device_info.alias.or(device_info.name.clone()), advertisement_name: device_info.name, + appearance: device_info.appearance, tx_power_level: device_info.tx_power, rssi: device_info.rssi, manufacturer_data: device_info.manufacturer_data, diff --git a/src/corebluetooth/peripheral.rs b/src/corebluetooth/peripheral.rs index cddb8fbf..6445a0dd 100644 --- a/src/corebluetooth/peripheral.rs +++ b/src/corebluetooth/peripheral.rs @@ -100,6 +100,7 @@ impl Peripheral { address_type: None, local_name, advertisement_name, + appearance: None, tx_power_level: None, rssi: None, manufacturer_data: HashMap::new(), diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 0f086e5e..81886f07 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -522,6 +522,10 @@ impl<'local> JScanResult<'local> { }; let rssi = Some(self.get_rssi(env)? as i16); + let appearance = record + .get_bytes(env)? + .as_deref() + .and_then(crate::advertisement::parse_appearance_from_advertisement); let mfr_data_obj = record.get_manufacturer_specific_data(env)?; let mut manufacturer_data = HashMap::new(); @@ -624,6 +628,7 @@ impl<'local> JScanResult<'local> { address_type: None, local_name: device_name.clone(), advertisement_name: device_name, + appearance, tx_power_level, manufacturer_data, service_data, @@ -644,6 +649,18 @@ bind_java_type! { } impl<'local> JScanRecord<'local> { + pub fn get_bytes(&self, env: &mut Env<'local>) -> Result>> { + let value = env + .call_method(self, jni_str!("getBytes"), jni_sig!("()[B"), &[])? + .l()?; + if value.is_null() { + Ok(None) + } else { + let value = unsafe { jni::objects::JByteArray::from_raw(env, value.into_raw()) }; + crate::droidplug::jni_utils::arrays::byte_array_to_vec(env, &value).map(Some) + } + } + pub fn get_device_name(&self, env: &mut Env<'local>) -> Result> { env.call_method( self, diff --git a/src/lib.rs b/src/lib.rs index dd7dc4f8..368b4690 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,8 @@ use crate::api::ParseBDAddrError; use std::result; use std::time::Duration; +#[cfg(any(target_os = "android", target_os = "windows", test))] +mod advertisement; pub mod api; #[cfg(target_os = "linux")] mod bluez; diff --git a/src/winrtble/peripheral.rs b/src/winrtble/peripheral.rs index 6d558bbf..8f35545a 100644 --- a/src/winrtble/peripheral.rs +++ b/src/winrtble/peripheral.rs @@ -89,6 +89,7 @@ struct Shared { local_name: RwLock>, has_complete_local_name: AtomicBool, advertisement_name: RwLock>, + appearance: RwLock>, last_tx_power_level: RwLock>, // XXX: would be nice to avoid lock here! last_rssi: RwLock>, // XXX: would be nice to avoid lock here! latest_manufacturer_data: RwLock>>, @@ -130,6 +131,7 @@ impl Peripheral { local_name: RwLock::new(None), has_complete_local_name: AtomicBool::new(false), advertisement_name: RwLock::new(None), + appearance: RwLock::new(None), last_tx_power_level: RwLock::new(None), last_rssi: RwLock::new(None), latest_manufacturer_data: RwLock::new(HashMap::new()), @@ -148,6 +150,7 @@ impl Peripheral { address_type: *self.shared.address_type.read().unwrap(), local_name: self.shared.local_name.read().unwrap().clone(), advertisement_name: self.shared.advertisement_name.read().unwrap().clone(), + appearance: *self.shared.appearance.read().unwrap(), tx_power_level: *self.shared.last_tx_power_level.read().unwrap(), rssi: *self.shared.last_rssi.read().unwrap(), manufacturer_data: self.shared.latest_manufacturer_data.read().unwrap().clone(), @@ -201,11 +204,14 @@ impl Peripheral { // The Windows Runtime API (as of 19041) does not directly expose Service Data as a friendly API (like Manufacturer Data above) // Instead they provide data sections for access to raw advertising data. That is processed here. if let Ok(data_sections) = advertisement.DataSections() { - // See if we have any advertised service data before taking a lock to update... let mut found_service_data = false; let mut advertised_name = None; + let mut appearance = None; for section in &data_sections { - match section.DataType().unwrap() { + let Ok(data_type) = section.DataType() else { + continue; + }; + match data_type { advertisement_data_type::SERVICE_DATA_16_BIT_UUID | advertisement_data_type::SERVICE_DATA_32_BIT_UUID | advertisement_data_type::SERVICE_DATA_128_BIT_UUID => { @@ -222,6 +228,14 @@ impl Peripheral { advertised_name = parse_advertised_name(&utils::to_vec(§ion.Data().unwrap()), false); } + crate::advertisement::APPEARANCE_DATA_TYPE => { + if let Ok(data) = section.Data() + && let Some(parsed) = + crate::advertisement::parse_appearance(&utils::to_vec(&data)) + { + appearance = Some(parsed); + } + } _ => {} } } @@ -246,6 +260,11 @@ impl Peripheral { .store(true, Ordering::Relaxed); } } + + if let Some(appearance) = appearance { + *self.shared.appearance.write().unwrap() = Some(appearance); + } + if found_service_data { let mut service_data_guard = self.shared.latest_service_data.write().unwrap(); From 39fddc14f3c61993c7c38157195e32498f7fd1af Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sat, 29 Aug 2026 22:06:43 -0700 Subject: [PATCH 49/77] test: cover GAP appearance in peripheral properties --- src/advertisement.rs | 8 ++++---- src/api/mod.rs | 2 +- test-peripheral/bumble/test_peripheral.py | 2 ++ test-peripheral/zephyr/src/main.c | 15 +++++++++------ tests/common/gatt_uuids.rs | 1 + tests/common/test_cases.rs | 11 +++++++++++ 6 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/advertisement.rs b/src/advertisement.rs index d2fb3612..d720d0ed 100644 --- a/src/advertisement.rs +++ b/src/advertisement.rs @@ -30,10 +30,10 @@ pub(crate) fn parse_appearance_from_advertisement(data: &[u8]) -> Option { let section = data.get(offset..end)?; let (&data_type, payload) = section.split_first()?; - if data_type == APPEARANCE_DATA_TYPE { - if let Some(appearance) = parse_appearance(payload) { - return Some(appearance); - } + if data_type == APPEARANCE_DATA_TYPE + && let Some(appearance) = parse_appearance(payload) + { + return Some(appearance); } offset = end; diff --git a/src/api/mod.rs b/src/api/mod.rs index 985a9bf7..2898f343 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -697,7 +697,7 @@ pub trait Manager { } #[cfg(all(test, feature = "serde"))] -mod tests { +mod serde_tests { use super::PeripheralProperties; #[test] diff --git a/test-peripheral/bumble/test_peripheral.py b/test-peripheral/bumble/test_peripheral.py index 2fedd33b..2f58d99a 100644 --- a/test-peripheral/bumble/test_peripheral.py +++ b/test-peripheral/bumble/test_peripheral.py @@ -66,6 +66,7 @@ CMD_SET_NOTIFICATION_PAYLOAD = 0x06 DEVICE_NAME = "btleplug-test" +TEST_APPEARANCE = 0x0340 MANUFACTURER_COMPANY_ID = 0xFFFF STATIC_READ_VALUE = bytes([0x01, 0x02, 0x03, 0x04]) NOTIFICATION_INTERVAL = 1.0 # seconds @@ -409,6 +410,7 @@ async def main(): struct.pack(" Date: Sat, 29 Aug 2026 23:58:16 -0700 Subject: [PATCH 50/77] docs: add 0.13.0 changelog --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e0a98d..d2a08a19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,48 @@ -# Unreleased +# 0.13.0 (2026-08-29) ## Features +- Add `Central::retrieve_peripherals()` and `RetrievePeripheralsOptions` for + retrieving peripherals by identifier or advertised service without scanning. + This is supported on Linux, macOS/iOS, and Windows; Android returns + `Error::NotSupported`. +- Add `Central::adapter_address()` for retrieving the local adapter address on + Linux and Windows. Apple platforms and ordinary Android applications return + `Ok(None)` because their public APIs do not expose this address. +- Implement `Central::add_peripheral()` on Windows, allowing bonded or already + connected devices to be reached by address without waiting for an + advertisement. - Add `appearance` to `PeripheralProperties`, populated from GAP Appearance advertising data on Windows, Linux, and Android. CoreBluetooth does not expose this advertising field, so it remains `None` on Apple platforms. +- Add support for receiving advertisements on the Bluetooth LE Coded PHY on + Windows where supported. + +## Bugfixes + +- Fix CoreBluetooth service discovery hanging when descriptor discovery fails. +- Fix CoreBluetooth `clear_peripherals()` so cleared devices can be rediscovered + and emit fresh discovery events. +- Report the negotiated CoreBluetooth MTU after service discovery instead of + always returning the default MTU. +- Preserve complete local names over shortened names across split or repeated + advertisements on Android, macOS/iOS, and Windows. +- Fix filtered Windows scans dropping scan-response packets that omit service + UUIDs, and prevent stale scan-response matches from leaking between scans. +- Propagate Android JNI callback and initialization failures consistently while + preserving Java exception details. ## Breaking Changes +- **Android minimum SDK**: Android API 24 (Android 7.0) or newer is now required. - **`PeripheralProperties` struct literals**: The new `appearance` field must be initialized by callers that construct this public struct directly. +## Dependencies + +- Update `jni` from 0.19 to 0.22 and migrate the Android backend to its current + API. + # 0.12.0 (2026-03-08) ## Features @@ -23,7 +55,6 @@ - Add `clear_peripherals()` to `Central` trait - Thanks danielstuart14! - Add `adapter_state()` to `Central` trait for querying Bluetooth on/off state -- Add optional `adapter_address()` to `Central` for platforms exposing a local adapter Bluetooth address; unsupported platforms return `Ok(None)` without breaking custom implementations - Add `add_peripheral()` to `Central` trait for adding a device by address without scanning (Android) - Add `advertisement_name` field to `PeripheralProperties` - Thanks szymonlesisz! From 90cadb0cb02f97893502108c9e515ec45bc10327 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 12:48:24 -0700 Subject: [PATCH 51/77] ci: limit JNI host tests to Linux --- .github/workflows/rust.yml | 4 ++-- Cargo.toml | 6 ++---- src/lib.rs | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 73b83b3e..b3af5c61 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,7 +46,7 @@ jobs: sudo apt-get update sudo apt-get install libdbus-1-dev openjdk-17-jdk - uses: actions/setup-java@v2 - if: ${{ matrix.target == 'android' || matrix.target == 'linux' || matrix.target == 'macos' }} + if: ${{ matrix.target == 'android' || matrix.target == 'linux' }} with: distribution: 'zulu' java-version: '17' @@ -72,7 +72,7 @@ jobs: - name: Check with all features run: cargo check --all --bins --examples --all-features - name: Run JNI host tests - if: ${{ matrix.target == 'linux' || matrix.target == 'macos' }} + if: ${{ matrix.target == 'linux' }} run: ./scripts/run-jni-tests.sh - name: Run tests if: ${{ matrix.target != 'android' }} diff --git a/Cargo.toml b/Cargo.toml index 43f8ec68..5563dcf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,15 +43,13 @@ tokio-stream = { version = "0.1.18", features = ["sync"] } [target.'cfg(target_os = "linux")'.dependencies] dbus = "0.9.10" bluez-async = "0.8.2" +jni = { version = "0.22", optional = true } +once_cell = { version = "1.21.3", optional = true } [target.'cfg(target_os = "android")'.dependencies] jni = "0.22" once_cell = "1.21.3" -[target.'cfg(not(target_os = "android"))'.dependencies] -jni = { version = "0.22", optional = true } -once_cell = { version = "1.21.3", optional = true } - [target.'cfg(target_vendor = "apple")'.dependencies] objc2 = "0.5.2" objc2-foundation = { version = "0.2.2", default-features = false, features = [ diff --git a/src/lib.rs b/src/lib.rs index 368b4690..8873e996 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,7 @@ mod common; mod corebluetooth; #[cfg(target_os = "android")] mod droidplug; -#[cfg(all(not(target_os = "android"), feature = "jni-host-tests"))] +#[cfg(all(target_os = "linux", feature = "jni-host-tests"))] #[allow(dead_code)] mod droidplug { mod jni_utils; From ac63d9c9ab37b4bd3acee66fc801678e1a6691f1 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 13:05:17 -0700 Subject: [PATCH 52/77] fix: include all Java JNI support classes in host tests --- scripts/run-jni-tests.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/run-jni-tests.sh b/scripts/run-jni-tests.sh index 6abb7147..e6084b55 100755 --- a/scripts/run-jni-tests.sh +++ b/scripts/run-jni-tests.sh @@ -97,16 +97,16 @@ compile_java() { rm -rf "$classes_dir" mkdir -p "$classes_dir" - # Only compile the gedgygedgy sources — the nonpolynomial sources - # depend on Android APIs and cannot be compiled with plain javac. - local gedgy_dir="$JAVA_SRC_DIR/io/github/gedgygedgy" + # Only compile the plain-Java rust support classes — the nonpolynomial + # sources depend on Android APIs and cannot be compiled with plain javac. + local rust_dir="$JAVA_SRC_DIR/io/github/gedgygedgy/rust" local sources=() while IFS= read -r -d '' f; do sources+=("$f") - done < <(find "$gedgy_dir" -name '*.java' -print0) + done < <(find "$rust_dir" -name '*.java' -print0) if [ ${#sources[@]} -eq 0 ]; then - die "No .java files found under $gedgy_dir" + die "No .java files found under $rust_dir" fi info "Found ${#sources[@]} Java source files" From 2c66ee7163024872944cb906fcb36e244071a994 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 13:09:48 -0700 Subject: [PATCH 53/77] fix: align JNI test jar with host cargo target --- scripts/run-jni-tests.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/run-jni-tests.sh b/scripts/run-jni-tests.sh index e6084b55..b312228c 100755 --- a/scripts/run-jni-tests.sh +++ b/scripts/run-jni-tests.sh @@ -13,7 +13,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" JAVA_SRC_DIR="$PROJECT_ROOT/src/droidplug/java/src/main/java" -BUILD_DIR="$PROJECT_ROOT/target/debug/java" +CARGO_TARGET_ROOT="${CARGO_TARGET_DIR:-$PROJECT_ROOT/target}" +HOST_TARGET="$(rustc -vV | sed -n 's/^host: //p')" +BUILD_DIR="$CARGO_TARGET_ROOT/$HOST_TARGET/debug/java" JAR_DIR="$BUILD_DIR/libs" JAR_PATH="$JAR_DIR/btleplug-jni.jar" @@ -132,7 +134,7 @@ run_tests() { info "Running jni_utils tests..." cd "$PROJECT_ROOT" - cargo test --features jni-host-tests -- --test-threads=1 + cargo test --target "$HOST_TARGET" --features jni-host-tests -- --test-threads=1 info "All tests passed!" } From f0636b493b0be935cb405f8d9d42c42ecde85974 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 13:18:17 -0700 Subject: [PATCH 54/77] chore: clean up Linux and Android build warnings --- examples/discover_adapters_peripherals.rs | 8 ++++---- examples/event_driven_discovery.rs | 2 +- examples/lights.rs | 4 ++-- examples/subscribe_notify_characteristic.rs | 8 ++++---- src/common/util.rs | 1 + src/droidplug/jni/mod.rs | 5 ++--- src/droidplug/jni_utils/exceptions.rs | 2 ++ src/droidplug/jni_utils/future.rs | 2 ++ src/droidplug/jni_utils/ops.rs | 2 ++ src/droidplug/jni_utils/stream.rs | 2 ++ src/droidplug/mod.rs | 2 +- src/droidplug/peripheral.rs | 13 +++++-------- 12 files changed, 28 insertions(+), 23 deletions(-) diff --git a/examples/discover_adapters_peripherals.rs b/examples/discover_adapters_peripherals.rs index 2bacfea2..a95d12d9 100644 --- a/examples/discover_adapters_peripherals.rs +++ b/examples/discover_adapters_peripherals.rs @@ -41,7 +41,7 @@ async fn main() -> anyhow::Result<()> { local_name, is_connected ); if !is_connected { - println!("Connecting to peripheral {:?}...", &local_name); + println!("Connecting to peripheral {:?}...", local_name); if let Err(err) = peripheral.connect().await { eprintln!("Error connecting to peripheral, skipping: {}", err); continue; @@ -50,10 +50,10 @@ async fn main() -> anyhow::Result<()> { let is_connected = peripheral.is_connected().await?; println!( "Now connected ({:?}) to peripheral {:?}...", - is_connected, &local_name + is_connected, local_name ); peripheral.discover_services().await?; - println!("Discover peripheral {:?} services...", &local_name); + println!("Discover peripheral {:?} services...", local_name); for service in peripheral.services() { println!( "Service UUID {}, primary: {}", @@ -64,7 +64,7 @@ async fn main() -> anyhow::Result<()> { } } if is_connected { - println!("Disconnecting from peripheral {:?}...", &local_name); + println!("Disconnecting from peripheral {:?}...", local_name); peripheral .disconnect() .await diff --git a/examples/event_driven_discovery.rs b/examples/event_driven_discovery.rs index fbc48b43..d22f6ec0 100644 --- a/examples/event_driven_discovery.rs +++ b/examples/event_driven_discovery.rs @@ -9,7 +9,7 @@ use futures::stream::StreamExt; async fn get_central(manager: &Manager) -> Adapter { let adapters = manager.adapters().await.unwrap(); - adapters.into_iter().nth(0).unwrap() + adapters.into_iter().next().unwrap() } #[tokio::main] diff --git a/examples/lights.rs b/examples/lights.rs index 6523cb59..35316671 100644 --- a/examples/lights.rs +++ b/examples/lights.rs @@ -40,7 +40,7 @@ async fn main() -> anyhow::Result<()> { .await .expect("Unable to fetch adapter list.") .into_iter() - .nth(0) + .next() .expect("Unable to find adapters."); // start scanning for devices @@ -78,7 +78,7 @@ async fn main() -> anyhow::Result<()> { 0xAA, ]; light - .write(&cmd_char, &color_cmd, WriteType::WithoutResponse) + .write(cmd_char, &color_cmd, WriteType::WithoutResponse) .await?; time::sleep(Duration::from_millis(200)).await; } diff --git a/examples/subscribe_notify_characteristic.rs b/examples/subscribe_notify_characteristic.rs index cd661b53..0f763ffc 100644 --- a/examples/subscribe_notify_characteristic.rs +++ b/examples/subscribe_notify_characteristic.rs @@ -11,7 +11,7 @@ use uuid::Uuid; /// Only devices whose name contains this string will be tried. const PERIPHERAL_NAME_MATCH_FILTER: &str = "Neuro"; /// UUID of the characteristic for which we should subscribe to notifications. -const NOTIFY_CHARACTERISTIC_UUID: Uuid = Uuid::from_u128(0x6e400002_b534_f393_67a9_e50e24dccA9e); +const NOTIFY_CHARACTERISTIC_UUID: Uuid = Uuid::from_u128(0x6e400002_b534_f393_67a9_e50e24dcca9e); #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -45,11 +45,11 @@ async fn main() -> anyhow::Result<()> { .unwrap_or(String::from("(peripheral name unknown)")); println!( "Peripheral {:?} is connected: {:?}", - &local_name, is_connected + local_name, is_connected ); // Check if it's the peripheral we want. if local_name.contains(PERIPHERAL_NAME_MATCH_FILTER) { - println!("Found matching peripheral {:?}...", &local_name); + println!("Found matching peripheral {:?}...", local_name); if !is_connected { // Connect if we aren't already connected. if let Err(err) = peripheral.connect().await { @@ -60,7 +60,7 @@ async fn main() -> anyhow::Result<()> { let is_connected = peripheral.is_connected().await?; println!( "Now connected ({:?}) to peripheral {:?}.", - is_connected, &local_name + is_connected, local_name ); if is_connected { println!("Discover peripheral {:?} services...", local_name); diff --git a/src/common/util.rs b/src/common/util.rs index 6f0d3962..47ef43a8 100644 --- a/src/common/util.rs +++ b/src/common/util.rs @@ -11,6 +11,7 @@ use std::pin::Pin; use tokio::sync::broadcast::Receiver; use tokio_stream::wrappers::BroadcastStream; +#[allow(dead_code)] pub fn notifications_stream_from_broadcast_receiver( receiver: Receiver, ) -> Pin + Send>> { diff --git a/src/droidplug/jni/mod.rs b/src/droidplug/jni/mod.rs index f812c681..15426e1c 100644 --- a/src/droidplug/jni/mod.rs +++ b/src/droidplug/jni/mod.rs @@ -127,10 +127,9 @@ fn adapter_on_connection_state_changed<'local>( ) -> jni::errors::Result<()> { if let Err(e) = super::adapter::adapter_on_connection_state_changed_internal(env, &obj, addr, connected) + && !env.exception_check() { - if !env.exception_check() { - let _ = env.throw(format!("Rust error: {e}")); - } + let _ = env.throw(format!("Rust error: {e}")); } Ok(()) } diff --git a/src/droidplug/jni_utils/exceptions.rs b/src/droidplug/jni_utils/exceptions.rs index 4b001d0c..86f269f7 100644 --- a/src/droidplug/jni_utils/exceptions.rs +++ b/src/droidplug/jni_utils/exceptions.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use jni::{ Env, descriptors::Desc, diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 3f805158..45bd3832 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use ::jni::{ Env, JavaVM, bind_java_type, errors::Result, diff --git a/src/droidplug/jni_utils/ops.rs b/src/droidplug/jni_utils/ops.rs index ba035121..245ca2e8 100644 --- a/src/droidplug/jni_utils/ops.rs +++ b/src/droidplug/jni_utils/ops.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use ::jni::errors::ThrowRuntimeExAndDefault; use ::jni::{ Env, EnvUnowned, bind_java_type, diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index 44b2c22a..1671fd4d 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use super::task::JPollResult; use ::jni::{ Env, JavaVM, bind_java_type, diff --git a/src/droidplug/mod.rs b/src/droidplug/mod.rs index 220ba518..1ffb3456 100644 --- a/src/droidplug/mod.rs +++ b/src/droidplug/mod.rs @@ -12,7 +12,7 @@ static GLOBAL_ADAPTER: OnceCell = OnceCell::new(); pub fn init(env: &mut Env) -> crate::Result<()> { self::jni::init(env)?; - GLOBAL_ADAPTER.get_or_try_init(|| adapter::Adapter::new())?; + GLOBAL_ADAPTER.get_or_try_init(adapter::Adapter::new)?; Ok(()) } diff --git a/src/droidplug/peripheral.rs b/src/droidplug/peripheral.rs index 20e401d8..04042913 100644 --- a/src/droidplug/peripheral.rs +++ b/src/droidplug/peripheral.rs @@ -140,7 +140,6 @@ struct PeripheralShared { services: BTreeSet, characteristics: BTreeSet, properties: Option, - mtu: AtomicU16, } #[derive(Clone)] @@ -162,7 +161,6 @@ impl Peripheral { services: BTreeSet::new(), characteristics: BTreeSet::new(), properties: None, - mtu: AtomicU16::new(crate::api::DEFAULT_MTU_SIZE), })), mtu: Arc::new(AtomicU16::new(crate::api::DEFAULT_MTU_SIZE)), }) @@ -221,12 +219,12 @@ impl api::Peripheral for Peripheral { async fn properties(&self) -> Result> { let guard = self.shared.lock().map_err(Into::::into)?; - Ok((&guard.properties).clone()) + Ok(guard.properties.clone()) } fn characteristics(&self) -> BTreeSet { let guard = self.shared.lock().unwrap(); - (&guard.characteristics).clone() + guard.characteristics.clone() } async fn is_connected(&self) -> Result { @@ -283,7 +281,7 @@ impl api::Peripheral for Peripheral { fn services(&self) -> BTreeSet { let guard = self.shared.lock().unwrap(); - (&guard.services).clone() + guard.services.clone() } async fn discover_services(&self) -> Result<()> { @@ -486,9 +484,8 @@ impl api::Peripheral for Peripheral { async fn connection_parameters(&self) -> Result> { self.with_obj(|env, obj| { - Ok(obj - .get_connection_parameters(env) - .map_err(|e| Error::Other(format!("{:?}", e).into()))?) + obj.get_connection_parameters(env) + .map_err(|e| Error::Other(format!("{:?}", e).into())) }) } From f1ef913e18b8a24fecd4402798fe2ad7d90503d6 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 13:26:05 -0700 Subject: [PATCH 55/77] build: Update dependencies --- Cargo.lock | 417 ++++++++++++++++++----------------------------------- Cargo.toml | 32 ++-- 2 files changed, 157 insertions(+), 292 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2eb3f812..30822e9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,20 +19,20 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block2" @@ -107,15 +107,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cfg-if" @@ -155,15 +155,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -175,15 +175,15 @@ dependencies = [ [[package]] name = "dbus" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ "futures-channel", "futures-util", "libc", "libdbus-sys", - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -230,9 +230,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -245,9 +245,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -255,15 +255,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -272,38 +272,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -353,9 +353,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -383,12 +383,12 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -401,7 +401,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -415,9 +415,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "java-locator" @@ -457,7 +457,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn", + "syn 2.0.119", ] [[package]] @@ -476,16 +476,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -503,9 +504,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libdbus-sys" @@ -537,25 +538,25 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -605,9 +606,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "parking_lot_core" @@ -630,9 +631,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "pretty_env_logger" @@ -651,23 +652,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.119", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -744,9 +745,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "same-file" @@ -771,9 +772,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -803,29 +804,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -836,9 +837,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -867,18 +868,18 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys", ] [[package]] @@ -889,9 +890,20 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -909,54 +921,54 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "tokio" -version = "1.50.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.4", ] [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -966,9 +978,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", @@ -979,9 +991,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.6+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -994,27 +1006,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "unicode-ident" @@ -1030,9 +1042,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uuid" -version = "1.22.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" dependencies = [ "js-sys", "serde_core", @@ -1075,9 +1087,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -1088,9 +1100,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1098,22 +1110,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -1158,7 +1170,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1214,7 +1226,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1225,7 +1237,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1262,24 +1274,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -1289,39 +1283,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "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]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.2.1" @@ -1331,107 +1292,11 @@ dependencies = [ "windows-link", ] -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" [[package]] name = "wit-bindgen" @@ -1463,7 +1328,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.119", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -1479,7 +1344,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.119", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -1529,6 +1394,6 @@ checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 5563dcf8..32a4b9a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,29 +26,29 @@ serde = ["uuid/serde", "serde_cr", "serde_bytes"] jni-host-tests = ["jni/invocation", "once_cell"] [dependencies] -async-trait = "0.1.89" -log = "0.4.29" -bitflags = "2.11.0" -thiserror = "2.0.18" -uuid = "1.22.0" -serde_cr = { package = "serde", version = "1.0.228", features = ["derive"], default-features = false, optional = true } +async-trait = "0.1.92" +log = "0.4.34" +bitflags = "2.13.1" +thiserror = "2.0.20" +uuid = "1.26.0" +serde_cr = { package = "serde", version = "1.0.229", features = ["derive"], default-features = false, optional = true } serde_bytes = { version = "0.11.19", optional = true } -dashmap = "6.1.0" -futures = "0.3.32" +dashmap = "6.2.1" +futures = "0.3.34" static_assertions = "1.1.0" # rt feature needed for block_on in macOS internal thread -tokio = { version = "1.50.0", features = ["sync", "rt", "time"] } -tokio-stream = { version = "0.1.18", features = ["sync"] } +tokio = { version = "1.53.1", features = ["sync", "rt", "time"] } +tokio-stream = { version = "0.1.19", features = ["sync"] } [target.'cfg(target_os = "linux")'.dependencies] -dbus = "0.9.10" +dbus = "0.9.12" bluez-async = "0.8.2" jni = { version = "0.22", optional = true } -once_cell = { version = "1.21.3", optional = true } +once_cell = { version = "1.21.4", optional = true } [target.'cfg(target_os = "android")'.dependencies] jni = "0.22" -once_cell = "1.21.3" +once_cell = "1.21.4" [target.'cfg(target_vendor = "apple")'.dependencies] objc2 = "0.5.2" @@ -87,8 +87,8 @@ windows-future = "0.3.2" [dev-dependencies] rand = "0.10" pretty_env_logger = "0.5.0" -tokio = { version = "1.50.0", features = ["macros", "rt", "rt-multi-thread"] } -serde_json = "1.0.149" -toml = "1.0.6" +tokio = { version = "1.53.1", features = ["macros", "rt", "rt-multi-thread"] } +serde_json = "1.0.151" +toml = "1.1.4" anyhow = "1" lazy_static = "1.5.0" From 1dee76d4a1384fbaebbac85ecc526c06df05c749 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 13:39:15 -0700 Subject: [PATCH 56/77] chore: clean up CI warnings --- .github/workflows/rust.yml | 14 +++++--------- src/bluez/peripheral.rs | 2 +- tests/common/test_cases.rs | 2 ++ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b3af5c61..3a23e517 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -39,13 +39,13 @@ jobs: CARGO_BUILD_TARGET: ${{ matrix.cbt }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install dependencies if: ${{ runner.os == 'Linux' }} run: | sudo apt-get update sudo apt-get install libdbus-1-dev openjdk-17-jdk - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v5 if: ${{ matrix.target == 'android' || matrix.target == 'linux' }} with: distribution: 'zulu' @@ -88,16 +88,12 @@ jobs: chmod +x gradlew ./gradlew assembleDebug assembleAndroidTest - name: Run clippy - uses: actions-rs/clippy-check@v1 - with: - name: clippy ${{ matrix.os }} - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features + run: cargo clippy --all-features -- -D warnings format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Format Rust code run: cargo fmt --all -- --check @@ -113,7 +109,7 @@ jobs: # env: # RUSTC_BOOTSTRAP: 1 # steps: -# - uses: actions/checkout@v2 +# - uses: actions/checkout@v4 # - name: Install dependencies # if: ${{ runner.os == 'Linux' }} # run: sudo apt-get install libdbus-1-dev diff --git a/src/bluez/peripheral.rs b/src/bluez/peripheral.rs index fba663a0..c9bed2fe 100644 --- a/src/bluez/peripheral.rs +++ b/src/bluez/peripheral.rs @@ -140,7 +140,7 @@ impl api::Peripheral for Peripheral { fn mtu(&self) -> u16 { let services = self.services.lock().unwrap(); - for (_, service) in services.iter() { + for service in services.values() { if let Some((_, characteristic)) = service.characteristics.iter().next() { return characteristic.info.mtu.unwrap(); } diff --git a/tests/common/test_cases.rs b/tests/common/test_cases.rs index 32416069..8bac085e 100644 --- a/tests/common/test_cases.rs +++ b/tests/common/test_cases.rs @@ -3,6 +3,8 @@ //! Each function contains the actual test logic, callable from both //! desktop `#[tokio::test]` wrappers and Android JNI test harness. +#![allow(dead_code)] + use btleplug::api::Peripheral as _; use super::gatt_uuids; From 2204f656da8a43bb950a1cad795aca2096d8bac5 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 19:32:25 -0700 Subject: [PATCH 57/77] docs: clarify peripheral properties and event discovery (#339 #460) --- examples/event_driven_discovery.rs | 5 ++--- src/api/mod.rs | 9 +++++++-- tests/documentation.rs | 13 +++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/examples/event_driven_discovery.rs b/examples/event_driven_discovery.rs index d22f6ec0..e16d6fd7 100644 --- a/examples/event_driven_discovery.rs +++ b/examples/event_driven_discovery.rs @@ -33,9 +33,8 @@ async fn main() -> anyhow::Result<()> { // start scanning for devices central.start_scan(ScanFilter::default()).await?; - // Print based on whatever the event receiver outputs. Note that the event - // receiver blocks, so in a real program, this should be run in its own - // thread (not task, as this library does not yet use async channels). + // Process events asynchronously. In a real program, run this loop in its own + // Tokio task if event handling should proceed independently of other work. while let Some(event) = events.next().await { match event { CentralEvent::DeviceDiscovered(id) => { diff --git a/src/api/mod.rs b/src/api/mod.rs index 2898f343..5ba3557f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -343,8 +343,13 @@ pub trait Peripheral: Send + Sync + Clone + Debug { /// Returns the currently negotiated mtu size fn mtu(&self) -> u16; - /// Returns the set of properties associated with the peripheral. These may be updated over time - /// as additional advertising reports are received. + /// Returns the properties currently known for the peripheral. + /// + /// `Ok(Some(_))` contains a snapshot of the properties available to the backend. The snapshot + /// may be updated as additional advertising reports are received, and individual fields may be + /// unavailable (`None`) when the peripheral has not advertised them or the platform does not + /// expose them. `Ok(None)` means that the backend has no properties snapshot available yet; + /// callers should handle this case rather than assuming that properties are always available. async fn properties(&self) -> Result>; /// The set of services we've discovered for this device. This will be empty until diff --git a/tests/documentation.rs b/tests/documentation.rs index 4bc6e2e3..2d04ceeb 100644 --- a/tests/documentation.rs +++ b/tests/documentation.rs @@ -15,3 +15,16 @@ fn central_adapter_address_default_is_source_compatible() { fn assert_default() {} assert_default::(); } + +#[test] +fn properties_and_event_example_document_current_async_contract() { + let api = include_str!("../src/api/mod.rs"); + assert!(api.contains("`Ok(Some(_))` contains a snapshot")); + assert!(api.contains("`Ok(None)` means that the backend has no properties snapshot")); + + let example = include_str!("../examples/event_driven_discovery.rs"); + assert!(example.contains("Process events asynchronously")); + assert!(example.contains("Tokio task")); + assert!(!example.contains("event receiver blocks")); + assert!(!example.contains("does not yet use async channels")); +} From cc1e55cb1419e6b70a979a8abd8b6c333cc30bcb Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 19:34:03 -0700 Subject: [PATCH 58/77] fix(#326): harden Windows subscription cleanup --- src/winrtble/ble/characteristic.rs | 70 +++++++++++++++++++++--------- tests/common/test_cases.rs | 33 +++++++++++++- 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/src/winrtble/ble/characteristic.rs b/src/winrtble/ble/characteristic.rs index 0792ad3d..eea70324 100644 --- a/src/winrtble/ble/characteristic.rs +++ b/src/winrtble/ble/characteristic.rs @@ -100,8 +100,28 @@ impl BLECharacteristic { } } + fn remove_notify_handler(&mut self) -> Result<()> { + if let Some(token) = self.notify_token { + // Only relinquish ownership after WinRT confirms removal. This keeps + // the token available for a later retry when removal fails. + self.characteristic.RemoveValueChanged(token)?; + self.notify_token = None; + } + Ok(()) + } + pub async fn subscribe(&mut self, on_value_changed: NotifiyEventHandler) -> Result<()> { - { + // Validate before changing the existing subscription state. + let config = to_descriptor_value(self.characteristic.CharacteristicProperties()?); + if config == GattClientCharacteristicConfigurationDescriptorValue::None { + return Err(Error::NotSupported("Can not subscribe to attribute".into())); + } + + // A replacement is allowed, but never leave two handlers installed. If + // removal fails, retain the old token and reject the replacement. + self.remove_notify_handler()?; + + let token = { let value_handler = TypedEventHandler::new( move |_: Ref, args: Ref| { if let Ok(args) = args.ok() { @@ -116,23 +136,32 @@ impl BLECharacteristic { Ok(()) }, ); - let token = self.characteristic.ValueChanged(&value_handler)?; - self.notify_token = Some(token); - } - let config = to_descriptor_value(self.characteristic.CharacteristicProperties()?); - if config == GattClientCharacteristicConfigurationDescriptorValue::None { - return Err(Error::NotSupported("Can not subscribe to attribute".into())); - } + self.characteristic.ValueChanged(&value_handler)? + }; + self.notify_token = Some(token); - let status = self + let status = match self .characteristic - .WriteClientCharacteristicConfigurationDescriptorAsync(config)? - .into_future() - .await?; + .WriteClientCharacteristicConfigurationDescriptorAsync(config) + { + Ok(operation) => operation.into_future().await, + Err(err) => { + let _ = self.remove_notify_handler(); + return Err(err.into()); + } + }; + let status = match status { + Ok(status) => status, + Err(err) => { + let _ = self.remove_notify_handler(); + return Err(err.into()); + } + }; trace!("subscribe {:?}", status); if status == GattCommunicationStatus::Success { Ok(()) } else { + let _ = self.remove_notify_handler(); Err(Error::Other( format!("Windows UWP threw error on subscribe: {:?}", status).into(), )) @@ -140,10 +169,8 @@ impl BLECharacteristic { } pub async fn unsubscribe(&mut self) -> Result<()> { - if let Some(token) = &self.notify_token { - self.characteristic.RemoveValueChanged(*token)?; - } - self.notify_token = None; + // Disable the CCCD first. If that fails, retain the token and handler so + // ownership is still available for a later cleanup retry. let config = GattClientCharacteristicConfigurationDescriptorValue::None; let status = self .characteristic @@ -151,13 +178,14 @@ impl BLECharacteristic { .into_future() .await?; trace!("unsubscribe {:?}", status); - if status == GattCommunicationStatus::Success { - Ok(()) - } else { - Err(Error::Other( + if status != GattCommunicationStatus::Success { + return Err(Error::Other( format!("Windows UWP threw error on unsubscribe: {:?}", status).into(), - )) + )); } + + // Keep the token if removal fails; the next unsubscribe (or Drop) can retry. + self.remove_notify_handler() } pub fn uuid(&self) -> Uuid { diff --git a/tests/common/test_cases.rs b/tests/common/test_cases.rs index 8bac085e..20b2e6cb 100644 --- a/tests/common/test_cases.rs +++ b/tests/common/test_cases.rs @@ -602,9 +602,40 @@ pub async fn test_unsubscribe_stops_notifications() { } assert!(got_one, "Should have received at least one notification"); + // Discard notifications that were already queued before unsubscribe. Stop + // draining once the stream is briefly quiet; do not wait indefinitely for + // a new item. + loop { + if time::timeout(Duration::from_millis(100), stream.next()) + .await + .is_err() + { + break; + } + } peripheral.unsubscribe(&char).await.unwrap(); - time::sleep(Duration::from_secs(2)).await; + let mut received_after_unsubscribe = false; + let timeout = time::sleep(Duration::from_secs(2)); + tokio::pin!(timeout); + loop { + tokio::select! { + Some(n) = stream.next() => { + if n.uuid == gatt_uuids::NOTIFY_CHAR { + received_after_unsubscribe = true; + break; + } + } + _ = &mut timeout => break, + } + } + assert!( + !received_after_unsubscribe, + "Should not receive notifications after unsubscribe" + ); + + // A second unsubscribe must be harmless and leave notifications disabled. + peripheral.unsubscribe(&char).await.unwrap(); peripheral_finder::send_control_command(&peripheral, gatt_uuids::CMD_STOP_NOTIFICATIONS).await; peripheral.disconnect().await.unwrap(); } From 72f97e144a77350ab81aefcf1313d36f4e6a32e4 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 19:34:54 -0700 Subject: [PATCH 59/77] test: harden JNI host threading coverage #427 --- Cargo.toml | 2 ++ src/droidplug/jni_utils/future.rs | 59 +++++++++++++++++++++++++++++++ src/droidplug/jni_utils/mod.rs | 38 ++++++++++++++++++++ src/droidplug/jni_utils/stream.rs | 55 ++++++++++++++++++++++++++++ src/lib.rs | 2 +- 5 files changed, 155 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 32a4b9a7..812f19af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,8 @@ tokio-stream = { version = "0.1.19", features = ["sync"] } [target.'cfg(target_os = "linux")'.dependencies] dbus = "0.9.12" bluez-async = "0.8.2" + +[target.'cfg(not(target_os = "android"))'.dependencies] jni = { version = "0.22", optional = true } once_cell = { version = "1.21.4", optional = true } diff --git a/src/droidplug/jni_utils/future.rs b/src/droidplug/jni_utils/future.rs index 45bd3832..f66b9c30 100644 --- a/src/droidplug/jni_utils/future.rs +++ b/src/droidplug/jni_utils/future.rs @@ -313,6 +313,65 @@ mod test { }); } + #[test] + fn test_jsendfuture_cross_thread_await() { + use super::super::task::JPollResult; + use futures::executor::block_on; + use std::sync::{Arc, Barrier, mpsc}; + + let (future, future_obj_global, obj_global) = test_utils::with_env(|env| { + let future_obj = env + .new_object( + jni_str!("io/github/gedgygedgy/rust/future/SimpleFuture"), + jni_sig!("()V"), + &[], + ) + .unwrap(); + let future_obj_global = env.new_global_ref(&future_obj).unwrap(); + let future = JSendFuture::from_env(env, &future_obj).unwrap(); + let obj = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj_global = env.new_global_ref(&obj).unwrap(); + Ok((future, future_obj_global, obj_global)) + }) + .unwrap(); + + let barrier = Arc::new(Barrier::new(2)); + let (tx, rx) = mpsc::channel(); + let worker_barrier = barrier.clone(); + let worker = std::thread::spawn(move || { + worker_barrier.wait(); + let global = block_on(future).unwrap(); + tx.send(global).unwrap(); + }); + + barrier.wait(); + test_utils::with_env(|env| { + let future_local = env.new_local_ref(future_obj_global.as_obj()).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + env.call_method( + &future_local, + jni_str!("wake"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&obj_local).into()], + )?; + Ok(()) + }) + .unwrap(); + worker.join().unwrap(); + let global = rx.recv().unwrap(); + test_utils::with_env(|env| { + let actual = env.new_local_ref(global.as_obj()).unwrap(); + let poll = env.cast_local::(actual).unwrap(); + let result = poll.get(env).unwrap(); + let expected = env.new_local_ref(obj_global.as_obj()).unwrap(); + assert!(env.is_same_object(&result, &expected).unwrap()); + Ok(()) + }) + .unwrap(); + } + #[test] fn test_jsendfuture_await() { use super::super::task::JPollResult; diff --git a/src/droidplug/jni_utils/mod.rs b/src/droidplug/jni_utils/mod.rs index 9611cc88..795f939f 100644 --- a/src/droidplug/jni_utils/mod.rs +++ b/src/droidplug/jni_utils/mod.rs @@ -174,4 +174,42 @@ pub(crate) mod test_utils { GlobalJVM { jvm, class_loader } }; } + + #[test] + fn with_env_is_safe_across_threads() { + use std::sync::{Arc, Barrier, mpsc}; + + let barrier = Arc::new(Barrier::new(2)); + let worker_barrier = barrier.clone(); + let (tx, rx) = mpsc::channel(); + let worker = std::thread::spawn(move || { + worker_barrier.wait(); + with_env(|env| { + let thread = env + .call_static_method( + jni_str!("java/lang/Thread"), + jni_str!("currentThread"), + jni_sig!("()Ljava/lang/Thread;"), + &[], + )? + .l()?; + let name = env + .call_method( + &thread, + jni_str!("getName"), + jni_sig!("()Ljava/lang/String;"), + &[], + )? + .l()?; + let name = env.cast_local::(name)?.to_string(); + tx.send(name).unwrap(); + Ok(()) + }) + .unwrap(); + }); + + barrier.wait(); + worker.join().unwrap(); + assert!(!rx.recv().unwrap().is_empty()); + } } diff --git a/src/droidplug/jni_utils/stream.rs b/src/droidplug/jni_utils/stream.rs index 1671fd4d..9249c4b2 100644 --- a/src/droidplug/jni_utils/stream.rs +++ b/src/droidplug/jni_utils/stream.rs @@ -304,6 +304,61 @@ mod test { }); } + #[test] + fn test_jsendstream_cross_thread_await() { + use futures::{StreamExt, executor::block_on}; + use std::sync::{Arc, Barrier, mpsc}; + + let (mut stream, stream_obj_global, obj_global) = test_utils::with_env(|env| { + let stream_obj = env + .new_object( + jni_str!("io/github/gedgygedgy/rust/stream/QueueStream"), + jni_sig!("()V"), + &[], + ) + .unwrap(); + let stream_obj_global = env.new_global_ref(&stream_obj).unwrap(); + let stream = JSendStream::from_env(env, &stream_obj).unwrap(); + let obj = env + .new_object(jni_str!("java/lang/Object"), jni_sig!("()V"), &[]) + .unwrap(); + let obj_global = env.new_global_ref(&obj).unwrap(); + Ok((stream, stream_obj_global, obj_global)) + }) + .unwrap(); + + let barrier = Arc::new(Barrier::new(2)); + let worker_barrier = barrier.clone(); + let (tx, rx) = mpsc::channel(); + let worker = std::thread::spawn(move || { + worker_barrier.wait(); + let actual = block_on(stream.next()).unwrap().unwrap(); + tx.send(actual).unwrap(); + }); + + barrier.wait(); + test_utils::with_env(|env| { + let stream_local = env.new_local_ref(stream_obj_global.as_obj()).unwrap(); + let obj_local = env.new_local_ref(obj_global.as_obj()).unwrap(); + env.call_method( + &stream_local, + jni_str!("add"), + jni_sig!("(Ljava/lang/Object;)V"), + &[(&obj_local).into()], + )?; + Ok(()) + }) + .unwrap(); + worker.join().unwrap(); + let actual = rx.recv().unwrap(); + test_utils::with_env(|env| { + let expected = env.new_local_ref(obj_global.as_obj()).unwrap(); + assert!(env.is_same_object(actual.as_obj(), &expected).unwrap()); + Ok(()) + }) + .unwrap(); + } + #[test] fn test_jsendstream_await() { use futures::{executor::block_on, join}; diff --git a/src/lib.rs b/src/lib.rs index 8873e996..368b4690 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,7 @@ mod common; mod corebluetooth; #[cfg(target_os = "android")] mod droidplug; -#[cfg(all(target_os = "linux", feature = "jni-host-tests"))] +#[cfg(all(not(target_os = "android"), feature = "jni-host-tests"))] #[allow(dead_code)] mod droidplug { mod jni_utils; From 23e870910a273b115ef9cdafed3cdfb5baa150c4 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 19:34:06 -0700 Subject: [PATCH 60/77] fix(corebluetooth): establish terminal completion and FIFO queues (#397 #422 #464 #469) --- src/corebluetooth/future.rs | 11 +++-- src/corebluetooth/internal.rs | 88 +++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 45 deletions(-) diff --git a/src/corebluetooth/future.rs b/src/corebluetooth/future.rs index 2228e42d..7a14d6a9 100644 --- a/src/corebluetooth/future.rs +++ b/src/corebluetooth/future.rs @@ -38,15 +38,18 @@ impl BtlePlugFutureState { /// - `msg`: Message to set as reply, which will be returned by the /// corresponding future. pub fn set_reply(&mut self, reply: T) { + // CoreBluetooth can deliver a late callback after a disconnect has + // already completed and drained the operation. Completion is + // terminal, so duplicate callbacks must be harmless (including after + // the reply has been polled by the caller). if self.reply_msg.is_some() { - // TODO Can we stop multiple calls to set_reply_msg at compile time? - panic!("set_reply_msg called multiple times on the same future."); + return; } self.reply_msg = Some(reply); - if self.waker.is_some() { - self.waker.take().unwrap().wake(); + if let Some(waker) = self.waker.take() { + waker.wake(); } } } diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 83098820..1b3e6444 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -295,15 +295,13 @@ impl PeripheralInternal { service_uuid: Uuid, characteristic_uuid: Uuid, descriptors: HashMap>, - ) { - let service = self - .services - .get_mut(&service_uuid) - .expect("Got descriptors for a service we don't know about"); - let characteristic = service - .characteristics - .get_mut(&characteristic_uuid) - .expect("Got descriptors for a characteristic we don't know about"); + ) -> bool { + let Some(service) = self.services.get_mut(&service_uuid) else { + return false; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + return false; + }; for (descriptor_uuid, cb_descriptor) in descriptors { if let Some(existing) = characteristic.descriptors.get_mut(&descriptor_uuid) { // Update the CB object reference but preserve in-flight future @@ -326,6 +324,7 @@ impl PeripheralInternal { service.discovered = true; self.check_discovered() } + true } fn check_discovered(&mut self) { @@ -392,47 +391,54 @@ impl PeripheralInternal { } pub fn confirm_disconnect(&mut self) { - // Fulfill the disconnected future, if there is one. - // There might not be a future if the device disconnects unexpectedly. - if let Some(future) = self.disconnected_future_state.take() { - future.lock().unwrap().set_reply(CoreBluetoothReply::Ok) - } + self.drain_pending_operations("Device disconnected"); + } - // Fulfill pending RSSI futures - let error = CoreBluetoothReply::Err(String::from("Device disconnected")); + /// Complete every operation that cannot receive a callback after the + /// peripheral disappears. Keep this centralized: adding a future-bearing + /// operation must also add its queue here. + fn drain_pending_operations(&mut self, message: &str) { + let error = CoreBluetoothReply::Err(message.to_string()); + for future in [ + self.disconnected_future_state.take(), + self.connected_future_state.take(), + self.services_discovered_future_state.take(), + ] + .into_iter() + .flatten() + { + future.lock().unwrap().set_reply(error.clone()); + } for state in self.read_rssi_future_state.drain(..) { state.lock().unwrap().set_reply(error.clone()); } - - // Fulfill pending write-without-response futures for pending in self.write_without_response_queue.drain(..) { pending.fut.lock().unwrap().set_reply(error.clone()); } - - // Fulfill all pending futures - self.services.iter().for_each(|(_, service)| { - service - .characteristics - .iter() - .for_each(|(_, characteristic)| { - let CharacteristicInternal { - read_future_state, - write_future_state, - subscribe_future_state, - unsubscribe_future_state, - .. - } = characteristic; - - let futures = read_future_state - .iter() - .chain(write_future_state) - .chain(subscribe_future_state) - .chain(unsubscribe_future_state); - for state in futures { + for service in self.services.values_mut() { + for characteristic in service.characteristics.values_mut() { + for queue in [ + &mut characteristic.read_future_state, + &mut characteristic.write_future_state, + &mut characteristic.subscribe_future_state, + &mut characteristic.unsubscribe_future_state, + ] { + for state in queue.drain(..) { state.lock().unwrap().set_reply(error.clone()); } - }); - }); + } + for descriptor in characteristic.descriptors.values_mut() { + for queue in [ + &mut descriptor.read_future_state, + &mut descriptor.write_future_state, + ] { + for state in queue.drain(..) { + state.lock().unwrap().set_reply(error.clone()); + } + } + } + } + } } } From 1e9242bcbf73fd0a5d363381058ec16b497cc336 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 19:35:53 -0700 Subject: [PATCH 61/77] fix(corebluetooth): evict peripherals with dead event senders (#397 #422 #464 #469) --- src/corebluetooth/internal.rs | 64 ++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 1b3e6444..57a0fc45 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -632,17 +632,21 @@ impl CoreBluetoothInternal { "Got manufacturer data advertisement! {}: {:?}", manufacturer_id, manufacturer_data ); - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) - && let Err(e) = p - .event_sender + let dead = if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { + p.event_sender .send(PeripheralEventInternal::ManufacturerData( manufacturer_id, manufacturer_data, rssi, )) .await - { - error!("Error sending notification event: {}", e); + .is_err() + } else { + false + }; + if dead { + error!("Removing CoreBluetooth peripheral {peripheral_uuid}: event receiver is gone"); + self.peripherals.remove(&peripheral_uuid); } } @@ -653,25 +657,33 @@ impl CoreBluetoothInternal { rssi: i16, ) { trace!("Got service data advertisement! {:?}", service_data); - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) - && let Err(e) = p - .event_sender + let dead = if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { + p.event_sender .send(PeripheralEventInternal::ServiceData(service_data, rssi)) .await - { - error!("Error sending notification event: {}", e); + .is_err() + } else { + false + }; + if dead { + error!("Removing CoreBluetooth peripheral {peripheral_uuid}: event receiver is gone"); + self.peripherals.remove(&peripheral_uuid); } } async fn on_services(&mut self, peripheral_uuid: Uuid, services: Vec, rssi: i16) { trace!("Got service advertisement! {:?}", services); - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) - && let Err(e) = p - .event_sender + let dead = if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { + p.event_sender .send(PeripheralEventInternal::Services(services, rssi)) .await - { - error!("Error sending notification event: {}", e); + .is_err() + } else { + false + }; + if dead { + error!("Removing CoreBluetooth peripheral {peripheral_uuid}: event receiver is gone"); + self.peripherals.remove(&peripheral_uuid); } } @@ -789,7 +801,13 @@ impl CoreBluetoothInternal { trace!("{}", id); } if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { - p.set_characteristic_descriptors(service_uuid, characteristic_uuid, descriptors); + if !p.set_characteristic_descriptors(service_uuid, characteristic_uuid, descriptors) { + if let Some(future) = p.services_discovered_future_state.take() { + future.lock().unwrap().set_reply(CoreBluetoothReply::Err( + format!("Unknown descriptor relationship for service {service_uuid}, characteristic {characteristic_uuid}"), + )); + } + } } } @@ -882,6 +900,14 @@ impl CoreBluetoothInternal { } } + fn complete_missing(fut: CoreBluetoothReplyStateShared, object: &str) { + fut.lock() + .unwrap() + .set_reply(CoreBluetoothReply::Err(format!( + "{object} no longer available" + ))); + } + /// Get the CBCharacteristic for the given characteristic of the given peripheral, if it exists. fn get_characteristic( &mut self, @@ -1005,6 +1031,10 @@ impl CoreBluetoothInternal { trace!("Connecting peripheral!"); p.connected_future_state = Some(fut); unsafe { self.manager.connectPeripheral_options(&p.peripheral, None) }; + } else { + fut.lock().unwrap().set_reply(CoreBluetoothReply::Err( + "Peripheral no longer available".into(), + )); } } @@ -1014,6 +1044,8 @@ impl CoreBluetoothInternal { trace!("Disconnecting peripheral!"); p.disconnected_future_state = Some(fut); unsafe { self.manager.cancelPeripheralConnection(&p.peripheral) }; + } else { + fut.lock().unwrap().set_reply(CoreBluetoothReply::Ok); } } From 6962642de36077b3754de224d3337fa8b16979c7 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 19:36:45 -0700 Subject: [PATCH 62/77] fix(corebluetooth): make operation completion queues FIFO (#397 #422 #464 #469) --- src/corebluetooth/internal.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 57a0fc45..941f5bd4 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -946,7 +946,7 @@ impl CoreBluetoothInternal { self.get_characteristic(peripheral_uuid, service_uuid, characteristic_uuid) { trace!("Got subscribed event!"); - if let Some(state) = characteristic.subscribe_future_state.pop_back() { + if let Some(state) = characteristic.subscribe_future_state.pop_front() { state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); } } @@ -962,7 +962,7 @@ impl CoreBluetoothInternal { self.get_characteristic(peripheral_uuid, service_uuid, characteristic_uuid) { trace!("Got unsubscribed event!"); - if let Some(state) = characteristic.unsubscribe_future_state.pop_back() { + if let Some(state) = characteristic.unsubscribe_future_state.pop_front() { state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); } } @@ -990,7 +990,7 @@ impl CoreBluetoothInternal { // fulfill. Otherwise, just treat the returned value as a // notification and use the event system. if !characteristic.read_future_state.is_empty() { - let state = characteristic.read_future_state.pop_back().unwrap(); + let state = characteristic.read_future_state.pop_front().unwrap(); state .lock() .unwrap() @@ -1019,7 +1019,7 @@ impl CoreBluetoothInternal { self.get_characteristic(peripheral_uuid, service_uuid, characteristic_uuid) { trace!("Got written event!"); - if let Some(state) = characteristic.write_future_state.pop_back() { + if let Some(state) = characteristic.write_future_state.pop_front() { state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); } } @@ -1110,7 +1110,7 @@ impl CoreBluetoothInternal { CBCharacteristicWriteType::CBCharacteristicWriteWithResponse, ); } - characteristic.write_future_state.push_front(fut); + characteristic.write_future_state.push_back(fut); } } } @@ -1120,7 +1120,7 @@ impl CoreBluetoothInternal { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { while let Some(pending) = peripheral.write_without_response_queue.pop_front() { if !unsafe { peripheral.peripheral.canSendWriteWithoutResponse() } { - peripheral.write_without_response_queue.push_front(pending); + peripheral.write_without_response_queue.push_back(pending); break; } if let Some(service) = peripheral.services.get(&pending.service_uuid) { @@ -1178,7 +1178,7 @@ impl CoreBluetoothInternal { .peripheral .readValueForCharacteristic(&characteristic.characteristic); } - characteristic.read_future_state.push_front(fut); + characteristic.read_future_state.push_back(fut); } } @@ -1199,7 +1199,7 @@ impl CoreBluetoothInternal { .peripheral .setNotifyValue_forCharacteristic(true, &characteristic.characteristic); } - characteristic.subscribe_future_state.push_front(fut); + characteristic.subscribe_future_state.push_back(fut); } } @@ -1220,7 +1220,7 @@ impl CoreBluetoothInternal { .peripheral .setNotifyValue_forCharacteristic(false, &characteristic.characteristic); } - characteristic.unsubscribe_future_state.push_front(fut); + characteristic.unsubscribe_future_state.push_back(fut); } } @@ -1244,7 +1244,7 @@ impl CoreBluetoothInternal { .peripheral .writeValue_forDescriptor(&NSData::from_vec(data), &descriptor.descriptor); } - descriptor.write_future_state.push_front(fut); + descriptor.write_future_state.push_back(fut); } } @@ -1267,7 +1267,7 @@ impl CoreBluetoothInternal { .peripheral .readValueForDescriptor(&descriptor.descriptor); } - descriptor.read_future_state.push_front(fut); + descriptor.read_future_state.push_back(fut); } } @@ -1277,14 +1277,14 @@ impl CoreBluetoothInternal { unsafe { peripheral.peripheral.readRSSI(); } - peripheral.read_rssi_future_state.push_front(fut); + peripheral.read_rssi_future_state.push_back(fut); } } async fn on_read_rssi(&mut self, peripheral_uuid: Uuid, rssi: i16) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { trace!("Got RSSI read event: {}", rssi); - if let Some(state) = peripheral.read_rssi_future_state.pop_back() { + if let Some(state) = peripheral.read_rssi_future_state.pop_front() { state .lock() .unwrap() @@ -1331,7 +1331,7 @@ impl CoreBluetoothInternal { for byte in data.iter() { data_clone.push(*byte); } - if let Some(state) = descriptor.read_future_state.pop_back() { + if let Some(state) = descriptor.read_future_state.pop_front() { state .lock() .unwrap() @@ -1354,7 +1354,7 @@ impl CoreBluetoothInternal { descriptor_uuid, ) { trace!("Got written event!"); - if let Some(state) = descriptor.write_future_state.pop_back() { + if let Some(state) = descriptor.write_future_state.pop_front() { state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); } } From b149dac175695e8a95ac78dafcb7cecea0b70d76 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 19:45:20 -0700 Subject: [PATCH 63/77] fix(corebluetooth): propagate read and descriptor callback errors (#397 #422 #464 #469) --- src/corebluetooth/central_delegate.rs | 23 ++++++++- src/corebluetooth/internal.rs | 74 +++++++++++++++++++++------ src/corebluetooth/peripheral.rs | 15 ++++-- 3 files changed, 92 insertions(+), 20 deletions(-) diff --git a/src/corebluetooth/central_delegate.rs b/src/corebluetooth/central_delegate.rs index 837d3467..e9df8241 100644 --- a/src/corebluetooth/central_delegate.rs +++ b/src/corebluetooth/central_delegate.rs @@ -99,22 +99,26 @@ pub enum CentralDelegateEvent { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, }, CharacteristicUnsubscribed { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, }, CharacteristicNotified { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, data: Vec, + error: Option, }, CharacteristicWritten { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, }, DescriptorNotified { peripheral_uuid: Uuid, @@ -122,12 +126,14 @@ pub enum CentralDelegateEvent { characteristic_uuid: Uuid, descriptor_uuid: Uuid, data: Vec, + error: Option, }, DescriptorWritten { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, descriptor_uuid: Uuid, + error: Option, }, TxPowerLevel { peripheral_uuid: Uuid, @@ -136,6 +142,7 @@ pub enum CentralDelegateEvent { DidReadRssi { peripheral_uuid: Uuid, rssi: i16, + error: Option, }, ReadyToSendWriteWithoutResponse { peripheral_uuid: Uuid, @@ -210,6 +217,7 @@ impl Debug for CentralDelegateEvent { peripheral_uuid, service_uuid, characteristic_uuid, + .. } => f .debug_struct("CharacteristicSubscribed") .field("peripheral_uuid", peripheral_uuid) @@ -220,6 +228,7 @@ impl Debug for CentralDelegateEvent { peripheral_uuid, service_uuid, characteristic_uuid, + .. } => f .debug_struct("CharacteristicUnsubscribed") .field("peripheral_uuid", peripheral_uuid) @@ -231,6 +240,7 @@ impl Debug for CentralDelegateEvent { service_uuid, characteristic_uuid, data, + .. } => f .debug_struct("CharacteristicNotified") .field("peripheral_uuid", peripheral_uuid) @@ -242,6 +252,7 @@ impl Debug for CentralDelegateEvent { peripheral_uuid, service_uuid, characteristic_uuid, + .. } => f .debug_struct("CharacteristicWritten") .field("service_uuid", service_uuid) @@ -290,6 +301,7 @@ impl Debug for CentralDelegateEvent { characteristic_uuid, descriptor_uuid, data, + .. } => f .debug_struct("DescriptorNotified") .field("peripheral_uuid", peripheral_uuid) @@ -303,6 +315,7 @@ impl Debug for CentralDelegateEvent { service_uuid, characteristic_uuid, descriptor_uuid, + .. } => f .debug_struct("DescriptorWritten") .field("service_uuid", service_uuid) @@ -321,6 +334,7 @@ impl Debug for CentralDelegateEvent { CentralDelegateEvent::DidReadRssi { peripheral_uuid, rssi, + .. } => f .debug_struct("DidReadRssi") .field("peripheral_uuid", peripheral_uuid) @@ -687,6 +701,7 @@ declare_class!( service_uuid, characteristic_uuid, data: get_characteristic_value(characteristic), + error: error.map(|e| e.localizedDescription().to_string()), }); // Notify BluetoothGATTCharacteristic::read_value that read was successful. } @@ -717,6 +732,7 @@ declare_class!( peripheral_uuid, service_uuid, characteristic_uuid, + error: error.map(|e| e.localizedDescription().to_string()), }); } } @@ -726,7 +742,7 @@ declare_class!( &self, peripheral: &CBPeripheral, characteristic: &CBCharacteristic, - _error: Option<&NSError>, + error: Option<&NSError>, ) { trace!("delegate_peripheral_didupdatenotificationstateforcharacteristic_error"); // TODO check for error here @@ -742,12 +758,14 @@ declare_class!( peripheral_uuid, service_uuid, characteristic_uuid, + error: error.map(|e| e.localizedDescription().to_string()), }); } else { self.send_event(CentralDelegateEvent::CharacteristicUnsubscribed { peripheral_uuid, service_uuid, characteristic_uuid, + error: error.map(|e| e.localizedDescription().to_string()), }); } } @@ -770,6 +788,7 @@ declare_class!( self.send_event(CentralDelegateEvent::DidReadRssi { peripheral_uuid, rssi: rssi_value, + error: error.map(|e| e.localizedDescription().to_string()), }); } } @@ -804,6 +823,7 @@ declare_class!( characteristic_uuid, descriptor_uuid, data: get_descriptor_value(descriptor), + error: error.map(|e| e.localizedDescription().to_string()), }); // Notify BluetoothGATTCharacteristic::read_value that read was successful. } @@ -838,6 +858,7 @@ declare_class!( service_uuid, characteristic_uuid, descriptor_uuid, + error: error.map(|e| e.localizedDescription().to_string()), }); } } diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 941f5bd4..d1cb4da4 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -974,12 +974,22 @@ impl CoreBluetoothInternal { service_uuid: Uuid, characteristic_uuid: Uuid, data: Vec, + error: Option, ) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) && let Some(service) = peripheral.services.get_mut(&service_uuid) && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) { trace!("Got read event!"); + if let Some(error) = error { + if let Some(state) = characteristic.read_future_state.pop_front() { + state + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Err(error)); + } + return; + } let mut data_clone = Vec::new(); for byte in data.iter() { @@ -1074,9 +1084,18 @@ impl CoreBluetoothInternal { kind: WriteType, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) - && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; { trace!("Writing value! With kind {:?}", kind); match kind { @@ -1168,9 +1187,18 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) - && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; { trace!("Reading value!"); unsafe { @@ -1281,14 +1309,14 @@ impl CoreBluetoothInternal { } } - async fn on_read_rssi(&mut self, peripheral_uuid: Uuid, rssi: i16) { + async fn on_read_rssi(&mut self, peripheral_uuid: Uuid, rssi: i16, error: Option) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { trace!("Got RSSI read event: {}", rssi); if let Some(state) = peripheral.read_rssi_future_state.pop_front() { - state - .lock() - .unwrap() - .set_reply(CoreBluetoothReply::ReadRssi(rssi)); + state.lock().unwrap().set_reply(match error { + Some(error) => CoreBluetoothReply::Err(error), + None => CoreBluetoothReply::ReadRssi(rssi), + }); } // Also send as a peripheral event for CentralEvent emission if let Err(e) = peripheral @@ -1319,6 +1347,7 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, descriptor_uuid: Uuid, data: Vec, + error: Option, ) { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) && let Some(service) = peripheral.services.get_mut(&service_uuid) @@ -1326,6 +1355,15 @@ impl CoreBluetoothInternal { && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) { trace!("Got read event!"); + if let Some(error) = error { + if let Some(state) = descriptor.read_future_state.pop_front() { + state + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Err(error)); + } + return; + } let mut data_clone = Vec::new(); for byte in data.iter() { @@ -1480,22 +1518,26 @@ impl CoreBluetoothInternal { peripheral_uuid, service_uuid, characteristic_uuid, + .. } => self.on_characteristic_subscribed(peripheral_uuid, service_uuid, characteristic_uuid), CentralDelegateEvent::CharacteristicUnsubscribed{ peripheral_uuid, service_uuid, characteristic_uuid, + .. } => self.on_characteristic_unsubscribed(peripheral_uuid, service_uuid,characteristic_uuid), CentralDelegateEvent::CharacteristicNotified{ peripheral_uuid, service_uuid, characteristic_uuid, data, - } => self.on_characteristic_read(peripheral_uuid, service_uuid,characteristic_uuid, data).await, + error, + } => self.on_characteristic_read(peripheral_uuid, service_uuid,characteristic_uuid, data, error).await, CentralDelegateEvent::CharacteristicWritten{ peripheral_uuid, service_uuid, characteristic_uuid, + .. } => self.on_characteristic_written(peripheral_uuid, service_uuid, characteristic_uuid), CentralDelegateEvent::ManufacturerData{peripheral_uuid, manufacturer_id, data, rssi} => { self.on_manufacturer_data(peripheral_uuid, manufacturer_id, data, rssi).await @@ -1515,18 +1557,20 @@ impl CoreBluetoothInternal { characteristic_uuid, descriptor_uuid, data, - } => self.on_descriptor_read(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, data).await, + error, + } => self.on_descriptor_read(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, data, error).await, CentralDelegateEvent::DescriptorWritten{ peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, + .. } => self.on_descriptor_written(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid), CentralDelegateEvent::TxPowerLevel{peripheral_uuid, tx_power_level} => { self.on_tx_power_level(peripheral_uuid, tx_power_level).await }, - CentralDelegateEvent::DidReadRssi{peripheral_uuid, rssi} => { - self.on_read_rssi(peripheral_uuid, rssi).await + CentralDelegateEvent::DidReadRssi{peripheral_uuid, rssi, error} => { + self.on_read_rssi(peripheral_uuid, rssi, error).await }, CentralDelegateEvent::ReadyToSendWriteWithoutResponse{peripheral_uuid} => { self.drain_write_without_response_queue(peripheral_uuid) diff --git a/src/corebluetooth/peripheral.rs b/src/corebluetooth/peripheral.rs index 6445a0dd..1796055e 100644 --- a/src/corebluetooth/peripheral.rs +++ b/src/corebluetooth/peripheral.rs @@ -543,7 +543,13 @@ impl api::Peripheral for Peripheral { .await?; match fut.await { CoreBluetoothReply::Ok => {} - reply => panic!("Unexpected reply: {:?}", reply), + CoreBluetoothReply::Err(msg) => return Err(Error::RuntimeError(msg)), + reply => { + return Err(Error::RuntimeError(format!( + "Unexpected reply: {:?}", + reply + ))); + } } Ok(()) } @@ -580,9 +586,10 @@ impl api::Peripheral for Peripheral { .await?; match fut.await { CoreBluetoothReply::ReadResult(chars) => Ok(chars), - _ => { - panic!("Shouldn't get anything but read result!"); - } + CoreBluetoothReply::Err(msg) => Err(Error::RuntimeError(msg)), + _ => Err(Error::RuntimeError( + "Unexpected reply for descriptor read".into(), + )), } } } From 389137acd095f4db464e7ab0f84070feea6baa6d Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 19:48:27 -0700 Subject: [PATCH 64/77] fix(corebluetooth): complete callback errors and missing requests (#397 #422 #464 #469) --- src/corebluetooth/internal.rs | 116 ++++++++++++++++++++++++++-------- 1 file changed, 89 insertions(+), 27 deletions(-) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index d1cb4da4..f523a1f2 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -941,13 +941,17 @@ impl CoreBluetoothInternal { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, ) { if let Some(characteristic) = self.get_characteristic(peripheral_uuid, service_uuid, characteristic_uuid) { trace!("Got subscribed event!"); if let Some(state) = characteristic.subscribe_future_state.pop_front() { - state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + state.lock().unwrap().set_reply(match error { + Some(error) => CoreBluetoothReply::Err(error), + None => CoreBluetoothReply::Ok, + }); } } } @@ -957,13 +961,17 @@ impl CoreBluetoothInternal { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, ) { if let Some(characteristic) = self.get_characteristic(peripheral_uuid, service_uuid, characteristic_uuid) { trace!("Got unsubscribed event!"); if let Some(state) = characteristic.unsubscribe_future_state.pop_front() { - state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + state.lock().unwrap().set_reply(match error { + Some(error) => CoreBluetoothReply::Err(error), + None => CoreBluetoothReply::Ok, + }); } } } @@ -1024,13 +1032,17 @@ impl CoreBluetoothInternal { peripheral_uuid: Uuid, service_uuid: Uuid, characteristic_uuid: Uuid, + error: Option, ) { if let Some(characteristic) = self.get_characteristic(peripheral_uuid, service_uuid, characteristic_uuid) { trace!("Got written event!"); if let Some(state) = characteristic.write_future_state.pop_front() { - state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + state.lock().unwrap().set_reply(match error { + Some(error) => CoreBluetoothReply::Err(error), + None => CoreBluetoothReply::Ok, + }); } } } @@ -1217,9 +1229,18 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) - && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; { trace!("Setting subscribe!"); unsafe { @@ -1238,9 +1259,18 @@ impl CoreBluetoothInternal { characteristic_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) - && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; { trace!("Setting subscribe!"); unsafe { @@ -1261,10 +1291,22 @@ impl CoreBluetoothInternal { data: Vec, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) - && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; + let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) else { + Self::complete_missing(fut, "Descriptor"); + return; + }; { trace!("Writing descriptor value!"); unsafe { @@ -1284,10 +1326,22 @@ impl CoreBluetoothInternal { descriptor_uuid: Uuid, fut: CoreBluetoothReplyStateShared, ) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) - && let Some(service) = peripheral.services.get_mut(&service_uuid) - && let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) - && let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + let Some(service) = peripheral.services.get_mut(&service_uuid) else { + Self::complete_missing(fut, "Service"); + return; + }; + let Some(characteristic) = service.characteristics.get_mut(&characteristic_uuid) else { + Self::complete_missing(fut, "Characteristic"); + return; + }; + let Some(descriptor) = characteristic.descriptors.get_mut(&descriptor_uuid) else { + Self::complete_missing(fut, "Descriptor"); + return; + }; { trace!("Reading descriptor value!"); unsafe { @@ -1300,7 +1354,11 @@ impl CoreBluetoothInternal { } fn read_rssi(&mut self, peripheral_uuid: Uuid, fut: CoreBluetoothReplyStateShared) { - if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { + let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) else { + Self::complete_missing(fut, "Peripheral"); + return; + }; + { trace!("Reading RSSI!"); unsafe { peripheral.peripheral.readRSSI(); @@ -1384,6 +1442,7 @@ impl CoreBluetoothInternal { service_uuid: Uuid, characteristic_uuid: Uuid, descriptor_uuid: Uuid, + error: Option, ) { if let Some(descriptor) = self.get_descriptor( peripheral_uuid, @@ -1393,7 +1452,10 @@ impl CoreBluetoothInternal { ) { trace!("Got written event!"); if let Some(state) = descriptor.write_future_state.pop_front() { - state.lock().unwrap().set_reply(CoreBluetoothReply::Ok); + state.lock().unwrap().set_reply(match error { + Some(error) => CoreBluetoothReply::Err(error), + None => CoreBluetoothReply::Ok, + }); } } } @@ -1518,14 +1580,14 @@ impl CoreBluetoothInternal { peripheral_uuid, service_uuid, characteristic_uuid, - .. - } => self.on_characteristic_subscribed(peripheral_uuid, service_uuid, characteristic_uuid), + error, + } => self.on_characteristic_subscribed(peripheral_uuid, service_uuid, characteristic_uuid, error), CentralDelegateEvent::CharacteristicUnsubscribed{ peripheral_uuid, service_uuid, characteristic_uuid, - .. - } => self.on_characteristic_unsubscribed(peripheral_uuid, service_uuid,characteristic_uuid), + error, + } => self.on_characteristic_unsubscribed(peripheral_uuid, service_uuid,characteristic_uuid, error), CentralDelegateEvent::CharacteristicNotified{ peripheral_uuid, service_uuid, @@ -1537,8 +1599,8 @@ impl CoreBluetoothInternal { peripheral_uuid, service_uuid, characteristic_uuid, - .. - } => self.on_characteristic_written(peripheral_uuid, service_uuid, characteristic_uuid), + error, + } => self.on_characteristic_written(peripheral_uuid, service_uuid, characteristic_uuid, error), CentralDelegateEvent::ManufacturerData{peripheral_uuid, manufacturer_id, data, rssi} => { self.on_manufacturer_data(peripheral_uuid, manufacturer_id, data, rssi).await }, @@ -1564,8 +1626,8 @@ impl CoreBluetoothInternal { service_uuid, characteristic_uuid, descriptor_uuid, - .. - } => self.on_descriptor_written(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid), + error, + } => self.on_descriptor_written(peripheral_uuid, service_uuid, characteristic_uuid, descriptor_uuid, error), CentralDelegateEvent::TxPowerLevel{peripheral_uuid, tx_power_level} => { self.on_tx_power_level(peripheral_uuid, tx_power_level).await }, From a19721202fba8ae659042152ba2d1f508e97ad81 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 19:50:28 -0700 Subject: [PATCH 65/77] fix(corebluetooth): route callback errors and reject missing objects (#397 #422 #464 #469) --- src/corebluetooth/central_delegate.rs | 34 ++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/corebluetooth/central_delegate.rs b/src/corebluetooth/central_delegate.rs index e9df8241..80f63c10 100644 --- a/src/corebluetooth/central_delegate.rs +++ b/src/corebluetooth/central_delegate.rs @@ -720,7 +720,15 @@ declare_class!( characteristic_debug(characteristic), localized_description(error) ); - if error.is_none() { + if error.is_some() { + let Some(service) = (unsafe { characteristic.service() }) else { return }; + self.send_event(CentralDelegateEvent::CharacteristicWritten { + peripheral_uuid: nsuuid_to_uuid(&*unsafe { peripheral.identifier() }), + service_uuid: cbuuid_to_uuid(&*unsafe { service.UUID() }), + characteristic_uuid: cbuuid_to_uuid(&*unsafe { characteristic.UUID() }), + error: error.map(|e| e.localizedDescription().to_string()), + }); + } else { let service = unsafe { characteristic.service() }.unwrap(); let id = unsafe { peripheral.identifier() }; let peripheral_uuid = nsuuid_to_uuid(&id); @@ -806,7 +814,17 @@ declare_class!( descriptor_debug(descriptor), localized_description(error) ); - if error.is_none() { + if let Some(error) = error { + let Some(characteristic) = (unsafe { descriptor.characteristic() }) else { return }; + let Some(service) = (unsafe { characteristic.service() }) else { return }; + self.send_event(CentralDelegateEvent::DescriptorNotified { + peripheral_uuid: nsuuid_to_uuid(&*unsafe { peripheral.identifier() }), + service_uuid: cbuuid_to_uuid(&*unsafe { service.UUID() }), + characteristic_uuid: cbuuid_to_uuid(&*unsafe { characteristic.UUID() }), + descriptor_uuid: cbuuid_to_uuid(&*unsafe { descriptor.UUID() }), + data: Vec::new(), error: Some(error.localizedDescription().to_string()), + }); + } else { let characteristic = unsafe { descriptor.characteristic() }.unwrap(); let service = unsafe { characteristic.service() }.unwrap(); let id = unsafe { peripheral.identifier() }; @@ -842,7 +860,17 @@ declare_class!( descriptor_debug(descriptor), localized_description(error) ); - if error.is_none() { + if let Some(error) = error { + let Some(characteristic) = (unsafe { descriptor.characteristic() }) else { return }; + let Some(service) = (unsafe { characteristic.service() }) else { return }; + self.send_event(CentralDelegateEvent::DescriptorWritten { + peripheral_uuid: nsuuid_to_uuid(&*unsafe { peripheral.identifier() }), + service_uuid: cbuuid_to_uuid(&*unsafe { service.UUID() }), + characteristic_uuid: cbuuid_to_uuid(&*unsafe { characteristic.UUID() }), + descriptor_uuid: cbuuid_to_uuid(&*unsafe { descriptor.UUID() }), + error: Some(error.localizedDescription().to_string()), + }); + } else { let characteristic = unsafe { descriptor.characteristic() }.unwrap(); let service = unsafe { characteristic.service() }.unwrap(); let id = unsafe { peripheral.identifier() }; From 3d905c0092662263b188938e4411123524966100 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 19:52:03 -0700 Subject: [PATCH 66/77] test(corebluetooth): reject late duplicate future completion (#397 #422 #464 #469) --- src/corebluetooth/future.rs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/corebluetooth/future.rs b/src/corebluetooth/future.rs index 7a14d6a9..1ab06df9 100644 --- a/src/corebluetooth/future.rs +++ b/src/corebluetooth/future.rs @@ -13,6 +13,7 @@ use std::task::{Context, Poll, Waker}; pub struct BtlePlugFutureState { reply_msg: Option, waker: Option, + completed: bool, } // For some reason, deriving default above doesn't work, but doing an explicit @@ -22,6 +23,7 @@ impl Default for BtlePlugFutureState { BtlePlugFutureState:: { reply_msg: None, waker: None, + completed: false, } } } @@ -42,10 +44,11 @@ impl BtlePlugFutureState { // already completed and drained the operation. Completion is // terminal, so duplicate callbacks must be harmless (including after // the reply has been polled by the caller). - if self.reply_msg.is_some() { + if self.completed { return; } + self.completed = true; self.reply_msg = Some(reply); if let Some(waker) = self.waker.take() { @@ -115,3 +118,28 @@ impl Future for BtlePlugFuture { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::task::{Context, Poll, Waker}; + + #[test] + fn late_duplicate_completion_after_poll_is_ignored() { + let mut future = BtlePlugFuture::::default(); + let state = future.get_state_clone(); + let waker = Waker::noop(); + let mut context = Context::from_waker(waker); + + state.lock().unwrap().set_reply(1); + assert_eq!(Pin::new(&mut future).poll(&mut context), Poll::Ready(1)); + + // A callback arriving after the reply was consumed must not resurrect + // the operation or replace its terminal result. + state.lock().unwrap().set_reply(2); + assert!(matches!( + Pin::new(&mut future).poll(&mut context), + Poll::Pending + )); + } +} From 42d23acbd0ee1dd7a03b9d3a1aff6c718cba1edd Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 20:00:01 -0700 Subject: [PATCH 67/77] Fix CoreBluetooth callback completion handling (#397 #422 #464) --- src/corebluetooth/central_delegate.rs | 55 ++++++++++++++------------- src/corebluetooth/internal.rs | 38 ++++++++++-------- 2 files changed, 51 insertions(+), 42 deletions(-) diff --git a/src/corebluetooth/central_delegate.rs b/src/corebluetooth/central_delegate.rs index 80f63c10..211ec799 100644 --- a/src/corebluetooth/central_delegate.rs +++ b/src/corebluetooth/central_delegate.rs @@ -688,23 +688,26 @@ declare_class!( characteristic_debug(characteristic), localized_description(error) ); - if error.is_none() { - let service = unsafe { characteristic.service() }.unwrap(); - let id = unsafe { peripheral.identifier() }; - let peripheral_uuid = nsuuid_to_uuid(&id); - let raw_service_uuid = unsafe { service.UUID() }; - let service_uuid = cbuuid_to_uuid(&raw_service_uuid); - let raw_char_uuid = unsafe { characteristic.UUID() }; - let characteristic_uuid = cbuuid_to_uuid(&raw_char_uuid); - self.send_event(CentralDelegateEvent::CharacteristicNotified { - peripheral_uuid, - service_uuid, - characteristic_uuid, - data: get_characteristic_value(characteristic), - error: error.map(|e| e.localizedDescription().to_string()), - }); - // Notify BluetoothGATTCharacteristic::read_value that read was successful. - } + let Some(service) = (unsafe { characteristic.service() }) else { + warn!( + "Characteristic value update for {} has no associated service", + characteristic_debug(characteristic) + ); + return; + }; + let id = unsafe { peripheral.identifier() }; + let peripheral_uuid = nsuuid_to_uuid(&id); + let raw_service_uuid = unsafe { service.UUID() }; + let service_uuid = cbuuid_to_uuid(&raw_service_uuid); + let raw_char_uuid = unsafe { characteristic.UUID() }; + let characteristic_uuid = cbuuid_to_uuid(&raw_char_uuid); + self.send_event(CentralDelegateEvent::CharacteristicNotified { + peripheral_uuid, + service_uuid, + characteristic_uuid, + data: get_characteristic_value(characteristic), + error: error.map(|e| e.localizedDescription().to_string()), + }); } #[method(peripheral:didWriteValueForCharacteristic:error:)] @@ -789,16 +792,14 @@ declare_class!( "delegate_peripheral_didreadrssi_error {}", peripheral_debug(peripheral) ); - if error.is_none() { - let id = unsafe { peripheral.identifier() }; - let peripheral_uuid = nsuuid_to_uuid(&id); - let rssi_value = rssi.as_i16(); - self.send_event(CentralDelegateEvent::DidReadRssi { - peripheral_uuid, - rssi: rssi_value, - error: error.map(|e| e.localizedDescription().to_string()), - }); - } + let id = unsafe { peripheral.identifier() }; + let peripheral_uuid = nsuuid_to_uuid(&id); + let rssi_value = rssi.as_i16(); + self.send_event(CentralDelegateEvent::DidReadRssi { + peripheral_uuid, + rssi: rssi_value, + error: error.map(|e| e.localizedDescription().to_string()), + }); } #[method(peripheral:didUpdateValueForDescriptor:error:)] diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index f523a1f2..bfd2e7b5 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -27,7 +27,7 @@ use futures::channel::mpsc::{self, Receiver, Sender}; use futures::select; use futures::sink::SinkExt; use futures::stream::{Fuse, StreamExt}; -use log::{error, trace, warn}; +use log::{debug, error, trace, warn}; use objc2::{ClassType, msg_send_id}; use objc2::{rc::Retained, runtime::AnyObject}; use objc2_core_bluetooth::{ @@ -817,13 +817,17 @@ impl CoreBluetoothInternal { .peripherals .get_mut(&peripheral_uuid) .expect("If we're here we should have an ID"); - peripheral - .connected_future_state - .take() - .unwrap() - .lock() - .unwrap() - .set_reply(CoreBluetoothReply::Connected); + if let Some(future) = peripheral.connected_future_state.take() { + future + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Connected); + } else { + debug!( + "Ignoring duplicate connection callback for peripheral {}", + peripheral_uuid + ); + } } } @@ -839,13 +843,17 @@ impl CoreBluetoothInternal { .peripherals .get_mut(&peripheral_uuid) .expect("If we're here we should have an ID"); - peripheral - .connected_future_state - .take() - .unwrap() - .lock() - .unwrap() - .set_reply(CoreBluetoothReply::Err(error)); + if let Some(future) = peripheral.connected_future_state.take() { + future + .lock() + .unwrap() + .set_reply(CoreBluetoothReply::Err(error)); + } else { + debug!( + "Ignoring duplicate connection failure callback for peripheral {}", + peripheral_uuid + ); + } } } From 75b3955af0fac0f0031e580fe506fad29b623f88 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 20:19:31 -0700 Subject: [PATCH 68/77] fix(corebluetooth): preserve write queue order under backpressure (#464) --- src/corebluetooth/internal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index bfd2e7b5..5b9a1b2f 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -1159,7 +1159,7 @@ impl CoreBluetoothInternal { if let Some(peripheral) = self.peripherals.get_mut(&peripheral_uuid) { while let Some(pending) = peripheral.write_without_response_queue.pop_front() { if !unsafe { peripheral.peripheral.canSendWriteWithoutResponse() } { - peripheral.write_without_response_queue.push_back(pending); + peripheral.write_without_response_queue.push_front(pending); break; } if let Some(service) = peripheral.services.get(&pending.service_uuid) { From 08420f1b48b675a70e540b07ba84cc8e43441f77 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 20:41:34 -0700 Subject: [PATCH 69/77] docs: document triaged bug fixes (#326 #339 #397 #422 #427 #460 #464 #469) --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2a08a19..747af2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,22 @@ UUIDs, and prevent stale scan-response matches from leaking between scans. - Propagate Android JNI callback and initialization failures consistently while preserving Java exception details. +- Harden Windows characteristic subscription state so repeated subscriptions do + not install duplicate handlers and failed CCCD operations remain retryable. + (#326) +- Clarify that `Peripheral::properties()` is a backend-dependent snapshot and + may be unavailable, incomplete, or stale. (#339) +- Prevent CoreBluetooth descriptor discovery and GATT operation failures from + hanging pending futures or panicking on missing relationships. (#397, #422) +- Run compatible CoreBluetooth operations through FIFO queues, preserve + write-without-response ordering under backpressure, and complete pending + operations safely on disconnect or late callbacks. (#464) +- Remove stale CoreBluetooth peripheral event senders after dispatch failures so + peripherals can be rediscovered cleanly. (#469) +- Expand Android JNI host-test coverage for futures, streams, and environment + setup across worker threads. (#427) +- Update the event-driven discovery example to describe its async-task usage. + (#460) ## Breaking Changes From 44c464cfc8011129811ebd2d1a036b3b3833f37f Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 21:07:22 -0700 Subject: [PATCH 70/77] chore: Fix if collapse issues (clippy) --- src/corebluetooth/internal.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 5b9a1b2f..cd538cfe 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -800,15 +800,13 @@ impl CoreBluetoothInternal { for id in descriptors.keys() { trace!("{}", id); } - if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) { - if !p.set_characteristic_descriptors(service_uuid, characteristic_uuid, descriptors) { - if let Some(future) = p.services_discovered_future_state.take() { + if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) + && !p.set_characteristic_descriptors(service_uuid, characteristic_uuid, descriptors) + && let Some(future) = p.services_discovered_future_state.take() { future.lock().unwrap().set_reply(CoreBluetoothReply::Err( format!("Unknown descriptor relationship for service {service_uuid}, characteristic {characteristic_uuid}"), )); } - } - } } fn on_peripheral_connect(&mut self, peripheral_uuid: Uuid) { From faa4626b1554e8e9066df5f829724f84f091f19c Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 21:10:06 -0700 Subject: [PATCH 71/77] chore: Fix formatting after clippy fix. God damnit. :| --- src/corebluetooth/internal.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index cd538cfe..48752355 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -802,11 +802,12 @@ impl CoreBluetoothInternal { } if let Some(p) = self.peripherals.get_mut(&peripheral_uuid) && !p.set_characteristic_descriptors(service_uuid, characteristic_uuid, descriptors) - && let Some(future) = p.services_discovered_future_state.take() { - future.lock().unwrap().set_reply(CoreBluetoothReply::Err( + && let Some(future) = p.services_discovered_future_state.take() + { + future.lock().unwrap().set_reply(CoreBluetoothReply::Err( format!("Unknown descriptor relationship for service {service_uuid}, characteristic {characteristic_uuid}"), )); - } + } } fn on_peripheral_connect(&mut self, peripheral_uuid: Uuid) { From 4f67739bba3c3af815ee46ffcc9bd75da43245bf Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 21:28:07 -0700 Subject: [PATCH 72/77] chore: Fix clippy issues on windows --- src/winrtble/adapter.rs | 2 +- src/winrtble/ble/device.rs | 5 ++--- src/winrtble/ble/watcher.rs | 5 ++--- src/winrtble/peripheral.rs | 18 ++++++------------ 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index dcfdcbd5..16d0665c 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -74,7 +74,7 @@ impl Adapter { let manager_clone = manager.clone(); let handler = TypedEventHandler::new(move |_sender, _args| { let state = get_central_state(&radio_clone); - manager_clone.emit(CentralEvent::StateUpdate(state.into())); + manager_clone.emit(CentralEvent::StateUpdate(state)); Ok(()) }); if let Err(err) = radio.StateChanged(&handler) { diff --git a/src/winrtble/ble/device.rs b/src/winrtble/ble/device.rs index 64bfef37..bf3c736f 100644 --- a/src/winrtble/ble/device.rs +++ b/src/winrtble/ble/device.rs @@ -62,8 +62,7 @@ impl BLEDevice { if let Some(sender) = sender.as_ref() { let is_connected = sender .ConnectionStatus() - .ok() - .map_or(false, |v| v == BluetoothConnectionStatus::Connected); + .ok() == Some(BluetoothConnectionStatus::Connected); connection_status_changed(is_connected); trace!("state {:?}", sender.ConnectionStatus()); } @@ -211,7 +210,7 @@ impl BLEDevice { let params = self.device.GetConnectionParameters().map_err(winrt_error)?; // ConnectionInterval is in units of 1.25ms, convert to microseconds let interval_us = (params.ConnectionInterval().map_err(winrt_error)? as u32) * 1250; - let latency = params.ConnectionLatency().map_err(winrt_error)? as u16; + let latency = params.ConnectionLatency().map_err(winrt_error)?; // LinkTimeout is in units of 10ms, convert to microseconds let supervision_timeout_us = (params.LinkTimeout().map_err(winrt_error)? as u32) * 10_000; Ok(crate::api::ConnectionParameters { diff --git a/src/winrtble/ble/watcher.rs b/src/winrtble/ble/watcher.rs index 1f09cb44..1a87bb6d 100644 --- a/src/winrtble/ble/watcher.rs +++ b/src/winrtble/ble/watcher.rs @@ -98,8 +98,8 @@ impl BLEWatcher { let address = args.BluetoothAddress().unwrap_or(0); let mut is_match = false; - if let Ok(ad) = args.Advertisement() { - if let Ok(ad_uuids) = ad.ServiceUuids() { + if let Ok(ad) = args.Advertisement() + && let Ok(ad_uuids) = ad.ServiceUuids() { let count = ad_uuids.Size().unwrap_or(0); if count > 0 { let advertised: Vec = @@ -107,7 +107,6 @@ impl BLEWatcher { is_match = filter_guids.iter().any(|g| advertised.contains(g)); } } - } let mut cache = matching_devices.lock().unwrap(); if is_match { diff --git a/src/winrtble/peripheral.rs b/src/winrtble/peripheral.rs index 8f35545a..d7879896 100644 --- a/src/winrtble/peripheral.rs +++ b/src/winrtble/peripheral.rs @@ -179,8 +179,8 @@ impl Peripheral { if let Some(name) = &projected_local_name { *self.shared.advertisement_name.write().unwrap() = Some(name.clone()); } - if let Ok(manufacturer_data) = advertisement.ManufacturerData() { - if manufacturer_data.Size().unwrap() > 0 { + if let Ok(manufacturer_data) = advertisement.ManufacturerData() + && manufacturer_data.Size().unwrap() > 0 { let mut manufacturer_data_guard = self.shared.latest_manufacturer_data.write().unwrap(); *manufacturer_data_guard = manufacturer_data @@ -199,7 +199,6 @@ impl Peripheral { manufacturer_data: manufacturer_data_guard.clone(), }); } - } // The Windows Runtime API (as of 19041) does not directly expose Service Data as a friendly API (like Manufacturer Data above) // Instead they provide data sections for access to raw advertising data. That is processed here. @@ -497,11 +496,10 @@ impl ApiPeripheral for Peripheral { shared.connected.store(is_connected, Ordering::Relaxed); } - if !is_connected { - if let Some(adapter) = adapter_clone.upgrade() { + if !is_connected + && let Some(adapter) = adapter_clone.upgrade() { adapter.emit(CentralEvent::DeviceDisconnected(address.into())); } - } } }); @@ -567,14 +565,10 @@ impl ApiPeripheral for Peripheral { HashMap::::new(), |mut map, gatt_characteristic| { let uuid = gatt_characteristic.Uuid().unwrap_or_default(); - if !map.contains_key(&uuid) { - map.insert(uuid, gatt_characteristic); - } + map.entry(uuid).or_insert(gatt_characteristic); map }, - ) - .into_iter() - .map(|(_, characteristic)| async { + ).into_values().map(|characteristic| async { let c = characteristic.clone(); ( characteristic, From 5adb1ad98409260a43fd64035bb6904684ebe9c7 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 21:37:03 -0700 Subject: [PATCH 73/77] chore: Fix windows fmt due to clippy updates oh my god make it stop --- src/winrtble/ble/device.rs | 5 ++-- src/winrtble/ble/watcher.rs | 15 ++++++------ src/winrtble/peripheral.rs | 49 +++++++++++++++++++------------------ 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/winrtble/ble/device.rs b/src/winrtble/ble/device.rs index bf3c736f..195f7fad 100644 --- a/src/winrtble/ble/device.rs +++ b/src/winrtble/ble/device.rs @@ -60,9 +60,8 @@ impl BLEDevice { let connection_status_handler = TypedEventHandler::::new(move |sender, _| { if let Some(sender) = sender.as_ref() { - let is_connected = sender - .ConnectionStatus() - .ok() == Some(BluetoothConnectionStatus::Connected); + let is_connected = sender.ConnectionStatus().ok() + == Some(BluetoothConnectionStatus::Connected); connection_status_changed(is_connected); trace!("state {:?}", sender.ConnectionStatus()); } diff --git a/src/winrtble/ble/watcher.rs b/src/winrtble/ble/watcher.rs index 1a87bb6d..88774a5f 100644 --- a/src/winrtble/ble/watcher.rs +++ b/src/winrtble/ble/watcher.rs @@ -99,14 +99,15 @@ impl BLEWatcher { let mut is_match = false; if let Ok(ad) = args.Advertisement() - && let Ok(ad_uuids) = ad.ServiceUuids() { - let count = ad_uuids.Size().unwrap_or(0); - if count > 0 { - let advertised: Vec = - (0..count).filter_map(|i| ad_uuids.GetAt(i).ok()).collect(); - is_match = filter_guids.iter().any(|g| advertised.contains(g)); - } + && let Ok(ad_uuids) = ad.ServiceUuids() + { + let count = ad_uuids.Size().unwrap_or(0); + if count > 0 { + let advertised: Vec = + (0..count).filter_map(|i| ad_uuids.GetAt(i).ok()).collect(); + is_match = filter_guids.iter().any(|g| advertised.contains(g)); } + } let mut cache = matching_devices.lock().unwrap(); if is_match { diff --git a/src/winrtble/peripheral.rs b/src/winrtble/peripheral.rs index d7879896..26602e9c 100644 --- a/src/winrtble/peripheral.rs +++ b/src/winrtble/peripheral.rs @@ -180,25 +180,25 @@ impl Peripheral { *self.shared.advertisement_name.write().unwrap() = Some(name.clone()); } if let Ok(manufacturer_data) = advertisement.ManufacturerData() - && manufacturer_data.Size().unwrap() > 0 { - let mut manufacturer_data_guard = - self.shared.latest_manufacturer_data.write().unwrap(); - *manufacturer_data_guard = manufacturer_data - .into_iter() - .map(|d| { - let manufacturer_id = d.CompanyId().unwrap(); - let data = utils::to_vec(&d.Data().unwrap()); - - (manufacturer_id, data) - }) - .collect(); - - // Emit event of newly received advertisement - self.emit_event(CentralEvent::ManufacturerDataAdvertisement { - id: self.shared.address.into(), - manufacturer_data: manufacturer_data_guard.clone(), - }); - } + && manufacturer_data.Size().unwrap() > 0 + { + let mut manufacturer_data_guard = self.shared.latest_manufacturer_data.write().unwrap(); + *manufacturer_data_guard = manufacturer_data + .into_iter() + .map(|d| { + let manufacturer_id = d.CompanyId().unwrap(); + let data = utils::to_vec(&d.Data().unwrap()); + + (manufacturer_id, data) + }) + .collect(); + + // Emit event of newly received advertisement + self.emit_event(CentralEvent::ManufacturerDataAdvertisement { + id: self.shared.address.into(), + manufacturer_data: manufacturer_data_guard.clone(), + }); + } // The Windows Runtime API (as of 19041) does not directly expose Service Data as a friendly API (like Manufacturer Data above) // Instead they provide data sections for access to raw advertising data. That is processed here. @@ -496,10 +496,9 @@ impl ApiPeripheral for Peripheral { shared.connected.store(is_connected, Ordering::Relaxed); } - if !is_connected - && let Some(adapter) = adapter_clone.upgrade() { - adapter.emit(CentralEvent::DeviceDisconnected(address.into())); - } + if !is_connected && let Some(adapter) = adapter_clone.upgrade() { + adapter.emit(CentralEvent::DeviceDisconnected(address.into())); + } } }); @@ -568,7 +567,9 @@ impl ApiPeripheral for Peripheral { map.entry(uuid).or_insert(gatt_characteristic); map }, - ).into_values().map(|characteristic| async { + ) + .into_values() + .map(|characteristic| async { let c = characteristic.clone(); ( characteristic, From c27f9d9e83ed40700fd909815f89b46871f51b97 Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 22:04:26 -0700 Subject: [PATCH 74/77] chore(corebluetooth): upgrade to objc2 0.6 (#455) --- Cargo.lock | 24 ++---- Cargo.toml | 6 +- src/corebluetooth/central_delegate.rs | 115 +++++++++++--------------- src/corebluetooth/internal.rs | 92 ++++++++++----------- 4 files changed, 103 insertions(+), 134 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30822e9b..50d3533e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,9 +36,9 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block2" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ "objc2", ] @@ -559,27 +559,20 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - [[package]] name = "objc2" -version = "0.5.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ - "objc-sys", "objc2-encode", ] [[package]] name = "objc2-core-bluetooth" -version = "0.2.2" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" +checksum = "79b30b9eacc37434a61377866f68b27acef9fa5496f25cc9ca8b2549032e0394" dependencies = [ "bitflags", "objc2", @@ -594,13 +587,12 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.2.2" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags", "block2", - "libc", "objc2", ] diff --git a/Cargo.toml b/Cargo.toml index 812f19af..8f71d5cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,8 +53,8 @@ jni = "0.22" once_cell = "1.21.4" [target.'cfg(target_vendor = "apple")'.dependencies] -objc2 = "0.5.2" -objc2-foundation = { version = "0.2.2", default-features = false, features = [ +objc2 = "0.6.4" +objc2-foundation = { version = "0.3.2", default-features = false, features = [ "std", "block2", "NSArray", @@ -67,7 +67,7 @@ objc2-foundation = { version = "0.2.2", default-features = false, features = [ "NSUUID", "NSValue", ] } -objc2-core-bluetooth = { version = "0.2.2", default-features = false, features = [ +objc2-core-bluetooth = { version = "0.3.2", default-features = false, features = [ "std", "CBAdvertisementData", "CBAttribute", diff --git a/src/corebluetooth/central_delegate.rs b/src/corebluetooth/central_delegate.rs index 211ec799..714b15bf 100644 --- a/src/corebluetooth/central_delegate.rs +++ b/src/corebluetooth/central_delegate.rs @@ -22,7 +22,7 @@ use futures::channel::mpsc::Sender; use futures::sink::SinkExt; use log::{error, trace, warn}; use objc2::runtime::{AnyObject, ProtocolObject}; -use objc2::{ClassType, DeclaredClass, declare_class, msg_send_id, mutability, rc::Retained}; +use objc2::{AnyThread, ClassType, DefinedClass, define_class, msg_send, rc::Retained}; use objc2_core_bluetooth::{ CBAdvertisementDataLocalNameKey, CBAdvertisementDataManufacturerDataKey, CBAdvertisementDataServiceDataKey, CBAdvertisementDataServiceUUIDsKey, @@ -348,24 +348,17 @@ impl Debug for CentralDelegateEvent { } } -declare_class!( +define_class!( #[derive(Debug)] + #[unsafe(super(NSObject))] + #[thread_kind = AnyThread] + #[ivars = Sender] pub struct CentralDelegate; - unsafe impl ClassType for CentralDelegate { - type Super = NSObject; - type Mutability = mutability::InteriorMutable; - const NAME: &'static str = "BtlePlugCentralManagerDelegate"; - } - - impl DeclaredClass for CentralDelegate { - type Ivars = Sender; - } - unsafe impl NSObjectProtocol for CentralDelegate {} unsafe impl CBCentralManagerDelegate for CentralDelegate { - #[method(centralManagerDidUpdateState:)] + #[unsafe(method(centralManagerDidUpdateState:))] fn delegate_centralmanagerdidupdatestate(&self, central: &CBCentralManager) { trace!("delegate_centralmanagerdidupdatestate"); let state = unsafe { central.state() }; @@ -377,7 +370,7 @@ declare_class!( // trace!("delegate_centralmanager_willrestorestate"); // } - #[method(centralManager:didConnectPeripheral:)] + #[unsafe(method(centralManager:didConnectPeripheral:))] fn delegate_centralmanager_didconnectperipheral( &self, _central: &CBCentralManager, @@ -393,7 +386,7 @@ declare_class!( self.send_event(CentralDelegateEvent::ConnectedDevice { peripheral_uuid }); } - #[method(centralManager:didDisconnectPeripheral:error:)] + #[unsafe(method(centralManager:didDisconnectPeripheral:error:))] fn delegate_centralmanager_diddisconnectperipheral_error( &self, _central: &CBCentralManager, @@ -410,7 +403,7 @@ declare_class!( self.send_event(CentralDelegateEvent::DisconnectedDevice { peripheral_uuid }); } - #[method(centralManager:didFailToConnectPeripheral:error:)] + #[unsafe(method(centralManager:didFailToConnectPeripheral:error:))] fn delegate_centralmanager_didfailtoconnectperipheral_error( &self, _central: &CBCentralManager, @@ -427,7 +420,7 @@ declare_class!( }); } - #[method(centralManager:didDiscoverPeripheral:advertisementData:RSSI:)] + #[unsafe(method(centralManager:didDiscoverPeripheral:advertisementData:RSSI:))] fn delegate_centralmanager_diddiscoverperipheral_advertisementdata_rssi( &self, _central: &CBCentralManager, @@ -441,12 +434,12 @@ declare_class!( ); let advertisement_name = adv_data - .get(unsafe { CBAdvertisementDataLocalNameKey }) - .map(|name| name as *const AnyObject as *const NSString) - .and_then(|name| unsafe { nsstring_to_string(name) }); + .objectForKey(unsafe { CBAdvertisementDataLocalNameKey }) + .and_then(|name| name.downcast::().ok()) + .and_then(|name| unsafe { nsstring_to_string(&*name as *const NSString) }); self.send_event(CentralDelegateEvent::DiscoveredPeripheral { - cbperipheral: peripheral.retain(), + cbperipheral: unsafe { Retained::retain(peripheral as *const _ as *mut _) }.unwrap(), advertisement_name, }); @@ -455,16 +448,14 @@ declare_class!( let id = unsafe { peripheral.identifier() }; let peripheral_uuid = nsuuid_to_uuid(&id); - let manufacturer_data = adv_data.get(unsafe { CBAdvertisementDataManufacturerDataKey }); + let manufacturer_data = adv_data.objectForKey(unsafe { CBAdvertisementDataManufacturerDataKey }); if let Some(manufacturer_data) = manufacturer_data { // SAFETY: manufacturer_data is `NSData` - let manufacturer_data: *const AnyObject = manufacturer_data; - let manufacturer_data: *const NSData = manufacturer_data.cast(); - let manufacturer_data = unsafe { &*manufacturer_data }; + let manufacturer_data = manufacturer_data.downcast::().unwrap(); if manufacturer_data.len() >= 2 { - let (manufacturer_id, manufacturer_data) = - manufacturer_data.bytes().split_at(2); + let manufacturer_data_vec = manufacturer_data.to_vec(); + let (manufacturer_id, manufacturer_data) = manufacturer_data_vec.split_at(2); self.send_event(CentralDelegateEvent::ManufacturerData { peripheral_uuid, @@ -475,17 +466,15 @@ declare_class!( } } - let service_data = adv_data.get(unsafe { CBAdvertisementDataServiceDataKey }); + let service_data = adv_data.objectForKey(unsafe { CBAdvertisementDataServiceDataKey }); if let Some(service_data) = service_data { // SAFETY: service_data is `NSDictionary` - let service_data: *const AnyObject = service_data; - let service_data: *const NSDictionary = service_data.cast(); - let service_data = unsafe { &*service_data }; + let service_data: Retained> = unsafe { Retained::cast_unchecked(service_data) }; let mut result = HashMap::new(); for uuid in service_data.keys() { - let data = &service_data[uuid]; - result.insert(cbuuid_to_uuid(uuid), data.bytes().to_vec()); + let data = service_data.objectForKey(&uuid).unwrap(); + result.insert(cbuuid_to_uuid(&uuid), data.to_vec()); } self.send_event(CentralDelegateEvent::ServiceData { @@ -495,16 +484,14 @@ declare_class!( }); } - let services = adv_data.get(unsafe { CBAdvertisementDataServiceUUIDsKey }); + let services = adv_data.objectForKey(unsafe { CBAdvertisementDataServiceUUIDsKey }); if let Some(services) = services { // SAFETY: services is `NSArray` - let services: *const AnyObject = services; - let services: *const NSArray = services.cast(); - let services = unsafe { &*services }; + let services: Retained> = unsafe { Retained::cast_unchecked(services) }; let mut service_uuids = Vec::new(); for uuid in services { - service_uuids.push(cbuuid_to_uuid(uuid)); + service_uuids.push(cbuuid_to_uuid(&uuid)); } self.send_event(CentralDelegateEvent::Services { @@ -515,11 +502,9 @@ declare_class!( } let tx_power_level = adv_data - .get(unsafe { CBAdvertisementDataTxPowerLevelKey }) + .objectForKey(unsafe { CBAdvertisementDataTxPowerLevelKey }) .map(|val| { - let val: *const AnyObject = val; - let val: *const NSNumber = val.cast(); - unsafe { &*val }.as_i16() + val.downcast::().unwrap().as_i16() }); if let Some(tx_power_level) = tx_power_level { @@ -532,7 +517,7 @@ declare_class!( } unsafe impl CBPeripheralDelegate for CentralDelegate { - #[method(peripheral:didDiscoverServices:)] + #[unsafe(method(peripheral:didDiscoverServices:))] fn delegate_peripheral_diddiscoverservices( &self, peripheral: &CBPeripheral, @@ -567,7 +552,7 @@ declare_class!( } } - #[method(peripheral:didDiscoverIncludedServicesForService:error:)] + #[unsafe(method(peripheral:didDiscoverIncludedServicesForService:error:))] fn delegate_peripheral_diddiscoverincludedservicesforservice_error( &self, peripheral: &CBPeripheral, @@ -588,7 +573,7 @@ declare_class!( } } - #[method(peripheral:didDiscoverCharacteristicsForService:error:)] + #[unsafe(method(peripheral:didDiscoverCharacteristicsForService:error:))] fn delegate_peripheral_diddiscovercharacteristicsforservice_error( &self, peripheral: &CBPeripheral, @@ -623,7 +608,7 @@ declare_class!( } } - #[method(peripheral:didDiscoverDescriptorsForCharacteristic:error:)] + #[unsafe(method(peripheral:didDiscoverDescriptorsForCharacteristic:error:))] fn delegate_peripheral_diddiscoverdescriptorsforcharacteristic_error( &self, peripheral: &CBPeripheral, @@ -675,7 +660,7 @@ declare_class!( }); } - #[method(peripheral:didUpdateValueForCharacteristic:error:)] + #[unsafe(method(peripheral:didUpdateValueForCharacteristic:error:))] fn delegate_peripheral_didupdatevalueforcharacteristic_error( &self, peripheral: &CBPeripheral, @@ -710,7 +695,7 @@ declare_class!( }); } - #[method(peripheral:didWriteValueForCharacteristic:error:)] + #[unsafe(method(peripheral:didWriteValueForCharacteristic:error:))] fn delegate_peripheral_didwritevalueforcharacteristic_error( &self, peripheral: &CBPeripheral, @@ -748,7 +733,7 @@ declare_class!( } } - #[method(peripheral:didUpdateNotificationStateForCharacteristic:error:)] + #[unsafe(method(peripheral:didUpdateNotificationStateForCharacteristic:error:))] fn delegate_peripheral_didupdatenotificationstateforcharacteristic_error( &self, peripheral: &CBPeripheral, @@ -781,7 +766,7 @@ declare_class!( } } - #[method(peripheral:didReadRSSI:error:)] + #[unsafe(method(peripheral:didReadRSSI:error:))] fn delegate_peripheral_didreadrssi_error( &self, peripheral: &CBPeripheral, @@ -802,7 +787,7 @@ declare_class!( }); } - #[method(peripheral:didUpdateValueForDescriptor:error:)] + #[unsafe(method(peripheral:didUpdateValueForDescriptor:error:))] fn delegate_peripheral_didupdatevaluefordescriptor_error( &self, peripheral: &CBPeripheral, @@ -848,7 +833,7 @@ declare_class!( } } - #[method(peripheral:didWriteValueForDescriptor:error:)] + #[unsafe(method(peripheral:didWriteValueForDescriptor:error:))] fn delegate_peripheral_didwritevaluefordescriptor_error( &self, peripheral: &CBPeripheral, @@ -892,7 +877,7 @@ declare_class!( } } - #[method(peripheral:didModifyServices:)] + #[unsafe(method(peripheral:didModifyServices:))] fn delegate_peripheral_didmodifyservices( &self, peripheral: &CBPeripheral, @@ -914,7 +899,7 @@ declare_class!( }); } - #[method(peripheralIsReadyToSendWriteWithoutResponse:)] + #[unsafe(method(peripheralIsReadyToSendWriteWithoutResponse:))] fn delegate_peripheral_is_ready_to_send_write_without_response( &self, peripheral: &CBPeripheral, @@ -935,7 +920,7 @@ declare_class!( impl CentralDelegate { pub fn new(sender: Sender) -> Retained { let this = CentralDelegate::alloc().set_ivars(sender); - unsafe { msg_send_id![super(this), init] } + unsafe { msg_send![super(this), init] } } fn send_event(&self, event: CentralDelegateEvent) { @@ -958,14 +943,14 @@ fn localized_description(error: Option<&NSError>) -> String { fn get_characteristic_value(characteristic: &CBCharacteristic) -> Vec { trace!("Getting data!"); - let v = unsafe { characteristic.value() }.map(|value| value.bytes().into()); + let v = unsafe { characteristic.value() }.map(|value| value.to_vec()); trace!("BluetoothGATTCharacteristic::get_value -> {:?}", v); v.unwrap_or_default() } fn get_descriptor_value(descriptor: &CBDescriptor) -> Vec { trace!("Getting data!"); - let v = unsafe { descriptor.value() }.map(|value| unsafe { + let v = unsafe { descriptor.value() }.map(|value| { let mut clazz = value.class(); // Find the root class until we reach NSObject while let Some(superclass) = clazz.superclass() { @@ -975,17 +960,17 @@ fn get_descriptor_value(descriptor: &CBDescriptor) -> Vec { clazz = superclass; } - match clazz.name() { - "NSString" => { - let d: Retained = Retained::cast(value); + match clazz.name().to_bytes() { + b"NSString" => { + let d: Retained = value.downcast().unwrap(); d.to_string().into_bytes() } - "NSData" => { - let d: Retained = Retained::cast(value); - d.bytes().into() + b"NSData" => { + let d: Retained = value.downcast().unwrap(); + d.to_vec() } - "NSNumber" => { - let d: Retained = Retained::cast(value); + b"NSNumber" => { + let d: Retained = value.downcast().unwrap(); d.stringValue().to_string().into_bytes() } _ => { diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 48752355..651ab778 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -28,14 +28,14 @@ use futures::select; use futures::sink::SinkExt; use futures::stream::{Fuse, StreamExt}; use log::{debug, error, trace, warn}; -use objc2::{ClassType, msg_send_id}; +use objc2::{AnyThread, msg_send}; use objc2::{rc::Retained, runtime::AnyObject}; use objc2_core_bluetooth::{ CBCentralManager, CBCentralManagerScanOptionAllowDuplicatesKey, CBCharacteristic, CBCharacteristicProperties, CBCharacteristicWriteType, CBDescriptor, CBManager, CBManagerAuthorization, CBManagerState, CBPeripheral, CBPeripheralState, CBService, CBUUID, }; -use objc2_foundation::{NSArray, NSData, NSMutableDictionary, NSNumber, NSUUID}; +use objc2_foundation::{NSArray, NSData, NSMutableDictionary, NSNumber, NSString, NSUUID}; use std::{ collections::{BTreeSet, HashMap, VecDeque}, ffi::CString, @@ -140,28 +140,25 @@ impl CharacteristicInternal { fn form_flags(characteristic: &CBCharacteristic) -> CharPropFlags { let flags = unsafe { characteristic.properties() }; let mut v = CharPropFlags::default(); - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyBroadcast) { + if flags.contains(CBCharacteristicProperties::Broadcast) { v |= CharPropFlags::BROADCAST; } - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyRead) { + if flags.contains(CBCharacteristicProperties::Read) { v |= CharPropFlags::READ; } - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyWriteWithoutResponse) - { + if flags.contains(CBCharacteristicProperties::WriteWithoutResponse) { v |= CharPropFlags::WRITE_WITHOUT_RESPONSE; } - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyWrite) { + if flags.contains(CBCharacteristicProperties::Write) { v |= CharPropFlags::WRITE; } - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyNotify) { + if flags.contains(CBCharacteristicProperties::Notify) { v |= CharPropFlags::NOTIFY; } - if flags.contains(CBCharacteristicProperties::CBCharacteristicPropertyIndicate) { + if flags.contains(CBCharacteristicProperties::Indicate) { v |= CharPropFlags::INDICATE; } - if flags - .contains(CBCharacteristicProperties::CBCharacteristicPropertyAuthenticatedSignedWrites) - { + if flags.contains(CBCharacteristicProperties::AuthenticatedSignedWrites) { v |= CharPropFlags::AUTHENTICATED_SIGNED_WRITES; } trace!("Flags: {:?}", v); @@ -373,9 +370,8 @@ impl PeripheralInternal { // a write, not the ATT MTU. Sample it after discovery, then account // for the ATT Write Command header to infer the full ATT MTU. let maximum_write_value_length = unsafe { - self.peripheral.maximumWriteValueLengthForType( - CBCharacteristicWriteType::CBCharacteristicWriteWithoutResponse, - ) + self.peripheral + .maximumWriteValueLengthForType(CBCharacteristicWriteType::WithoutResponse) }; let reply = match maximum_write_value_length_to_att_mtu(maximum_write_value_length) { Ok(mtu) => CoreBluetoothReply::ServicesDiscovered(services, mtu), @@ -601,7 +597,7 @@ impl CoreBluetoothInternal { let queue: *mut AnyObject = queue.cast(); let manager = unsafe { - msg_send_id![CBCentralManager::alloc(), initWithDelegate: &*delegate, queue: queue] + msg_send![CBCentralManager::alloc(), initWithDelegate: &*delegate, queue: queue] }; Self { @@ -1124,7 +1120,7 @@ impl CoreBluetoothInternal { peripheral.peripheral.writeValue_forCharacteristic_type( &NSData::from_vec(data), &characteristic.characteristic, - CBCharacteristicWriteType::CBCharacteristicWriteWithoutResponse, + CBCharacteristicWriteType::WithoutResponse, ); } fut.lock().unwrap().set_reply(CoreBluetoothReply::Ok); @@ -1145,7 +1141,7 @@ impl CoreBluetoothInternal { peripheral.peripheral.writeValue_forCharacteristic_type( &NSData::from_vec(data), &characteristic.characteristic, - CBCharacteristicWriteType::CBCharacteristicWriteWithResponse, + CBCharacteristicWriteType::WithResponse, ); } characteristic.write_future_state.push_back(fut); @@ -1169,7 +1165,7 @@ impl CoreBluetoothInternal { peripheral.peripheral.writeValue_forCharacteristic_type( &NSData::from_vec(pending.data), &characteristic.characteristic, - CBCharacteristicWriteType::CBCharacteristicWriteWithoutResponse, + CBCharacteristicWriteType::WithoutResponse, ); } pending @@ -1489,7 +1485,9 @@ impl CoreBluetoothInternal { } let mut retrieved = Vec::new(); if let Some(services) = options.services.filter(|services| !services.is_empty()) { - let services = NSArray::from_vec(services.into_iter().map(uuid_to_cbuuid).collect()); + let services = NSArray::from_retained_slice( + &services.into_iter().map(uuid_to_cbuuid).collect::>(), + ); retrieved.extend(unsafe { self.manager .retrieveConnectedPeripheralsWithServices(&services) @@ -1499,14 +1497,14 @@ impl CoreBluetoothInternal { .identifiers .filter(|identifiers| !identifiers.is_empty()) { - let identifiers = NSArray::from_vec( - identifiers + let identifiers = NSArray::from_retained_slice( + &identifiers .into_iter() .map(|id| { NSUUID::from_string(&objc2_foundation::NSString::from_str(&id.to_string())) .unwrap() }) - .collect(), + .collect::>(), ); retrieved.extend(unsafe { self.manager @@ -1719,18 +1717,19 @@ impl CoreBluetoothInternal { fn start_discovery(&mut self, filter: ScanFilter) { trace!("BluetoothAdapter::start_discovery"); let service_uuids = scan_filter_to_service_uuids(filter); - let mut options = NSMutableDictionary::new(); + let mut options: Retained> = + NSMutableDictionary::new(); // NOTE: If duplicates are not allowed then a peripheral will not show // up again once connected and then disconnected. - options.insert_id( + options.insert( unsafe { CBCentralManagerScanOptionAllowDuplicatesKey }, - Retained::into_super(Retained::into_super(Retained::into_super( - NSNumber::new_bool(true), - ))), + &*Retained::into_super(Retained::into_super(NSNumber::new_bool(true))), ); unsafe { - self.manager - .scanForPeripheralsWithServices_options(service_uuids.as_deref(), Some(&options)) + self.manager.scanForPeripheralsWithServices_options( + service_uuids.as_deref(), + Some(&*Retained::into_super(options)), + ) }; } @@ -1751,7 +1750,7 @@ fn scan_filter_to_service_uuids(filter: ScanFilter) -> Option>(); - Some(NSArray::from_vec(service_uuids)) + Some(NSArray::from_retained_slice(&service_uuids)) } } @@ -1767,40 +1766,33 @@ impl Drop for CoreBluetoothInternal { mod tests { use super::*; use futures::StreamExt; - use objc2::{DeclaredClass, declare_class, mutability}; + use objc2::{DefinedClass, define_class}; use objc2_core_bluetooth::{ CBAttributePermissions, CBMutableCharacteristic, CBMutableService, CBPeripheralDelegate, }; use objc2_foundation::{NSError, NSObjectProtocol, NSString, ns_string}; use std::time::Duration; - declare_class!( + define_class!( + #[unsafe(super(CBPeripheral))] + #[thread_kind = AnyThread] + #[ivars = Retained] struct TestPeripheral; - unsafe impl ClassType for TestPeripheral { - type Super = CBPeripheral; - type Mutability = mutability::InteriorMutable; - const NAME: &'static str = "BtlePlugTestPeripheral"; - } - - impl DeclaredClass for TestPeripheral { - type Ivars = Retained; - } - unsafe impl NSObjectProtocol for TestPeripheral {} - unsafe impl TestPeripheral { - #[method_id(identifier)] + impl TestPeripheral { + #[unsafe(method_id(identifier))] fn identifier(&self) -> Retained { self.ivars().clone() } - #[method_id(name)] + #[unsafe(method_id(name))] fn name(&self) -> Option> { None } - #[method(maximumWriteValueLengthForType:)] + #[unsafe(method(maximumWriteValueLengthForType:))] fn maximum_write_value_length_for_type( &self, _write_type: CBCharacteristicWriteType, @@ -1813,7 +1805,7 @@ mod tests { impl TestPeripheral { fn new(identifier: Retained) -> Retained { let this = Self::alloc().set_ivars(identifier); - unsafe { msg_send_id![super(this), init] } + unsafe { msg_send![super(this), init] } } } @@ -1857,7 +1849,7 @@ mod tests { CBMutableCharacteristic::initWithType_properties_value_permissions( CBMutableCharacteristic::alloc(), &characteristic_cbuuid, - CBCharacteristicProperties::CBCharacteristicPropertyRead, + CBCharacteristicProperties::Read, None, CBAttributePermissions::Readable, ) @@ -1866,7 +1858,7 @@ mod tests { CBMutableService::initWithType_primary(CBMutableService::alloc(), &service_cbuuid, true) }; let characteristic: Retained = Retained::into_super(characteristic); - let characteristics = NSArray::from_vec(vec![characteristic.clone()]); + let characteristics = NSArray::from_retained_slice(&[characteristic.clone()]); unsafe { service.setCharacteristics(Some(&characteristics)) }; let service: Retained = Retained::into_super(service); From 2641a38066c5cbbbb30515d96c8eb04f2dea7a6c Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 22:09:05 -0700 Subject: [PATCH 75/77] chore: Fix even more clippy issues but also fmt this time --- src/corebluetooth/internal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 651ab778..bebf2a72 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -1717,7 +1717,7 @@ impl CoreBluetoothInternal { fn start_discovery(&mut self, filter: ScanFilter) { trace!("BluetoothAdapter::start_discovery"); let service_uuids = scan_filter_to_service_uuids(filter); - let mut options: Retained> = + let options: Retained> = NSMutableDictionary::new(); // NOTE: If duplicates are not allowed then a peripheral will not show // up again once connected and then disconnected. From 4c12de8ef15d6d6ef810521a2226349e89d5c59e Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 22:19:26 -0700 Subject: [PATCH 76/77] fix android tx power advertisement fallback --- src/droidplug/jni/objects.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/droidplug/jni/objects.rs b/src/droidplug/jni/objects.rs index 81886f07..ad48b2e4 100644 --- a/src/droidplug/jni/objects.rs +++ b/src/droidplug/jni/objects.rs @@ -516,7 +516,10 @@ impl<'local> JScanResult<'local> { let tx_power_level = self.get_tx_power(env)?; const TX_POWER_NOT_PRESENT: jint = 127; let tx_power_level = if tx_power_level == TX_POWER_NOT_PRESENT { - None + match record.get_tx_power_level(env)? { + TX_POWER_NOT_PRESENT => None, + tx_power_level => Some(tx_power_level as i16), + } } else { Some(tx_power_level as i16) }; From 07861c543f2ddb9f7ea07dde937393f56d7040ce Mon Sep 17 00:00:00 2001 From: Kyle Machulis Date: Sun, 30 Aug 2026 22:31:44 -0700 Subject: [PATCH 77/77] fix android test JNI macro declaration order --- tests/android/rust/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/android/rust/src/lib.rs b/tests/android/rust/src/lib.rs index f6781c4f..d34baa37 100644 --- a/tests/android/rust/src/lib.rs +++ b/tests/android/rust/src/lib.rs @@ -102,11 +102,6 @@ pub extern "system" fn Java_com_nonpolynomial_btleplug_test_NativeTests_initBtle // ── Test JNI exports ──────────────────────────────────────────────── -jni_test!( - Java_com_nonpolynomial_btleplug_test_NativeTests_testAdapterAddress, - test_cases::test_adapter_address -); -// // Each function follows the JNI naming convention: // Java_com_nonpolynomial_btleplug_test_NativeTests_ @@ -123,6 +118,11 @@ macro_rules! jni_test { }; } +jni_test!( + Java_com_nonpolynomial_btleplug_test_NativeTests_testAdapterAddress, + test_cases::test_adapter_address +); + jni_test!( Java_com_nonpolynomial_btleplug_test_NativeTests_testDiscoverPeripheralByName, test_cases::test_discover_peripheral_by_name