blob: 129d2e62b58edf1e54a796208445c0eaab1dd99c [file] [log] [blame] [edit]
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(unsafe_code, clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use jni::{
descriptors::Desc,
objects::{JObject, JString},
sys::jint,
JNIEnv, JavaVM,
};
use pourover::{
desc::ClassDesc,
exception::{null_pointer_exception, runtime_exception, JniResultExt, ThrowableJniResultExt},
};
use std::error::Error;
mod common;
use common::foo_class::*;
#[pourover::jni_method(package = "com.example", class = "Foo")]
extern "system" fn nativeReturnsInt<'local>(
mut env: JNIEnv<'local>,
_this: JObject<'local>,
n: jint,
) -> jint {
(|| match n {
-1 => Err(runtime_exception("test runtime exception"))?,
-2 => Err(null_pointer_exception())?,
_ => Ok(n),
})()
.unwrap_or_throw(&mut env)
}
#[pourover::jni_method(
package = "com.example",
class = "Foo",
panic_returns = JObject::null().into(),
)]
extern "system" fn nativeReturnsObject<'local>(
_env: JNIEnv<'local>,
_this: JObject<'local>,
_n: jint,
) -> JString<'local> {
unimplemented!()
}
pub static RUNTIME_EXCEPTION_CLASS: ClassDesc = ClassDesc::new("java/lang/RuntimeException");
#[test]
fn can_call_native_method() -> Result<(), Box<dyn Error>> {
// Create the environment
let vm = JavaVM::new(
jni::InitArgsBuilder::new()
.version(jni::JNIVersion::V8)
.option("-Xcheck:jni")
.build()?,
)?;
let mut env = vm.attach_current_thread()?;
// Load `Foo.class`
{
let foo_class = compile_foo()?;
let loaded_foo = env.define_class(CLASS_DESC, &JObject::null(), &foo_class)?;
env.delete_local_ref(loaded_foo)?;
}
let obj_foo = {
let method_id = CONSTRUCTOR.lookup(&mut env)?;
let args = &[jni::sys::jvalue { i: 123 }];
// Safety: `args` must match the constructor arg count and types.
unsafe { env.new_object_unchecked(CONSTRUCTOR.cls(), method_id, args) }?
};
let obj_foo = env.auto_local(obj_foo);
// TODO: It would be better if the JVM was able to find the method on its own,
// but, since we are using the invocation API to create the JVM, I haven't found a way to make
// it visible to the JVM. Normally the methods are loaded dynamically with
// `System.loadLibrary`, but in this case the symbols already exist in the executable, and I'm
// unsure why the JVM cannot find them. I have tried compiling with
// `-Zexport-executable-symbols` but that wasn't working for me.
env.register_native_methods(
&FOO,
&[
jni::NativeMethod {
name: "nativeReturnsInt".into(),
sig: "(I)I".into(),
fn_ptr: crate::nativeReturnsInt as *mut std::ffi::c_void,
},
jni::NativeMethod {
name: "nativeReturnsObject".into(),
sig: "(I)Ljava/lang/String;".into(),
fn_ptr: crate::nativeReturnsObject as *mut std::ffi::c_void,
},
],
)?;
// Sub-test 1: Call nativeReturnsInt(99) returns the argument without exceptions
{
let result =
pourover::call_method!(&mut env, FOO, "nativeReturnsInt", "(I)I", &obj_foo, 99);
assert_eq!(99, result.unwrap());
}
// Sub-test 2: Call nativeReturnsInt(99) returns the argument without exceptions, so
// extract_exception just returns Ok
{
let result =
pourover::call_method!(&mut env, FOO, "nativeReturnsInt", "(I)I", &obj_foo, 99);
let extracted_result = result.extract_exception(&mut env, "java/lang/Throwable");
let Ok(Ok(99)) = extracted_result else {
panic!("extracted_result should be Ok(Ok(99))")
};
}
// Sub-test 3: Call nativeReturnsInt(-1) throws RuntimeException("test runtime exception")
{
let result =
pourover::call_method!(&mut env, FOO, "nativeReturnsInt", "(I)I", &obj_foo, -1);
let throwable = result
.extract_exception(&mut env, "java/lang/RuntimeException")
.unwrap()
.unwrap_err();
let message = pourover::call_method!(
&mut env,
RUNTIME_EXCEPTION_CLASS,
"getMessage",
"()Ljava/lang/String;",
&throwable
)
.unwrap();
let message = env.get_string(&message).map(String::from).unwrap();
assert_eq!("test runtime exception", message);
}
// Sub-test 4: Call nativeReturnsInt(-2) throws NullPointerException()
{
let result =
pourover::call_method!(&mut env, FOO, "nativeReturnsInt", "(I)I", &obj_foo, -2);
let _ = result
.extract_exception(&mut env, "java/lang/NullPointerException")
.unwrap()
.unwrap_err();
}
// Sub-test 5: Call nativeReturnsInt(-1) throws RuntimeException(). Calling
// extract_exception with NullPointerException should return Err(JavaException).
{
let result =
pourover::call_method!(&mut env, FOO, "nativeReturnsInt", "(I)I", &obj_foo, -1);
let Err(jni::errors::Error::JavaException) =
result.extract_exception(&mut env, "java/lang/NullPointerException")
else {
panic!(concat!(
"Extracting NullPointerException from a result that contains RuntimeException ",
"should return Err(JavaException)"
))
};
// The exception should still be pending on the JVM
assert!(env.exception_check().unwrap());
}
Ok(())
}