Getting started

RJNI takes a built jar and produces a protected jar. It does not touch your build process beyond that one extra step. Run it after your normal package or assemble step, before you ship the artifact.

rjni protect --in build/libs/app.jar --out build/libs/app-protected.jar

The protected jar is a drop-in replacement: same entry point, same public behavior. It runs on an ordinary JVM. Nothing about how you deploy or launch it changes.

How it works

Every eligible method is translated to a Rust intermediate representation, compiled to a native cdylib, and relinked into your class through JNI's RegisterNatives. The method's bytecode is gone from the jar. A decompiler sees a native method declaration with no body. On top of that, a chain of independent protection passes runs: name obfuscation, string encryption, control-flow flattening, decompiler crashers, anti-tamper checksums, and anti-debug detection. Each pass is self-contained, so a class that opts out of nativization can still be renamed and have its strings encrypted.

Exclusions and keep rules

Not everything can be safely translated or renamed. RJNI ships pluggable exclusion profiles for the common cases: reflection by name (Class.forName), ServiceLoader provider lookups, JPMS module-info descriptors, and common proxy-framework internals (Spring AOP, CGLIB and ByteBuddy-style subclassing). A class or method a profile excludes is left untouched, written through to the output jar byte for byte.

If your project needs something the built-in profiles do not cover, add your own keep rules:

rjni protect --in app.jar --out app-protected.jar --keep "com.example.plugins.**"

Platform support

Class file major versions 52 through 70 (JDK 8 through 26) are supported. Native library targets:

  • macOS: Apple Silicon (stable), Intel (stable)
  • Windows: x86-64 (stable)
  • Linux: x86-64 (stable), ARM64 (experimental)

One shared native library is built per platform and architecture combination in your build matrix and bundled into the output jar as a resource. The correct one loads automatically at runtime based on the host it runs on.

Trial mode

Pass --trial to protect only a deterministic subset of eligible classes instead of the whole jar: enough to see the pipeline working end to end on a real build before fully committing.

rjni protect --in app.jar --out app-trial.jar --trial 0.2

Known limitations

Documented directly, not left implicit:

  • A class with any external (JDK or third-party library) supertype or interface only gets the conservative, private-members-only renaming slice. Not a bug, a deliberate correctness boundary.
  • Method and field names, and JVM type descriptors, for nativized methods are structurally visible in the compiled native library. RegisterNatives is a name-based API and there is no way around that without abandoning JNI entirely.
  • A handful of bytecode shapes are not yet supported and are cleanly rejected rather than mistranslated. See the changelog for the current list.