diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0bdd2c9..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 @@ -28,3 +26,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 dfaee43..8d51fdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["macro"] +members = ["macro", "src/test"] [package] name = "jni-toolbox" @@ -13,6 +13,11 @@ 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 } + +[features] +default = [] +uuid = ["dep:uuid"] diff --git a/README.md b/README.md index 0a7ffbf..594b70a 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,14 @@ impl<'j> IntoJava for MyClass { } ``` -### pointers -to return pointer type values, add the `ptr` attribute +### Pointers +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. -note that, while 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,113 +50,75 @@ 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 { - tokio().block_on(Client::connect(config)) + super::tokio().block_on(Client::connect(config)) } ``` -gets turned into these two functions: - -
show macro expansion +generates a matching expanded function invoking it: ```rust fn connect(config: Config) -> Result { - tokio().block_on(Client::connect(config)) + 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) => { - 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(e.jclass(), format!("{e:?}")); return std::ptr::null_mut(); } }; - match connect(config_new) { - 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}" - )); - } - 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}" - )); - } + let result = connect(config_new); + let ret = match result { + Ok(x) => x, + Err(e) => match env.find_class(e.jclass()) { + Err(e) => panic!("error throwing Java exception -- failed resolving error class: {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.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.throw(jni::objects::JThrowable::from(obj)) { + Err(e) => panic!("error throwing Java exception -- failed throwing: {e}"), Ok(_) => return std::ptr::null_mut(), }, }, }, }, - 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) { + Ok(fin) => fin, + Err(e) => { + let _ = env.throw_new(e.jclass(), format!("{e:?}")); + std::ptr::null_mut() + } } } ``` -
- - -## 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/build.gradle b/build.gradle new file mode 100644 index 0000000..5cfd2ca --- /dev/null +++ b/build.gradle @@ -0,0 +1,24 @@ +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() +} + +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/macro/src/args.rs b/macro/src/args.rs index 870446f..0719ec5 100644 --- a/macro/src/args.rs +++ b/macro/src/args.rs @@ -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>,)); @@ -104,12 +106,12 @@ 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; }, }; }); - 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,)); } diff --git a/macro/src/attrs.rs b/macro/src/attrs.rs index 3872693..5cd99ff 100644 --- a/macro/src/attrs.rs +++ b/macro/src/attrs.rs @@ -4,7 +4,7 @@ pub(crate) struct AttrsOptions { pub(crate) package: String, pub(crate) class: String, pub(crate) exception: Option, - pub(crate) return_pointer: bool, + pub(crate) inline: bool, } impl AttrsOptions { @@ -14,7 +14,7 @@ impl AttrsOptions { let mut package = None; let mut class = None; let mut exception = None; - let mut return_pointer = false; + let mut inline = false; for attr in attrs { match what_next { @@ -24,7 +24,8 @@ 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 + "inline" => inline = true, _ => return Err(syn::Error::new(Span::call_site(), "unexpected attribute on macro: {attr}")), } } @@ -53,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, return_pointer }) + Ok(Self { package, class, exception, inline }) } } 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/macro/src/ret.rs b/macro/src/ret.rs index f642fe2..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")), }, @@ -42,8 +50,8 @@ 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) => quote::quote!( -> <#t as jni_toolbox::IntoJava<'local>>::Ret ) } } } diff --git a/macro/src/wrapper.rs b/macro/src/wrapper.rs index 5bdaa89..1be51cb 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")); }; @@ -15,7 +12,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 ) @@ -24,99 +21,103 @@ 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 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)] + #[allow(unused_unit)] pub extern "system" fn #fn_name<'local>(#incoming) #return_type }; - // ^----------------------------------^ - let env_ident = args.env; - let forwarding = args.forwarding; + let transforming = args.transforming; - let body = if ret.result { // wrap errors + let transformations = quote::quote! { + use jni_toolbox::{JniToolboxError, FromJava, IntoJava}; + #transforming + }; + + + let env_iden = args.env; + let forwarding = args.forwarding; + let invocation = quote::quote! { + 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::{JniToolboxError, FromJava, IntoJava}; - #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_iden.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::{JniToolboxError, FromJava, IntoJava}; - // 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_iden.find_class(e.jclass()) { + Err(e) => panic!("error throwing Java exception -- failed resolving error class: {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_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_iden.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, - 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::{JniToolboxError, FromJava, IntoJava}; - #transforming - match #fn_name_inner(#forwarding).into_java(&mut #env_ident) { - Ok(res) => return res, - 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; ) }; - out.append_all(input); - out.append_all(header); - out.append_all(body); - Ok(out) + + let reverse_transformations = quote::quote! { + match ret.into_java(&mut #env_iden) { + Ok(fin) => fin, + Err(e) => { + // TODO should we panic instead? + let _ = #env_iden.throw_new(e.jclass(), format!("{e:?}")); + #return_expr + } + } + }; + + let inline_macro = if attrs.inline { + quote::quote!(#[inline]) + } else { + quote::quote!() + }; + + Ok(quote::quote! { + #inline_macro + #input + + #header { + + #transformations + + #invocation + + #error_handling + + #reverse_transformations + + } + }) } diff --git a/src/from_java.rs b/src/from_java.rs new file mode 100644 index 0000000..ff20c10 --- /dev/null +++ b/src/from_java.rs @@ -0,0 +1,121 @@ +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::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 From : Sized; + /// Attempts to convert this Java object into its Rust counterpart. + 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 From = $j; + + #[inline] + fn from_java(_: &mut jni::JNIEnv, value: Self::From) -> 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>); +auto_from_java!(JString<'j>, JString<'j>); +auto_from_java!(JObjectArray<'j>, JObjectArray<'j>); + +impl<'j, T: TypeArray> FromJava<'j> for JPrimitiveArray<'j, T> { + type From = JPrimitiveArray<'j, T>; + + #[inline] + 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 From = JString<'j>; + + 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> for Option +where + T: FromJava<'j, From: AsRef>>, +{ + type From = T::From; + + 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 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()? + .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..bff8909 --- /dev/null +++ b/src/into_java.rs @@ -0,0 +1,139 @@ +use jni::objects::JObject; + + +/// Specifies how a Rust type should be converted into a Java primitive. +pub trait IntoJava<'j> { + /// The JNI type representing the output. + type Ret; + /// 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> IntoJava<'j> for $t { + type Ret = $j; + + #[inline] + 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> IntoJava<'j> for bool { + type Ret = jni::sys::jboolean; + + #[inline] + 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; + + #[inline] + 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> { + /// The Java class associated with this type. + const CLASS: &'static str; + /// Attempts to convert this Rust object into a Java object. + 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"; + #[inline] + 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; + #[inline] + 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> { + Ok(env.new_string(self)?.into()) + } +} + +impl<'j> IntoJavaObject<'j> for String { + const CLASS: &'static str = "java/lang/String"; + fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + self.as_str().into_java_object(env) + } +} + +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_object(env)?; + env.set_object_array_element(&mut array, n as i32, &el)?; + } + Ok(array.into()) + } +} + +impl<'j, T: IntoJavaObject<'j>> IntoJavaObject<'j> for Option { + const CLASS: &'static str = T::CLASS; + fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + match self { + Some(x) => x.into_java_object(env), + None => Ok(JObject::null()) + } + } +} + +#[cfg(feature = "uuid")] +impl<'j> IntoJavaObject<'j> for uuid::Uuid { + const CLASS: &'static str = "java/util/UUID"; + 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()); + 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 cadd8ef..73d966a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,179 +1,53 @@ -use jni::{objects::{JObject, JObjectArray, JString}, sys::jobject}; -pub use jni_toolbox_macro::jni; +pub mod into_java; +pub mod from_java; +pub use jni_toolbox_macro::jni; +pub use into_java::{IntoJavaObject, IntoJava}; +pub use from_java::{FromJava, from_java_static}; + + +/// 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; } 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() - } -} - -pub fn from_java_static<'j, T: FromJava<'j>>(env: &mut jni::JNIEnv<'j>, val: T::T) -> Result { - T::from_java(env, val) -} - -pub trait FromJava<'j> : Sized { - type T : Sized; - 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!(f32, jni::sys::jfloat); -auto_from_java!(f64, jni::sys::jdouble); -auto_from_java!(JObject<'j>, JObject<'j>); -auto_from_java!(JObjectArray<'j>, JObjectArray<'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 = jni::objects::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(unsafe { env.get_string_unchecked(&value) }?.into()) // unsafe for efficiency - } -} - -impl<'j, T: FromJava<'j, T = jni::objects::JObject<'j>>> FromJava<'j> for Option { - type T = jni::objects::JObject<'j>; - - fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result { - if value.is_null() { return Ok(None) }; - Ok(Some(T::from_java(env, value)?)) - } -} - -#[cfg(feature = "uuid")] -impl<'j> FromJava<'j> for uuid::Uuid { - type T = jni::objects::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)) - } -} - -pub trait IntoJava<'j> { - type T; - - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result; -} - -macro_rules! auto_into_java { - ($t:ty, $j:ty) => { - impl<'j> IntoJava<'j> for $t { - type T = $j; - - fn into_java(self, _: &mut jni::JNIEnv<'j>) -> Result { - Ok(self) - } - } - }; -} - -auto_into_java!(i64, jni::sys::jlong); -auto_into_java!(i32, jni::sys::jint); -auto_into_java!(i16, jni::sys::jshort); -auto_into_java!(f32, jni::sys::jfloat); -auto_into_java!(f64, jni::sys::jdouble); -auto_into_java!((), ()); - -impl<'j> IntoJava<'j> for bool { - type T = jni::sys::jboolean; - - #[inline] - fn into_java(self, _: &mut jni::JNIEnv) -> Result { - Ok(if self { 1 } else { 0 }) - } -} - -impl<'j> IntoJava<'j> for &str { - type T = jni::sys::jstring; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - Ok(env.new_string(self)?.as_raw()) - } -} - -impl<'j> IntoJava<'j> for String { - type T = jni::sys::jstring; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - self.as_str().into_java(env) - } -} - -impl<'j> IntoJava<'j> for Vec { - 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())?; - 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)?; - } - Ok(array.into_raw()) - } -} - -impl<'j, T: IntoJava<'j, T = jni::sys::jobject>> IntoJava<'j> for Option { - type T = T::T; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { match self { - Some(x) => x.into_java(env), - None => Ok(std::ptr::null_mut()), + _ => "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!(), } - } -} - -#[cfg(feature = "uuid")] -impl<'j> IntoJava<'j> for uuid::Uuid { - type T = jni::sys::jobject; - fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result { - let class = env.find_class("java/util/UUID")?; - 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()) + .to_string() } } 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..f74f56b --- /dev/null +++ b/src/test/java/toolbox/Main.java @@ -0,0 +1,65 @@ +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; + + +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); + 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 + public void argumentsByValue() { + assertEquals(Main.sum(42, 13), 42 + 13); + } + + @Test + public void argumentsByReference() { + assertEquals(Main.concat("hello", "world"), "hello -- world"); + } + + @Test + public void checksForNull() { + // TODO maybe these should throw NullPtrException + assertThrows(NullPointerException.class, () -> Main.concat("a", null)); + assertThrows(NullPointerException.class, () -> Main.concat(null, "a")); + assertThrows(NullPointerException.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); + } + + @Test + 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 new file mode 100644 index 0000000..4780982 --- /dev/null +++ b/src/test/test.rs @@ -0,0 +1,35 @@ +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}") +} + +#[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() +} + +#[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!") +}