jni-simple
This crate contains a simple dumb handwritten rust binding for the JNI (Java Native Interface) API. It does absolutely no magic around the JNI Calls and lets you just use them as you would in C.
In addition to JNI, this crate also provides a similar simplistic binding for the JVMTI (Java VM Tool Interface) API. The JVMTI Api can be used to write, for example, a Java Agent (like a Java debugger) in Rust or perform similar deep instrumentation with the JVM. Most applications do not need JVMTI.
Examples
Loading a JVM from a shared object file or dll
Note: this example assumes the loadjvm feature is enabled!
use ;
use null;
Writing a JNI shared library that implements a native method
The complete version of this example (including the java code) can be found in the repository inside the example_project folder.
Note: this example assumes the dynlink feature is enabled!
use Write;
use ;
use ;
use ;
use thread;
use Duration;
use stdout;
pub unsafe extern "system"
//Would be called from java. the signature in java is org.example.JNITest#test()
pub unsafe extern "system"
Writing a Java Agent (JVMTI)
Note: this example assumes the dynlink feature is enabled!
= ["cdylib"]
use ;
use c_void;
use *;
unsafe extern "system"
Using JVMTI from JNI
Note: It's probably not a good idea to do this in productive code, but can be useful in some debugging situations.
Note: this example assumes the dynlink feature is enabled!
use c_void;
use ;
//This could also be an ordinary native method invoked from java.
pub unsafe extern "system"
Main goals of this crate
Dont pretend that JNI is "safe"
JNI is inherently unsafe (from a rust point of view), and any attempt to enforce safety will lead to performance or API complexity issues. All JNI methods provided by this crate are marked as "unsafe" as they should be.
Simple type system of JNI is kept as is
All types like jobject, jstring, jarray,... which are opaque handles represented as pointers in C are represented as raw opaque pointers in Rust that are type aliases of each other. This essentially makes them just hint to the user and doesn't enforce any type safety as that would sometimes be a big hindrance when working with JNI.
Why?
For example, conversion between jobject and jobjectArray or any other array type is required for some jni methods and trivial to do in C,
yet using the existing jni crate this requires convoluted casts that probably have side effects (like calling IsInstanceOf) for 'safety' reasons.
Using jni-simple (this crate) you don't even need a cast. Naturally, passing the wrong type of object to the JVM is UB, hence the need for unsafe.
Designed for runtime dynamic linking of the JVM
The Problem: The existing jni crate depends on the jni-sys crate which requires the JVM to be resolvable by the dynamic linker. There are 2 ways to do this. The first is to statically link the JVM into the binary, this is rarely done, very cumbersome and poorly documented. The other is to provide the JVM on the linker path so ldd can find it, but I have never seen this occur in the real world either.
This crate is developed for the more common use case that the JVM is available somewhere on the system. It leaves it up to the user of the crate to write the necessary code to find and load the JVM.
This allows for maximum flexibility when writing a launcher app which, for example,
may first download and extract a JVM from the internet.
As should be obvious, when writing a native library (cdylib) that does not launch the JVM itself and
is instead loaded by System.load or System.loadLibrary then this is irrelevant.
Supported Targets
Targets that are tested with GitHub CI
- linux: aarch64 and x86_64 gnu-abi
- windows: aarch64 and x86_64 msvc.
- macOS: aarch64 and x86_64
- freebsd: x86_64
- netbsd: x86_64
Targets that are known to work
- windows: x86_64 gnu target
Targets that are likely to work
- linux: any other architecture that java supports
- linux: musl-abi, but you MUST compile your application as a dynamically linked application. This will likely require setting specific rust compiler flags.
- windows: i686 gnu or msvc
- freebsd: any other architecture that supports java
- netbsd: any other architecture that supports java
- openbsd: any architecture that supports java (launching the JVM probably won't work due to openbsd's W^X)
Targets where I recommend careful testing
Any Big Endian target, especially if you intend to use JVMTI.
Supported Java Versions
In general, JNI and JVMTI are designed in a version agnostic manner; this means that jni-simple should work with nearly any version of Java. If a newer version of Java adds new functionality to JNI or JVMTI, then jni-simple will require an update to support that new functionality; however, existing functions should just work like they did in the previous versions.
Java Versions that are tested with GitHub CI
- java 8
- java 11
- java 17
- java 21
- java 25
Test coverage and maturity
JNI
Test coverage is near 100%.
This means that all functions are at least called by a test, and for some even most edge cases are tested.
The implementation is mature and has been used in production without any known issues.
JVMTI
Test coverage is very low.
The overall maturity of the JVMTI Api provided in the current version of jni-simple is low. Expect bugs that will cause undefined behavior in functions that I have not yet tested. (due to human error when manually translating the jvmti specification into rust code)
I have mostly prepared this JVMTI implementation ahead of time for when I need it, so it may take some time until everything is tested and fixed. Naturally, this has no impact on JNI and should not affect many people as JVMTI is not needed for 99% of programs.
If you encounter a bug and need an urgent fix, then open up an issue on GitHub or make a pull request.
Naturally, if you wish to submit tests, then that would also be appreciated.
Features
std
This feature is enabled by default! Adds support for some types in the rust standard library. It also adds a few optional convenience functions.
If you wish to compile your library/launcher app without the rust standard library, then disable default features.
loadjvm
This feature is not enabled by default!
This feature provides functions to dynamically link the jvm using the libloading crate
from a string containing the absolute path to libjvm.so or jvm.dll.
Note: If you do not want to use the libloading crate but still start the JVM then there are methods provided to
load the JVM from a pointer to JNI_CreateJavaVM function instead of a dll/so file.
Do that if you want to do dynamic linking yourself using dlopen or LoadLibraryA for example.
Note: This feature should not be used when writing a library that is loaded by System.load or System.loadLibrary.
It would just add a dependency that is not needed.
dynlink
This feature is not enabled by default!
If this feature is enabled, it is assumed that the symbols exported by the JVM can be found by the dynamic linker by itself. When this feature is enabled, all functions that "load" the jvm become a noop.
This feature should be enabled when making a shared library that is loaded by the JVM using System.load, System.loadLibrary,
or when writing a java agent that is loaded by specifying the -agentlib VM argument when starting the VM
either from the command line or the JNI Invoker interface.
In those cases, the dynamic linker is guaranteed to be able to find the symbols exported by the JVM.
Note: This feature should not be used when writing a jvm launcher application.
Note: Enabling both dynlink and loadjvm makes no sense, it just adds the libloading crate as an unnecessary dependency.
asserts
This feature is not enabled by default!
This feature enables assertions in the code. This is useful for debugging and testing purposes. These checks will cause a big performance hit and should not be used in production builds.
I would not even recommend using this feature for normal debugging builds unless you are specifically debugging issues that occur in relation to UB with the JNI interface. There is no need to enable this feature when you are just debugging a problem that occurs in pure rust code.
Enabling this feature for automated unit/integration tests that perform complicated JNI calls can be a good idea if you can tolerate the performance penalty of enabling this feature. Enabling this feature for benchmarks is not recommended as it falsifies the results of the benchmark.
This feature should NOT be used with the jvm launch option -Xcheck:jni
as the assertions contain calls to env.ExceptionCheck() before nearly every normal call to the jvm,
which will fool the JVM into thinking that your user code checks for exceptions, which it may not do.
It will also generate many false negatives as some 'assertion' code does not call env.ExceptionCheck()
as the only realistic scenario for some calls that are made to fail is for the JVM to run out of memory.
I recommend using this feature before or after you have tested your code with -Xcheck:jni depending
on what problem your troubleshooting. The assertions are generally much better at detecting things like null pointers
or invalid parameters than the JVM checks, while the JVM checks are able to catch missing exception checks or JVM Local Stack overflows better.
The asserts are implemented using rust panics.
If you compile your project with unwinding panics, the panics can be caught.
It is not recommended to continue or try to "recover" from these panics as the
assertions do NOT perform cleanup actions when a panic occurs.
You will leak JVM locals or leave the JVM in an otherwise unrecoverable state if you continue.
I recommend aborting the processes on such a panic as such a panic only occurs
if the rust code had triggered UB in the JVM in the absence of the assertions.
This can either be done by calling abort when "catching" the panic or compiling your rust code with panic=abort
if you do not need to catch panics anywhere in your rust code.
Info: There are no asserts present in the JVMTI implementation as JVMTI has more robust checking implemented in the java vm itself, forgoing the need to do any checks ourselves. Most of the potential for UB in JVMTI is impossible to check anyway, because the API heavily relies on callbacks which are passed to JVMTI as raw function pointers. Checking those for validity is not really possible without changing the API at a higher level, which is not a goal of this crate. In addition to that, jvmti is not needed for most use cases, so I suspect that it will see little use.
Info: The asserts perform some best effort checks that verify that misuse of critical arrays or strings occurs. These checks require the use of thread local which is only available with the std feature. Should the std feature be disabled, then those checks are not done.
Further Info
ABI for bool, jboolean etc
Thanks to Robin Stenvi (https://github.com/rstenvi) for making me aware of this. See https://github.com/AlexanderSchuetz97/jni-simple/pull/5
Basically JNI's bool type is an u8 where 0 is false and 1 true. 2-255 are also possible values which may be evaluated as true on some JVM implementations. It is unclear how values 2-255 behave for JNI C programs since if they compare jboolean's to the constant JNI_TRUE defined in jni.h then those values would also be false. However, they are NEVER undefined behavior in C (at least in the jni library). The JNI specification explicitly mentions that the type for jboolean is "unsigned char" in c. (see jni.h)
This is not ABI compatible with rust's bool type at all. Rusts bool type is forced to be 1 byte in size and be either 0 or 1. LLVM optimizes for this and setting a rust bool byte to 2 for example will cause undefined behavior in rust.
On some architectures the return ABI is different. I.e. on
AARCH64 the android jvm apparently does not clear the return register
and only clears the lower 8 bits of the return register to for example indicate false.
Rusts bool however would have a different behavior. LLVM expects that the register
was fully cleared and not just the lower 8 bits set to 0 in this case.
This difference would cause UB in the Rust program.
core::ffi::c_uchar has a fully compatible abi and jni-simple uses that for all fii calls that involve booleans.
Because of this, rusts bool type can only be used as input/parameter to a jni function call without causing any problems but
any jboolean handed out by the jvm to Rust code must either be treated as a core::ffi::c_uchar or
explicitly narrowed to a rust boolean by doing a "jboolean != 0" narrowing.
jni-simple will do this automatically for you:
- Methods that simply return a boolean will automatically narrow the jboolean to a rust boolean.
JNIEnv::ExceptionCheckfor example does this.JNIEnv::CallStaticBooleanMethodAand its friends also do this.
- Methods that accepts a mutable boolean pointer to return some flag to you automatically narrow the jboolean to a rust boolean.
Writes to that pointer, if it is a *mut bool pointer, are "intercepted" and automatically narrowed to a rust bool.
JNIEnv::GetIntArrayElements- for example has a "is_copy" boolean pointer parameter.
- Methods that accept a const ptr to a boolean as input will accept a const ptr to bool or jboolean.
JNIEnv::SetBooleanArrayRegionfor example does this
- Methods that read data into a rust owned pointer cannot be used with rusts native bool type.
JNIEnv::GetBooleanArrayRegionfor example does this- A convenience method called
GetBooleanArrayRegion_as_bool_vecwhich performs narrowing, in this case toVec<bool>exists in such cases.
- A convenience method called
Behavior of jboolean values that are not 0 or 1.
The hotspot jvm on x86_64 appears to treat jboolean values that are not 0 or 1 inconsistently. For branching in java the JVM appears to evaluate all values that are not 0 as true. When returning a boolean to native code it considers all values that have their least signficant bit set as true. (The JVM basically appears to do "AND 0x1" on the values before it returns them)
It appears the behavior is entirely implementation dependant. It is completely unclear how different jvm implementations such as Android JVM's handle this.
jni-simple treats any boolean value that is non 0 as true when performing narrowing.
When the asserts feature is enabled jni-simple will treat an attempt to set a boolean value
to a non 0 or 1 value as an error. I would recommend avoiding storing
any non 0 or 1 value in a boolean field or array.
String handling
Many JNI functions accept a zero-terminated utf-8 string as a parameter. It is possible to call all those functions with all commonly used rust string types as well as raw *const c_char pointers.
The following types are supported:
- &str
- String
- &String
- &Cow<&str>
- CString
- &CString
- &CStr
- &OsStr
- OsString
- &OsString
- &[u8]
- Vec<u8>
- * const c_char
- * const u8
- * const i8
- * mut c_char
- * mut u8
- * mut i8
Because JNI expects a zero terminated string, some types are copied if necessary, and a zero byte is appended. Raw pointer types are assumed to already be zero terminated and are passed as is. If a string already contains a 0-byte, then it is not copied and passed to jni as is. Should the 0-byte be somewhere in the middle of the string, then this 0-byte effectively truncates the string at that point for the jni call.
Example:
use null;
use c_char;
use ;
pub unsafe extern "system"
Beware of Strings that contain supplementary characters.
Java uses a "modified" utf-8 encoding (Sometimes it is called CESU-8 encoding) that is different from rusts utf-8 encoding for supplementary characters. All functions that accept a 0-terminated utf-8 string are implemented in the jvm to use the "modified" utf-8 encoding. This crate makes no attempt at re-encoding the strings to the "modified" utf-8 encoding, mainly because it only matters if your string contains non-language symbols. (Such as Emoji characters) Even if by accident (or user input) it does contain such a symbol, then that does NOT appear to cause UB in the jvm. The string simply will contain "random" characters instead of the characters that it should contain.
If this is relevant to you, then I recommend using the functions that accept a jstring instead of a 0-terminated utf-8 string. A jstring can be constructed from and turned into an utf-16 array which does properly handle all supplementary characters. Convenience functions exist to transform String and its friends to jstring and back via this utf-16 transformation. However, since those functions have to perform re-encoding of Strings as well as allocation of the required buffers to do so, they are most likely slower than their utf-8/"modified" utf-8 counterparts.
Variadic up-calls
Currently, variadic up-calls into JVM code are only implemented for 0 to 3 parameters. (Do not confuse this with java "Variadic" methods, for JNI they are just a single parameter that is an Array)
JNI provides 3 ways of up-calling into JVM code:
- CallStatic(TYPE)Method(class, methodID, ...)
- CallStatic(TYPE)MethodA(class, methodID, jtype*)
- CallStatic(TYPE)MethodV(class, methodID, va_list)
Substitute (TYPE) for the return type of the java method called. For example, "Object" or "Int".
This crate only implements variants 1 and 2. This crate does not implement variant 3 because "va_list" cannot be created inside rust code.
Variant 2 is relatively straight forward and fully supported by this crate. This means you can call ANY java method using Variant 2.
Variant 1 has a Variadic parameter. This is what Variadic up-calls refer to. Rust does support this but only for explicit extern "C" functions and not for any functions implemented in rust itself. To call Variant 1 this crate provides concrete implementations to call this Variadic function with 0, 1, 2 and 3 parameters. This should cover 99% of your up-call needs. To call methods with more than 3 parameters, simply use Variant 2.
As you can see below, calling Variant 2 is a bit unwieldy, so it is probably the better choice to use Variant 1 of up-calling for most smaller functions. Example:
use null;
use ;
pub unsafe extern "system"