Java .jar to native library transpiler and repacker for JNI-based deployment.
The tool runs an IR-only pipeline:
JAR -> ASM -> custom IR -> IR passes -> LLVM IR -> native libs -> repacked JAR
For large applications, whiteList and blackList filtering is still recommended. Native lowering and the protection layers add overhead, so avoid obfuscating an entire large game jar unless you have validated the result.
This tool provides string obfuscation, but it is not fundamentally irreversible because the key material still has to be derivable at runtime.
This tool currently provides:
- Java/JAR side:
- method native rewriting
- loader injection
RegisterNatives-based binding- loader generation aligned to the input jar class version
- Native side:
- string obfuscation
- symbol de-semanticization
- call indirection and dispatcher-based call routing
- integer/long constant splitting
- light CFG perturbation
- runtime helper metadata obfuscation
-
JDK 25
- Windows: install a JDK 25 distribution such as Oracle JDK 25.
- Linux/macOS: install a JDK 25 distribution with your package manager or your preferred vendor package.
-
Zig 0.15.2
If Zig 0.15.2 is not available, the tool can download it automatically from the official website into the same directory as the obfuscator jar under
zig-0.15.2/.You can also pre-download the host Zig archive into the same directory as the obfuscator jar. In that case, place both the official archive and its
.minisigfile there, and keep the original filenames exactly as published by Zig. For example on Windows:zig-x86_64-windows-0.15.2.zipzig-x86_64-windows-0.15.2.zip.minisig
This project is intended to be driven through Config.json.
Run it as:
java -jar j2ll.jar
java -jar j2ll.jar --config /path/to/Config.json
java -jar j2ll.jar --debug --config /path/to/Config.json
java -jar j2ll.jar --analyze --config /path/to/Config.json
If Config.json does not exist, the tool will create a template in the current directory.
--debug keeps native build intermediates and prints extra IR/native timing information.
--analyze runs the frontend analysis only. It writes analysis-report.json
and any frontend skip reports into the timestamped workspace without building
native libraries or repacking the input jar.
{
"jarFile": "/absolute/path/to/input.jar",
"outputDirectory": "out",
"blackList": [],
"whiteList": [],
"target": {
"windowsX64": true,
"windowsArm64": false,
"linuxX64": true,
"linuxArm64": false,
"macosX64": true,
"macosArm64": true
},
"libraryName": null,
"embeddedLibraryDirectory": "native0",
"stringObfuscation": {
"enabled": true,
"cacheStrings": false
},
"maxShardMB": 16
}jarFile - input .jar file to obfuscate
outputDirectory - output root directory. Every run creates a separate timestamped workspace inside it, for example:
out/build_2026-04-12_14-42-24
whiteList - list of classes and methods to include
blackList - list of classes and methods to exclude
Both lists use entries like:
"whiteList": [
"<class>",
"<class>#<method name>!<method descriptor>",
"mypackage/myotherpackage/Class1",
"mypackage/myotherpackage/Class1#doSomething!()V",
"mypackage/myotherpackage/Class1$SubClass#doOther!(I)V"
]Filtering uses JVM internal class names and method descriptors.
Wildcard matchers are also supported:
"whiteList": [
"mypackage/myotherpackage/*",
"mypackage/myotherpackagewithnested/**",
"mypackage/myotherpackage/*/Class1",
"mypackage/myotherpackagewithnested/**/Class1",
"mypackage/myotherpackage/Class*"
]* matches a single entry separated by /
** matches multiple nested entries
libraryName - if the output jar should load native libraries from the system library path, set the plain library name used by LoaderPlain
embeddedLibraryDirectory - sets the embedded native library directory inside the output jar
stringObfuscation.enabled - enables string obfuscation pass
stringObfuscation.cacheStrings - enables runtime string caching for decrypted string values
maxShardMB - best-effort upper bound for generated LLVM shards and runtime helper shards, in megabytes. This is useful when you want smaller compile/link units for large jars.
If you want to ship the jar with embedded native libraries, leave libraryName as null. The automatic Zig build step will place them into the output jar in the form of:
x64-windows.dll
x64-linux.so
x64-macos.dylib
arm64-linux.so
arm64-windows.dll
arm64-macos.dylib
inside the directory printed in stdout (by default native0/, or the resolved embeddedLibraryDirectory value if present).
- The tool validates
Config.json. - A timestamped workspace is created inside
outputDirectory. - The input jar is parsed through ASM and lowered into custom IR.
- IR validation and method-pass processing are applied.
- Current method passes include:
- CFG cleanup
- string obfuscation
- constant splitting
- light CFG perturbation
- LLVM IR is emitted into
llvm-modules/program.lland split into shard modules underllvm-modules/. - Runtime C sources are generated into
runtime/. - Zig 0.15.2 is checked before native export starts.
- If a matching Zig archive already exists next to the obfuscator jar, its
.minisigis verified and the cached archive is reused. - Otherwise Zig is downloaded, its
.minisigis verified, and it is extracted intozig-0.15.2/next to the obfuscator jar. - The selected targets from
config.targetare built automatically with Zig. - A loader is generated, native methods are rewritten, native libraries are embedded, and the output jar is repacked automatically.
Ctrl+C cancels the Java process and also terminates spawned Zig child processes.
Each run creates a build workspace like:
out/build_YYYY-MM-DD_HH-mm-ss/
Important files and directories:
- Repacked jar:
<workspace>/<input-jar-name>.jar - Native libraries:
<workspace>/native/ - Logs:
<workspace>/logs/ - LLVM monolithic IR:
<workspace>/llvm-modules/program.ll - LLVM shard modules:
<workspace>/llvm-modules/*.ll - Runtime C sources:
<workspace>/runtime/*.c - Frontend skip report:
<workspace>/frontend-skips.txtwhen skips are present - Structured frontend skip report:
<workspace>/frontend-skips.jsonwhen skips are present
Notes:
frontend-skips.txtandfrontend-skips.jsonare not generated when the frontend skip count is0.- Annotation classes are intentionally skipped by the frontend today and are not native-lowered.
- Record-synthesized
equals,hashCode, andtoStringare intentionally kept as bytecode so their JVMObjectMethodssemantics remain exact in large whole-jar workloads.
frontend-skips.json contains the total skip count, counts grouped by reason category,
and one entry per skipped method with class name, method name, descriptor, raw reason,
and category.
Intermediate native build directories:
native-obj/zig-cache/zig-build/
These are deleted automatically after the build unless --debug is enabled.
Common commands:
./gradlew clean buildBuilds the project and runs the full test suite../gradlew assembleBuilds the project jar without tests../gradlew shadowJarProduces the runnable fat jar.
The runnable jar is written to:
build/libs/j2ll.jar
Run ./gradlew test for the unit and integration suite.
You can also run the benchmark fixture end to end with:
powershell -ExecutionPolicy Bypass -File scripts/run-obf-bench.ps1or:
./gradlew obfBench
obfBench currently acts as a strict gate:
- the obfuscated benchmark must run successfully
- frontend skips must be zero
- native rewriting must succeed