RJNI translates real JVM bytecode into a Rust cdylib and relinks it back in
through JNI, then obfuscates whatever is left. A decompiler does not see confusing code.
It sees a method with no body at all.
Each protection feature is its own pass, chained in order. Nothing below is planned. Every row runs in the current build.
Eligible methods are translated to Rust, compiled to a real cdylib, and
relinked through RegisterNatives. The method body is not in the jar anymore.
Classes, and non-private methods and fields wherever a hierarchy is provably self-contained, get short opaque names, not your original API surface.
String constants are encrypted per build and decrypted at runtime. A raw scan of the jar or the compiled native library does not recover them.
Eligible methods get their control flow rewritten into an encrypted dispatch table, defeating a plain reconstruction of the original graph.
Bytecode shapes specifically chosen to make common decompilers fail or produce garbage, not just harder to follow.
Per-class checksums catch a patched class file at load time. A JDWP check catches an attached debugger. Both fail loudly, not silently.
Bundled resources loaded through getResourceAsStream are transparently
encrypted and decrypted at the call site.
The injected runtime bootstrap classes are renamed and protected through the same pipeline as your own code, not left as an unobfuscated giveaway.
This is a tier-one protection posture. It raises the bar for a casual to moderate attacker. It is not a claim of unbreakable, and nothing below pretends otherwise.
| Threat | Effect |
|---|---|
| Casual decompilation (CFR, Fernflower, JD-GUI) | DEFEATED no method body to decompile, several decompilers crash outright on the surrounding shell |
| Raw scan for string literals | DEFEATED string constants are encrypted, verified against the actual compiled output |
| Class, method, and field name reading | DEFEATED wherever a hierarchy is self-contained; JDK-facing names are conservatively left alone rather than risk breaking your program |
| Casual bytecode patching | DEFEATED anti-tamper checksums fail loudly at load time |
| An attached debugger (JDWP) | DEFEATED detected, fails loudly |
| A determined reverse engineer with the compiled library and unlimited time | OUT OF SCOPE see the FAQ |
| An attacker who reimplements this exact loader logic | PARTIAL raises the bar, does not remove the seam. Same posture as every native-code obfuscator on the market |
native-image compiles your whole application ahead of time into one standalone executable. It is a deployment and startup-time tool with real constraints on reflection, dynamic class loading, and JVM feature coverage. RJNI does the opposite trade: your jar stays a normal jar on a normal JVM, and only the methods safe to translate get nativized. Reflection, dynamic loading, your existing deployment: all untouched. native-image aims for faster startup and a smaller footprint. RJNI aims to make compiled logic hard to read while the jar keeps behaving exactly like a jar.
No, and it is not trying to. Crossing the JNI boundary has a real, mostly fixed cost per call. A trivial method that is mostly overhead can see a real slowdown; a method doing meaningful work sees a much smaller relative cost. Treat any speed change as a side effect, not the point.
Class file major versions 52 through 70: JDK 8 through JDK 26.
RJNI ships pluggable exclusion profiles for the common cases: reflection by name,
ServiceLoader, JPMS module descriptors, and common proxy-framework internals.
Matched code is excluded from nativization and renaming rather than silently broken. Custom
keep rules cover anything the built-in profiles do not.
Windows, macOS (Intel and Apple Silicon), and Linux x86-64/ARM64. Coverage is tiered honestly in the docs: stable versus experimental, not one blanket claim.
The output is not signed. RJNI ships a jar with a bundled native library, loaded
in-process through System.load() from already-running JVM code. The
shell-launched-executable warning that Windows SmartScreen and macOS Gatekeeper apply does
not target this shape. The one real exception is macOS quarantine on a browser-downloaded
jar, which has a documented one-line workaround.