blob: 4d623d976fdf0fbfae86c2fea58632c4235b4b30 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
//! Common Parquet errors and macros.
use arrow::error::ArrowError;
use datafusion_common::DataFusionError;
use jni::errors::{Exception, ToException};
use regex::Regex;
use std::{
any::Any,
convert,
fmt::Write,
panic::{catch_unwind, UnwindSafe},
result, str,
str::Utf8Error,
sync::{Arc, Mutex},
};
// This is just a pointer. We'll be returning it from our function. We
// can't return one of the objects with lifetime information because the
// lifetime checker won't let us.
use jni::sys::{jboolean, jbyte, jchar, jdouble, jfloat, jint, jlong, jobject, jshort};
use crate::execution::operators::ExecutionError;
use datafusion_comet_spark_expr::SparkError;
use jni::objects::{GlobalRef, JThrowable, JValue};
use jni::JNIEnv;
use lazy_static::lazy_static;
use parquet::errors::ParquetError;
use thiserror::Error;
lazy_static! {
static ref PANIC_BACKTRACE: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
}
#[derive(thiserror::Error, Debug)]
pub enum CometError {
#[error("Configuration Error: {0}")]
Config(String),
#[error("{0}")]
NullPointer(String),
#[error("Out of bounds{0}")]
IndexOutOfBounds(usize),
#[error("Comet Internal Error: {0}")]
Internal(String),
/// CometError::Spark is typically used in native code to emulate the same errors
/// that Spark would return
#[error(transparent)]
Spark(SparkError),
#[error(transparent)]
Arrow {
#[from]
source: ArrowError,
},
#[error(transparent)]
Parquet {
#[from]
source: ParquetError,
},
#[error(transparent)]
Expression {
#[from]
source: ExpressionError,
},
#[error(transparent)]
Execution {
#[from]
source: ExecutionError,
},
#[error(transparent)]
IO {
#[from]
source: std::io::Error,
},
#[error(transparent)]
NumberIntFormat {
#[from]
source: std::num::ParseIntError,
},
#[error(transparent)]
NumberFloatFormat {
#[from]
source: std::num::ParseFloatError,
},
#[error(transparent)]
BoolFormat {
#[from]
source: std::str::ParseBoolError,
},
#[error(transparent)]
Format {
#[from]
source: Utf8Error,
},
#[error(transparent)]
JNI {
#[from]
source: jni::errors::Error,
},
#[error("{msg}")]
Panic { msg: String },
#[error("{msg}")]
DataFusion {
msg: String,
#[source]
source: DataFusionError,
},
#[error("{class}: {msg}")]
JavaException {
class: String,
msg: String,
throwable: GlobalRef,
},
}
pub fn init() {
std::panic::set_hook(Box::new(|_panic_info| {
// Capture the backtrace for a panic
*PANIC_BACKTRACE.lock().unwrap() =
Some(std::backtrace::Backtrace::force_capture().to_string());
}));
}
/// Converts the results from `panic::catch_unwind` (e.g. a panic) to a `CometError`
impl convert::From<Box<dyn Any + Send>> for CometError {
fn from(e: Box<dyn Any + Send>) -> Self {
CometError::Panic {
msg: match e.downcast_ref::<&str>() {
Some(s) => s.to_string(),
None => match e.downcast_ref::<String>() {
Some(msg) => msg.to_string(),
None => "unknown panic".to_string(),
},
},
}
}
}
impl From<DataFusionError> for CometError {
fn from(value: DataFusionError) -> Self {
CometError::DataFusion {
msg: value.message().to_string(),
source: value,
}
}
}
impl From<CometError> for DataFusionError {
fn from(value: CometError) -> Self {
match value {
CometError::DataFusion { msg: _, source } => source,
_ => DataFusionError::Execution(value.to_string()),
}
}
}
impl From<CometError> for ExecutionError {
fn from(value: CometError) -> Self {
match value {
CometError::Execution { source } => source,
CometError::JavaException {
class,
msg,
throwable,
} => ExecutionError::JavaException {
class,
msg,
throwable,
},
_ => ExecutionError::GeneralError(value.to_string()),
}
}
}
impl jni::errors::ToException for CometError {
fn to_exception(&self) -> Exception {
match self {
CometError::IndexOutOfBounds(..) => Exception {
class: "java/lang/IndexOutOfBoundsException".to_string(),
msg: self.to_string(),
},
CometError::NullPointer(..) => Exception {
class: "java/lang/NullPointerException".to_string(),
msg: self.to_string(),
},
CometError::Spark { .. } => Exception {
class: "org/apache/spark/SparkException".to_string(),
msg: self.to_string(),
},
CometError::NumberIntFormat { source: s } => Exception {
class: "java/lang/NumberFormatException".to_string(),
msg: s.to_string(),
},
CometError::NumberFloatFormat { source: s } => Exception {
class: "java/lang/NumberFormatException".to_string(),
msg: s.to_string(),
},
CometError::IO { .. } => Exception {
class: "java/io/IOException".to_string(),
msg: self.to_string(),
},
CometError::Parquet { .. } => Exception {
class: "org/apache/comet/ParquetRuntimeException".to_string(),
msg: self.to_string(),
},
_other => Exception {
class: "org/apache/comet/CometNativeException".to_string(),
msg: self.to_string(),
},
}
}
}
/// Error returned when there is an error during executing an expression.
#[derive(thiserror::Error, Debug)]
pub enum ExpressionError {
/// Simple error
#[error("General expression error with reason {0}.")]
General(String),
/// Deserialization error
#[error("Fail to deserialize to native expression with reason {0}.")]
Deserialize(String),
/// Evaluation error
#[error("Fail to evaluate native expression with reason {0}.")]
Evaluation(String),
/// Error when processing Arrow array.
#[error("Fail to process Arrow array with reason {0}.")]
ArrowError(String),
}
/// A specialized `Result` for Comet errors.
pub type CometResult<T> = result::Result<T, CometError>;
// ----------------------------------------------------------------------
// Convenient macros for different errors
macro_rules! general_err {
($fmt:expr, $($args:expr),*) => (crate::CometError::from(parquet::errors::ParquetError::General(format!($fmt, $($args),*))));
}
/// Returns the "default value" for a type. This is used for JNI code in order to facilitate
/// returning a value in cases where an exception is thrown. This value will never be used, as the
/// JVM will note the pending exception.
///
/// Default values are often some kind of initial value, identity value, or anything else that
/// may make sense as a default.
///
/// NOTE: We can't just use [Default] since both the trait and the object are defined in other
/// crates.
/// See [Rust Compiler Error Index - E0117](https://doc.rust-lang.org/error-index.html#E0117)
pub trait JNIDefault {
fn default() -> Self;
}
impl JNIDefault for jboolean {
fn default() -> jboolean {
0
}
}
impl JNIDefault for jbyte {
fn default() -> jbyte {
0
}
}
impl JNIDefault for jchar {
fn default() -> jchar {
0
}
}
impl JNIDefault for jdouble {
fn default() -> jdouble {
0.0
}
}
impl JNIDefault for jfloat {
fn default() -> jfloat {
0.0
}
}
impl JNIDefault for jint {
fn default() -> jint {
0
}
}
impl JNIDefault for jlong {
fn default() -> jlong {
0
}
}
/// The "default value" for all returned objects, such as [jstring], [jlongArray], etc.
impl JNIDefault for jobject {
fn default() -> jobject {
std::ptr::null_mut()
}
}
impl JNIDefault for jshort {
fn default() -> jshort {
0
}
}
impl JNIDefault for () {
fn default() {}
}
// Unwrap the result returned from `panic::catch_unwind` when `Ok`, otherwise throw a
// `RuntimeException` back to the calling Java. Since a return result is required, use `JNIDefault`
// to create a reasonable result. This returned default value will be ignored due to the exception.
pub fn unwrap_or_throw_default<T: JNIDefault>(
env: &mut JNIEnv,
result: std::result::Result<T, CometError>,
) -> T {
match result {
Ok(value) => value,
Err(err) => {
let backtrace = match err {
CometError::Panic { msg: _ } => PANIC_BACKTRACE.lock().unwrap().take(),
_ => None,
};
throw_exception(env, &err, backtrace);
T::default()
}
}
}
fn throw_exception(env: &mut JNIEnv, error: &CometError, backtrace: Option<String>) {
// If there isn't already an exception?
if env.exception_check().is_ok() {
// ... then throw new exception
match error {
CometError::JavaException {
class: _,
msg: _,
throwable,
} => env.throw(<&JThrowable>::from(throwable.as_obj())),
CometError::Execution {
source:
ExecutionError::JavaException {
class: _,
msg: _,
throwable,
},
} => env.throw(<&JThrowable>::from(throwable.as_obj())),
CometError::DataFusion {
msg: _,
source: DataFusionError::External(e),
} if matches!(e.downcast_ref(), Some(SparkError::CastOverFlow { .. })) => {
match e.downcast_ref() {
Some(SparkError::CastOverFlow {
value,
from_type,
to_type,
}) => {
let throwable: JThrowable = env
.new_object(
"org/apache/spark/sql/comet/CastOverflowException",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
&[
JValue::Object(&env.new_string(value).unwrap()),
JValue::Object(&env.new_string(from_type).unwrap()),
JValue::Object(&env.new_string(to_type).unwrap()),
],
)
.unwrap()
.into();
env.throw(throwable)
}
_ => unreachable!(),
}
}
_ => {
let exception = error.to_exception();
match backtrace {
Some(backtrace_string) => env.throw_new(
exception.class,
to_stacktrace_string(exception.msg, backtrace_string).unwrap(),
),
_ => env.throw_new(exception.class, exception.msg),
}
}
}
.expect("Thrown exception")
}
}
#[derive(Debug, Error)]
enum StacktraceError {
#[error("Unable to initialize message: {0}")]
Message(String),
#[error("Unable to initialize backtrace regex: {0}")]
Regex(#[from] regex::Error),
#[error("Required field missing: {0}")]
Required_Field(String),
#[error("Unable to format stacktrace element: {0}")]
Element(#[from] std::fmt::Error),
}
fn to_stacktrace_string(msg: String, backtrace_string: String) -> Result<String, StacktraceError> {
let mut res = String::new();
write!(&mut res, "{}", msg).map_err(|error| StacktraceError::Message(error.to_string()))?;
// Use multi-line mode and named capture groups to identify the following stacktrace fields:
// - dc = declaredClass
// - mn = methodName
// - fn = fileName (optional)
// - line = file line number (optional)
// - col = file col number within the line (optional)
let re = Regex::new(
r"(?m)^\s*\d+: (?<dc>.*?)(?<mn>[^:]+)\n(\s*at\s+(?<fn>[^:]+):(?<line>\d+):(?<col>\d+)$)?",
)?;
for c in re.captures_iter(backtrace_string.as_str()) {
write!(
&mut res,
"\n at {}{}({}:{})",
c.name("dc")
.ok_or_else(|| StacktraceError::Required_Field("declared class".to_string()))?
.as_str(),
c.name("mn")
.ok_or_else(|| StacktraceError::Required_Field("method name".to_string()))?
.as_str(),
// There are internal calls within the backtrace that don't provide file information
c.name("fn").map(|m| m.as_str()).unwrap_or("__internal__"),
c.name("line")
.map(|m| m.as_str().parse().expect("numeric line number"))
.unwrap_or(0)
)?;
}
Ok(res)
}
fn flatten<T, E>(result: Result<Result<T, E>, E>) -> Result<T, E> {
result.and_then(convert::identity)
}
// Implements "currying" from `FnOnce(T) -> R` to `FnOnce() -> R`, given
// an instance of T. Curring is not supported in Rust so we have to use this
// custom function to achieve something similar here.
fn curry<'a, T: 'a, F, R>(f: F, t: T) -> impl FnOnce() -> R + 'a
where
F: FnOnce(T) -> R + 'a,
{
|| f(t)
}
// It is currently undefined behavior to unwind from Rust code into foreign code, so we can wrap
// our JNI functions and turn these panics into a `RuntimeException`.
pub fn try_unwrap_or_throw<T, F>(env: &JNIEnv, f: F) -> T
where
T: JNIDefault,
F: FnOnce(JNIEnv) -> Result<T, CometError> + UnwindSafe,
{
let mut env1 = unsafe { JNIEnv::from_raw(env.get_raw()).unwrap() };
let env2 = unsafe { JNIEnv::from_raw(env.get_raw()).unwrap() };
unwrap_or_throw_default(
&mut env1,
flatten(catch_unwind(curry(f, env2)).map_err(CometError::from)),
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
fs::File,
io,
io::Read,
path::PathBuf,
sync::{Arc, Once},
};
use jni::{
objects::{JClass, JIntArray, JString, JThrowable},
sys::{jintArray, jstring},
AttachGuard, InitArgsBuilder, JNIEnv, JNIVersion, JavaVM,
};
use assertables::{assert_starts_with, assert_starts_with_as_result};
pub fn jvm() -> &'static Arc<JavaVM> {
static mut JVM: Option<Arc<JavaVM>> = None;
static INIT: Once = Once::new();
// Capture panic backtraces
init();
INIT.call_once(|| {
// Add common classes to the classpath in so that we can find CometException
let mut common_classes = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
common_classes.push("../../common/target/classes");
let mut class_path = common_classes
.as_path()
.to_str()
.expect("common classes as an str")
.to_string();
class_path.insert_str(0, "-Djava.class.path=");
// Build the VM properties
let jvm_args = InitArgsBuilder::new()
// Pass the JNI API version (default is 8)
.version(JNIVersion::V8)
// You can additionally pass any JVM options (standard, like a system property,
// or VM-specific).
// Here we enable some extra JNI checks useful during development
.option("-Xcheck:jni")
.option(class_path.as_str())
.build()
.unwrap_or_else(|e| panic!("{:#?}", e));
let jvm = JavaVM::new(jvm_args).unwrap_or_else(|e| panic!("{:#?}", e));
unsafe {
JVM = Some(Arc::new(jvm));
}
});
unsafe { JVM.as_ref().unwrap() }
}
fn attach_current_thread() -> AttachGuard<'static> {
jvm().attach_current_thread().expect("Unable to attach JVM")
}
#[test]
#[cfg_attr(miri, ignore)] // miri can't call foreign function `dlopen`
pub fn error_from_panic() {
let _guard = attach_current_thread();
let mut env = jvm().get_env().unwrap();
try_unwrap_or_throw(&env, |_| -> CometResult<()> {
panic!("oops!");
});
assert_pending_java_exception_detailed(
&mut env,
Some("java/lang/RuntimeException"),
Some("oops!"),
);
}
// Verify that functions that return an object are handled correctly. This is basically
// a test of the "happy path".
#[test]
#[cfg_attr(miri, ignore)] // miri can't call foreign function `dlopen`
pub fn object_result() {
let _guard = attach_current_thread();
let mut env = jvm().get_env().unwrap();
let clazz = env.find_class("java/lang/Object").unwrap();
let input = env.new_string("World".to_string()).unwrap();
let actual = Java_Errors_hello(&env, clazz, input);
let actual_s = unsafe { JString::from_raw(actual) };
let actual_string = String::from(env.get_string(&actual_s).unwrap().to_str().unwrap());
assert_eq!("Hello, World!", actual_string);
}
// Verify that functions that return an native time are handled correctly. This is basically
// a test of the "happy path".
#[test]
#[cfg_attr(miri, ignore)] // miri can't call foreign function `dlopen`
pub fn jlong_result() {
let _guard = attach_current_thread();
let mut env = jvm().get_env().unwrap();
// Class java.lang.object is just a stand-in
let class = env.find_class("java/lang/Object").unwrap();
let a: jlong = 6;
let b: jlong = 3;
let actual = Java_Errors_div(&env, class, a, b);
assert_eq!(2, actual);
}
// Verify that functions that return an array can handle throwing exceptions. The test
// causes an exception by dividing by zero.
#[test]
#[cfg_attr(miri, ignore)] // miri can't call foreign function `dlopen`
pub fn jlong_panic_exception() {
let _guard = attach_current_thread();
let mut env = jvm().get_env().unwrap();
// Class java.lang.object is just a stand-in
let class = env.find_class("java/lang/Object").unwrap();
let a: jlong = 6;
let b: jlong = 0;
let _actual = Java_Errors_div(&env, class, a, b);
assert_pending_java_exception_detailed(
&mut env,
Some("java/lang/RuntimeException"),
Some("attempt to divide by zero"),
);
}
// Verify that functions that return an native time are handled correctly. This is basically
// a test of the "happy path".
#[test]
#[cfg_attr(miri, ignore)] // miri can't call foreign function `dlopen`
pub fn jlong_result_ok() {
let _guard = attach_current_thread();
let mut env = jvm().get_env().unwrap();
// Class java.lang.object is just a stand-in
let class = env.find_class("java/lang/Object").unwrap();
let a: JString = env.new_string("9".to_string()).unwrap();
let b: JString = env.new_string("3".to_string()).unwrap();
let actual = Java_Errors_div_with_parse(&env, class, a, b);
assert_eq!(3, actual);
}
// Verify that functions that return an native time are handled correctly. This is basically
// a test of the "happy path".
#[test]
#[cfg_attr(miri, ignore)] // miri can't call foreign function `dlopen`
pub fn jlong_result_err() {
let _guard = attach_current_thread();
let mut env = jvm().get_env().unwrap();
// Class java.lang.object is just a stand-in
let class = env.find_class("java/lang/Object").unwrap();
let a: JString = env.new_string("NaN".to_string()).unwrap();
let b: JString = env.new_string("3".to_string()).unwrap();
let _actual = Java_Errors_div_with_parse(&env, class, a, b);
assert_pending_java_exception_detailed(
&mut env,
Some("java/lang/NumberFormatException"),
Some("invalid digit found in string"),
);
}
// Verify that functions that return an array are handled correctly. This is basically
// a test of the "happy path".
#[test]
#[cfg_attr(miri, ignore)] // miri can't call foreign function `dlopen`
pub fn jint_array_result() {
let _guard = attach_current_thread();
let mut env = jvm().get_env().unwrap();
// Class java.lang.object is just a stand-in
let class = env.find_class("java/lang/Object").unwrap();
let buf = [2, 4, 6];
let input = env.new_int_array(3).unwrap();
env.set_int_array_region(&input, 0, &buf).unwrap();
let actual = Java_Errors_array_div(&env, class, &input, 2);
let actual_s = unsafe { JIntArray::from_raw(actual) };
let mut buf: [i32; 3] = [0; 3];
env.get_int_array_region(&actual_s, 0, &mut buf).unwrap();
assert_eq!([1, 2, 3], buf);
}
// Verify that functions that return an array can handle throwing exceptions. The test
// causes an exception by dividing by zero.
#[test]
#[cfg_attr(miri, ignore)] // miri can't call foreign function `dlopen`
pub fn jint_array_panic_exception() {
let _guard = attach_current_thread();
let mut env = jvm().get_env().unwrap();
// Class java.lang.object is just a stand-in
let class = env.find_class("java/lang/Object").unwrap();
let buf = [2, 4, 6];
let input = env.new_int_array(3).unwrap();
env.set_int_array_region(&input, 0, &buf).unwrap();
let _actual = Java_Errors_array_div(&env, class, &input, 0);
assert_pending_java_exception_detailed(
&mut env,
Some("java/lang/RuntimeException"),
Some("attempt to divide by zero"),
);
}
/// Test that conversion of a serialized backtrace to an equivalent stacktrace message.
///
/// See [`object_panic_exception`] for a test which involves generating a panic and verifying
/// that the resulting stack trace includes the offending call.
#[test]
#[cfg_attr(miri, ignore)] // miri can't call foreign function `dlopen`
pub fn stacktrace_string() {
// Setup: Start with a backtrace that includes all of the expected scenarios, including
// cases where the file and location are not provided as part of the backtrace capture
let backtrace_string = read_resource("testdata/backtrace.txt").expect("backtrace content");
// Test: Reformat the serialized backtrace as a multi-line message which includes the
// backtrace formatted as a stacktrace
let stacktrace_string =
to_stacktrace_string("Some Error Message".to_string(), backtrace_string).unwrap();
// Verify: The message matches the expected output. Trim the expected string to remove
// the carriage return
let expected_string = read_resource("testdata/stacktrace.txt").expect("stacktrace content");
assert_eq!(expected_string.trim(), stacktrace_string.as_str());
}
fn read_resource(path: &str) -> Result<String, io::Error> {
let mut path_buf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path_buf.push(path);
let mut f = File::open(path_buf.as_path())?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
// Example of a simple JNI "Hello World" program. It can be used to demonstrate:
// * returning an object
// * throwing an exception from `.expect()`
#[no_mangle]
pub extern "system" fn Java_Errors_hello(
e: &JNIEnv,
_class: JClass,
input: JString,
) -> jstring {
try_unwrap_or_throw(e, |mut env| {
let input: String = env
.get_string(&input)
.expect("Couldn't get java string!")
.into();
let output = env
.new_string(format!("Hello, {}!", input))
.expect("Couldn't create java string!");
Ok(output.into_raw())
})
}
// Example of a simple JNI function that divides. It can be used to demonstrate:
// * returning an native type
// * throwing an exception when dividing by zero
#[no_mangle]
pub extern "system" fn Java_Errors_div(
env: &JNIEnv,
_class: JClass,
a: jlong,
b: jlong,
) -> jlong {
try_unwrap_or_throw(env, |_| Ok(a / b))
}
#[no_mangle]
pub extern "system" fn Java_Errors_div_with_parse(
e: &JNIEnv,
_class: JClass,
a: JString,
b: JString,
) -> jlong {
try_unwrap_or_throw(e, |mut env| {
let a_value: i64 = env.get_string(&a)?.to_str()?.parse()?;
let b_value: i64 = env.get_string(&b)?.to_str()?.parse()?;
Ok(a_value / b_value)
})
}
// Example of a simple JNI function that divides. It can be used to demonstrate:
// * returning an array
// * throwing an exception when dividing by zero
#[no_mangle]
pub extern "system" fn Java_Errors_array_div(
e: &JNIEnv,
_class: JClass,
input: &JIntArray,
divisor: jint,
) -> jintArray {
try_unwrap_or_throw(e, |env| {
let mut input_buf: [jint; 3] = [0; 3];
env.get_int_array_region(input, 0, &mut input_buf)?;
let buf = input_buf.map(|v| -> jint { v / divisor });
let result = env.new_int_array(3)?;
env.set_int_array_region(&result, 0, &buf)?;
Ok(result.into_raw())
})
}
// Helper method that asserts there is a pending Java exception which is an `instance_of`
// `expected_type` with a message matching `expected_message` and clears it if any.
fn assert_pending_java_exception_detailed(
env: &mut JNIEnv,
expected_type: Option<&str>,
expected_message: Option<&str>,
) {
assert!(env.exception_check().unwrap());
let exception = env.exception_occurred().expect("Unable to get exception");
env.exception_clear().unwrap();
if let Some(expected_type) = expected_type {
assert_exception_type(env, &exception, expected_type);
}
if let Some(expected_message) = expected_message {
assert_exception_message(env, exception, expected_message);
}
}
// Asserts that exception is an `instance_of` `expected_type` type.
fn assert_exception_type(env: &mut JNIEnv, exception: &JThrowable, expected_type: &str) {
if !env.is_instance_of(exception, expected_type).unwrap() {
let class: JClass = env.get_object_class(exception).unwrap();
let name = env
.call_method(class, "getName", "()Ljava/lang/String;", &[])
.unwrap()
.l()
.unwrap();
let name_string = name.into();
let class_name: String = env.get_string(&name_string).unwrap().into();
assert_eq!(class_name.replace('.', "/"), expected_type);
};
}
// Asserts that exception's message matches `expected_message`.
fn assert_exception_message(env: &mut JNIEnv, exception: JThrowable, expected_message: &str) {
let message = env
.call_method(exception, "getMessage", "()Ljava/lang/String;", &[])
.unwrap()
.l()
.unwrap();
let message_string = message.into();
let msg_rust: String = env.get_string(&message_string).unwrap().into();
println!("{}", msg_rust);
// Since panics result in multi-line messages which include the backtrace, just use the
// first line.
assert_starts_with!(msg_rust, expected_message);
}
}