jni-rs: 0.21 mutability requirements on `JNIEnv` methods break use of `catch_unwind`
Prior to 0.21 it was possible to wrap calls to JNIEnv methods in a closure to catch_unwind, allowing graceful handling of panics to raise Java exceptions. The UnwindSafe trait is not automatically implemented for &mut T meaning that the new mutability requirements on many of the JNIEnv methods have made this approach invalid.
Here’s a basic example of how I use catch_unwind that now won’t compile:
#[no_mangle]
pub extern "system" fn Java_HelloWorld_hello<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
input: JString<'local>,
) -> JString<'local> {
match std::panic::catch_unwind(|| {
let input: String = env
.get_string(&input) // requires &mut JNIEnv
.expect("Couldn't get java string!")
.into();
let output = env
.new_string(format!("Hello, {}!", input))
.expect("Couldn't create java string!");
output
}) {
Ok(v) => v,
Err(e) => todo!("throw Java exception")
}
}
I’m fairly new to JNI, but I believe it’s standard practice to catch panics this way when working with it. I know you can leverage AssertUnwindSafe to assert UnwindSafe but it’s not clear whether it’s safe to do so for &mut JNIEnv.
Finally, apologies if I’ve missed something in the docs or the migration guide, but I can’t seem to find anything about handling panics.
Is it safe to use AssertUnwindSafe with &mut JNIEnv? For example, the following compiles but I don’t know if it’s actually safe (fyi I’m not familiar with this approach but I’d prefer not to wrap the entire closure):
#[no_mangle]
pub extern "system" fn Java_HelloWorld_hello<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
input: JString<'local>,
) -> JString<'local> {
match std::panic::catch_unwind(AssertUnwindSafe(|| {
let input: String = env
.get_string(&input) // requires &mut JNIEnv
.expect("Couldn't get java string!")
.into();
let output = env
.new_string(format!("Hello, {}!", input))
.expect("Couldn't create java string!");
output
})) {
Ok(v) => v,
Err(e) => todo!("throw Java exception")
}
}
I’m not sure if it’s possible to manually impl UnwindSafe for &mut JNIEnv but is there an issue/feature to add this, or is there a different approach entirely?
Thanks for your time!
About this issue
- Original URL
- State: open
- Created a year ago
- Reactions: 3
- Comments: 16 (10 by maintainers)
Commits related to this issue
- Upgrade jni crate to 0.20.0 The 0.21.X crates feature a major refactor that breaks the code. Don't upgrade to them until some issues are resolved. (See https://github.com/jni-rs/jni-rs/issues/432 for... — committed to droark/ed25519-zebra by droark a year ago
- Add DER & PEM support for SigningKeySeed and VerificationKeyBytes (RFC 8410) (#46) * Add DER & PEM support for SigningKeySeed and VerificationKeyBytes (RFC 8410) - Add encoding to and decoding fro... — committed to ZcashFoundation/ed25519-zebra by droark a year ago
- Removes the Options around the function pointers None of the JNI 1.1 function pointers were optional, and neither are any of the function pointers in later versions of the spec. Having the `Option` ... — committed to jni-rs/jni-sys by rib a year ago
- Removes the Options around the function pointers None of the JNI 1.1 function pointers were optional, and neither are any of the function pointers in later versions of the spec. Having the `Option` ... — committed to jni-rs/jni-sys by rib a year ago
- Removes the Options around the function pointers None of the JNI 1.1 function pointers were optional, and neither are any of the function pointers in later versions of the spec. Having the `Option` ... — committed to jni-rs/jni-sys by rib a year ago
- Removes the Options around the function pointers None of the JNI 1.1 function pointers were optional, and neither are any of the function pointers in later versions of the spec. Having the `Option` ... — committed to jni-rs/jni-sys by rib a year ago
- Removes the Options around the function pointers None of the JNI 1.1 function pointers were optional, and neither are any of the function pointers in later versions of the spec. Having the `Option` ... — committed to jni-rs/jni-sys by rib a year ago
- Removes the Options around the function pointers None of the JNI 1.1 function pointers were optional, and neither are any of the function pointers in later versions of the spec. Having the `Option` ... — committed to jni-rs/jni-sys by rib a year ago
- Removes the Options around the function pointers None of the JNI 1.1 function pointers were optional, and neither are any of the function pointers in later versions of the spec. Having the `Option` ... — committed to jni-rs/jni-sys by rib a year ago
Right, this is an unfortunate side effect of the changes made for 0.21, sorry about that. More than once now I’ve found myself compare this project to a game of whack a mole when it comes to addressing safety concerns.
As you say, you can currently use
AssertUnwindSafeThe
JNIEnvis really just a thin wrapper over a pointer to a table of function pointers managed by JNI / the JVM implementation.As far as Rust is concerned this isn’t really stateful (so you shouldn’t be able to observe an inconsistent state for this table due to a panic).
The design allows the JVM to provide a different table of functions for different threads but I’ve never seen anything to suggest that JNI functions would ever try and dynamically fiddle with that table of pointers on the fly - and even if it did it would still have to remain consistent from the pov of Rust code.
The JVM can store private thread-local storage within the JNIEnv but that also has to always remain consistent from the pov of Rust code. It might be mutated within the JNI/JVM implementation but that code won’t panic / unwind.
At the very least the new
&mut JNIEnvis as unwind safe as the previous&JNIEnvwas.One thing I’ve wondered about briefly is whether the
jnicrate can provide some built-in helpers for catching panics and re-throwing as Java exceptions, similar towith_local_frame. Maybe this would be able to improve the ergonomics here.Although the closure would still need to be passed a
&mut JNIEnvargument we could at least deal with wrapping it withinAssertUnwindSafeautomatically. I’m not quite sure what the ergonomics of that would feel like without trying it out.I’ve also been thinking about this from the pov of trying to make
JNIEnvstateful - whereby it wouldn’t just be a thin wrapper over the JNI C pointer but would instead be our own struct that letsjni-rscache certain things like method IDs for commonly used types. Right now the utility APIs for things likeJListorJMaparen’t ideal at all because they have to repeatedly query the method IDs for those classes since we have nowhere to cache them.If we were to introduce a
StatefulJNIEnvtype of some kind then it would be good if we had a built-in API for handling catching panics (+ re-throwing exceptions) that could always be used consistently to map from a rawJNIEnvpassed to a native method to a closure that would be passed aStatefulJNIEnv.Right now there isn’t another issue tracking this, so thanks for bringing this up.
Sorry that it’s not a great answer for now.
I want to handle panics and errors consistently and without boiler plate code; I simply want to throw Java exceptions and print to stdout if that fails. I think this is probably befitting to what most people want to do, so for what it’s worth I put the below traits and implementations together. I toyed with putting this on crates.io or submitting a PR but I didn’t want to start something that might deviate from this project or cause you problems.
Note, it’s not properly tested! Any feedback would be welcome and appreciated. This approach (especially the dummy value trait) is heavily inspired by libsingal.
And usage:
No need to apologise! I understand the whack-a-mole analogy completely. The 0.21 update was mostly great to see from my perspective, having more guarantees surrounding lifetimes is a good thing in FFIs.
By the sounds of it, AssertUnwind is safe to use so I’ll roll with that for now.
I’ll update this thread should anything strange come up. I think it’s probably worth popping something in the docs regarding this for those trying to catch panics in this way.
@AlexKorn: Yes, that’s legal.
Perfect, yeah that’s the kind of thing I’d been imagining but hadn’t considered the idea of making it a trait like this. I wasn’t sure off the top of my head whether it was going to be possible to move the
envinto thecatch_unwindclosure viaAssertUnwindSafebut effectively hide that and unwrap when calling the user’s closure but it looks like that works fine - nice.It seems like it would be helpful to have something like this just be part of the
jniAPI, out of the box, ideally, since pretty much any code that’s implementing a native Java method needs to do something like this to stop panics crossing the FFI boundary.I like the idea of making it a trait like this so it’s possible to just override how you map an
Errororpanicinto an exception that’s appropriate for your app / library.Regarding the vague ideas around having some kind of stateful
JNIEnv, that’s not something I’m investigating / prioritizing atm, so I’m not going to worry about how that might potentially interact with this for now, since there’s nothing concrete to consider yet.It would be great if you’d be able to even create a PR for this considering that it looks like you’ve done most of the work already.
I’d maybe call the “Dummy” values “Default” values from the pov that it’s conceptually like you’re passing back default initialized values. “Default” matches the Java and Rust parlance here.
Actually, we already implement the
Defaulttrait for types likeJObject,JClass,JStringetc so can we just rely on theDefaulttrait instead of needing the dummy types here?