Internals
How the generated code works and why.
Tracking the JNI environment
Representing Java objects
Java objects are represented by a dummy struct:
pub struct MyObject {
_dummy: ()
}
which implements the JavaObject
trait:
unsafe impl JavaObject for MyObject { }
References to java objects
This unsafe impl asserts that every reference &MyObject
is actually a sys::jobject
. This allows us to create a sys::jobject
simply by casting the &MyObject
. We maintain that invariant by never allowing users to own a MyObject
directly; they can only get various kinds of pointers to MyObject
types (covered below).
Given a reference &'l MyObject
, the lifetime 'l
is tied to the JVM's "local frame" length. If this Rust code is being invoked via the JNI, then 'l
is the duration of the outermost JNI call.
Important: Our design does not support nested local frames and thus we don't expose those in our API. This simplifying assumption means that we can connect the lifetimes of local variables to one another, rather than having to tie them back to some jni
context.
Local
Java objects
Whenever we invoke a JNI method, or execute a construct, it creates a new local handle. These are returned to the user as a Local<'jni, MyObject>
struct, where the 'jni
is (again) the lifetime of the local frame. Internally, the Local
struct is actually just a jobject
pointer, though we cast it to *mut MyObject
; it supports deref to &'jni MyObject
in the natural way. Note that this maintains the representation invariant for &MyObject
(i.e., it is still a jobject pointer).
Local
has a Drop
impl that deletes the local handle. This is important because there is a limit to the number of references you can have in the JNI, so you may have to ensure that you drop locals in a timely fashion. Also note that all JNI function calls that return Java objects implicitly create a local ref!
Global
Java objects
The jdk
object offers a method to create a Global reference a Java object. Global references can outlive the current frame. They are represented by a Java<MyObject>
type, which is a newtype'd sys::jobject
as well that represents a global handle. This type has a Drop
impl which deletes the global reference and supports Deref
in the same way as Local
.
null
The underlying sys::jobject
can be null, but we maintain the invariant that this is never the case, instead using Option<&R>
etc.
Exceptions
The JNI exposes Java exception state via
ExceptionCheck()
returningtrue
if an unhandled exception has been thrownExceptionOccurred()
returning a local reference to the thrown objectExceptionClear()
clearing the exception (if any)
If an exception has occurred and isn't cleared before the next JNI call, the invoked Java code will immediately "see" the exception. Since this can cause an exception to propagate outside of the normal stack bubble-up, we must always call duchess::EnvPtr::check_exception()?
after any JNI call that could throw. It will return Err(duchess::Error::Thrown)
if one has occurred. The duchess::EnvPtr::invoke()
will both ensure the exception check occurred and that it was done in a way that any created local ref will be dropped correctly.
Frequently asked questions
Covers various bits of rationale.
Why do you not supported nested frames in the JNI?
We do not want users to have to supply a context object on every method call, so instead we take the lifetime of the returned java reference and tie it to the inputs:
// from Java, and ignoring exceptions / null for clarity:
//
// class MyObject { ReturnType some_method(); }
impl MyObject {
pub fn some_method<'jvm>(&'jvm self) -> Local<'jvm, ReturnType> {
// ---- ----
// Lifetime in the return is derived from `self`.
...
}
}
This implies though that every
We have a conflict:
- Either we make every method take a jdk pointer context.
- Or... we go into a suspended mode...
MyObject::new(x, y, z)
.execute(jdk);
MyObject::new(x, y, z)
.blah(something)
.blah(somethingElse)
.execute(jdk);
MyObject::new(x, y, z)
.blah(something)
.blah(somethingElse)
.map(|x| {
x.someMethod()
})
.execute(jdk);
...this can start by compiling to jdk calls... and then later we can generate byte code and a custom class, no?
If we supported nested frames, we would have to always take a "context" object and use that to derive the lifetime of each Local<'l, MyObject>
reference. But that is annoying for users, who then have to add an artificial seeming environment as a parameter to various operations. (As it is, we still need it for static methods and constructors, which is unfortunate.)