From a12c26328ca930e0e8619ae9258dcd167889c99b Mon Sep 17 00:00:00 2001 From: zaaarf Date: Sun, 22 Sep 2024 18:53:39 +0200 Subject: [PATCH 01/24] feat: tentative Vec impl for objects --- src/lib.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f8542d0..a6670df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,14 +140,14 @@ impl<'j> IntoJava<'j> for String { } } -impl<'j> IntoJava<'j> for Vec { +impl<'j, E> IntoJava<'j> for Vec where E: JavaArrayElement<'j> + IntoJava<'j, T: std::convert::AsRef>> { type T = jni::sys::jobjectArray; fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - let mut array = env.new_object_array(self.len() as i32, "java/lang/String", JObject::null())?; + let mut array = env.new_object_array(self.len() as i32, E::class(), JObject::null())?; for (n, el) in self.into_iter().enumerate() { - let string = env.new_string(el)?; - env.set_object_array_element(&mut array, n as i32, string)?; + let el = el.into_java(env)?; + env.set_object_array_element(&mut array, n as i32, &el)?; } Ok(array.into_raw()) } @@ -175,3 +175,15 @@ impl<'j> IntoJava<'j> for uuid::Uuid { .map(|j| j.as_raw()) } } + +pub trait JavaArrayElement<'j> { + type T; + fn class() -> &'static str; +} + +impl<'j> JavaArrayElement<'j> for String { + type T = jni::objects::JString<'j>; + fn class() -> &'static str { + "java/lang/String" + } +} From 521fc360935ea7be50e795913089d8e15dabdb1c Mon Sep 17 00:00:00 2001 From: zaaarf Date: Sun, 22 Sep 2024 20:40:13 +0200 Subject: [PATCH 02/24] feat: intermediate trait between objects and primitives --- src/lib.rs | 114 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 38 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a6670df..07ca45b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ -use jni::objects::JObject; pub use jni_toolbox_macro::jni; +use jni::objects::{JObject, JString, JObjectArray}; pub trait JniToolboxError: std::error::Error { fn jclass(&self) -> String; @@ -27,7 +27,7 @@ pub trait FromJava<'j> : Sized { } macro_rules! auto_from_java { - ($t:ty, $j:ty) => { + ($t: ty, $j: ty) => { impl<'j> FromJava<'j> for $t { type T = $j; @@ -55,7 +55,7 @@ impl<'j> FromJava<'j> for bool { } impl<'j> FromJava<'j> for String { - type T = jni::objects::JString<'j>; + type T = JString<'j>; fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result { if value.is_null() { return Err(jni::errors::Error::NullPtr("string can't be null")) }; @@ -63,8 +63,8 @@ impl<'j> FromJava<'j> for String { } } -impl<'j, T: FromJava<'j, T = jni::objects::JObject<'j>>> FromJava<'j> for Option { - type T = jni::objects::JObject<'j>; +impl<'j, T: FromJava<'j, T = JObject<'j>>> FromJava<'j> for Option { + type T = JObject<'j>; fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result { if value.is_null() { return Ok(None) }; @@ -74,7 +74,7 @@ impl<'j, T: FromJava<'j, T = jni::objects::JObject<'j>>> FromJava<'j> for Option #[cfg(feature = "uuid")] impl<'j> FromJava<'j> for uuid::Uuid { - type T = jni::objects::JObject<'j>; + type T = JObject<'j>; fn from_java(env: &mut jni::JNIEnv<'j>, uuid: Self::T) -> Result { let lsb = u64::from_ne_bytes( env.call_method(&uuid, "getLeastSignificantBits", "()J", &[])? @@ -92,18 +92,38 @@ impl<'j> FromJava<'j> for uuid::Uuid { } } -pub trait IntoJava<'j> { - type T; +trait JavaType {} - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result; +/// Intermediate trait used to guess the JNI return type. +/// Usually doesn't need to be manually implemented. +pub trait IntoJavaRaw<'j, T> { + fn into_java_raw(self, env: &mut jni::JNIEnv<'j>) -> Result; +} + +impl <'j, E, T: IntoJavaPrimitive<'j, T = E>> IntoJavaRaw<'j, E> for T { + fn into_java_raw(self, env: &mut jni::JNIEnv<'j>) -> Result { + self.into_java_primitive(env) + } +} + +impl<'j, T: IntoJavaObject<'j>> IntoJavaRaw<'j, jni::sys::jobject> for T { + fn into_java_raw(self, env: &mut jni::JNIEnv<'j>) -> Result { + self.into_java(env) + .map(|j| j.as_ref().as_raw()) + } +} + +pub trait IntoJavaPrimitive<'j> { + type T; + fn into_java_primitive(self, _: &mut jni::JNIEnv<'j>) -> Result; } macro_rules! auto_into_java { - ($t:ty, $j:ty) => { - impl<'j> IntoJava<'j> for $t { + ($t: ty, $j: ty) => { + impl<'j> IntoJavaPrimitive<'j> for $t { type T = $j; - fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result { + fn into_java_primitive(self, _: &mut jni::JNIEnv<'j>) -> Result { Ok(self) } } @@ -117,73 +137,91 @@ auto_into_java!(f32, jni::sys::jfloat); auto_into_java!(f64, jni::sys::jdouble); auto_into_java!((), ()); -impl<'j> IntoJava<'j> for bool { +impl<'j> IntoJavaPrimitive<'j> for bool { type T = jni::sys::jboolean; #[inline] - fn into_java(self, _: &mut jni::JNIEnv) -> Result { + fn into_java_primitive(self, _: &mut jni::JNIEnv) -> Result { Ok(if self { 1 } else { 0 }) } } -impl<'j> IntoJava<'j> for &str { - type T = jni::sys::jstring; +pub trait IntoJavaObject<'j> { + type T: std::convert::AsRef>; + const CLASS: &'static str; + fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result; +} + +impl<'j> IntoJavaObject<'j> for &str { + type T = JString<'j>; + const CLASS: &'static str = "java/lang/String"; fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - Ok(env.new_string(self)?.as_raw()) + env.new_string(self) } } -impl<'j> IntoJava<'j> for String { - type T = jni::sys::jstring; +impl<'j> IntoJavaObject<'j> for String { + type T = JString<'j>; + const CLASS: &'static str = "java/lang/String"; fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { self.as_str().into_java(env) } } -impl<'j, E> IntoJava<'j> for Vec where E: JavaArrayElement<'j> + IntoJava<'j, T: std::convert::AsRef>> { - type T = jni::sys::jobjectArray; +impl<'j, E: IntoJavaObject<'j>> IntoJavaObject<'j> for Vec { + type T = JObjectArray<'j>; + const CLASS: &'static str = E::CLASS; fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - let mut array = env.new_object_array(self.len() as i32, E::class(), JObject::null())?; + let mut array = env.new_object_array(self.len() as i32, E::CLASS, JObject::null())?; for (n, el) in self.into_iter().enumerate() { let el = el.into_java(env)?; env.set_object_array_element(&mut array, n as i32, &el)?; } - Ok(array.into_raw()) + Ok(array) } } -impl<'j, T: IntoJava<'j, T = jni::sys::jobject>> IntoJava<'j> for Option { - type T = T::T; +impl<'j, E: std::convert::AsRef> + JavaFromRaw, T: IntoJavaObject<'j, T = E>> IntoJavaObject<'j> for Option { + type T = E; + const CLASS: &'static str = T::CLASS; fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { match self { Some(x) => x.into_java(env), - None => Ok(std::ptr::null_mut()), + None => Ok(unsafe { E::from_raw(std::ptr::null_mut()) }) // safe, that's what JObject::null does } } } #[cfg(feature = "uuid")] -impl<'j> IntoJava<'j> for uuid::Uuid { - type T = jni::sys::jobject; +impl<'j> IntoJavaObject<'j> for uuid::Uuid { + type T = jni::objects::JObject<'j>; + const CLASS: &'static str = "java/util/UUID"; fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - let class = env.find_class("java/util/UUID")?; + let class = env.find_class(Self::CLASS)?; let (msb, lsb) = self.as_u64_pair(); let msb = i64::from_ne_bytes(msb.to_ne_bytes()); let lsb = i64::from_ne_bytes(lsb.to_ne_bytes()); env.new_object(&class, "(JJ)V", &[jni::objects::JValueGen::Long(msb), jni::objects::JValueGen::Long(lsb)]) - .map(|j| j.as_raw()) } } -pub trait JavaArrayElement<'j> { - type T; - fn class() -> &'static str; +/// Needed internally to perform some operations. +trait JavaFromRaw { + unsafe fn from_raw(raw: jni::sys::jobject) -> Self; } -impl<'j> JavaArrayElement<'j> for String { - type T = jni::objects::JString<'j>; - fn class() -> &'static str { - "java/lang/String" - } +macro_rules! auto_from_raw { + ($type: ty) => { + impl JavaFromRaw for $type { + #[inline] + unsafe fn from_raw(raw: jni::sys::jobject) -> Self { + Self::from_raw(raw) + } + } + }; } + +auto_from_raw!(JObject<'_>); +auto_from_raw!(JString<'_>); +auto_from_raw!(JObjectArray<'_>); From d6c0aa2f9eaa35be6f85e1e81549767a9fb9c4ee Mon Sep 17 00:00:00 2001 From: zaaarf Date: Mon, 23 Sep 2024 00:23:42 +0200 Subject: [PATCH 03/24] feat: working but questionable intermediate trait implementation --- macro/src/ret.rs | 8 +++- macro/src/wrapper.rs | 39 +++++++++++++----- src/lib.rs | 95 ++++++++++++++++++++++++++++---------------- 3 files changed, 95 insertions(+), 47 deletions(-) diff --git a/macro/src/ret.rs b/macro/src/ret.rs index f642fe2..254fcda 100644 --- a/macro/src/ret.rs +++ b/macro/src/ret.rs @@ -42,8 +42,14 @@ impl ReturnOptions { pub(crate) fn tokens(&self) -> TokenStream { match &self.ty { // TODO why do we need to invoke syn::Token! macro ??? - Some(t) => quote::quote!( -> <#t as jni_toolbox::IntoJava<'local>>::T ), None => ReturnType::Default.to_token_stream(), + Some(t) => { + if t.to_token_stream().to_string() == "bool" {// TODO: there DEFINITELY is a better way + quote::quote!( -> jni::sys::jboolean ) + } else { + quote::quote!( -> <#t as jni_toolbox::IntoJavaRaw>::T ) + } + } } } } diff --git a/macro/src/wrapper.rs b/macro/src/wrapper.rs index 5bdaa89..b774e33 100644 --- a/macro/src/wrapper.rs +++ b/macro/src/wrapper.rs @@ -24,21 +24,35 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re // TODO a bit ugly passing the return expr down... we should probably manage returns here let args = ArgumentOptions::parse_args(&fn_item, return_expr.clone())?; - let return_type = ret.tokens(); + let return_type_import = if attrs.return_pointer { + syn::Ident::new("IntoJavaObject", Span::call_site()) + } else { + syn::Ident::new("IntoJavaPrimitive", Span::call_site()) + }; + let name = fn_item.sig.ident.to_string(); let name_jni = name.replace("_", "_1"); let fn_name_inner = syn::Ident::new(&name, Span::call_site()); let fn_name = syn::Ident::new(&format!("Java_{}_{}_{name_jni}", attrs.package, attrs.class), Span::call_site()); - let incoming = args.incoming; // V----------------------------------V - let header = quote::quote! { - #[no_mangle] - #[allow(unused_mut)] - pub extern "system" fn #fn_name<'local>(#incoming) #return_type + let header = if attrs.return_pointer { + quote::quote! { + #[no_mangle] + #[allow(unused_mut)] + pub extern "system" fn #fn_name<'local>(#incoming) -> jni::sys::jobject + } + } else { + quote::quote! { + #[no_mangle] + #[allow(unused_mut)] + pub extern "system" fn #fn_name<'local>(#incoming) #return_type + } }; + + // ^----------------------------------^ let env_ident = args.env; @@ -49,7 +63,8 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re // V----------------------------------V quote::quote! { { - use jni_toolbox::{JniToolboxError, FromJava, IntoJava}; + use jni_toolbox::#return_type_import; + use jni_toolbox::{JniToolboxError, FromJava}; #transforming match #fn_name_inner(#forwarding) { Ok(ret) => ret, @@ -65,7 +80,8 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re // V----------------------------------V quote::quote! { { - use jni_toolbox::{JniToolboxError, FromJava, IntoJava}; + use jni_toolbox::#return_type_import; + use jni_toolbox::{JniToolboxError, FromJava, IntoJavaRaw}; // NOTE: this should be SAFE! the cloned env reference lives less than the actual one, we just lack a // way to get it back from the called function and thus resort to unsafe cloning let mut env_copy = unsafe { #env_ident.unsafe_clone() }; @@ -85,7 +101,7 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re }, } Ok(ret) => match ret.into_java(&mut env_copy) { - Ok(fin) => return fin, + Ok(fin) => return fin.into_java_raw(), Err(e) => { // TODO should we panic instead? let _ = env_copy.throw_new("java/lang/RuntimeException", format!("{e:?}")); @@ -100,10 +116,11 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re // V----------------------------------V quote::quote! { { - use jni_toolbox::{JniToolboxError, FromJava, IntoJava}; + use jni_toolbox::#return_type_import; + use jni_toolbox::{JniToolboxError, FromJava, IntoJavaRaw}; #transforming match #fn_name_inner(#forwarding).into_java(&mut #env_ident) { - Ok(res) => return res, + Ok(res) => return res.into_java_raw(), Err(e) => { // TODO should we panic instead? let _ = #env_ident.throw_new("java/lang/RuntimeException", format!("{e:?}")); diff --git a/src/lib.rs b/src/lib.rs index 07ca45b..a3895eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ pub use jni_toolbox_macro::jni; -use jni::objects::{JObject, JString, JObjectArray}; +use jni::objects::{JObject, JObjectArray, JString}; pub trait JniToolboxError: std::error::Error { fn jclass(&self) -> String; @@ -42,8 +42,10 @@ macro_rules! auto_from_java { auto_from_java!(i64, jni::sys::jlong); auto_from_java!(i32, jni::sys::jint); auto_from_java!(i16, jni::sys::jshort); +auto_from_java!(i8, jni::sys::jbyte); auto_from_java!(f32, jni::sys::jfloat); auto_from_java!(f64, jni::sys::jdouble); +auto_from_java!(JObject<'j>, JObject<'j>); impl<'j> FromJava<'j> for bool { type T = jni::sys::jboolean; @@ -63,11 +65,11 @@ impl<'j> FromJava<'j> for String { } } -impl<'j, T: FromJava<'j, T = JObject<'j>>> FromJava<'j> for Option { - type T = JObject<'j>; +impl<'j, T: FromJava<'j, T: std::convert::AsRef>>> FromJava<'j> for Option { + type T = T::T; fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result { - if value.is_null() { return Ok(None) }; + if value.as_ref().is_null() { return Ok(None) }; Ok(Some(T::from_java(env, value)?)) } } @@ -92,30 +94,9 @@ impl<'j> FromJava<'j> for uuid::Uuid { } } -trait JavaType {} - -/// Intermediate trait used to guess the JNI return type. -/// Usually doesn't need to be manually implemented. -pub trait IntoJavaRaw<'j, T> { - fn into_java_raw(self, env: &mut jni::JNIEnv<'j>) -> Result; -} - -impl <'j, E, T: IntoJavaPrimitive<'j, T = E>> IntoJavaRaw<'j, E> for T { - fn into_java_raw(self, env: &mut jni::JNIEnv<'j>) -> Result { - self.into_java_primitive(env) - } -} - -impl<'j, T: IntoJavaObject<'j>> IntoJavaRaw<'j, jni::sys::jobject> for T { - fn into_java_raw(self, env: &mut jni::JNIEnv<'j>) -> Result { - self.into_java(env) - .map(|j| j.as_ref().as_raw()) - } -} - pub trait IntoJavaPrimitive<'j> { type T; - fn into_java_primitive(self, _: &mut jni::JNIEnv<'j>) -> Result; + fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result; } macro_rules! auto_into_java { @@ -123,16 +104,19 @@ macro_rules! auto_into_java { impl<'j> IntoJavaPrimitive<'j> for $t { type T = $j; - fn into_java_primitive(self, _: &mut jni::JNIEnv<'j>) -> Result { + fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result { Ok(self) } } }; } +// TODO: primitive arrays! + auto_into_java!(i64, jni::sys::jlong); auto_into_java!(i32, jni::sys::jint); auto_into_java!(i16, jni::sys::jshort); +auto_into_java!(i8, jni::sys::jbyte); auto_into_java!(f32, jni::sys::jfloat); auto_into_java!(f64, jni::sys::jdouble); auto_into_java!((), ()); @@ -141,7 +125,7 @@ impl<'j> IntoJavaPrimitive<'j> for bool { type T = jni::sys::jboolean; #[inline] - fn into_java_primitive(self, _: &mut jni::JNIEnv) -> Result { + fn into_java(self, _: &mut jni::JNIEnv) -> Result { Ok(if self { 1 } else { 0 }) } } @@ -171,7 +155,6 @@ impl<'j> IntoJavaObject<'j> for String { impl<'j, E: IntoJavaObject<'j>> IntoJavaObject<'j> for Vec { type T = JObjectArray<'j>; const CLASS: &'static str = E::CLASS; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { let mut array = env.new_object_array(self.len() as i32, E::CLASS, JObject::null())?; for (n, el) in self.into_iter().enumerate() { @@ -182,13 +165,13 @@ impl<'j, E: IntoJavaObject<'j>> IntoJavaObject<'j> for Vec { } } -impl<'j, E: std::convert::AsRef> + JavaFromRaw, T: IntoJavaObject<'j, T = E>> IntoJavaObject<'j> for Option { +impl<'j, E: std::convert::AsRef> + FromJavaRaw, T: IntoJavaObject<'j, T = E>> IntoJavaObject<'j> for Option { type T = E; const CLASS: &'static str = T::CLASS; fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { match self { Some(x) => x.into_java(env), - None => Ok(unsafe { E::from_raw(std::ptr::null_mut()) }) // safe, that's what JObject::null does + None => Ok(unsafe { E::from_java_raw(std::ptr::null_mut()) }) // safe, that's what JObject::null does } } } @@ -207,15 +190,15 @@ impl<'j> IntoJavaObject<'j> for uuid::Uuid { } /// Needed internally to perform some operations. -trait JavaFromRaw { - unsafe fn from_raw(raw: jni::sys::jobject) -> Self; +trait FromJavaRaw { + unsafe fn from_java_raw(raw: jni::sys::jobject) -> Self; } macro_rules! auto_from_raw { ($type: ty) => { - impl JavaFromRaw for $type { + impl FromJavaRaw for $type { #[inline] - unsafe fn from_raw(raw: jni::sys::jobject) -> Self { + unsafe fn from_java_raw(raw: jni::sys::jobject) -> Self { Self::from_raw(raw) } } @@ -225,3 +208,45 @@ macro_rules! auto_from_raw { auto_from_raw!(JObject<'_>); auto_from_raw!(JString<'_>); auto_from_raw!(JObjectArray<'_>); + +/// Intermediate trait used to guess the JNI return type. +/// Usually doesn't need to be manually implemented. +pub trait IntoJavaRaw { + type T; + fn into_java_raw(self) -> Self::T; +} + +macro_rules! auto_into_raw_primitive { + ($type: ty) => { + impl IntoJavaRaw for $type { + type T = $type; + fn into_java_raw(self) -> Self::T { + self + } + } + } +} + +auto_into_raw_primitive!(jni::sys::jlong); +auto_into_raw_primitive!(jni::sys::jint); +auto_into_raw_primitive!(jni::sys::jshort); +auto_into_raw_primitive!(jni::sys::jbyte); +auto_into_raw_primitive!(jni::sys::jdouble); +auto_into_raw_primitive!(jni::sys::jfloat); +auto_into_raw_primitive!(jni::sys::jboolean); +auto_into_raw_primitive!(()); + +macro_rules! auto_into_raw_object { + ($lt: lifetime, $type: ty) => { + impl<'j> IntoJavaRaw for $type { + type T = jni::sys::jobject; + fn into_java_raw(self) -> Self::T { + self.as_raw() + } + } + }; +} + +auto_into_raw_object!('j, JObject<'j>); +auto_into_raw_object!('j, JString<'j>); +auto_into_raw_object!('j, JObjectArray<'j>); From 6f424dadf7f80f7660679f07f8b5b3560d8b4248 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Mon, 23 Sep 2024 00:50:52 +0200 Subject: [PATCH 04/24] docs: updated README, minimal rustdocs --- README.md | 57 ++++++++++++++++++++++-------------------------- macro/src/lib.rs | 2 +- src/lib.rs | 13 +++++++++++ 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 0a7ffbf..a8f4c0f 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,12 @@ [![Crates.io Version](https://img.shields.io/crates/v/jni-toolbox)](https://crates.io/crates/jni-toolbox) [![docs.rs](https://img.shields.io/docsrs/jni-toolbox)](https://docs.rs/jni-toolbox) +This is a simple crate built around [jni-rs](https://github.com/jni-rs/jni-rs) to automatically generate JNI-compatible extern functions. -this is a simple crate built around [jni-rs](https://github.com/jni-rs/jni-rs) to automatically generate JNI-compatible extern functions +It also wraps functions returning `Result<>`, making short-circuiting easy. -it also wraps functions returning `Result<>`, making short-circuiting easy - -## usage -just specify package and class on your function, and done! +## Usage +Just specify package and class on your function, and done! ```rust #[jni_toolbox::jni(package = "your.package.path", class = "ContainerClass")] @@ -18,14 +17,13 @@ fn your_function_name(arg: String) -> Result, String> { } ``` -### conversions -every type that must go into/from Java must implement `IntoJava` or `FromJava` (methods will receive a `&mut JNIEnv` and can return errors). -most primitives already have them implemented. conversions are automatic and the wrapper function will invoke IntoJava/FromJava for every type, -passing an environment reference. +### Conversions +Every type that is meant to be sent to Java must implement `IntoJavaObject` (or, unlikely, `IntoJavaPrimitive`); every type that is meant to be +received from Java must implement `FromJava`. Most primitives and a few common types should already be implemented. ```rust -impl<'j> IntoJava for MyClass { - type T = jni::sys::jobject; +impl<'j> IntoJavaObject for MyClass { + type T = jni::objects::JObject<'j> fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { let hello = env.new_string("world")?; // TODO!! @@ -33,15 +31,16 @@ impl<'j> IntoJava for MyClass { } ``` -### pointers -to return pointer type values, add the `ptr` attribute +### Pointers +To return pointer type values, add the `ptr` attribute. -note that, while possible to pass raw pointers to the JVM, it is not safe by default and must be done with extreme care. +Note that, while it is possible to pass raw pointers to the JVM, it is not safe by default and must be done with extreme care. -### exceptions +### Exceptions Errors are thrown automatically when a `Result` is an error. For your errors to work, you must implement the `JniToolboxError` trait for your errors, (which just returns the path to your Java error class) and then make a Java error wrapper which can be constructed with a single string argument. -functions returning `Result`s will automatically have their return value unwrapped and, if is an err, throw an exception and return early. + +Functions returning `Result`s will automatically have their return value unwrapped and, if is an err, throw an exception and return early. ```rust impl JniToolboxError for MyError { @@ -53,19 +52,18 @@ impl JniToolboxError for MyError { ```java package my.package.some; -public class MyError { +public class MyError extends Throwable { public MyError(String x) { // TODO } } ``` -to throw simple exceptions, it's possible to use the `exception` attribute. just pass your exception's path (must be constructable with a single string argument!) +To throw simple exceptions, it's possible to use the `exception` attribute. Pass the exception's fully qualified name (must have a constructor +that takes in a single `String` argument). - - -### examples -the following function: +### Examples +The following function: ```rust #[jni(package = "mp.code", class = "Client", ptr)] fn connect(config: Config) -> Result { @@ -73,15 +71,11 @@ fn connect(config: Config) -> Result { } ``` -gets turned into these two functions: +generates a matching expanded function invoking it: -
show macro expansion +
Show macro expansion ```rust -fn connect(config: Config) -> Result { - tokio().block_on(Client::connect(config)) -} - #[no_mangle] #[allow(unused_mut)] pub extern "system" fn Java_mp_code_Client_connect<'local>( @@ -157,9 +151,10 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>( } } ``` -
-## status -this crate is rather early and intended mostly to maintain [`codemp`](https://github.com/hexedtech/codemp) java bindings, however it's also quite small and only runs at comptime, so should be rather safe to use +## Status +This crate is early and intended mostly to maintain [`codemp`](https://github.com/hexedtech/codemp)'s Java bindings, so things not used +there may be missing or slightly broken. However, the crate is also quite small and only runs at compile time, so trying it out in your +own project should not be a problem. diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 4843a24..6f3e595 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -3,7 +3,7 @@ mod wrapper; mod args; mod ret; -/// wrap this function in in a JNI exported fn +/// Wrap this function in in a JNI exported fn. #[proc_macro_attribute] pub fn jni( attrs: proc_macro::TokenStream, diff --git a/src/lib.rs b/src/lib.rs index a3895eb..56284c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ pub use jni_toolbox_macro::jni; use jni::objects::{JObject, JObjectArray, JString}; +/// An error that is meant to be used with jni-toolbox. pub trait JniToolboxError: std::error::Error { + /// The Java class for the matching exception. fn jclass(&self) -> String; } @@ -17,12 +19,17 @@ impl JniToolboxError for jni::errors::JniError { } } +/// Used in the generated code to have proper type bindings. You probably didn't want +/// to call this directly. pub fn from_java_static<'j, T: FromJava<'j>>(env: &mut jni::JNIEnv<'j>, val: T::T) -> Result { T::from_java(env, val) } +/// Specifies how a Java type should be converted before being fed to Rust. pub trait FromJava<'j> : Sized { + /// The JNI type representing the input. type T : Sized; + /// Attempts to convert this Java object into its Rust counterpart. fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result; } @@ -94,8 +101,11 @@ impl<'j> FromJava<'j> for uuid::Uuid { } } +/// Specifies how a Rust type should be converted into a Java primitive. pub trait IntoJavaPrimitive<'j> { + /// The JNI type representing the output. type T; + /// Attempts to convert this Rust object into a Java primitive. fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result; } @@ -130,9 +140,12 @@ impl<'j> IntoJavaPrimitive<'j> for bool { } } +/// Specifies how a Rust type should be converted into a Java object. pub trait IntoJavaObject<'j> { type T: std::convert::AsRef>; + /// The Java class associated with this type. const CLASS: &'static str; + /// Attempts to convert this Rust object into a Java object. fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result; } From 50be86d2a4cf99fa6e4a739fdb8c785dd3a48caa Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 23 Sep 2024 14:45:26 +0200 Subject: [PATCH 05/24] chore: split down main lib file with traits --- src/from_java.rs | 84 ++++++++++++++++ src/into_java.rs | 105 ++++++++++++++++++++ src/lib.rs | 254 ++--------------------------------------------- src/raw_java.rs | 63 ++++++++++++ 4 files changed, 260 insertions(+), 246 deletions(-) create mode 100644 src/from_java.rs create mode 100644 src/into_java.rs create mode 100644 src/raw_java.rs diff --git a/src/from_java.rs b/src/from_java.rs new file mode 100644 index 0000000..e6af7fa --- /dev/null +++ b/src/from_java.rs @@ -0,0 +1,84 @@ +use jni::objects::{JObject, JString}; + + +/// Used in the generated code to have proper type bindings. You probably didn't want +/// to call this directly. +pub fn from_java_static<'j, T: FromJava<'j>>(env: &mut jni::JNIEnv<'j>, val: T::T) -> Result { + T::from_java(env, val) +} + +/// Specifies how a Java type should be converted before being fed to Rust. +pub trait FromJava<'j> : Sized { + /// The JNI type representing the input. + type T : Sized; + /// Attempts to convert this Java object into its Rust counterpart. + fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result; +} + +macro_rules! auto_from_java { + ($t: ty, $j: ty) => { + impl<'j> FromJava<'j> for $t { + type T = $j; + + #[inline] + fn from_java(_: &mut jni::JNIEnv, value: Self::T) -> Result { + Ok(value) + } + } + }; +} + +auto_from_java!(i64, jni::sys::jlong); +auto_from_java!(i32, jni::sys::jint); +auto_from_java!(i16, jni::sys::jshort); +auto_from_java!(i8, jni::sys::jbyte); +auto_from_java!(f32, jni::sys::jfloat); +auto_from_java!(f64, jni::sys::jdouble); +auto_from_java!(JObject<'j>, JObject<'j>); + +impl<'j> FromJava<'j> for bool { + type T = jni::sys::jboolean; + + #[inline] + fn from_java(_: &mut jni::JNIEnv, value: Self::T) -> Result { + Ok(value == 1) + } +} + +impl<'j> FromJava<'j> for String { + type T = JString<'j>; + + fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result { + if value.is_null() { return Err(jni::errors::Error::NullPtr("string can't be null")) }; + Ok(env.get_string(&value)?.into()) + } +} + +impl<'j, T: FromJava<'j, T: std::convert::AsRef>>> FromJava<'j> for Option { + type T = T::T; + + fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result { + if value.as_ref().is_null() { return Ok(None) }; + Ok(Some(T::from_java(env, value)?)) + } +} + +#[cfg(feature = "uuid")] +impl<'j> FromJava<'j> for uuid::Uuid { + type T = JObject<'j>; + fn from_java(env: &mut jni::JNIEnv<'j>, uuid: Self::T) -> Result { + let lsb = u64::from_ne_bytes( + env.call_method(&uuid, "getLeastSignificantBits", "()J", &[])? + .j()? + .to_ne_bytes() + ); + + let msb = u64::from_ne_bytes( + env.call_method(&uuid, "getMostSignificantBits", "()J", &[])? + .j()? + .to_ne_bytes() + ); + + Ok(uuid::Uuid::from_u64_pair(msb, lsb)) + } +} diff --git a/src/into_java.rs b/src/into_java.rs new file mode 100644 index 0000000..abc5721 --- /dev/null +++ b/src/into_java.rs @@ -0,0 +1,105 @@ +use jni::objects::{JObject, JObjectArray, JString}; + +use crate::raw_java::FromJavaRaw; + + +/// Specifies how a Rust type should be converted into a Java primitive. +pub trait IntoJavaPrimitive<'j> { + /// The JNI type representing the output. + type T; + /// Attempts to convert this Rust object into a Java primitive. + fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result; +} + +macro_rules! auto_into_java { + ($t: ty, $j: ty) => { + impl<'j> IntoJavaPrimitive<'j> for $t { + type T = $j; + + fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result { + Ok(self) + } + } + }; +} + +// TODO: primitive arrays! + +auto_into_java!(i64, jni::sys::jlong); +auto_into_java!(i32, jni::sys::jint); +auto_into_java!(i16, jni::sys::jshort); +auto_into_java!(i8, jni::sys::jbyte); +auto_into_java!(f32, jni::sys::jfloat); +auto_into_java!(f64, jni::sys::jdouble); +auto_into_java!((), ()); + +impl<'j> IntoJavaPrimitive<'j> for bool { + type T = jni::sys::jboolean; + + #[inline] + fn into_java(self, _: &mut jni::JNIEnv) -> Result { + Ok(if self { 1 } else { 0 }) + } +} + +/// Specifies how a Rust type should be converted into a Java object. +pub trait IntoJavaObject<'j> { + type T: std::convert::AsRef>; + /// The Java class associated with this type. + const CLASS: &'static str; + /// Attempts to convert this Rust object into a Java object. + fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result; +} + +impl<'j> IntoJavaObject<'j> for &str { + type T = JString<'j>; + const CLASS: &'static str = "java/lang/String"; + fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { + env.new_string(self) + } +} + +impl<'j> IntoJavaObject<'j> for String { + type T = JString<'j>; + const CLASS: &'static str = "java/lang/String"; + fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { + self.as_str().into_java(env) + } +} + +impl<'j, E: IntoJavaObject<'j>> IntoJavaObject<'j> for Vec { + type T = JObjectArray<'j>; + const CLASS: &'static str = E::CLASS; + fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { + let mut array = env.new_object_array(self.len() as i32, E::CLASS, JObject::null())?; + for (n, el) in self.into_iter().enumerate() { + let el = el.into_java(env)?; + env.set_object_array_element(&mut array, n as i32, &el)?; + } + Ok(array) + } +} + +impl<'j, E: std::convert::AsRef> + FromJavaRaw, T: IntoJavaObject<'j, T = E>> IntoJavaObject<'j> for Option { + type T = E; + const CLASS: &'static str = T::CLASS; + fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { + match self { + Some(x) => x.into_java(env), + None => Ok(unsafe { E::from_java_raw(std::ptr::null_mut()) }) // safe, that's what JObject::null does + } + } +} + +#[cfg(feature = "uuid")] +impl<'j> IntoJavaObject<'j> for uuid::Uuid { + type T = jni::objects::JObject<'j>; + const CLASS: &'static str = "java/util/UUID"; + fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { + let class = env.find_class(Self::CLASS)?; + let (msb, lsb) = self.as_u64_pair(); + let msb = i64::from_ne_bytes(msb.to_ne_bytes()); + let lsb = i64::from_ne_bytes(lsb.to_ne_bytes()); + env.new_object(&class, "(JJ)V", &[jni::objects::JValueGen::Long(msb), jni::objects::JValueGen::Long(lsb)]) + } +} diff --git a/src/lib.rs b/src/lib.rs index 56284c3..065043e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,12 @@ +pub mod into_java; +pub mod from_java; +pub mod raw_java; + pub use jni_toolbox_macro::jni; -use jni::objects::{JObject, JObjectArray, JString}; +pub use into_java::{IntoJavaObject, IntoJavaPrimitive}; +pub use from_java::{FromJava, from_java_static}; +pub use raw_java::IntoJavaRaw; + /// An error that is meant to be used with jni-toolbox. pub trait JniToolboxError: std::error::Error { @@ -18,248 +25,3 @@ impl JniToolboxError for jni::errors::JniError { "java/lang/RuntimeException".to_string() } } - -/// Used in the generated code to have proper type bindings. You probably didn't want -/// to call this directly. -pub fn from_java_static<'j, T: FromJava<'j>>(env: &mut jni::JNIEnv<'j>, val: T::T) -> Result { - T::from_java(env, val) -} - -/// Specifies how a Java type should be converted before being fed to Rust. -pub trait FromJava<'j> : Sized { - /// The JNI type representing the input. - type T : Sized; - /// Attempts to convert this Java object into its Rust counterpart. - fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result; -} - -macro_rules! auto_from_java { - ($t: ty, $j: ty) => { - impl<'j> FromJava<'j> for $t { - type T = $j; - - #[inline] - fn from_java(_: &mut jni::JNIEnv, value: Self::T) -> Result { - Ok(value) - } - } - }; -} - -auto_from_java!(i64, jni::sys::jlong); -auto_from_java!(i32, jni::sys::jint); -auto_from_java!(i16, jni::sys::jshort); -auto_from_java!(i8, jni::sys::jbyte); -auto_from_java!(f32, jni::sys::jfloat); -auto_from_java!(f64, jni::sys::jdouble); -auto_from_java!(JObject<'j>, JObject<'j>); - -impl<'j> FromJava<'j> for bool { - type T = jni::sys::jboolean; - - #[inline] - fn from_java(_: &mut jni::JNIEnv, value: Self::T) -> Result { - Ok(value == 1) - } -} - -impl<'j> FromJava<'j> for String { - type T = JString<'j>; - - fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result { - if value.is_null() { return Err(jni::errors::Error::NullPtr("string can't be null")) }; - Ok(env.get_string(&value)?.into()) - } -} - -impl<'j, T: FromJava<'j, T: std::convert::AsRef>>> FromJava<'j> for Option { - type T = T::T; - - fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result { - if value.as_ref().is_null() { return Ok(None) }; - Ok(Some(T::from_java(env, value)?)) - } -} - -#[cfg(feature = "uuid")] -impl<'j> FromJava<'j> for uuid::Uuid { - type T = JObject<'j>; - fn from_java(env: &mut jni::JNIEnv<'j>, uuid: Self::T) -> Result { - let lsb = u64::from_ne_bytes( - env.call_method(&uuid, "getLeastSignificantBits", "()J", &[])? - .j()? - .to_ne_bytes() - ); - - let msb = u64::from_ne_bytes( - env.call_method(&uuid, "getMostSignificantBits", "()J", &[])? - .j()? - .to_ne_bytes() - ); - - Ok(uuid::Uuid::from_u64_pair(msb, lsb)) - } -} - -/// Specifies how a Rust type should be converted into a Java primitive. -pub trait IntoJavaPrimitive<'j> { - /// The JNI type representing the output. - type T; - /// Attempts to convert this Rust object into a Java primitive. - fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result; -} - -macro_rules! auto_into_java { - ($t: ty, $j: ty) => { - impl<'j> IntoJavaPrimitive<'j> for $t { - type T = $j; - - fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result { - Ok(self) - } - } - }; -} - -// TODO: primitive arrays! - -auto_into_java!(i64, jni::sys::jlong); -auto_into_java!(i32, jni::sys::jint); -auto_into_java!(i16, jni::sys::jshort); -auto_into_java!(i8, jni::sys::jbyte); -auto_into_java!(f32, jni::sys::jfloat); -auto_into_java!(f64, jni::sys::jdouble); -auto_into_java!((), ()); - -impl<'j> IntoJavaPrimitive<'j> for bool { - type T = jni::sys::jboolean; - - #[inline] - fn into_java(self, _: &mut jni::JNIEnv) -> Result { - Ok(if self { 1 } else { 0 }) - } -} - -/// Specifies how a Rust type should be converted into a Java object. -pub trait IntoJavaObject<'j> { - type T: std::convert::AsRef>; - /// The Java class associated with this type. - const CLASS: &'static str; - /// Attempts to convert this Rust object into a Java object. - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result; -} - -impl<'j> IntoJavaObject<'j> for &str { - type T = JString<'j>; - const CLASS: &'static str = "java/lang/String"; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - env.new_string(self) - } -} - -impl<'j> IntoJavaObject<'j> for String { - type T = JString<'j>; - const CLASS: &'static str = "java/lang/String"; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - self.as_str().into_java(env) - } -} - -impl<'j, E: IntoJavaObject<'j>> IntoJavaObject<'j> for Vec { - type T = JObjectArray<'j>; - const CLASS: &'static str = E::CLASS; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - let mut array = env.new_object_array(self.len() as i32, E::CLASS, JObject::null())?; - for (n, el) in self.into_iter().enumerate() { - let el = el.into_java(env)?; - env.set_object_array_element(&mut array, n as i32, &el)?; - } - Ok(array) - } -} - -impl<'j, E: std::convert::AsRef> + FromJavaRaw, T: IntoJavaObject<'j, T = E>> IntoJavaObject<'j> for Option { - type T = E; - const CLASS: &'static str = T::CLASS; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - match self { - Some(x) => x.into_java(env), - None => Ok(unsafe { E::from_java_raw(std::ptr::null_mut()) }) // safe, that's what JObject::null does - } - } -} - -#[cfg(feature = "uuid")] -impl<'j> IntoJavaObject<'j> for uuid::Uuid { - type T = jni::objects::JObject<'j>; - const CLASS: &'static str = "java/util/UUID"; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - let class = env.find_class(Self::CLASS)?; - let (msb, lsb) = self.as_u64_pair(); - let msb = i64::from_ne_bytes(msb.to_ne_bytes()); - let lsb = i64::from_ne_bytes(lsb.to_ne_bytes()); - env.new_object(&class, "(JJ)V", &[jni::objects::JValueGen::Long(msb), jni::objects::JValueGen::Long(lsb)]) - } -} - -/// Needed internally to perform some operations. -trait FromJavaRaw { - unsafe fn from_java_raw(raw: jni::sys::jobject) -> Self; -} - -macro_rules! auto_from_raw { - ($type: ty) => { - impl FromJavaRaw for $type { - #[inline] - unsafe fn from_java_raw(raw: jni::sys::jobject) -> Self { - Self::from_raw(raw) - } - } - }; -} - -auto_from_raw!(JObject<'_>); -auto_from_raw!(JString<'_>); -auto_from_raw!(JObjectArray<'_>); - -/// Intermediate trait used to guess the JNI return type. -/// Usually doesn't need to be manually implemented. -pub trait IntoJavaRaw { - type T; - fn into_java_raw(self) -> Self::T; -} - -macro_rules! auto_into_raw_primitive { - ($type: ty) => { - impl IntoJavaRaw for $type { - type T = $type; - fn into_java_raw(self) -> Self::T { - self - } - } - } -} - -auto_into_raw_primitive!(jni::sys::jlong); -auto_into_raw_primitive!(jni::sys::jint); -auto_into_raw_primitive!(jni::sys::jshort); -auto_into_raw_primitive!(jni::sys::jbyte); -auto_into_raw_primitive!(jni::sys::jdouble); -auto_into_raw_primitive!(jni::sys::jfloat); -auto_into_raw_primitive!(jni::sys::jboolean); -auto_into_raw_primitive!(()); - -macro_rules! auto_into_raw_object { - ($lt: lifetime, $type: ty) => { - impl<'j> IntoJavaRaw for $type { - type T = jni::sys::jobject; - fn into_java_raw(self) -> Self::T { - self.as_raw() - } - } - }; -} - -auto_into_raw_object!('j, JObject<'j>); -auto_into_raw_object!('j, JString<'j>); -auto_into_raw_object!('j, JObjectArray<'j>); diff --git a/src/raw_java.rs b/src/raw_java.rs new file mode 100644 index 0000000..f0d13e3 --- /dev/null +++ b/src/raw_java.rs @@ -0,0 +1,63 @@ +use jni::objects::{JObject, JObjectArray, JString}; + + +/// Needed internally to perform some operations. +pub trait FromJavaRaw { + unsafe fn from_java_raw(raw: jni::sys::jobject) -> Self; +} + +macro_rules! auto_from_raw { + ($type: ty) => { + impl FromJavaRaw for $type { + #[inline] + unsafe fn from_java_raw(raw: jni::sys::jobject) -> Self { + Self::from_raw(raw) + } + } + }; +} + +auto_from_raw!(JObject<'_>); +auto_from_raw!(JString<'_>); +auto_from_raw!(JObjectArray<'_>); + +/// Intermediate trait used to guess the JNI return type. +/// Usually doesn't need to be manually implemented. +pub trait IntoJavaRaw { + type T; + fn into_java_raw(self) -> Self::T; +} + +macro_rules! auto_into_raw_primitive { + ($type: ty) => { + impl IntoJavaRaw for $type { + type T = $type; + fn into_java_raw(self) -> Self::T { + self + } + } + } +} + +auto_into_raw_primitive!(jni::sys::jlong); +auto_into_raw_primitive!(jni::sys::jint); +auto_into_raw_primitive!(jni::sys::jshort); +auto_into_raw_primitive!(jni::sys::jbyte); +auto_into_raw_primitive!(jni::sys::jdouble); +auto_into_raw_primitive!(jni::sys::jfloat); +auto_into_raw_primitive!(()); + +macro_rules! auto_into_raw_object { + ($lt: lifetime, $type: ty) => { + impl<'j> IntoJavaRaw for $type { + type T = jni::sys::jobject; + fn into_java_raw(self) -> Self::T { + self.as_raw() + } + } + }; +} + +auto_into_raw_object!('j, JObject<'j>); +auto_into_raw_object!('j, JString<'j>); +auto_into_raw_object!('j, JObjectArray<'j>); From 5e69eca9d6859bcfc7afabc5bca9b3848eeab70c Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 23 Sep 2024 17:59:31 +0200 Subject: [PATCH 06/24] feat: simplify IntoJava family basically it's all IntoJava, but there's another trait layer, IntoJavaObject, which downstream library users should implement. Every IntoJavaObject automatically implements IntoJava too --- macro/src/args.rs | 26 +++++++- macro/src/attrs.rs | 6 +- macro/src/ret.rs | 24 ++++---- macro/src/wrapper.rs | 137 +++++++++++++++++++------------------------ src/from_java.rs | 69 +++++++++++++++++----- src/into_java.rs | 68 ++++++++++----------- src/lib.rs | 4 +- src/raw_java.rs | 63 -------------------- 8 files changed, 187 insertions(+), 210 deletions(-) delete mode 100644 src/raw_java.rs diff --git a/macro/src/args.rs b/macro/src/args.rs index 870446f..8c7c1a0 100644 --- a/macro/src/args.rs +++ b/macro/src/args.rs @@ -1,5 +1,5 @@ use proc_macro2::{Span, TokenStream}; -use quote::TokenStreamExt; +use quote::{ToTokens, TokenStreamExt}; use syn::Ident; pub(crate) struct ArgumentOptions { @@ -109,7 +109,7 @@ impl ArgumentOptions { }, }; }); - incoming.append_all(quote::quote!( mut #pat: <#ty as jni_toolbox::FromJava<'local>>::T,)); + incoming.append_all(quote::quote!( #pat: <#ty as jni_toolbox::FromJava<'local>>::From,)); forwarding.append_all(quote::quote!( #new_pat,)); } @@ -121,3 +121,25 @@ struct SingleArgument { pat: syn::Ident, ty: Box, } + +#[allow(unused)] +fn bare_type(t: syn::Type) -> TokenStream { + match t { + syn::Type::Array(x) => bare_type(*x.elem), + syn::Type::BareFn(f) => f.to_token_stream(), + syn::Type::Group(x) => bare_type(*x.elem), + syn::Type::ImplTrait(t) => t.to_token_stream(), + syn::Type::Infer(x) => x.to_token_stream(), + syn::Type::Macro(x) => x.to_token_stream(), + syn::Type::Never(x) => x.to_token_stream(), + syn::Type::Paren(p) => bare_type(*p.elem), + syn::Type::Path(p) => p.to_token_stream(), + syn::Type::Ptr(x) => bare_type(*x.elem), + syn::Type::Reference(r) => bare_type(*r.elem), + syn::Type::Slice(s) => bare_type(*s.elem), + syn::Type::TraitObject(t) => t.to_token_stream(), + syn::Type::Tuple(x) => x.to_token_stream(), + syn::Type::Verbatim(x) => x.to_token_stream(), + _ => todo!(), + } +} diff --git a/macro/src/attrs.rs b/macro/src/attrs.rs index 3872693..12ac108 100644 --- a/macro/src/attrs.rs +++ b/macro/src/attrs.rs @@ -4,7 +4,6 @@ pub(crate) struct AttrsOptions { pub(crate) package: String, pub(crate) class: String, pub(crate) exception: Option, - pub(crate) return_pointer: bool, } impl AttrsOptions { @@ -14,7 +13,6 @@ impl AttrsOptions { let mut package = None; let mut class = None; let mut exception = None; - let mut return_pointer = false; for attr in attrs { match what_next { @@ -24,7 +22,7 @@ impl AttrsOptions { "package" => what_next = WhatNext::Package, "class" => what_next = WhatNext::Class, "exception" => what_next = WhatNext::Exception, - "ptr" => return_pointer = true, + "ptr" => {}, // accepted for backwards compatibility _ => return Err(syn::Error::new(Span::call_site(), "unexpected attribute on macro: {attr}")), } } @@ -53,7 +51,7 @@ impl AttrsOptions { let Some(package) = package else { return Err(syn::Error::new(Span::call_site(), "missing required attribute 'package'")) }; let Some(class) = class else { return Err(syn::Error::new(Span::call_site(), "missing required attribute 'class'")) }; - Ok(Self { package, class, exception, return_pointer }) + Ok(Self { package, class, exception }) } } diff --git a/macro/src/ret.rs b/macro/src/ret.rs index 254fcda..5934e30 100644 --- a/macro/src/ret.rs +++ b/macro/src/ret.rs @@ -6,13 +6,16 @@ use syn::{ReturnType, Type}; pub(crate) struct ReturnOptions { pub(crate) ty: Option>, pub(crate) result: bool, + pub(crate) pointer: bool, pub(crate) void: bool, } +const PRIMITIVE_TYPES: [&str; 7] = ["i8", "i16", "i32", "i64", "f32", "f64", "bool"]; + impl ReturnOptions { pub(crate) fn parse_signature(ret: &ReturnType) -> Result { match ret { - syn::ReturnType::Default => Ok(Self { ty: None, result: false, void: true }), + syn::ReturnType::Default => Ok(Self { ty: None, result: false, void: true, pointer: false }), syn::ReturnType::Type(_tok, ty) => match *ty.clone() { syn::Type::Path(path) => { let Some(last) = path.path.segments.last() else { @@ -26,14 +29,19 @@ impl ReturnOptions { syn::PathArguments::AngleBracketed(ref generics) => for generic in generics.args.iter() { match generic { syn::GenericArgument::Lifetime(_) => continue, - syn::GenericArgument::Type(ty) => return Ok(Self { ty: Some(Box::new(ty.clone())), result: true, void: is_void(ty) }), - _ => return Err(syn::Error::new(Span::call_site(), "unexpected type in Result")), + syn::GenericArgument::Type(ty) => { + // TODO checking by making ty a token stream and then string equals is not exactly great! + let pointer = !PRIMITIVE_TYPES.iter().any(|t| ty.to_token_stream().to_string() == *t); + return Ok(Self { ty: Some(Box::new(ty.clone())), result: true, void: is_void(ty), pointer }); + }, + _ => return Err(syn::Error::new(Span::call_site(), "unexpected type in Result")) } } } } - Ok(Self { ty: Some(Box::new(Type::Path(path.clone()))), result: false, void: false }) + let pointer = !PRIMITIVE_TYPES.iter().any(|t| last.ident == t); + Ok(Self { ty: Some(Box::new(Type::Path(path.clone()))), result: false, void: false, pointer }) }, _ => Err(syn::Error::new(Span::call_site(), "unsupported return type")), }, @@ -43,13 +51,7 @@ impl ReturnOptions { pub(crate) fn tokens(&self) -> TokenStream { match &self.ty { // TODO why do we need to invoke syn::Token! macro ??? None => ReturnType::Default.to_token_stream(), - Some(t) => { - if t.to_token_stream().to_string() == "bool" {// TODO: there DEFINITELY is a better way - quote::quote!( -> jni::sys::jboolean ) - } else { - quote::quote!( -> <#t as jni_toolbox::IntoJavaRaw>::T ) - } - } + Some(t) => quote::quote!( -> <#t as jni_toolbox::IntoJava<'local>>::Ret ) } } } diff --git a/macro/src/wrapper.rs b/macro/src/wrapper.rs index b774e33..50872c4 100644 --- a/macro/src/wrapper.rs +++ b/macro/src/wrapper.rs @@ -15,7 +15,7 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re let ret = ReturnOptions::parse_signature(&fn_item.sig.output)?; let return_expr = if ret.void { quote::quote!( () ) - } else if attrs.return_pointer { + } else if ret.pointer { quote::quote!( std::ptr::null_mut() ) } else { quote::quote!( 0 ) @@ -25,11 +25,6 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re let args = ArgumentOptions::parse_args(&fn_item, return_expr.clone())?; let return_type = ret.tokens(); - let return_type_import = if attrs.return_pointer { - syn::Ident::new("IntoJavaObject", Span::call_site()) - } else { - syn::Ident::new("IntoJavaPrimitive", Span::call_site()) - }; let name = fn_item.sig.ident.to_string(); let name_jni = name.replace("_", "_1"); @@ -38,100 +33,88 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re let incoming = args.incoming; // V----------------------------------V - let header = if attrs.return_pointer { - quote::quote! { - #[no_mangle] - #[allow(unused_mut)] - pub extern "system" fn #fn_name<'local>(#incoming) -> jni::sys::jobject - } - } else { - quote::quote! { - #[no_mangle] - #[allow(unused_mut)] - pub extern "system" fn #fn_name<'local>(#incoming) #return_type - } + let header = quote::quote! { + #[no_mangle] + #[allow(unused_unit)] + pub extern "system" fn #fn_name<'local>(#incoming) #return_type }; - // ^----------------------------------^ + let transforming = args.transforming; + let transformations = quote::quote! { + use jni_toolbox::{JniToolboxError, FromJava, IntoJava}; + #transforming + }; + let env_ident = args.env; let forwarding = args.forwarding; - let transforming = args.transforming; - let body = if ret.result { // wrap errors + let invocation = quote::quote! { + let mut env_copy = unsafe { #env_ident.unsafe_clone() }; + let result = #fn_name_inner(#forwarding); + }; + + + let error_handling = if ret.result { if let Some(exception) = attrs.exception { - // V----------------------------------V quote::quote! { - { - use jni_toolbox::#return_type_import; - use jni_toolbox::{JniToolboxError, FromJava}; - #transforming - match #fn_name_inner(#forwarding) { - Ok(ret) => ret, - Err(e) => match #env_ident.throw_new(#exception, format!("{e:?}")) { - Ok(_) => return #return_expr, - Err(e) => panic!("error throwing java exception: {e}"), - } + let ret = match result { + Ok(x) => x, + Err(e) => match env_copy.throw_new(#exception, format!("{e:?}")) { + Ok(_) => return #return_expr, + Err(e) => panic!("error throwing java exception: {e}"), } - } + }; } - // ^----------------------------------^ } else { - // V----------------------------------V quote::quote! { - { - use jni_toolbox::#return_type_import; - use jni_toolbox::{JniToolboxError, FromJava, IntoJavaRaw}; - // NOTE: this should be SAFE! the cloned env reference lives less than the actual one, we just lack a - // way to get it back from the called function and thus resort to unsafe cloning - let mut env_copy = unsafe { #env_ident.unsafe_clone() }; - #transforming - match #fn_name_inner(#forwarding) { - Err(e) => match env_copy.find_class(e.jclass()) { - Err(e) => panic!("error throwing Java exception -- failed resolving error class: {e}"), - Ok(class) => match env_copy.new_string(format!("{e:?}")) { - Err(e) => panic!("error throwing Java exception -- failed creating error string: {e}"), - Ok(msg) => match env_copy.new_object(class, "(Ljava/lang/String;)V", &[jni::objects::JValueGen::Object(&msg)]) { - Err(e) => panic!("error throwing Java exception -- failed creating object: {e}"), - Ok(obj) => match env_copy.throw(jni::objects::JThrowable::from(obj)) { - Err(e) => panic!("error throwing Java exception -- failed throwing: {e}"), - Ok(_) => return #return_expr, - }, + let ret = match result { + Ok(x) => x, + Err(e) => match env_copy.find_class(e.jclass()) { + Err(e) => panic!("error throwing Java exception -- failed resolving error class: {e}"), + Ok(class) => match env_copy.new_string(format!("{e:?}")) { + Err(e) => panic!("error throwing Java exception -- failed creating error string: {e}"), + Ok(msg) => match env_copy.new_object(class, "(Ljava/lang/String;)V", &[jni::objects::JValueGen::Object(&msg)]) { + Err(e) => panic!("error throwing Java exception -- failed creating object: {e}"), + Ok(obj) => match env_copy.throw(jni::objects::JThrowable::from(obj)) { + Err(e) => panic!("error throwing Java exception -- failed throwing: {e}"), + Ok(_) => return #return_expr, }, }, - } - Ok(ret) => match ret.into_java(&mut env_copy) { - Ok(fin) => return fin.into_java_raw(), - Err(e) => { - // TODO should we panic instead? - let _ = env_copy.throw_new("java/lang/RuntimeException", format!("{e:?}")); - return #return_expr; - } }, } - } + }; } } } else { - // V----------------------------------V - quote::quote! { - { - use jni_toolbox::#return_type_import; - use jni_toolbox::{JniToolboxError, FromJava, IntoJavaRaw}; - #transforming - match #fn_name_inner(#forwarding).into_java(&mut #env_ident) { - Ok(res) => return res.into_java_raw(), - Err(e) => { - // TODO should we panic instead? - let _ = #env_ident.throw_new("java/lang/RuntimeException", format!("{e:?}")); - return #return_expr; - }, - } + quote::quote!( let ret = result; ) + }; + + + let reverse_transformations = quote::quote! { + match ret.into_java(&mut env_copy) { + Ok(fin) => fin, + Err(e) => { + // TODO should we panic instead? + let _ = env_copy.throw_new("java/lang/RuntimeException", format!("{e:?}")); + #return_expr } } - // ^----------------------------------^ }; + let body = quote::quote! { + { + #transformations + + #invocation + + #error_handling + + #reverse_transformations + } + }; + + out.append_all(input); out.append_all(header); out.append_all(body); diff --git a/src/from_java.rs b/src/from_java.rs index e6af7fa..ff20c10 100644 --- a/src/from_java.rs +++ b/src/from_java.rs @@ -1,27 +1,27 @@ -use jni::objects::{JObject, JString}; +use jni::objects::{JObject, JObjectArray, JPrimitiveArray, JString, TypeArray}; /// Used in the generated code to have proper type bindings. You probably didn't want /// to call this directly. -pub fn from_java_static<'j, T: FromJava<'j>>(env: &mut jni::JNIEnv<'j>, val: T::T) -> Result { +pub fn from_java_static<'j, T: FromJava<'j>>(env: &mut jni::JNIEnv<'j>, val: T::From) -> Result { T::from_java(env, val) } /// Specifies how a Java type should be converted before being fed to Rust. pub trait FromJava<'j> : Sized { /// The JNI type representing the input. - type T : Sized; + type From : Sized; /// Attempts to convert this Java object into its Rust counterpart. - fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result; + fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::From) -> Result; } macro_rules! auto_from_java { ($t: ty, $j: ty) => { impl<'j> FromJava<'j> for $t { - type T = $j; + type From = $j; #[inline] - fn from_java(_: &mut jni::JNIEnv, value: Self::T) -> Result { + fn from_java(_: &mut jni::JNIEnv, value: Self::From) -> Result { Ok(value) } } @@ -35,38 +35,75 @@ auto_from_java!(i8, jni::sys::jbyte); auto_from_java!(f32, jni::sys::jfloat); auto_from_java!(f64, jni::sys::jdouble); auto_from_java!(JObject<'j>, JObject<'j>); +auto_from_java!(JString<'j>, JString<'j>); +auto_from_java!(JObjectArray<'j>, JObjectArray<'j>); -impl<'j> FromJava<'j> for bool { - type T = jni::sys::jboolean; +impl<'j, T: TypeArray> FromJava<'j> for JPrimitiveArray<'j, T> { + type From = JPrimitiveArray<'j, T>; #[inline] - fn from_java(_: &mut jni::JNIEnv, value: Self::T) -> Result { + fn from_java(_: &mut jni::JNIEnv, value: Self::From) -> Result { + Ok(value) + } +} + +impl<'j> FromJava<'j> for char { + type From = jni::sys::jchar; + + #[inline] + fn from_java(_: &mut jni::JNIEnv, value: Self::From) -> Result { + char::from_u32(value.into()).ok_or_else(|| jni::errors::Error::WrongJValueType("char", "invalid u16")) + } +} + +impl<'j> FromJava<'j> for bool { + type From = jni::sys::jboolean; + + #[inline] + fn from_java(_: &mut jni::JNIEnv, value: Self::From) -> Result { Ok(value == 1) } } impl<'j> FromJava<'j> for String { - type T = JString<'j>; + type From = JString<'j>; - fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result { + fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::From) -> Result { if value.is_null() { return Err(jni::errors::Error::NullPtr("string can't be null")) }; Ok(env.get_string(&value)?.into()) } } -impl<'j, T: FromJava<'j, T: std::convert::AsRef>>> FromJava<'j> for Option { - type T = T::T; +impl<'j, T> FromJava<'j> for Option +where + T: FromJava<'j, From: AsRef>>, +{ + type From = T::From; - fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result { + fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::From) -> Result { if value.as_ref().is_null() { return Ok(None) }; Ok(Some(T::from_java(env, value)?)) } } +impl<'j, T: FromJava<'j, From = JObject<'j>>> FromJava<'j> for Vec { + type From = JObjectArray<'j>; + + fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::From) -> Result { + let len = env.get_array_length(&value)?; + let mut out = Vec::new(); + for i in 0..len { + let el = env.get_object_array_element(&value, i)?; + out.push(T::from_java(env, el)?); + } + Ok(out) + } +} + #[cfg(feature = "uuid")] impl<'j> FromJava<'j> for uuid::Uuid { - type T = JObject<'j>; - fn from_java(env: &mut jni::JNIEnv<'j>, uuid: Self::T) -> Result { + type From = JObject<'j>; + fn from_java(env: &mut jni::JNIEnv<'j>, uuid: Self::From) -> Result { let lsb = u64::from_ne_bytes( env.call_method(&uuid, "getLeastSignificantBits", "()J", &[])? .j()? diff --git a/src/into_java.rs b/src/into_java.rs index abc5721..5860da6 100644 --- a/src/into_java.rs +++ b/src/into_java.rs @@ -1,22 +1,20 @@ -use jni::objects::{JObject, JObjectArray, JString}; - -use crate::raw_java::FromJavaRaw; +use jni::objects::JObject; /// Specifies how a Rust type should be converted into a Java primitive. -pub trait IntoJavaPrimitive<'j> { +pub trait IntoJava<'j> { /// The JNI type representing the output. - type T; + type Ret; /// Attempts to convert this Rust object into a Java primitive. - fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result; + fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result; } macro_rules! auto_into_java { ($t: ty, $j: ty) => { - impl<'j> IntoJavaPrimitive<'j> for $t { - type T = $j; + impl<'j> IntoJava<'j> for $t { + type Ret = $j; - fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result { + fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result { Ok(self) } } @@ -33,69 +31,71 @@ auto_into_java!(f32, jni::sys::jfloat); auto_into_java!(f64, jni::sys::jdouble); auto_into_java!((), ()); -impl<'j> IntoJavaPrimitive<'j> for bool { - type T = jni::sys::jboolean; +impl<'j> IntoJava<'j> for bool { + type Ret = jni::sys::jboolean; #[inline] - fn into_java(self, _: &mut jni::JNIEnv) -> Result { + fn into_java(self, _: &mut jni::JNIEnv) -> Result { Ok(if self { 1 } else { 0 }) } } +impl<'j, X: IntoJavaObject<'j>> IntoJava<'j> for X { + type Ret = jni::sys::jobject; + + fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { + Ok(self.into_java_object(env)?.as_raw()) + } +} + /// Specifies how a Rust type should be converted into a Java object. pub trait IntoJavaObject<'j> { - type T: std::convert::AsRef>; /// The Java class associated with this type. const CLASS: &'static str; /// Attempts to convert this Rust object into a Java object. - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result; + fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error>; } impl<'j> IntoJavaObject<'j> for &str { - type T = JString<'j>; const CLASS: &'static str = "java/lang/String"; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - env.new_string(self) + fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + Ok(env.new_string(self)?.into()) } } impl<'j> IntoJavaObject<'j> for String { - type T = JString<'j>; const CLASS: &'static str = "java/lang/String"; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - self.as_str().into_java(env) + fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + self.as_str().into_java_object(env) } } -impl<'j, E: IntoJavaObject<'j>> IntoJavaObject<'j> for Vec { - type T = JObjectArray<'j>; - const CLASS: &'static str = E::CLASS; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - let mut array = env.new_object_array(self.len() as i32, E::CLASS, JObject::null())?; +impl<'j, T: IntoJavaObject<'j>> IntoJavaObject<'j> for Vec { + const CLASS: &'static str = T::CLASS; // TODO shouldnt it be 'Object[]' ? + fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + let mut array = env.new_object_array(self.len() as i32, T::CLASS, JObject::null())?; for (n, el) in self.into_iter().enumerate() { - let el = el.into_java(env)?; + let el = el.into_java_object(env)?; env.set_object_array_element(&mut array, n as i32, &el)?; } - Ok(array) + Ok(array.into()) } } -impl<'j, E: std::convert::AsRef> + FromJavaRaw, T: IntoJavaObject<'j, T = E>> IntoJavaObject<'j> for Option { - type T = E; +impl<'j, T: IntoJavaObject<'j>> IntoJavaObject<'j> for Option { const CLASS: &'static str = T::CLASS; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { + fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { match self { - Some(x) => x.into_java(env), - None => Ok(unsafe { E::from_java_raw(std::ptr::null_mut()) }) // safe, that's what JObject::null does + Some(x) => x.into_java_object(env), + None => Ok(JObject::null()) } } } #[cfg(feature = "uuid")] impl<'j> IntoJavaObject<'j> for uuid::Uuid { - type T = jni::objects::JObject<'j>; const CLASS: &'static str = "java/util/UUID"; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { + fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { let class = env.find_class(Self::CLASS)?; let (msb, lsb) = self.as_u64_pair(); let msb = i64::from_ne_bytes(msb.to_ne_bytes()); diff --git a/src/lib.rs b/src/lib.rs index 065043e..ef25319 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,9 @@ pub mod into_java; pub mod from_java; -pub mod raw_java; pub use jni_toolbox_macro::jni; -pub use into_java::{IntoJavaObject, IntoJavaPrimitive}; +pub use into_java::{IntoJavaObject, IntoJava}; pub use from_java::{FromJava, from_java_static}; -pub use raw_java::IntoJavaRaw; /// An error that is meant to be used with jni-toolbox. diff --git a/src/raw_java.rs b/src/raw_java.rs deleted file mode 100644 index f0d13e3..0000000 --- a/src/raw_java.rs +++ /dev/null @@ -1,63 +0,0 @@ -use jni::objects::{JObject, JObjectArray, JString}; - - -/// Needed internally to perform some operations. -pub trait FromJavaRaw { - unsafe fn from_java_raw(raw: jni::sys::jobject) -> Self; -} - -macro_rules! auto_from_raw { - ($type: ty) => { - impl FromJavaRaw for $type { - #[inline] - unsafe fn from_java_raw(raw: jni::sys::jobject) -> Self { - Self::from_raw(raw) - } - } - }; -} - -auto_from_raw!(JObject<'_>); -auto_from_raw!(JString<'_>); -auto_from_raw!(JObjectArray<'_>); - -/// Intermediate trait used to guess the JNI return type. -/// Usually doesn't need to be manually implemented. -pub trait IntoJavaRaw { - type T; - fn into_java_raw(self) -> Self::T; -} - -macro_rules! auto_into_raw_primitive { - ($type: ty) => { - impl IntoJavaRaw for $type { - type T = $type; - fn into_java_raw(self) -> Self::T { - self - } - } - } -} - -auto_into_raw_primitive!(jni::sys::jlong); -auto_into_raw_primitive!(jni::sys::jint); -auto_into_raw_primitive!(jni::sys::jshort); -auto_into_raw_primitive!(jni::sys::jbyte); -auto_into_raw_primitive!(jni::sys::jdouble); -auto_into_raw_primitive!(jni::sys::jfloat); -auto_into_raw_primitive!(()); - -macro_rules! auto_into_raw_object { - ($lt: lifetime, $type: ty) => { - impl<'j> IntoJavaRaw for $type { - type T = jni::sys::jobject; - fn into_java_raw(self) -> Self::T { - self.as_raw() - } - } - }; -} - -auto_into_raw_object!('j, JObject<'j>); -auto_into_raw_object!('j, JString<'j>); -auto_into_raw_object!('j, JObjectArray<'j>); From 0580422abf35c2225777023caff6b667a954fb51 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 23 Sep 2024 18:00:31 +0200 Subject: [PATCH 07/24] chore: add uuid feature --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index dfaee43..efae4af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,7 @@ edition = "2021" jni-toolbox-macro = "0.1.3" jni = "0.21" uuid = { version = "1.10", optional = true } + +[features] +default = [] +uuid = ["dep:uuid"] From 8e53c475c2ef6c8154115b36f9181bbd0f9114d9 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 23 Sep 2024 18:02:27 +0200 Subject: [PATCH 08/24] docs: updated readme --- README.md | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a8f4c0f..6b782d6 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,6 @@ impl<'j> IntoJavaObject for MyClass { ``` ### Pointers -To return pointer type values, add the `ptr` attribute. - Note that, while it is possible to pass raw pointers to the JVM, it is not safe by default and must be done with extreme care. ### Exceptions @@ -67,7 +65,7 @@ The following function: ```rust #[jni(package = "mp.code", class = "Client", ptr)] fn connect(config: Config) -> Result { - tokio().block_on(Client::connect(config)) + super::tokio().block_on(Client::connect(config)) } ``` @@ -76,15 +74,18 @@ generates a matching expanded function invoking it:
Show macro expansion ```rust +#[doc = " Connect using the given credentials to the default server, and return a [Client] to interact with it."] +fn connect(config: Config) -> Result { + super::tokio().block_on(Client::connect(config)) +} #[no_mangle] -#[allow(unused_mut)] +#[allow(unused_unit)] pub extern "system" fn Java_mp_code_Client_connect<'local>( mut env: jni::JNIEnv<'local>, _class: jni::objects::JClass<'local>, - mut config: >::T, -) -> >::T { + config: >::From, +) -> >::Ret { use jni_toolbox::{FromJava, IntoJava, JniToolboxError}; - let mut env_copy = unsafe { env.unsafe_clone() }; let config_new = match jni_toolbox::from_java_static::(&mut env, config) { Ok(x) => x, Err(e) => { @@ -98,7 +99,10 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>( return std::ptr::null_mut(); } }; - match connect(config_new) { + let mut env_copy = unsafe { env.unsafe_clone() }; + let result = connect(config_new); + let ret = match result { + Ok(x) => x, Err(e) => match env_copy.find_class(e.jclass()) { Err(e) => { $crate::panicking::panic_fmt($crate::const_format_args!( @@ -135,19 +139,19 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>( }, }, }, - Ok(ret) => match ret.into_java(&mut env_copy) { - Ok(fin) => return fin, - Err(e) => { - let _ = env_copy.throw_new( - "java/lang/RuntimeException", - $crate::__export::must_use({ - let res = $crate::fmt::format($crate::__export::format_args!("{e:?}")); - res - }), - ); - return std::ptr::null_mut(); - } - }, + }; + match ret.into_java(&mut env_copy) { + Ok(fin) => fin, + Err(e) => { + let _ = env_copy.throw_new( + "java/lang/RuntimeException", + $crate::__export::must_use({ + let res = $crate::fmt::format($crate::__export::format_args!("{e:?}")); + res + }), + ); + std::ptr::null_mut() + } } } ``` From 10547d92b02b1af622706443cef835110efcdd0d Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 23 Sep 2024 18:08:43 +0200 Subject: [PATCH 09/24] chore: temp relative dep to use this from git TODO change it back before publishing --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index efae4af..16da19c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ version = "0.1.3" edition = "2021" [dependencies] -jni-toolbox-macro = "0.1.3" +#jni-toolbox-macro = "0.1.3" +jni-toolbox-macro = { path = "./macro" } jni = "0.21" uuid = { version = "1.10", optional = true } From a1f9e8dfc1b81c4e688c0a0165d2833bc3b0a6aa Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 01:58:10 +0200 Subject: [PATCH 10/24] test: add simple tests on java side --- .github/workflows/test.yml | 2 ++ .gitignore | 9 +++++++++ Cargo.lock | 13 +++++-------- Cargo.toml | 2 +- build.gradle | 17 +++++++++++++++++ src/test/Cargo.toml | 13 +++++++++++++ src/test/java/toolbox/Main.java | 25 +++++++++++++++++++++++++ src/test/test.rs | 11 +++++++++++ 8 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 build.gradle create mode 100644 src/test/Cargo.toml create mode 100644 src/test/java/toolbox/Main.java create mode 100644 src/test/test.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0bdd2c9..578305d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,3 +28,5 @@ jobs: toolchain: ${{ matrix.toolchain }} - run: cargo build --verbose - run: cargo test --verbose + - uses: gradle/actions/setup-gradle@v4 + - run: gradle test diff --git a/.gitignore b/.gitignore index ea8c4bf..09b57b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,10 @@ +# rust /target + +# gradle +/build +/bin +/.settings +/.gradle +.project +.classpath diff --git a/Cargo.lock b/Cargo.lock index d0da809..66660d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,7 +57,7 @@ name = "jni-toolbox" version = "0.1.3" dependencies = [ "jni", - "jni-toolbox-macro 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "jni-toolbox-macro", "uuid", ] @@ -71,14 +71,11 @@ dependencies = [ ] [[package]] -name = "jni-toolbox-macro" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9970cad895bb316f70956593710d675d27a480ddbb8099f7e313042463a16d9b" +name = "jni-toolbox-test" +version = "0.1.0" dependencies = [ - "proc-macro2", - "quote", - "syn", + "jni", + "jni-toolbox", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 16da19c..8d51fdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["macro"] +members = ["macro", "src/test"] [package] name = "jni-toolbox" diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..b7766ff --- /dev/null +++ b/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java-library' +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' +} + +repositories { + mavenCentral() +} + +test { + useJUnitPlatform() + systemProperty 'java.library.path','target/debug' +} diff --git a/src/test/Cargo.toml b/src/test/Cargo.toml new file mode 100644 index 0000000..2ab0e59 --- /dev/null +++ b/src/test/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "jni-toolbox-test" +description = "test binary for jni-toolbox" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] +path = "test.rs" + +[dependencies] +jni-toolbox = { path = "../.." } +jni = "0.21" diff --git a/src/test/java/toolbox/Main.java b/src/test/java/toolbox/Main.java new file mode 100644 index 0000000..01072e9 --- /dev/null +++ b/src/test/java/toolbox/Main.java @@ -0,0 +1,25 @@ +package toolbox; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class Main { + static { + System.loadLibrary("jni_toolbox_test"); + } + + static native int sum(int a, int b); + static native String concat(String a, String b); + + @Test + public void argumentsByValue() { + assertEquals(Main.sum(42, 13), 42 + 13); + } + + @Test + public void argumentsByReference() { + assertEquals(Main.concat("hello", "world"), "hello -- world"); + } + +} diff --git a/src/test/test.rs b/src/test/test.rs new file mode 100644 index 0000000..d018fb9 --- /dev/null +++ b/src/test/test.rs @@ -0,0 +1,11 @@ +use jni_toolbox::jni; + +#[jni(package = "toolbox", class = "Main")] +fn sum(a: i32, b: i32) -> i32 { + a + b +} + +#[jni(package = "toolbox", class = "Main")] +fn concat(a: String, b: String) -> String { + format!("{a} -- {b}") +} From b61165c1f585e3bded015453fd0bf48a62ae7b6a Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 01:59:30 +0200 Subject: [PATCH 11/24] ci: run tests on any branch --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 578305d..9163d21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,8 +2,6 @@ name: test on: push: - branches: - - dev env: CARGO_TERM_COLOR: always From 218a816c2df6d37ede3f9e4d6b46ace3005a9ede Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 02:01:37 +0200 Subject: [PATCH 12/24] ci: build test lib --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9163d21..99c4c96 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,4 +27,5 @@ jobs: - run: cargo build --verbose - run: cargo test --verbose - uses: gradle/actions/setup-gradle@v4 + - run: cargo build -p jni-toolbox-test - run: gradle test From 3d3bc8b6d5bfd9a44f9851018d997e526f25e0d2 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 02:29:31 +0200 Subject: [PATCH 13/24] test: vecs, options, nulls, also build with gradle --- .github/workflows/test.yml | 1 - build.gradle | 7 +++++++ src/test/java/toolbox/Main.java | 26 ++++++++++++++++++++++++++ src/test/test.rs | 10 ++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99c4c96..9163d21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,5 +27,4 @@ jobs: - run: cargo build --verbose - run: cargo test --verbose - uses: gradle/actions/setup-gradle@v4 - - run: cargo build -p jni-toolbox-test - run: gradle test diff --git a/build.gradle b/build.gradle index b7766ff..5cfd2ca 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,14 @@ repositories { mavenCentral() } +task cargoBuild(type: Exec) { + workingDir '.' + commandLine 'cargo', 'build', '-p', 'jni-toolbox-test' +} + test { + dependsOn cargoBuild + outputs.upToDateWhen { false } useJUnitPlatform() systemProperty 'java.library.path','target/debug' } diff --git a/src/test/java/toolbox/Main.java b/src/test/java/toolbox/Main.java index 01072e9..68365a7 100644 --- a/src/test/java/toolbox/Main.java +++ b/src/test/java/toolbox/Main.java @@ -2,6 +2,7 @@ package toolbox; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; public class Main { @@ -11,6 +12,8 @@ public class Main { static native int sum(int a, int b); static native String concat(String a, String b); + static native String[] to_vec(String a, String b, String c); + static native boolean maybe(String optional); @Test public void argumentsByValue() { @@ -21,5 +24,28 @@ public class Main { public void argumentsByReference() { assertEquals(Main.concat("hello", "world"), "hello -- world"); } + + @Test + public void checksForNull() { + // TODO maybe these should throw NullPtrException + assertThrows(RuntimeException.class, () -> Main.concat("a", null)); + assertThrows(RuntimeException.class, () -> Main.concat(null, "a")); + assertThrows(RuntimeException.class, () -> Main.concat(null, null)); + } + + @Test + public void returnVec() { + String[] actual = new String[]{"a", "b", "c"}; + String[] from_rust = Main.to_vec("a", "b", "c"); + for (int i = 0; i < 3; i++) { + assertEquals(actual[i], from_rust[i]); + } + } + + @Test + public void optional() { + assertEquals(Main.maybe(null), false); + assertEquals(Main.maybe("aa"), true); + } } diff --git a/src/test/test.rs b/src/test/test.rs index d018fb9..4511549 100644 --- a/src/test/test.rs +++ b/src/test/test.rs @@ -9,3 +9,13 @@ fn sum(a: i32, b: i32) -> i32 { fn concat(a: String, b: String) -> String { format!("{a} -- {b}") } + +#[jni(package = "toolbox", class = "Main")] +fn to_vec(a: String, b: String, c: String) -> Vec { + vec![a, b, c] +} + +#[jni(package = "toolbox", class = "Main")] +fn maybe(idk: Option) -> bool { + idk.is_some() +} From ef9237b74ad49fc1fcd7480c2ed6d9c026b54578 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 02:51:59 +0200 Subject: [PATCH 14/24] docs: simplify a bit example expansion remove recursive macro expansions basically --- README.md | 57 +++++++++---------------------------------------------- 1 file changed, 9 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 6b782d6..c33dc90 100644 --- a/README.md +++ b/README.md @@ -71,13 +71,11 @@ fn connect(config: Config) -> Result { generates a matching expanded function invoking it: -
Show macro expansion - ```rust -#[doc = " Connect using the given credentials to the default server, and return a [Client] to interact with it."] fn connect(config: Config) -> Result { super::tokio().block_on(Client::connect(config)) } + #[no_mangle] #[allow(unused_unit)] pub extern "system" fn Java_mp_code_Client_connect<'local>( @@ -89,13 +87,7 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>( let config_new = match jni_toolbox::from_java_static::(&mut env, config) { Ok(x) => x, Err(e) => { - let _ = env.throw_new( - "java/lang/RuntimeException", - $crate::__export::must_use({ - let res = $crate::fmt::format($crate::__export::format_args!("{e:?}")); - res - }), - ); + let _ = env.throw_new("java/lang/RuntimeException", format!("{e:?}")); return std::ptr::null_mut(); } }; @@ -104,36 +96,13 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>( let ret = match result { Ok(x) => x, Err(e) => match env_copy.find_class(e.jclass()) { - Err(e) => { - $crate::panicking::panic_fmt($crate::const_format_args!( - "error throwing Java exception -- failed resolving error class: {e}" - )); - } - Ok(class) => match env_copy.new_string($crate::__export::must_use({ - let res = $crate::fmt::format($crate::__export::format_args!("{e:?}")); - res - })) { - Err(e) => { - $crate::panicking::panic_fmt($crate::const_format_args!( - "error throwing Java exception -- failed creating error string: {e}" - )); - } - Ok(msg) => match env_copy.new_object( - class, - "(Ljava/lang/String;)V", - &[jni::objects::JValueGen::Object(&msg)], - ) { - Err(e) => { - $crate::panicking::panic_fmt($crate::const_format_args!( - "error throwing Java exception -- failed creating object: {e}" - )); - } + Err(e) => panic!("error throwing Java exception -- failed resolving error class: {e}"), + Ok(class) => match env_copy.new_string(format!("{e:?}")) { + Err(e) => panic!("error throwing Java exception -- failed creating error string: {e}"), + Ok(msg) => match env_copy.new_object(class, "(Ljava/lang/String;)V", &[jni::objects::JValueGen::Object(&msg)]) { + Err(e) => panic!("error throwing Java exception -- failed creating object: {e}")); Ok(obj) => match env_copy.throw(jni::objects::JThrowable::from(obj)) { - Err(e) => { - $crate::panicking::panic_fmt($crate::const_format_args!( - "error throwing Java exception -- failed throwing: {e}" - )); - } + Err(e) => panic!("error throwing Java exception -- failed throwing: {e}"), Ok(_) => return std::ptr::null_mut(), }, }, @@ -143,20 +112,12 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>( match ret.into_java(&mut env_copy) { Ok(fin) => fin, Err(e) => { - let _ = env_copy.throw_new( - "java/lang/RuntimeException", - $crate::__export::must_use({ - let res = $crate::fmt::format($crate::__export::format_args!("{e:?}")); - res - }), - ); + let _ = env_copy.throw_new("java/lang/RuntimeException", format!("{e:?}")); std::ptr::null_mut() } } } ``` -
- ## Status This crate is early and intended mostly to maintain [`codemp`](https://github.com/hexedtech/codemp)'s Java bindings, so things not used From a7c0b6881edee423ac1983318a2b44e123dc0d5b Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 03:14:45 +0200 Subject: [PATCH 15/24] fix: don't unsafely clone env was safe but better no unsafe at all --- macro/src/wrapper.rs | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/macro/src/wrapper.rs b/macro/src/wrapper.rs index 50872c4..543d755 100644 --- a/macro/src/wrapper.rs +++ b/macro/src/wrapper.rs @@ -1,12 +1,9 @@ use proc_macro2::{Span, TokenStream}; -use quote::TokenStreamExt; use syn::Item; use crate::{args::ArgumentOptions, attrs::AttrsOptions, ret::ReturnOptions}; pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Result { - let mut out = TokenStream::new(); - let Item::Fn(fn_item) = syn::parse2(input.clone())? else { return Err(syn::Error::new(Span::call_site(), "#[jni] is only supported on functions")); }; @@ -47,10 +44,9 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re }; - let env_ident = args.env; + let env_iden = args.env; let forwarding = args.forwarding; let invocation = quote::quote! { - let mut env_copy = unsafe { #env_ident.unsafe_clone() }; let result = #fn_name_inner(#forwarding); }; @@ -60,7 +56,7 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re quote::quote! { let ret = match result { Ok(x) => x, - Err(e) => match env_copy.throw_new(#exception, format!("{e:?}")) { + Err(e) => match #env_iden.throw_new(#exception, format!("{e:?}")) { Ok(_) => return #return_expr, Err(e) => panic!("error throwing java exception: {e}"), } @@ -70,13 +66,13 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re quote::quote! { let ret = match result { Ok(x) => x, - Err(e) => match env_copy.find_class(e.jclass()) { + Err(e) => match #env_iden.find_class(e.jclass()) { Err(e) => panic!("error throwing Java exception -- failed resolving error class: {e}"), - Ok(class) => match env_copy.new_string(format!("{e:?}")) { + Ok(class) => match #env_iden.new_string(format!("{e:?}")) { Err(e) => panic!("error throwing Java exception -- failed creating error string: {e}"), - Ok(msg) => match env_copy.new_object(class, "(Ljava/lang/String;)V", &[jni::objects::JValueGen::Object(&msg)]) { + Ok(msg) => match #env_iden.new_object(class, "(Ljava/lang/String;)V", &[jni::objects::JValueGen::Object(&msg)]) { Err(e) => panic!("error throwing Java exception -- failed creating object: {e}"), - Ok(obj) => match env_copy.throw(jni::objects::JThrowable::from(obj)) { + Ok(obj) => match #env_iden.throw(jni::objects::JThrowable::from(obj)) { Err(e) => panic!("error throwing Java exception -- failed throwing: {e}"), Ok(_) => return #return_expr, }, @@ -92,18 +88,28 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re let reverse_transformations = quote::quote! { - match ret.into_java(&mut env_copy) { + match ret.into_java(&mut #env_iden) { Ok(fin) => fin, Err(e) => { // TODO should we panic instead? - let _ = env_copy.throw_new("java/lang/RuntimeException", format!("{e:?}")); + let _ = #env_iden.throw_new("java/lang/RuntimeException", format!("{e:?}")); #return_expr } } }; - let body = quote::quote! { - { + let inline_macro = if attrs.inline { + quote::quote!(#[inline]) + } else { + quote::quote!() + }; + + Ok(quote::quote! { + #inline_macro + #input + + #header { + #transformations #invocation @@ -111,12 +117,7 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re #error_handling #reverse_transformations + } - }; - - - out.append_all(input); - out.append_all(header); - out.append_all(body); - Ok(out) + }) } From d52677eed8311302b20d399a023533324e130c5e Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 03:15:05 +0200 Subject: [PATCH 16/24] feat: option to force inline inner fn --- macro/src/attrs.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/macro/src/attrs.rs b/macro/src/attrs.rs index 12ac108..5cd99ff 100644 --- a/macro/src/attrs.rs +++ b/macro/src/attrs.rs @@ -4,6 +4,7 @@ pub(crate) struct AttrsOptions { pub(crate) package: String, pub(crate) class: String, pub(crate) exception: Option, + pub(crate) inline: bool, } impl AttrsOptions { @@ -13,6 +14,7 @@ impl AttrsOptions { let mut package = None; let mut class = None; let mut exception = None; + let mut inline = false; for attr in attrs { match what_next { @@ -23,6 +25,7 @@ impl AttrsOptions { "class" => what_next = WhatNext::Class, "exception" => what_next = WhatNext::Exception, "ptr" => {}, // accepted for backwards compatibility + "inline" => inline = true, _ => return Err(syn::Error::new(Span::call_site(), "unexpected attribute on macro: {attr}")), } } @@ -51,7 +54,7 @@ impl AttrsOptions { let Some(package) = package else { return Err(syn::Error::new(Span::call_site(), "missing required attribute 'package'")) }; let Some(class) = class else { return Err(syn::Error::new(Span::call_site(), "missing required attribute 'class'")) }; - Ok(Self { package, class, exception }) + Ok(Self { package, class, exception, inline }) } } From f67b1a7b3ff15bc3af3f9c56ba56882c2ff5df7d Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 03:15:19 +0200 Subject: [PATCH 17/24] fix: passing &mut env doesnt break jni fn --- macro/src/args.rs | 72 +++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 46 deletions(-) diff --git a/macro/src/args.rs b/macro/src/args.rs index 8c7c1a0..bd8d307 100644 --- a/macro/src/args.rs +++ b/macro/src/args.rs @@ -1,5 +1,5 @@ use proc_macro2::{Span, TokenStream}; -use quote::{ToTokens, TokenStreamExt}; +use quote::TokenStreamExt; use syn::Ident; pub(crate) struct ArgumentOptions { @@ -22,31 +22,33 @@ fn unpack_pat(pat: syn::Pat) -> Result { } } -fn type_equals(ty: Box, search: impl AsRef) -> bool { +fn bare_type(ty: Box) -> Option { match *ty { - syn::Type::Array(_) => false, - syn::Type::BareFn(_) => false, - syn::Type::ImplTrait(_) => false, - syn::Type::Infer(_) => false, - syn::Type::Macro(_) => false, - syn::Type::Never(_) => false, - syn::Type::Ptr(_) => false, - syn::Type::Slice(_) => false, - syn::Type::TraitObject(_) => false, - syn::Type::Tuple(_) => false, - syn::Type::Verbatim(_) => false, - syn::Type::Group(g) => type_equals(g.elem, search), - syn::Type::Paren(p) => type_equals(p.elem, search), - syn::Type::Reference(r) => type_equals(r.elem, search), - syn::Type::Path(ty) => { - ty.path.segments - .last() - .map_or(false, |e| e.ident == search.as_ref()) - }, - _ => false, + syn::Type::Array(a) => bare_type(a.elem), + syn::Type::BareFn(_) => None, + syn::Type::ImplTrait(_) => None, + syn::Type::Infer(_) => None, + syn::Type::Macro(_) => None, + syn::Type::Never(_) => None, + syn::Type::TraitObject(_) => None, + syn::Type::Verbatim(_) => None, + syn::Type::Ptr(p) => bare_type(p.elem), + syn::Type::Slice(s) => bare_type(s.elem), + syn::Type::Tuple(t) => bare_type(Box::new(t.elems.first()?.clone())), // TODO + syn::Type::Group(g) => bare_type(g.elem), + syn::Type::Paren(p) => bare_type(p.elem), + syn::Type::Reference(r) => bare_type(r.elem), + syn::Type::Path(ty) => Some(ty), + _ => todo!(), } } +fn type_equals(ty: Box, search: impl AsRef) -> bool { + let Some(ty) = bare_type(ty) else { return false }; + let Some(last) = ty.path.segments.last() else { return false }; + last.ident == search.as_ref() +} + impl ArgumentOptions { pub(crate) fn parse_args(fn_item: &syn::ItemFn, ret_expr: TokenStream) -> Result { let mut arguments = Vec::new(); @@ -83,9 +85,9 @@ impl ArgumentOptions { if pass_env { if let Some(arg) = args_iter.next() { let pat = arg.pat; - let ty = arg.ty; + let ty = bare_type(arg.ty); incoming.append_all(quote::quote!( mut #pat: #ty,)); - forwarding.append_all(quote::quote!( #pat,)); + forwarding.append_all(quote::quote!( &mut #pat,)); } } else { incoming.append_all(quote::quote!( mut #env: jni::JNIEnv<'local>,)); @@ -121,25 +123,3 @@ struct SingleArgument { pat: syn::Ident, ty: Box, } - -#[allow(unused)] -fn bare_type(t: syn::Type) -> TokenStream { - match t { - syn::Type::Array(x) => bare_type(*x.elem), - syn::Type::BareFn(f) => f.to_token_stream(), - syn::Type::Group(x) => bare_type(*x.elem), - syn::Type::ImplTrait(t) => t.to_token_stream(), - syn::Type::Infer(x) => x.to_token_stream(), - syn::Type::Macro(x) => x.to_token_stream(), - syn::Type::Never(x) => x.to_token_stream(), - syn::Type::Paren(p) => bare_type(*p.elem), - syn::Type::Path(p) => p.to_token_stream(), - syn::Type::Ptr(x) => bare_type(*x.elem), - syn::Type::Reference(r) => bare_type(*r.elem), - syn::Type::Slice(s) => bare_type(*s.elem), - syn::Type::TraitObject(t) => t.to_token_stream(), - syn::Type::Tuple(x) => x.to_token_stream(), - syn::Type::Verbatim(x) => x.to_token_stream(), - _ => todo!(), - } -} From 1389f71c97133a72a56ba78be0f23a32bf721445 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 03:26:24 +0200 Subject: [PATCH 18/24] feat: auto into java object for JNI types --- src/into_java.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/into_java.rs b/src/into_java.rs index 5860da6..9b7c42a 100644 --- a/src/into_java.rs +++ b/src/into_java.rs @@ -56,6 +56,36 @@ pub trait IntoJavaObject<'j> { fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error>; } +impl<'j> IntoJavaObject<'j> for JObject<'j> { + const CLASS: &'static str = "java/lang/Object"; + fn into_java_object(self, _: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + Ok(self) + } +} + +macro_rules! auto_into_java_object { + ($t:ty, $cls:literal) => { + impl<'j> IntoJavaObject<'j> for $t { + const CLASS: &'static str = $cls; + fn into_java_object(self, _: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + Ok(self.into()) + } + } + }; +} + +auto_into_java_object!(jni::objects::JString<'j>, "java/lang/String"); +//auto_into_java_object!(jni::objects::JObjectArray<'j>, "java/lang/Object[]"); +//auto_into_java_object!(jni::objects::JIntArray<'j>, "java/lang/Integer[]"); +//auto_into_java_object!(jni::objects::JLongArray<'j>, "java/lang/Long[]"); +//auto_into_java_object!(jni::objects::JShortArray<'j>, "java/lang/Short[]"); +//auto_into_java_object!(jni::objects::JByteArray<'j>, "java/lang/Byte[]"); +//auto_into_java_object!(jni::objects::JCharArray<'j>, "java/lang/Char[]"); +//auto_into_java_object!(jni::objects::JFloatArray<'j>, "java/lang/Float[]"); +//auto_into_java_object!(jni::objects::JDoubleArray<'j>, "java/lang/Double[]"); +//auto_into_java_object!(jni::objects::JBooleanArray<'j>, "java/lang/Boolean[]"); + + impl<'j> IntoJavaObject<'j> for &str { const CLASS: &'static str = "java/lang/String"; fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { From a3918afaaf8569ef4e58053c3bae1d8fb11e6b66 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 03:26:37 +0200 Subject: [PATCH 19/24] test: passing env and JString --- src/test/java/toolbox/Main.java | 6 ++++++ src/test/test.rs | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/test/java/toolbox/Main.java b/src/test/java/toolbox/Main.java index 68365a7..c30dcbf 100644 --- a/src/test/java/toolbox/Main.java +++ b/src/test/java/toolbox/Main.java @@ -14,6 +14,7 @@ public class Main { static native String concat(String a, String b); static native String[] to_vec(String a, String b, String c); static native boolean maybe(String optional); + static native String raw(); @Test public void argumentsByValue() { @@ -47,5 +48,10 @@ public class Main { assertEquals(Main.maybe(null), false); assertEquals(Main.maybe("aa"), true); } + + @Test + public void passEnv() { + assertEquals(Main.raw(), "hello world!"); + } } diff --git a/src/test/test.rs b/src/test/test.rs index 4511549..ab10f52 100644 --- a/src/test/test.rs +++ b/src/test/test.rs @@ -19,3 +19,8 @@ fn to_vec(a: String, b: String, c: String) -> Vec { fn maybe(idk: Option) -> bool { idk.is_some() } + +#[jni(package = "toolbox", class = "Main")] +fn raw<'local>(env: &mut jni::JNIEnv<'local>) -> Result, jni::errors::Error> { + env.new_string("hello world!") +} From a5befdc4811e3e8a6d8a98fc4d593bebc7afe143 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 04:44:09 +0200 Subject: [PATCH 20/24] docs: doesnt do the unsafe clone anymore --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c33dc90..586b783 100644 --- a/README.md +++ b/README.md @@ -91,17 +91,16 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>( return std::ptr::null_mut(); } }; - let mut env_copy = unsafe { env.unsafe_clone() }; let result = connect(config_new); let ret = match result { Ok(x) => x, - Err(e) => match env_copy.find_class(e.jclass()) { + Err(e) => match env.find_class(e.jclass()) { Err(e) => panic!("error throwing Java exception -- failed resolving error class: {e}"), - Ok(class) => match env_copy.new_string(format!("{e:?}")) { + Ok(class) => match env.new_string(format!("{e:?}")) { Err(e) => panic!("error throwing Java exception -- failed creating error string: {e}"), - Ok(msg) => match env_copy.new_object(class, "(Ljava/lang/String;)V", &[jni::objects::JValueGen::Object(&msg)]) { + Ok(msg) => match env.new_object(class, "(Ljava/lang/String;)V", &[jni::objects::JValueGen::Object(&msg)]) { Err(e) => panic!("error throwing Java exception -- failed creating object: {e}")); - Ok(obj) => match env_copy.throw(jni::objects::JThrowable::from(obj)) { + Ok(obj) => match env.throw(jni::objects::JThrowable::from(obj)) { Err(e) => panic!("error throwing Java exception -- failed throwing: {e}"), Ok(_) => return std::ptr::null_mut(), }, @@ -109,10 +108,10 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>( }, }, }; - match ret.into_java(&mut env_copy) { + match ret.into_java(&mut env) { Ok(fin) => fin, Err(e) => { - let _ = env_copy.throw_new("java/lang/RuntimeException", format!("{e:?}")); + let _ = env.throw_new("java/lang/RuntimeException", format!("{e:?}")); std::ptr::null_mut() } } From 358586c51369f82bbf450fd37bb30e3ad3d5226e Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 04:46:02 +0200 Subject: [PATCH 21/24] fix: inline more --- src/into_java.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/into_java.rs b/src/into_java.rs index 9b7c42a..bff8909 100644 --- a/src/into_java.rs +++ b/src/into_java.rs @@ -14,6 +14,7 @@ macro_rules! auto_into_java { impl<'j> IntoJava<'j> for $t { type Ret = $j; + #[inline] fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result { Ok(self) } @@ -43,6 +44,7 @@ impl<'j> IntoJava<'j> for bool { impl<'j, X: IntoJavaObject<'j>> IntoJava<'j> for X { type Ret = jni::sys::jobject; + #[inline] fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { Ok(self.into_java_object(env)?.as_raw()) } @@ -58,6 +60,7 @@ pub trait IntoJavaObject<'j> { impl<'j> IntoJavaObject<'j> for JObject<'j> { const CLASS: &'static str = "java/lang/Object"; + #[inline] fn into_java_object(self, _: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { Ok(self) } @@ -67,6 +70,7 @@ macro_rules! auto_into_java_object { ($t:ty, $cls:literal) => { impl<'j> IntoJavaObject<'j> for $t { const CLASS: &'static str = $cls; + #[inline] fn into_java_object(self, _: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { Ok(self.into()) } From 47bb45a3875273521c829aab89aead820d9d18c8 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 05:02:11 +0200 Subject: [PATCH 22/24] test: nullptr class, nullable returns --- src/test/java/toolbox/Main.java | 14 +++++++++++--- src/test/test.rs | 9 +++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/test/java/toolbox/Main.java b/src/test/java/toolbox/Main.java index c30dcbf..f74f56b 100644 --- a/src/test/java/toolbox/Main.java +++ b/src/test/java/toolbox/Main.java @@ -2,6 +2,7 @@ package toolbox; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -14,6 +15,7 @@ public class Main { static native String concat(String a, String b); static native String[] to_vec(String a, String b, String c); static native boolean maybe(String optional); + static native String optional(boolean present); static native String raw(); @Test @@ -29,9 +31,9 @@ public class Main { @Test public void checksForNull() { // TODO maybe these should throw NullPtrException - assertThrows(RuntimeException.class, () -> Main.concat("a", null)); - assertThrows(RuntimeException.class, () -> Main.concat(null, "a")); - assertThrows(RuntimeException.class, () -> Main.concat(null, null)); + assertThrows(NullPointerException.class, () -> Main.concat("a", null)); + assertThrows(NullPointerException.class, () -> Main.concat(null, "a")); + assertThrows(NullPointerException.class, () -> Main.concat(null, null)); } @Test @@ -53,5 +55,11 @@ public class Main { public void passEnv() { assertEquals(Main.raw(), "hello world!"); } + + @Test + public void nullableReturn() { + assertNull(Main.optional(false)); + assertEquals(Main.optional(true), "hello world!"); + } } diff --git a/src/test/test.rs b/src/test/test.rs index ab10f52..4780982 100644 --- a/src/test/test.rs +++ b/src/test/test.rs @@ -20,6 +20,15 @@ fn maybe(idk: Option) -> bool { idk.is_some() } +#[jni(package = "toolbox", class = "Main")] +fn optional(present: bool) -> Option { + if present { + Some("hello world!".into()) + } else { + None + } +} + #[jni(package = "toolbox", class = "Main")] fn raw<'local>(env: &mut jni::JNIEnv<'local>) -> Result, jni::errors::Error> { env.new_string("hello world!") From 4d2debf789541bfee48e0e54635aa62eab71cb74 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 05:02:23 +0200 Subject: [PATCH 23/24] fix: jni::errors::Error::NullPtr throws NullPtr --- macro/src/args.rs | 2 +- macro/src/wrapper.rs | 2 +- src/lib.rs | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/macro/src/args.rs b/macro/src/args.rs index bd8d307..0719ec5 100644 --- a/macro/src/args.rs +++ b/macro/src/args.rs @@ -106,7 +106,7 @@ impl ArgumentOptions { Ok(x) => x, Err(e) => { // TODO should we panic here instead? - let _ = #env.throw_new("java/lang/RuntimeException", format!("{e:?}")); + let _ = #env.throw_new(e.jclass(), format!("{e:?}")); return #ret_expr; }, }; diff --git a/macro/src/wrapper.rs b/macro/src/wrapper.rs index 543d755..1be51cb 100644 --- a/macro/src/wrapper.rs +++ b/macro/src/wrapper.rs @@ -92,7 +92,7 @@ pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Re Ok(fin) => fin, Err(e) => { // TODO should we panic instead? - let _ = #env_iden.throw_new("java/lang/RuntimeException", format!("{e:?}")); + let _ = #env_iden.throw_new(e.jclass(), format!("{e:?}")); #return_expr } } diff --git a/src/lib.rs b/src/lib.rs index ef25319..73d966a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,12 +14,40 @@ pub trait JniToolboxError: std::error::Error { impl JniToolboxError for jni::errors::Error { fn jclass(&self) -> String { - "java/lang/RuntimeException".to_string() + match self { + jni::errors::Error::NullPtr(_) => "java/lang/NullPointerException", + _ => "java/lang/RuntimeException", + // jni::errors::Error::WrongJValueType(_, _) => todo!(), + // jni::errors::Error::InvalidCtorReturn => todo!(), + // jni::errors::Error::InvalidArgList(_) => todo!(), + // jni::errors::Error::MethodNotFound { name, sig } => todo!(), + // jni::errors::Error::FieldNotFound { name, sig } => todo!(), + // jni::errors::Error::JavaException => todo!(), + // jni::errors::Error::JNIEnvMethodNotFound(_) => todo!(), + // jni::errors::Error::NullDeref(_) => todo!(), + // jni::errors::Error::TryLock => todo!(), + // jni::errors::Error::JavaVMMethodNotFound(_) => todo!(), + // jni::errors::Error::FieldAlreadySet(_) => todo!(), + // jni::errors::Error::ThrowFailed(_) => todo!(), + // jni::errors::Error::ParseFailed(_, _) => todo!(), + // jni::errors::Error::JniCall(_) => todo!(), + } + .to_string() } } impl JniToolboxError for jni::errors::JniError { fn jclass(&self) -> String { - "java/lang/RuntimeException".to_string() + match self { + _ => "java/lang/RuntimeException", + // jni::errors::JniError::Unknown => todo!(), + // jni::errors::JniError::ThreadDetached => todo!(), + // jni::errors::JniError::WrongVersion => todo!(), + // jni::errors::JniError::NoMemory => todo!(), + // jni::errors::JniError::AlreadyCreated => todo!(), + // jni::errors::JniError::InvalidArguments => todo!(), + // jni::errors::JniError::Other(_) => todo!(), + } + .to_string() } } From 9160772fd43d8104c1ab3e5fe68abbc365462167 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 24 Sep 2024 05:03:42 +0200 Subject: [PATCH 24/24] docs: put .jclass() in example expansion --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 586b783..594b70a 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>( let config_new = match jni_toolbox::from_java_static::(&mut env, config) { Ok(x) => x, Err(e) => { - let _ = env.throw_new("java/lang/RuntimeException", format!("{e:?}")); + let _ = env.throw_new(e.jclass(), format!("{e:?}")); return std::ptr::null_mut(); } }; @@ -111,7 +111,7 @@ pub extern "system" fn Java_mp_code_Client_connect<'local>( match ret.into_java(&mut env) { Ok(fin) => fin, Err(e) => { - let _ = env.throw_new("java/lang/RuntimeException", format!("{e:?}")); + let _ = env.throw_new(e.jclass(), format!("{e:?}")); std::ptr::null_mut() } }