blob: 24e7962bd85e420b87b1a6711a4031d5821799d4 [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.
//! Utilities for handling errors and propagating them into exceptions in Java.
use jni::{
descriptors::Desc,
objects::{JClass, JThrowable},
JNIEnv,
};
use std::fmt::Debug;
/// An error that can be thrown as a Java exception, or an error in the JNI layer.
///
/// This allows using Rust's error propagation mechanism (the `?` operator) to surface both JNI
/// errors and Java throwables, and convert them to Java exceptions at the top level. At the top
/// level of a JNI function implementation, it can use [`ThrowableJniResultExt::unwrap_or_throw`] to
/// convert the error to Java exception, or terminate the JVM with a fatal error in the case of JNI
/// errors.
pub enum ThrowableJniError<'local> {
/// A java throwable object instance. The throwable can be received from the Java side, or
/// created using [`call_constructor`][crate::call_constructor].
JavaThrowable(JThrowable<'local>),
/// A [`jni::errors::Exception`], which contains the class and the message to be able to create
/// the exception to be thrown.
JavaException(jni::errors::Exception),
/// An error from the [`jni`] crate.
JniError(jni::errors::Error),
}
impl<'local> ThrowableJniError<'local> {
/// Throws the error as a Java exception.
///
/// If the error is a `JavaThrowable` or a `JavaException`, it will be thrown on the given
/// `env`. If the error is a `JniError`, this will turn it into a [`JNIEnv::fatal_error`],
/// unless the error type is [`jni::errors::Error::JavaException`], in which case the error will
/// be ignored, allowing the already-thrown exception to remain in JVM.
///
/// In typical usages, callers should return some default value immediately after calling this.
/// See the example in [`try_throwable`].
pub fn throw_on_jvm(self, env: &mut JNIEnv<'_>) {
match self {
ThrowableJniError::JavaThrowable(throwable) => match env.throw(throwable) {
Ok(()) => {}
Err(jni::errors::Error::JavaException) => {
let _ = env.exception_describe();
env.fatal_error("Throwing exception failed");
}
Err(_) => env.fatal_error("Throwing exception failed"),
},
ThrowableJniError::JavaException(exception) => match env.throw(exception) {
Ok(()) => {}
Err(jni::errors::Error::JavaException) => {
let _ = env.exception_describe();
env.fatal_error("Throwing exception failed");
}
Err(_) => env.fatal_error("Throwing exception failed"),
},
ThrowableJniError::JniError(err) => {
match err {
jni::errors::Error::JavaException => {
// Ignore the exception, it's already pending in the JVM
}
_ => env.fatal_error(format!("{err}")),
}
}
}
}
}
impl Debug for ThrowableJniError<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::JavaThrowable(_) => f.debug_tuple("JavaThrowable").finish(),
Self::JavaException(arg0) => f
.debug_struct("JavaException")
.field("class", &arg0.class)
.field("msg", &arg0.msg)
.finish(),
Self::JniError(arg0) => f.debug_tuple("JniError").field(arg0).finish(),
}
}
}
impl<'local> From<JThrowable<'local>> for ThrowableJniError<'local> {
fn from(value: JThrowable<'local>) -> Self {
Self::JavaThrowable(value)
}
}
impl From<jni::errors::Error> for ThrowableJniError<'_> {
fn from(value: jni::errors::Error) -> Self {
Self::JniError(value)
}
}
impl From<jni::errors::Exception> for ThrowableJniError<'_> {
fn from(value: jni::errors::Exception) -> Self {
Self::JavaException(value)
}
}
/// Extension trait for `Result<T, ThrowableJniError>`.
pub trait ThrowableJniResultExt<T> {
/// Returns the contained `Ok` value, or throw an exception on the JVM and return the default.
///
/// Consumes the `self` argument then, if `Ok`, returns the contained value. Otherwise, if
/// `Err`, the error will be thrown on the JVM using [`ThrowableJniError::throw_on_jvm`] and a
/// default value will be returned. The value returned by `default` typically does not matter,
/// because while there is an exception pending on the JVM, the return value from your JNI
/// method is ignored.
fn unwrap_or_throw(self, env: &mut JNIEnv<'_>) -> T
where
T: Default,
Self: Sized,
{
self.unwrap_or_throw_with_default(env, Default::default)
}
/// Returns the contained `Ok` value, or throw an exception on the JVM and return some default.
///
/// Consumes the `self` argument then, if `Ok`, returns the contained value. Otherwise, if
/// `Err`, the error will be thrown on the JVM using [`ThrowableJniError::throw_on_jvm`] and the
/// value from `default` will be returned. The value returned by `default` typically does not
/// matter, because while there is an exception pending on the JVM, the return value from your
/// JNI method is ignored.
fn unwrap_or_throw_with_default(self, env: &mut JNIEnv<'_>, default: impl FnOnce() -> T) -> T;
}
impl<'local, T> ThrowableJniResultExt<T> for Result<T, ThrowableJniError<'local>> {
fn unwrap_or_throw_with_default(self, env: &mut JNIEnv<'_>, default: impl FnOnce() -> T) -> T {
match self {
Ok(v) => v,
Err(e) => {
e.throw_on_jvm(env);
default()
}
}
}
}
/// Creates a `NullPointerException` with a default error message.
pub fn null_pointer_exception() -> jni::errors::Exception {
jni::errors::Exception {
class: "java/lang/NullPointerException".into(),
msg: "Null pointer".into(),
}
}
/// Creates a runtime exception with the given message.
pub fn runtime_exception(msg: impl ToString) -> jni::errors::Exception {
jni::errors::Exception {
class: "java/lang/RuntimeException".into(),
msg: msg.to_string(),
}
}
/// Runs the given `func` and turns any [`ThrowableJniError`] into Java exceptions.
///
/// A convenience function that takes a closure `func`, runs it immediately, and calls
/// [`ThrowableJniResultExt::unwrap_or_throw`] on the result. This allows the code inside the
/// closure to propagate [`ThrowableJniErrors`][ThrowableJniError] using the `?` operator.
///
/// # Example
///
/// ```
/// use pourover::{jni_method, exception::{runtime_exception, try_throwable}};
/// use jni::{JNIEnv, objects::{JByteArray, JClass}, sys::{jboolean, JNI_TRUE}};
///
/// #[jni_method(package = "com.example", class = "Foo")]
/// extern "system" fn nativeMaybeThrowException<'local>(
/// mut env: JNIEnv<'local>,
/// _cls: JClass<'local>,
/// value: JByteArray<'local>,
/// ) -> jboolean {
/// try_throwable(&mut env, |env| {
/// let value = env.convert_byte_array(value)?;
/// if value == b"Throw exception" {
/// Err(runtime_exception("Argument was \"throw exception\""))?
/// }
/// Ok(JNI_TRUE)
/// })
/// }
/// ```
pub fn try_throwable<'local, R: Default>(
env: &mut JNIEnv<'local>,
func: impl FnOnce(&mut JNIEnv<'local>) -> Result<R, ThrowableJniError<'local>>,
) -> R {
func(env).unwrap_or_throw(env)
}
/// Extension trait on `jni::errors::Result<T>`.
pub trait JniResultExt<T> {
/// Extracts an exception of the given class from a [`jni::errors::Result`].
///
/// If `self` is `Err(JavaException)` and the pending exception on the JVM matches the given
/// `desc`, the exception will be cleared on the JVM and the associated throwable will be
/// returned.
///
/// # Returns
/// A nested result object. If `self` contains an exception matching the given `desc`, this will
/// return `Ok(Err(throwable))`. If `self` is `Ok`, this will return `Ok(Ok(value))`. Otherwise
/// if there are other errors or non-matching exceptions, the error will be propagated as
/// `Err(jni_error)`.
fn extract_exception<'local>(
self,
env: &mut JNIEnv<'local>,
desc: impl Desc<'local, JClass<'local>>,
) -> jni::errors::Result<Result<T, JThrowable<'local>>>;
}
impl<T> JniResultExt<T> for jni::errors::Result<T> {
fn extract_exception<'local>(
self,
env: &mut JNIEnv<'local>,
desc: impl Desc<'local, JClass<'local>>,
) -> jni::errors::Result<Result<T, JThrowable<'local>>> {
match self {
Ok(v) => Ok(Ok(v)),
Err(jni::errors::Error::JavaException) => {
let throwable = env.exception_occurred()?;
// Need to clear the exception ahead of time, otherwise `is_instance_of` will fail
env.exception_clear()?;
if env.is_instance_of(&throwable, desc)? {
Ok(Err(throwable))
} else {
env.throw(throwable)?;
Err(jni::errors::Error::JavaException)
}
}
Err(e) => Err(e),
}
}
}