This project helps annotate a Rust/Java JNI layer to statically check that it agrees. It allows you to check:
- Method names
- Method signatures (arity, parameter types)
- Ownership of
long-as-pointer handles
You annotate the Java side with @Owned / @Ref / @Mut (from jnisafe-annotations), implement the natives in Rust with the jnisafe wrapper types, and run jnisafe-check to verify the two sides match before they fail at runtime.
// Java: Annotate pointer types
private static native @Owned("Box<String>") long create(String value);
private static native String tryGet(@Ref("Box<String>") long ptr);// Rust: jnisafe-check verifies these line up with the Java declarations above
#[no_mangle]
pub extern "system" fn Java_example_HandWritten_create(/* … */) -> JOwned<Box<String>> { /* … */ }Can you get around it? Sure. But it's better than nothing.
On top of the static check, the jnisafe wrapper types add debug-only
runtime validation: in debug builds they track every live handle and catch a
wrong-type, freed, double-freed, or bogus long the moment Java passes it back
(surfaced as a Java exception), plus a checked borrow_mut() for mutable
aliasing. It compiles out entirely in release — see the jnisafe README.
cargo install jnisafe-checkDepend on jnisafe-annotations from Maven Central:
<dependency>
<groupId>io.github.mailmindlin</groupId>
<artifactId>jnisafe-annotations</artifactId>
<version>0.1.0</version>
</dependency>Then annotate the pointer parameters and return values of your native methods:
import io.github.mailmindlin.jnisafe.Mut;
import io.github.mailmindlin.jnisafe.Owned;
import io.github.mailmindlin.jnisafe.Ref;
public class Counter {
// create() hands ownership of the pointer to Java
private static native @Owned("Box<u64>") long create();
// get() borrows it immutably, set() borrows it mutably
private static native long get(@Ref("Box<u64>") long ptr);
private static native void set(@Mut("Box<u64>") long ptr, long value);
// drop() takes ownership back so Rust can free it
private static native void drop(@Owned("Box<u64>") long ptr);
}Add jnisafe to your crate and use JOwned / JRef / JMut in place of raw longs.
See example/rust for a complete, compiling implementation.
jnisafe-check also understands natives declared with the jni crate's macros, so
you keep the static check whichever style you adopt:
-
#[jni_mangle("pkg.Class")]— the function keeps its real signature, soJOwned/JRef/JMutwork transparently, exactly as in a hand-writtenJava_*export. The Java method name is derived from the Rust fn name (set_value→setValue). Seemangle.rs. -
native_method! { … }andbind_java_type! { native_methods { … } }— carry an owned handle through the macro with atype_mapentry that maps it tolong:native_method! { java_type = "example.Foo", type_map = { unsafe JOwned<Box<String>> => long }, static fn create(value: JString) -> JOwned<Box<String>>, }
The
unsafemapping asserts the handle is layout-compatible withjlong(it is — the jnisafe types are#[repr(transparent)]over ajlong), and the macro emits the realJOwned<…>type into your impl. Seenative_method.rsandbind_type.rs.Borrowed handles (
JRef/JMut) carry a'locallifetime that can't be named from these macros'const-evaluated context, so use#[jni_mangle]for borrow parameters;native_method!/bind_java_type!cover owned handles and plain JNI types. -
Overloaded natives are handled too. When a class declares two natives of the same name, both the JVM and the checker mangle each to the long
Java_pkg_Class_name__<args>form; the checker derives the Rust-side argument descriptor from your signatures so the two overloads pair up cleanly. Seeoverloaded.rs. -
Custom object types. Built-in wrappers (
JString,JObject,JByteBuffer, …) map to their Java class automatically. A user wrapper type gets its class from thebind_java_type! { pub JPose => "com.example.Pose", … }header, so it resolves wherever it appears in a native signature — as a bare parameter/return or as aJObjectArrayelement:JObjectArray<'local, JPose<'local>>checks against a JavaPose[]([Lcom/example/Pose;). Resolution is independent of which file declares the binding. -
bind_java_type! { methods { … } fields { … } constructors { … } }— the Rust→Java direction: these clauses generate type-safe wrappers that call into Java. The checker verifies each named method/field/constructor exists on the bound class with a matching receiver (static/instance) and JVM descriptor — a missing or mis-typed member is reported (E040–E044) instead of failing at runtime. If the bound class isn't passed to--java, the binding can't be verified and a warning (W004) is emitted. Seebind_type.rs. -
Handle fields. A jnisafe smart pointer can also live in a Java object — a
longfield holding aJOwned/JRef/JMuthandle across calls. Reconstruct it on the Rust side with thefrom_raw/into_rawprimitives, and store the field on the Java side guarded by theNativeObjectread/write-lock pattern so every access reaches Rust as a parameter under a held lock (seeNativeObject.java,Document.java, anddocument.rs). When alongfield is declared as a handle inbind_java_type!'sfields { … }, the checker cross-checks it against the field's@Owned/@Ref/@Mutannotation: a mismatched pointee or kind is E045, and a handle field with no annotation is W005.
jnisafe-check \
--rust-crate path/to/your/crate \
--java path/to/classes-or.jar--java accepts a .class file, a directory of classes, or a .jar, and is repeatable.
Add [--format human|json] and [--quiet] to control output.
Exit codes: 0 clean, 1 mismatches found, 3 internal error.
Binding to JDK stdlib types. When a bind_java_type! binding targets a JDK
class you don't pass on --java (e.g. java.nio.ByteBuffer), point the checker at
a JDK with --java-home (it defaults to $JAVA_HOME). The referenced classes and
their supertypes are resolved from the JDK's jmods/ (Java 9+) or rt.jar
(Java 8) — so inherited members are checked too — which needs a full JDK, not a
JRE. Without a usable JDK home, such bindings can't be verified and are reported
as W004 instead.
Wire this into CI to catch signature/ownership drift before it becomes a runtime crash.
To build the workspace, run the test suite, or publish releases, see development.md.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/license/mit)
at your option.