// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Cacheable descriptors. These `jni` crate compatible descriptors will cache their value on first //! lookup. They are meant to be combined with static variables in order to globally cache their //! associated id values. //! //! ### Example //! //! ```java //! package com.example; //! //! public class MyClass { //! public int foo; //! public int getBar() { /* ... */ } //! } //! ``` //! //! ```rust //! use pourover::desc::*; //! //! static MY_CLASS_DESC: ClassDesc = ClassDesc::new("com/example/MyClass"); //! static MY_CLASS_FOO_FIELD: FieldDesc = MY_CLASS_DESC.field("foo", "I"); //! static MY_CLASS_GET_BAR_METHOD: MethodDesc = MY_CLASS_DESC.method("getBar", "I()"); //! ``` #![allow(unsafe_code)] use jni::{ descriptors::Desc, objects::{GlobalRef, JClass, JFieldID, JMethodID, JObject, JStaticFieldID, JStaticMethodID}, JNIEnv, }; use std::sync::{LockResult, RwLock, RwLockReadGuard}; /// JNI descriptor that caches a Java class. pub struct ClassDesc { /// The JNI descriptor for this class. descriptor: &'static str, /// The cached class /// /// The implementation assumes that `None` is only written to the lock when `&mut self` is /// held. Only `Some` is valid to write to this lock while `&self` is held. cls: RwLock>, } impl ClassDesc { /// Create a new descriptor with the given JNI descriptor string. pub const fn new(descriptor: &'static str) -> Self { Self { descriptor, cls: RwLock::new(None), } } /// Create a new descriptor for a field member of this class. pub const fn field<'cls>(&'cls self, name: &'static str, sig: &'static str) -> FieldDesc<'cls> { FieldDesc::new(self, name, sig) } /// Create a new descriptor for a static field member of this class. pub const fn static_field<'cls>( &'cls self, name: &'static str, sig: &'static str, ) -> StaticFieldDesc<'cls> { StaticFieldDesc::new(self, name, sig) } /// Create a new descriptor for a method member of this class. pub const fn method<'cls>( &'cls self, name: &'static str, sig: &'static str, ) -> MethodDesc<'cls> { MethodDesc::new(self, name, sig) } /// Create a new descriptor for a constructor for this class. pub const fn constructor<'cls>(&'cls self, sig: &'static str) -> MethodDesc<'cls> { MethodDesc::new(self, "", sig) } /// Create a new descriptor for a static method member of this class. pub const fn static_method<'cls>( &'cls self, name: &'static str, sig: &'static str, ) -> StaticMethodDesc<'cls> { StaticMethodDesc::new(self, name, sig) } /// Free the cached GlobalRef to the class object. This will happen automatically on drop, but /// this method is provided to allow the value to be dropped early. This can be used to perform /// cleanup on a thread that is already attached to the JVM. pub fn free(&mut self) { // Get a mutable reference ignoring poison state let mut guard = self.cls.write().ignore_poison(); let global = guard.take(); // Drop the guard before global in case it panics. We don't want to poison the lock. core::mem::drop(guard); // Drop the GlobalRef value to cleanup core::mem::drop(global); } fn get_cached(&self) -> Option> { CachedClass::from_lock(&self.cls) } } /// Wrapper to allow RwLock references to be returned from Desc. Use the `AsRef` impl to get the /// associated `JClass` reference. The inner `Option` must always be `Some`. This is enfocred by /// the `from_lock` constructor. pub struct CachedClass<'lock>(RwLockReadGuard<'lock, Option>); impl<'lock> CachedClass<'lock> { /// Read from the given lock and create a `CachedClass` instance if the lock contains a cached /// class value. The given lock must have valid data even if it is poisoned. fn from_lock(lock: &'lock RwLock>) -> Option> { let guard = lock.read().ignore_poison(); // Validate that there is a GlobalRef value already, otherwise avoid constructing `Self`. if guard.is_some() { Some(Self(guard)) } else { None } } } // Implement AsRef so that we can use this type as `Desc::Output` in [`ClassDesc`]. impl<'lock> AsRef> for CachedClass<'lock> { fn as_ref(&self) -> &JClass<'static> { // `unwrap` is valid since we checked for `Some` in the constructor. #[allow(clippy::expect_used)] let global = self .0 .as_ref() .expect("Created CachedClass in an invalid state"); // No direct conversion to JClass, so let's go through JObject first. let obj: &JObject<'static> = global.as_ref(); // This assumes our object is a class object. let cls: &JClass<'static> = From::from(obj); cls } } /// # Safety /// /// This returns the correct class instance via `JNIEnv::find_class`. The cached class is held in a /// [`GlobalRef`] so that it cannot be unloaded. unsafe impl<'a, 'local> Desc<'local, JClass<'static>> for &'a ClassDesc { type Output = CachedClass<'a>; fn lookup(self, env: &mut JNIEnv<'local>) -> jni::errors::Result { // Check the cache if let Some(cls) = self.get_cached() { return Ok(cls); } { // Ignoring poison is fine because we only write fully-constructed values. let mut guard = self.cls.write().ignore_poison(); // Multiple threads could have hit this block at the same time. Only allocate the // `GlobalRef` if it was not already allocated. if guard.is_none() { let cls = env.find_class(self.descriptor)?; let global = env.new_global_ref(cls)?; // Only directly assign valid values. That way poison state can't have broken // invariants. If the above panicked then it will poison without changing the // lock's data, and everything will still be fine albeit with a sprinkle of // possibly-leaked memory. *guard = Some(global); } } // Safe to unwrap since we just set `self.cls` to `Some`. `ClassDesc::free` can't be called // before this point because it takes a mutable reference to `*self`. #[allow(clippy::unwrap_used)] Ok(self.get_cached().unwrap()) } } /// A descriptor for a class member. `Id` is expected to implement the [`MemberId`] trait. /// /// See [`FieldDesc`], [`StaticFieldDesc`], [`MethodDesc`], and [`StaticMethodDesc`] aliases. pub struct MemberDesc<'cls, Id> { cls: &'cls ClassDesc, name: &'static str, sig: &'static str, id: RwLock>, } /// Descriptor for a field. pub type FieldDesc<'cls> = MemberDesc<'cls, JFieldID>; /// Descriptor for a static field. pub type StaticFieldDesc<'cls> = MemberDesc<'cls, JStaticFieldID>; /// Descriptor for a method. pub type MethodDesc<'cls> = MemberDesc<'cls, JMethodID>; /// Descriptor for a static method. pub type StaticMethodDesc<'cls> = MemberDesc<'cls, JStaticMethodID>; impl<'cls, Id: MemberId> MemberDesc<'cls, Id> { /// Create a new descriptor for a member of the given class with the given name and signature. /// /// Please use the helpers on [`ClassDesc`] instead of directly calling this method. pub const fn new(cls: &'cls ClassDesc, name: &'static str, sig: &'static str) -> Self { Self { cls, name, sig, id: RwLock::new(None), } } /// Get the class descriptor that this member is associated to. pub const fn cls(&self) -> &'cls ClassDesc { self.cls } } /// # Safety /// /// This returns the correct id. It is the same id obtained from the JNI. This id can be a pointer /// in some JVM implementations. See trait [`MemberId`]. unsafe impl<'cls, 'local, Id: MemberId> Desc<'local, Id> for &MemberDesc<'cls, Id> { type Output = Id; fn lookup(self, env: &mut JNIEnv<'local>) -> jni::errors::Result { // Check the cache. if let Some(id) = *self.id.read().ignore_poison() { return Ok(id); } { // Ignoring poison is fine because we only write valid values. let mut guard = self.id.write().ignore_poison(); // Multiple threads could have hit this block at the same time. Only lookup the id if // the lookup was not already performed. if guard.is_none() { let id = Id::lookup(env, self)?; // Only directly assign valid values. That way poison state can't have broken // invariants. If the above panicked then it will poison without changing the // lock's data and everything will still be fine. *guard = Some(id); Ok(id) } else { // Can unwrap since we just checked for `None`. #[allow(clippy::unwrap_used)] Ok(*guard.as_ref().unwrap()) } } } } /// Helper trait that calls into `jni` to lookup the id values. This is specialized on the id's /// type to call the correct lookup function. /// /// # Safety /// /// Implementers must be an ID returned from the JNI. `lookup` must be implemented such that the /// returned ID matches the JNI descriptor given. All values must be sourced from the JNI APIs. /// See [`::jni::descriptors::Desc`]. pub unsafe trait MemberId: Sized + Copy + AsRef { /// Lookup the id of the given descriptor via the given environment. fn lookup(env: &mut JNIEnv, desc: &MemberDesc) -> jni::errors::Result; } /// # Safety /// /// This fetches the matching ID from the JNI APIs. unsafe impl MemberId for JFieldID { fn lookup(env: &mut JNIEnv, desc: &MemberDesc) -> jni::errors::Result { env.get_field_id(desc.cls(), desc.name, desc.sig) } } /// # Safety /// /// This fetches the matching ID from the JNI APIs. unsafe impl MemberId for JStaticFieldID { fn lookup(env: &mut JNIEnv, desc: &MemberDesc) -> jni::errors::Result { env.get_static_field_id(desc.cls(), desc.name, desc.sig) } } /// # Safety /// /// This fetches the matching ID from the JNI APIs. unsafe impl MemberId for JMethodID { fn lookup(env: &mut JNIEnv, desc: &MemberDesc) -> jni::errors::Result { env.get_method_id(desc.cls(), desc.name, desc.sig) } } /// # Safety /// /// This fetches the matching ID from the JNI APIs. unsafe impl MemberId for JStaticMethodID { fn lookup(env: &mut JNIEnv, desc: &MemberDesc) -> jni::errors::Result { env.get_static_method_id(desc.cls(), desc.name, desc.sig) } } /// Internal helper to ignore the poison state of `LockResult`. /// /// The poison state occurs when a panic occurs during the lock's critical section. This means that /// the invariants of the locked data that were protected by the lock may be broken. When this /// trait is used below, it is used in scenarios where the locked data does not have invariants /// that can be broken in this way. In these cases, the possibly-poisoned lock is being used purely /// for synchronization, so the poison state may be ignored. trait IgnoreLockPoison { type Guard; /// Extract the inner `Guard` of this `LockResult`. This ignores whether the lock state is /// poisoned or not. fn ignore_poison(self) -> Self::Guard; } impl IgnoreLockPoison for LockResult { type Guard = G; fn ignore_poison(self) -> Self::Guard { self.unwrap_or_else(|poison| poison.into_inner()) } } #[cfg(test)] mod test { use super::*; const DESC: &str = "com/example/Foo"; static FOO: ClassDesc = ClassDesc::new(DESC); static FIELD: FieldDesc = FOO.field("foo", "I"); static STATIC_FIELD: StaticFieldDesc = FOO.static_field("sfoo", "J"); static CONSTRUCTOR: MethodDesc = FOO.constructor("(I)V"); static METHOD: MethodDesc = FOO.method("mfoo", "()Z"); static STATIC_METHOD: StaticMethodDesc = FOO.static_method("smfoo", "()I"); #[test] fn class_desc_created_properly() { assert_eq!(DESC, FOO.descriptor); assert!(FOO.cls.read().ignore_poison().is_none()); } #[test] fn field_desc_created_properly() { assert!(std::ptr::eq(&FOO, FIELD.cls())); assert_eq!("foo", FIELD.name); assert_eq!("I", FIELD.sig); } #[test] fn static_field_desc_created_properly() { assert!(std::ptr::eq(&FOO, STATIC_FIELD.cls())); assert_eq!("sfoo", STATIC_FIELD.name); assert_eq!("J", STATIC_FIELD.sig); } #[test] fn constructor_desc_created_properly() { assert!(std::ptr::eq(&FOO, CONSTRUCTOR.cls())); assert_eq!("", CONSTRUCTOR.name); assert_eq!("(I)V", CONSTRUCTOR.sig); } #[test] fn method_desc_created_properly() { assert!(std::ptr::eq(&FOO, METHOD.cls())); assert_eq!("mfoo", METHOD.name); assert_eq!("()Z", METHOD.sig); } #[test] fn static_method_desc_created_properly() { assert!(std::ptr::eq(&FOO, STATIC_METHOD.cls())); assert_eq!("smfoo", STATIC_METHOD.name); assert_eq!("()I", STATIC_METHOD.sig); } }