chore: Merge branch 'feat/intermediate-trait'

This commit is contained in:
əlemi 2024-09-24 20:39:32 +02:00
commit 5259d8716d
Signed by: alemi
GPG key ID: A4895B84D311642C
17 changed files with 622 additions and 369 deletions

View file

@ -2,8 +2,6 @@ name: test
on: on:
push: push:
branches:
- dev
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@ -28,3 +26,5 @@ jobs:
toolchain: ${{ matrix.toolchain }} toolchain: ${{ matrix.toolchain }}
- run: cargo build --verbose - run: cargo build --verbose
- run: cargo test --verbose - run: cargo test --verbose
- uses: gradle/actions/setup-gradle@v4
- run: gradle test

9
.gitignore vendored
View file

@ -1 +1,10 @@
# rust
/target /target
# gradle
/build
/bin
/.settings
/.gradle
.project
.classpath

13
Cargo.lock generated
View file

@ -57,7 +57,7 @@ name = "jni-toolbox"
version = "0.1.3" version = "0.1.3"
dependencies = [ dependencies = [
"jni", "jni",
"jni-toolbox-macro 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "jni-toolbox-macro",
"uuid", "uuid",
] ]
@ -71,14 +71,11 @@ dependencies = [
] ]
[[package]] [[package]]
name = "jni-toolbox-macro" name = "jni-toolbox-test"
version = "0.1.3" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9970cad895bb316f70956593710d675d27a480ddbb8099f7e313042463a16d9b"
dependencies = [ dependencies = [
"proc-macro2", "jni",
"quote", "jni-toolbox",
"syn",
] ]
[[package]] [[package]]

View file

@ -1,5 +1,5 @@
[workspace] [workspace]
members = ["macro"] members = ["macro", "src/test"]
[package] [package]
name = "jni-toolbox" name = "jni-toolbox"
@ -13,6 +13,11 @@ version = "0.1.3"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
jni-toolbox-macro = "0.1.3" #jni-toolbox-macro = "0.1.3"
jni-toolbox-macro = { path = "./macro" }
jni = "0.21" jni = "0.21"
uuid = { version = "1.10", optional = true } uuid = { version = "1.10", optional = true }
[features]
default = []
uuid = ["dep:uuid"]

137
README.md
View file

@ -3,13 +3,12 @@
[![Crates.io Version](https://img.shields.io/crates/v/jni-toolbox)](https://crates.io/crates/jni-toolbox) [![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) [![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 ```rust
#[jni_toolbox::jni(package = "your.package.path", class = "ContainerClass")] #[jni_toolbox::jni(package = "your.package.path", class = "ContainerClass")]
@ -18,14 +17,13 @@ fn your_function_name(arg: String) -> Result<Vec<String>, String> {
} }
``` ```
### conversions ### Conversions
every type that must go into/from Java must implement `IntoJava` or `FromJava` (methods will receive a `&mut JNIEnv` and can return errors). Every type that is meant to be sent to Java must implement `IntoJavaObject` (or, unlikely, `IntoJavaPrimitive`); every type that is meant to be
most primitives already have them implemented. conversions are automatic and the wrapper function will invoke IntoJava/FromJava for every type, received from Java must implement `FromJava`. Most primitives and a few common types should already be implemented.
passing an environment reference.
```rust ```rust
impl<'j> IntoJava for MyClass { impl<'j> IntoJavaObject for MyClass {
type T = jni::sys::jobject; type T = jni::objects::JObject<'j>
fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result<Self::T, jni::errors::Error> { fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result<Self::T, jni::errors::Error> {
let hello = env.new_string("world")?; let hello = env.new_string("world")?;
// TODO!! // TODO!!
@ -33,15 +31,14 @@ impl<'j> IntoJava for MyClass {
} }
``` ```
### pointers ### 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.
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, 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. (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 ```rust
impl JniToolboxError for MyError { impl JniToolboxError for MyError {
@ -53,113 +50,75 @@ impl JniToolboxError for MyError {
```java ```java
package my.package.some; package my.package.some;
public class MyError { public class MyError extends Throwable {
public MyError(String x) { public MyError(String x) {
// TODO // 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 ```rust
#[jni(package = "mp.code", class = "Client", ptr)] #[jni(package = "mp.code", class = "Client", ptr)]
fn connect(config: Config) -> Result<Client, ConnectionError> { fn connect(config: Config) -> Result<Client, ConnectionError> {
tokio().block_on(Client::connect(config)) super::tokio().block_on(Client::connect(config))
} }
``` ```
gets turned into these two functions: generates a matching expanded function invoking it:
<details><summary>show macro expansion</summary>
```rust ```rust
fn connect(config: Config) -> Result<Client, ConnectionError> { fn connect(config: Config) -> Result<Client, ConnectionError> {
tokio().block_on(Client::connect(config)) super::tokio().block_on(Client::connect(config))
} }
#[no_mangle] #[no_mangle]
#[allow(unused_mut)] #[allow(unused_unit)]
pub extern "system" fn Java_mp_code_Client_connect<'local>( pub extern "system" fn Java_mp_code_Client_connect<'local>(
mut env: jni::JNIEnv<'local>, mut env: jni::JNIEnv<'local>,
_class: jni::objects::JClass<'local>, _class: jni::objects::JClass<'local>,
mut config: <Config as jni_toolbox::FromJava<'local>>::T, config: <Config as jni_toolbox::FromJava<'local>>::From,
) -> <Client as jni_toolbox::IntoJava<'local>>::T { ) -> <Client as jni_toolbox::IntoJava<'local>>::Ret {
use jni_toolbox::{FromJava, IntoJava, JniToolboxError}; use jni_toolbox::{FromJava, IntoJava, JniToolboxError};
let mut env_copy = unsafe { env.unsafe_clone() };
let config_new = match jni_toolbox::from_java_static::<Config>(&mut env, config) { let config_new = match jni_toolbox::from_java_static::<Config>(&mut env, config) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
let _ = env.throw_new( let _ = env.throw_new(e.jclass(), format!("{e:?}"));
"java/lang/RuntimeException",
$crate::__export::must_use({
let res = $crate::fmt::format($crate::__export::format_args!("{e:?}"));
res
}),
);
return std::ptr::null_mut(); return std::ptr::null_mut();
} }
}; };
match connect(config_new) { let result = connect(config_new);
Err(e) => match env_copy.find_class(e.jclass()) { let ret = match result {
Err(e) => { Ok(x) => x,
$crate::panicking::panic_fmt($crate::const_format_args!( Err(e) => match env.find_class(e.jclass()) {
"error throwing Java exception -- failed resolving error class: {e}" 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(class) => match env_copy.new_string($crate::__export::must_use({ Ok(msg) => match env.new_object(class, "(Ljava/lang/String;)V", &[jni::objects::JValueGen::Object(&msg)]) {
let res = $crate::fmt::format($crate::__export::format_args!("{e:?}")); Err(e) => panic!("error throwing Java exception -- failed creating object: {e}"));
res Ok(obj) => match env.throw(jni::objects::JThrowable::from(obj)) {
})) { Err(e) => panic!("error throwing Java exception -- failed throwing: {e}"),
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}"
));
}
Ok(_) => return std::ptr::null_mut(), Ok(_) => return std::ptr::null_mut(),
}, },
}, },
}, },
}, },
Ok(ret) => match ret.into_java(&mut env_copy) { };
Ok(fin) => return fin, match ret.into_java(&mut env) {
Err(e) => { Ok(fin) => fin,
let _ = env_copy.throw_new( Err(e) => {
"java/lang/RuntimeException", let _ = env.throw_new(e.jclass(), format!("{e:?}"));
$crate::__export::must_use({ std::ptr::null_mut()
let res = $crate::fmt::format($crate::__export::format_args!("{e:?}")); }
res
}),
);
return std::ptr::null_mut();
}
},
} }
} }
``` ```
</details> ## 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
## status own project should not be a problem.
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

24
build.gradle Normal file
View file

@ -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'
}

View file

@ -22,31 +22,33 @@ fn unpack_pat(pat: syn::Pat) -> Result<TokenStream, syn::Error> {
} }
} }
fn type_equals(ty: Box<syn::Type>, search: impl AsRef<str>) -> bool { fn bare_type(ty: Box<syn::Type>) -> Option<syn::TypePath> {
match *ty { match *ty {
syn::Type::Array(_) => false, syn::Type::Array(a) => bare_type(a.elem),
syn::Type::BareFn(_) => false, syn::Type::BareFn(_) => None,
syn::Type::ImplTrait(_) => false, syn::Type::ImplTrait(_) => None,
syn::Type::Infer(_) => false, syn::Type::Infer(_) => None,
syn::Type::Macro(_) => false, syn::Type::Macro(_) => None,
syn::Type::Never(_) => false, syn::Type::Never(_) => None,
syn::Type::Ptr(_) => false, syn::Type::TraitObject(_) => None,
syn::Type::Slice(_) => false, syn::Type::Verbatim(_) => None,
syn::Type::TraitObject(_) => false, syn::Type::Ptr(p) => bare_type(p.elem),
syn::Type::Tuple(_) => false, syn::Type::Slice(s) => bare_type(s.elem),
syn::Type::Verbatim(_) => false, syn::Type::Tuple(t) => bare_type(Box::new(t.elems.first()?.clone())), // TODO
syn::Type::Group(g) => type_equals(g.elem, search), syn::Type::Group(g) => bare_type(g.elem),
syn::Type::Paren(p) => type_equals(p.elem, search), syn::Type::Paren(p) => bare_type(p.elem),
syn::Type::Reference(r) => type_equals(r.elem, search), syn::Type::Reference(r) => bare_type(r.elem),
syn::Type::Path(ty) => { syn::Type::Path(ty) => Some(ty),
ty.path.segments _ => todo!(),
.last()
.map_or(false, |e| e.ident == search.as_ref())
},
_ => false,
} }
} }
fn type_equals(ty: Box<syn::Type>, search: impl AsRef<str>) -> 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 { impl ArgumentOptions {
pub(crate) fn parse_args(fn_item: &syn::ItemFn, ret_expr: TokenStream) -> Result<Self, syn::Error> { pub(crate) fn parse_args(fn_item: &syn::ItemFn, ret_expr: TokenStream) -> Result<Self, syn::Error> {
let mut arguments = Vec::new(); let mut arguments = Vec::new();
@ -83,9 +85,9 @@ impl ArgumentOptions {
if pass_env { if pass_env {
if let Some(arg) = args_iter.next() { if let Some(arg) = args_iter.next() {
let pat = arg.pat; let pat = arg.pat;
let ty = arg.ty; let ty = bare_type(arg.ty);
incoming.append_all(quote::quote!( mut #pat: #ty,)); incoming.append_all(quote::quote!( mut #pat: #ty,));
forwarding.append_all(quote::quote!( #pat,)); forwarding.append_all(quote::quote!( &mut #pat,));
} }
} else { } else {
incoming.append_all(quote::quote!( mut #env: jni::JNIEnv<'local>,)); incoming.append_all(quote::quote!( mut #env: jni::JNIEnv<'local>,));
@ -104,12 +106,12 @@ impl ArgumentOptions {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
// TODO should we panic here instead? // 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; 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,)); forwarding.append_all(quote::quote!( #new_pat,));
} }

View file

@ -4,7 +4,7 @@ pub(crate) struct AttrsOptions {
pub(crate) package: String, pub(crate) package: String,
pub(crate) class: String, pub(crate) class: String,
pub(crate) exception: Option<String>, pub(crate) exception: Option<String>,
pub(crate) return_pointer: bool, pub(crate) inline: bool,
} }
impl AttrsOptions { impl AttrsOptions {
@ -14,7 +14,7 @@ impl AttrsOptions {
let mut package = None; let mut package = None;
let mut class = None; let mut class = None;
let mut exception = None; let mut exception = None;
let mut return_pointer = false; let mut inline = false;
for attr in attrs { for attr in attrs {
match what_next { match what_next {
@ -24,7 +24,8 @@ impl AttrsOptions {
"package" => what_next = WhatNext::Package, "package" => what_next = WhatNext::Package,
"class" => what_next = WhatNext::Class, "class" => what_next = WhatNext::Class,
"exception" => what_next = WhatNext::Exception, "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}")), _ => 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(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'")) }; 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 })
} }
} }

View file

@ -3,7 +3,7 @@ mod wrapper;
mod args; mod args;
mod ret; mod ret;
/// wrap this function in in a JNI exported fn /// Wrap this function in in a JNI exported fn.
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn jni( pub fn jni(
attrs: proc_macro::TokenStream, attrs: proc_macro::TokenStream,

View file

@ -6,13 +6,16 @@ use syn::{ReturnType, Type};
pub(crate) struct ReturnOptions { pub(crate) struct ReturnOptions {
pub(crate) ty: Option<Box<Type>>, pub(crate) ty: Option<Box<Type>>,
pub(crate) result: bool, pub(crate) result: bool,
pub(crate) pointer: bool,
pub(crate) void: bool, pub(crate) void: bool,
} }
const PRIMITIVE_TYPES: [&str; 7] = ["i8", "i16", "i32", "i64", "f32", "f64", "bool"];
impl ReturnOptions { impl ReturnOptions {
pub(crate) fn parse_signature(ret: &ReturnType) -> Result<Self, syn::Error> { pub(crate) fn parse_signature(ret: &ReturnType) -> Result<Self, syn::Error> {
match ret { 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::ReturnType::Type(_tok, ty) => match *ty.clone() {
syn::Type::Path(path) => { syn::Type::Path(path) => {
let Some(last) = path.path.segments.last() else { 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() { syn::PathArguments::AngleBracketed(ref generics) => for generic in generics.args.iter() {
match generic { match generic {
syn::GenericArgument::Lifetime(_) => continue, syn::GenericArgument::Lifetime(_) => continue,
syn::GenericArgument::Type(ty) => return Ok(Self { ty: Some(Box::new(ty.clone())), result: true, void: is_void(ty) }), syn::GenericArgument::Type(ty) => {
_ => return Err(syn::Error::new(Span::call_site(), "unexpected type in Result")), // 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")), _ => Err(syn::Error::new(Span::call_site(), "unsupported return type")),
}, },
@ -42,8 +50,8 @@ impl ReturnOptions {
pub(crate) fn tokens(&self) -> TokenStream { pub(crate) fn tokens(&self) -> TokenStream {
match &self.ty { // TODO why do we need to invoke syn::Token! macro ??? 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(), None => ReturnType::Default.to_token_stream(),
Some(t) => quote::quote!( -> <#t as jni_toolbox::IntoJava<'local>>::Ret )
} }
} }
} }

View file

@ -1,12 +1,9 @@
use proc_macro2::{Span, TokenStream}; use proc_macro2::{Span, TokenStream};
use quote::TokenStreamExt;
use syn::Item; use syn::Item;
use crate::{args::ArgumentOptions, attrs::AttrsOptions, ret::ReturnOptions}; use crate::{args::ArgumentOptions, attrs::AttrsOptions, ret::ReturnOptions};
pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Result<TokenStream, syn::Error> { pub(crate) fn generate_jni_wrapper(attrs: TokenStream, input: TokenStream) -> Result<TokenStream, syn::Error> {
let mut out = TokenStream::new();
let Item::Fn(fn_item) = syn::parse2(input.clone())? else { let Item::Fn(fn_item) = syn::parse2(input.clone())? else {
return Err(syn::Error::new(Span::call_site(), "#[jni] is only supported on functions")); 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 ret = ReturnOptions::parse_signature(&fn_item.sig.output)?;
let return_expr = if ret.void { let return_expr = if ret.void {
quote::quote!( () ) quote::quote!( () )
} else if attrs.return_pointer { } else if ret.pointer {
quote::quote!( std::ptr::null_mut() ) quote::quote!( std::ptr::null_mut() )
} else { } else {
quote::quote!( 0 ) 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 // 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 args = ArgumentOptions::parse_args(&fn_item, return_expr.clone())?;
let return_type = ret.tokens(); let return_type = ret.tokens();
let name = fn_item.sig.ident.to_string(); let name = fn_item.sig.ident.to_string();
let name_jni = name.replace("_", "_1"); let name_jni = name.replace("_", "_1");
let fn_name_inner = syn::Ident::new(&name, Span::call_site()); 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 fn_name = syn::Ident::new(&format!("Java_{}_{}_{name_jni}", attrs.package, attrs.class), Span::call_site());
let incoming = args.incoming; let incoming = args.incoming;
// V----------------------------------V // V----------------------------------V
let header = quote::quote! { let header = quote::quote! {
#[no_mangle] #[no_mangle]
#[allow(unused_mut)] #[allow(unused_unit)]
pub extern "system" fn #fn_name<'local>(#incoming) #return_type pub extern "system" fn #fn_name<'local>(#incoming) #return_type
}; };
// ^----------------------------------^
let env_ident = args.env;
let forwarding = args.forwarding;
let transforming = args.transforming; 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 { if let Some(exception) = attrs.exception {
// V----------------------------------V
quote::quote! { quote::quote! {
{ let ret = match result {
use jni_toolbox::{JniToolboxError, FromJava, IntoJava}; Ok(x) => x,
#transforming Err(e) => match #env_iden.throw_new(#exception, format!("{e:?}")) {
match #fn_name_inner(#forwarding) { Ok(_) => return #return_expr,
Ok(ret) => ret, Err(e) => panic!("error throwing java exception: {e}"),
Err(e) => match #env_ident.throw_new(#exception, format!("{e:?}")) {
Ok(_) => return #return_expr,
Err(e) => panic!("error throwing java exception: {e}"),
}
} }
} };
} }
// ^----------------------------------^
} else { } else {
// V----------------------------------V
quote::quote! { quote::quote! {
{ let ret = match result {
use jni_toolbox::{JniToolboxError, FromJava, IntoJava}; Ok(x) => x,
// NOTE: this should be SAFE! the cloned env reference lives less than the actual one, we just lack a Err(e) => match #env_iden.find_class(e.jclass()) {
// way to get it back from the called function and thus resort to unsafe cloning Err(e) => panic!("error throwing Java exception -- failed resolving error class: {e}"),
let mut env_copy = unsafe { #env_ident.unsafe_clone() }; Ok(class) => match #env_iden.new_string(format!("{e:?}")) {
#transforming Err(e) => panic!("error throwing Java exception -- failed creating error string: {e}"),
match #fn_name_inner(#forwarding) { Ok(msg) => match #env_iden.new_object(class, "(Ljava/lang/String;)V", &[jni::objects::JValueGen::Object(&msg)]) {
Err(e) => match env_copy.find_class(e.jclass()) { Err(e) => panic!("error throwing Java exception -- failed creating object: {e}"),
Err(e) => panic!("error throwing Java exception -- failed resolving error class: {e}"), Ok(obj) => match #env_iden.throw(jni::objects::JThrowable::from(obj)) {
Ok(class) => match env_copy.new_string(format!("{e:?}")) { Err(e) => panic!("error throwing Java exception -- failed throwing: {e}"),
Err(e) => panic!("error throwing Java exception -- failed creating error string: {e}"), Ok(_) => return #return_expr,
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,
Err(e) => {
// TODO should we panic instead?
let _ = env_copy.throw_new("java/lang/RuntimeException", format!("{e:?}"));
return #return_expr;
}
}, },
} }
} };
} }
} }
} else { } else {
// V----------------------------------V quote::quote!( let ret = result; )
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;
},
}
}
}
// ^----------------------------------^
}; };
out.append_all(input);
out.append_all(header); let reverse_transformations = quote::quote! {
out.append_all(body); match ret.into_java(&mut #env_iden) {
Ok(out) 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
}
})
} }

121
src/from_java.rs Normal file
View file

@ -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, jni::errors::Error> {
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<Self, jni::errors::Error>;
}
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<Self, jni::errors::Error> {
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<Self, jni::errors::Error> {
Ok(value)
}
}
impl<'j> FromJava<'j> for char {
type From = jni::sys::jchar;
#[inline]
fn from_java(_: &mut jni::JNIEnv, value: Self::From) -> Result<Self, jni::errors::Error> {
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<Self, jni::errors::Error> {
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<Self, jni::errors::Error> {
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<T>
where
T: FromJava<'j, From: AsRef<JObject<'j>>>,
{
type From = T::From;
fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::From) -> Result<Self, jni::errors::Error> {
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<T> {
type From = JObjectArray<'j>;
fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::From) -> Result<Self, jni::errors::Error> {
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<Self, jni::errors::Error> {
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))
}
}

139
src/into_java.rs Normal file
View file

@ -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<Self::Ret, jni::errors::Error>;
}
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<Self::Ret, jni::errors::Error> {
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<Self::Ret, jni::errors::Error> {
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<Self::Ret, jni::errors::Error> {
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<JObject<'j>, 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<JObject<'j>, 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<JObject<'j>, 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<JObject<'j>, 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<JObject<'j>, jni::errors::Error> {
self.as_str().into_java_object(env)
}
}
impl<'j, T: IntoJavaObject<'j>> IntoJavaObject<'j> for Vec<T> {
const CLASS: &'static str = T::CLASS; // TODO shouldnt it be 'Object[]' ?
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<JObject<'j>, 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<T> {
const CLASS: &'static str = T::CLASS;
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<JObject<'j>, 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<JObject<'j>, 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)])
}
}

View file

@ -1,179 +1,53 @@
use jni::{objects::{JObject, JObjectArray, JString}, sys::jobject}; pub mod into_java;
pub use jni_toolbox_macro::jni; 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 { pub trait JniToolboxError: std::error::Error {
/// The Java class for the matching exception.
fn jclass(&self) -> String; fn jclass(&self) -> String;
} }
impl JniToolboxError for jni::errors::Error { impl JniToolboxError for jni::errors::Error {
fn jclass(&self) -> String { 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 { impl JniToolboxError for jni::errors::JniError {
fn jclass(&self) -> String { 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, jni::errors::Error> {
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<Self, jni::errors::Error>;
}
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<Self, jni::errors::Error> {
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<Self, jni::errors::Error> {
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<Self, jni::errors::Error> {
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<T> {
type T = jni::objects::JObject<'j>;
fn from_java(env: &mut jni::JNIEnv<'j>, value: Self::T) -> Result<Self, jni::errors::Error> {
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<Self, jni::errors::Error> {
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<Self::T, jni::errors::Error>;
}
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<Self::T, jni::errors::Error> {
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<Self::T, jni::errors::Error> {
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<Self::T, jni::errors::Error> {
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::T, jni::errors::Error> {
self.as_str().into_java(env)
}
}
impl<'j> IntoJava<'j> for Vec<String> {
type T = jni::sys::jobjectArray;
fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result<Self::T, jni::errors::Error> {
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<T> {
type T = T::T;
fn into_java(self, env: &mut jni::JNIEnv<'j>) -> Result<Self::T, jni::errors::Error> {
match self { match self {
Some(x) => x.into_java(env), _ => "java/lang/RuntimeException",
None => Ok(std::ptr::null_mut()), // 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()
}
#[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<Self::T, jni::errors::Error> {
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())
} }
} }

13
src/test/Cargo.toml Normal file
View file

@ -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"

View file

@ -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!");
}
}

35
src/test/test.rs Normal file
View file

@ -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<String> {
vec![a, b, c]
}
#[jni(package = "toolbox", class = "Main")]
fn maybe(idk: Option<String>) -> bool {
idk.is_some()
}
#[jni(package = "toolbox", class = "Main")]
fn optional(present: bool) -> Option<String> {
if present {
Some("hello world!".into())
} else {
None
}
}
#[jni(package = "toolbox", class = "Main")]
fn raw<'local>(env: &mut jni::JNIEnv<'local>) -> Result<jni::objects::JString<'local>, jni::errors::Error> {
env.new_string("hello world!")
}