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:
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

9
.gitignore vendored
View file

@ -1 +1,10 @@
# rust
/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"
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]]

View file

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

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)
[![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<Vec<String>, 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<Self::T, jni::errors::Error> {
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<Client, ConnectionError> {
tokio().block_on(Client::connect(config))
super::tokio().block_on(Client::connect(config))
}
```
gets turned into these two functions:
<details><summary>show macro expansion</summary>
generates a matching expanded function invoking it:
```rust
fn connect(config: Config) -> Result<Client, ConnectionError> {
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: <Config as jni_toolbox::FromJava<'local>>::T,
) -> <Client as jni_toolbox::IntoJava<'local>>::T {
config: <Config as jni_toolbox::FromJava<'local>>::From,
) -> <Client as jni_toolbox::IntoJava<'local>>::Ret {
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) {
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()
}
}
}
```
</details>
## 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.

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 {
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<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 {
pub(crate) fn parse_args(fn_item: &syn::ItemFn, ret_expr: TokenStream) -> Result<Self, syn::Error> {
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,));
}

View file

@ -4,7 +4,7 @@ pub(crate) struct AttrsOptions {
pub(crate) package: String,
pub(crate) class: String,
pub(crate) exception: Option<String>,
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 })
}
}

View file

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

View file

@ -6,13 +6,16 @@ use syn::{ReturnType, Type};
pub(crate) struct ReturnOptions {
pub(crate) ty: Option<Box<Type>>,
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<Self, syn::Error> {
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 )
}
}
}

View file

@ -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<TokenStream, syn::Error> {
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
}
})
}

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 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, 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 {
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<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())
.to_string()
}
}

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