Add Android JNI support with FalsoJNI + real ART fallback - #8
Conversation
Ports and restructures the android_jni/fakeffi branch work into master's
src/ layout under src/android/.
Two initialization paths:
- FalsoJNI (default, all Android versions): fake JNIEnv/JavaVM vtable from
github.com/v-atamanenko/FalsoJNI. Sufficient for calling through JNI_OnLoad
to register native methods, then invoking those methods via cliffi's FFI.
Stubs log any Java callbacks the native side makes.
- Real ART JVM (--real flag): bootstraps JNI_CreateJavaVM via libart.so/
libandroid_runtime.so. Lets native libs round-trip into real Java classes.
Works on Android < 8 or custom/rooted devices; falls back to FalsoJNI with
a clear error message on modern Android where ART restricts this.
Auto-detection: library_manager calls jni_notify_library_loaded after every
dlopen. If the lib exports JNI_OnLoad, FalsoJNI is auto-initialized and
JNI_OnLoad is called immediately so native methods are registered without
any manual setup needed.
REPL command: initjni <lib> <vm_var> <env_var> [--real [jvm_opts...]]
Files:
- src/android/jni_support.c/h core logic, signal chain stubs
- src/android/jni_impl.c FalsoJNI method/field dispatch tables (empty
by default; populate for specific Java interop)
- src/android/FalsoJNI/ vendored FalsoJNI library (MIT)
Mirrors the CI dockcross + ADB emulator flow so you can iterate on Android builds and run the test suite locally without pushing to GitHub. Supports: build (dockcross cross-compile), push (adb), test (ctest via adb), shell, and run. CLIFFI_ANDROID_ARCH env var selects the target. Includes emulator quickstart instructions in --help.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bfc1f7864b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * of the MIT license. See the LICENSE file for details. | ||
| */ | ||
|
|
||
| #include "FalsoJNI_Impl.h" |
There was a problem hiding this comment.
Add the missing FalsoJNI_Impl.h header
The Android JNI integration currently cannot compile because this source now includes FalsoJNI_Impl.h, but that header is not present anywhere under src/android/FalsoJNI and no generation step adds it. Since CMakeLists.txt compiles this file on Android, builds fail immediately with a missing-header error, so the newly added JNI support is unusable.
Useful? React with 👍 / 👎.
| #ifdef __ANDROID__ | ||
| jni_notify_library_loaded(handle, libraryPath); | ||
| #endif |
There was a problem hiding this comment.
Skip auto JNI_OnLoad when initjni requests --real JVM
loadLibraryDirectly now always triggers jni_notify_library_loaded right after dlopen, which means initjni <lib> --real ... will call JNI_OnLoad via FalsoJNI before parseInitJNI can bootstrap a real ART JVM. In practice this can break libraries whose JNI_OnLoad depends on a real Java environment, and it also causes JNI_OnLoad to run twice (auto hook plus explicit call), which is not guaranteed to be safe.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Adds Android-focused JNI support to cliffi, enabling automatic JNI_OnLoad execution via a default FalsoJNI stubbed JVM and an optional “real ART” bootstrap path for deeper Java round-trips.
Changes:
- Adds Android JNI support module (
src/android/*) with auto-init ondlopenand REPL commandinitjni. - Vendors FalsoJNI (plus UTF conversion helpers) and wires it into the Android build via CMake.
- Adds a local Android build/push/test helper script.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main.c | Adds Android-only include and REPL initjni command routing + help text. |
| src/library_manager.c | Calls jni_notify_library_loaded() after successful dlopen on Android. |
| src/android/jni_support.h | Declares Android JNI hook + REPL command entry point. |
| src/android/jni_support.c | Implements FalsoJNI init, optional ART bootstrap, and auto JNI_OnLoad invocation. |
| src/android/jni_impl.c | Provides (currently empty) FalsoJNI dispatch tables for method/field callbacks. |
| src/android/FalsoJNI/jni.h | Vendored JNI header used by the FalsoJNI implementation. |
| src/android/FalsoJNI/LICENSE | Vendored FalsoJNI license. |
| src/android/FalsoJNI/FalsoJNI_Logger.h | Vendored logging header for FalsoJNI. |
| src/android/FalsoJNI/FalsoJNI_Logger.c | Vendored logging implementation for FalsoJNI. |
| src/android/FalsoJNI/FalsoJNI_ImplBridge.h | Vendored bridge API for method/field/array dispatch in FalsoJNI. |
| src/android/FalsoJNI/FalsoJNI_ImplBridge.c | Vendored bridge implementation for method/field/array dispatch in FalsoJNI. |
| src/android/FalsoJNI/FalsoJNI.h | Vendored top-level FalsoJNI API (exports jvm, jni, jni_init). |
| src/android/FalsoJNI/FalsoJNI.c | Vendored FalsoJNI core implementation (fake JavaVM/JNIEnv). |
| src/android/FalsoJNI/ConvertUTF.h | Vendored UTF conversion header used by FalsoJNI string APIs. |
| src/android/FalsoJNI/ConvertUTF.c | Vendored UTF conversion implementation. |
| dev_android.sh | Adds local dockcross + adb workflow helper for Android builds/tests. |
| CMakeLists.txt | Adds Android-only sources, include dirs, and export-dynamic linker option. |
| .gitignore | Updates ignored entries related to cliffi init/build artifacts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| #ifdef __ANDROID__ | ||
|
|
||
| #include "FalsoJNI/FalsoJNI_Impl.h" |
There was a problem hiding this comment.
#include "FalsoJNI/FalsoJNI_Impl.h" won’t compile as-is: the vendored FalsoJNI directory in this PR doesn’t contain FalsoJNI_Impl.h, and the include path also doesn’t match the CMake include dirs (src/android/FalsoJNI). Vendor the missing header (if required by FalsoJNI) and/or adjust the include to the correct path (e.g., FalsoJNI_ImplBridge.h or FalsoJNI.h, depending on what you actually need here).
| #include "FalsoJNI/FalsoJNI_Impl.h" | |
| #include "FalsoJNI_ImplBridge.h" |
| #include "FalsoJNI/FalsoJNI.h" | ||
| #include "FalsoJNI/FalsoJNI_Impl.h" |
There was a problem hiding this comment.
The FalsoJNI includes here don’t match the include directories added in CMake (src/android/FalsoJNI). With the current CMake setup, #include "FalsoJNI/FalsoJNI.h" will not resolve (it would require src/android on the include path). Also FalsoJNI/FalsoJNI_Impl.h doesn’t exist in the vendored tree, so this will fail to compile. Consider switching to includes like FalsoJNI.h / FalsoJNI_ImplBridge.h (or add the correct header from upstream and adjust include paths consistently).
| #include "FalsoJNI/FalsoJNI.h" | |
| #include "FalsoJNI/FalsoJNI_Impl.h" | |
| #include "FalsoJNI.h" | |
| #include "FalsoJNI_ImplBridge.h" |
| typedef bool (*SpecialSignalHandlerFn)(int, siginfo_t*, void*); | ||
|
|
There was a problem hiding this comment.
siginfo_t is used in the SpecialSignalHandlerFn typedef, but this file doesn’t include <signal.h>, which will cause a build failure on Android toolchains. Add the appropriate include (typically <signal.h>).
| // JavaVM and JNIEnv are both pointer typedefs (pointer-to-interface-struct). | ||
| // vm/env here are JavaVM*/JNIEnv* — store those pointer values directly. | ||
| setVar(vmStr, getPVar((void*)vm)); | ||
| setVar(envStr, getPVar((void*)env)); |
There was a problem hiding this comment.
getPVar() is used to store vm/env, but this file doesn’t include the header that declares it (parse_address.h). With -Werror this becomes a hard error (implicit declaration). Include the proper header rather than relying on implicit declarations.
| * of the MIT license. See the LICENSE file for details. | ||
| */ | ||
|
|
||
| #include "FalsoJNI_Impl.h" |
There was a problem hiding this comment.
This file includes FalsoJNI/FalsoJNI_Impl.h, but that header is not present in the vendored src/android/FalsoJNI directory, so the build will fail. Either vendor the missing header from upstream or update the FalsoJNI bridge code to not depend on it.
| #include "FalsoJNI_Impl.h" | |
| #if defined(__has_include) | |
| # if __has_include("FalsoJNI_Impl.h") | |
| # include "FalsoJNI_Impl.h" | |
| # endif | |
| #endif |
| NameToMethodID nameToMethodId[] = {}; | ||
|
|
||
| MethodsBoolean methodsBoolean[] = {}; | ||
| MethodsByte methodsByte[] = {}; | ||
| MethodsChar methodsChar[] = {}; | ||
| MethodsDouble methodsDouble[] = {}; | ||
| MethodsFloat methodsFloat[] = {}; | ||
| MethodsInt methodsInt[] = {}; | ||
| MethodsLong methodsLong[] = {}; | ||
| MethodsObject methodsObject[] = {}; | ||
| MethodsShort methodsShort[] = {}; | ||
| MethodsVoid methodsVoid[] = {}; | ||
|
|
||
| NameToFieldID nameToFieldId[] = {}; | ||
|
|
||
| FieldsBoolean fieldsBoolean[] = {}; | ||
| FieldsByte fieldsByte[] = {}; | ||
| FieldsChar fieldsChar[] = {}; | ||
| FieldsDouble fieldsDouble[] = {}; | ||
| FieldsFloat fieldsFloat[] = {}; | ||
| FieldsInt fieldsInt[] = {}; | ||
| FieldsObject fieldsObject[] = {}; | ||
| FieldsLong fieldsLong[] = {}; | ||
| FieldsShort fieldsShort[] = {}; |
There was a problem hiding this comment.
Type name[] = {}; is not valid C99 (empty initializer list). This will fail to compile with the project’s CMAKE_C_STANDARD 99 (and especially with -Werror). If you want empty tables, use a C-compatible pattern (e.g., explicit size-0 arrays where supported, or provide separate pointers + size constants/functions as FalsoJNI expects).
| NameToMethodID nameToMethodId[] = {}; | |
| MethodsBoolean methodsBoolean[] = {}; | |
| MethodsByte methodsByte[] = {}; | |
| MethodsChar methodsChar[] = {}; | |
| MethodsDouble methodsDouble[] = {}; | |
| MethodsFloat methodsFloat[] = {}; | |
| MethodsInt methodsInt[] = {}; | |
| MethodsLong methodsLong[] = {}; | |
| MethodsObject methodsObject[] = {}; | |
| MethodsShort methodsShort[] = {}; | |
| MethodsVoid methodsVoid[] = {}; | |
| NameToFieldID nameToFieldId[] = {}; | |
| FieldsBoolean fieldsBoolean[] = {}; | |
| FieldsByte fieldsByte[] = {}; | |
| FieldsChar fieldsChar[] = {}; | |
| FieldsDouble fieldsDouble[] = {}; | |
| FieldsFloat fieldsFloat[] = {}; | |
| FieldsInt fieldsInt[] = {}; | |
| FieldsObject fieldsObject[] = {}; | |
| FieldsLong fieldsLong[] = {}; | |
| FieldsShort fieldsShort[] = {}; | |
| /* | |
| * C99 does not allow empty initializer lists (`= {}`), so keep these | |
| * tables compilable by providing a single zero/empty placeholder entry. | |
| * They remain effectively unpopulated until real mappings are added. | |
| */ | |
| NameToMethodID nameToMethodId[1] = { { 0, "", 0 } }; | |
| MethodsBoolean methodsBoolean[1] = { { 0 } }; | |
| MethodsByte methodsByte[1] = { { 0 } }; | |
| MethodsChar methodsChar[1] = { { 0 } }; | |
| MethodsDouble methodsDouble[1] = { { 0 } }; | |
| MethodsFloat methodsFloat[1] = { { 0 } }; | |
| MethodsInt methodsInt[1] = { { 0 } }; | |
| MethodsLong methodsLong[1] = { { 0 } }; | |
| MethodsObject methodsObject[1] = { { 0 } }; | |
| MethodsShort methodsShort[1] = { { 0 } }; | |
| MethodsVoid methodsVoid[1] = { { 0 } }; | |
| NameToFieldID nameToFieldId[1] = { { 0, "", 0 } }; | |
| FieldsBoolean fieldsBoolean[1] = { { 0 } }; | |
| FieldsByte fieldsByte[1] = { { 0 } }; | |
| FieldsChar fieldsChar[1] = { { 0 } }; | |
| FieldsDouble fieldsDouble[1] = { { 0 } }; | |
| FieldsFloat fieldsFloat[1] = { { 0 } }; | |
| FieldsInt fieldsInt[1] = { { 0 } }; | |
| FieldsObject fieldsObject[1] = { { 0 } }; | |
| FieldsLong fieldsLong[1] = { { 0 } }; | |
| FieldsShort fieldsShort[1] = { { 0 } }; |
- FalsoJNI headers use bare names (FalsoJNI.h, not FalsoJNI/FalsoJNI.h) since CMakeLists adds src/android/FalsoJNI/ to the include path - Add missing signal.h (for siginfo_t) and parse_address.h (for getPVar) - Add missing FalsoJNI_Impl.h and FalsoJNI_ImplSample.h to vendored files
- Replace `= {}` array initializers in jni_impl.c with C99-valid sentinel
entries (id=0, name="") — the zero ID and empty name never match real
method/field lookups so FalsoJNI behaviour is unchanged
- Add g_suppress_auto_jni_onload flag: set around getOrLoadLibrary() in
parseInitJNI so jni_notify_library_loaded() does not fire JNI_OnLoad via
FalsoJNI before the caller has set up the intended JVM (fixes double-call
regression when using initjni --real)
https://claude.ai/code/session_01J3u2WQxDNsfyDFs5uDaiay
Missing newline had merged the two entries into one malformed pattern. https://claude.ai/code/session_01J3u2WQxDNsfyDFs5uDaiay
Default behavior when a library exporting JNI_OnLoad is loaded is now to
print a reminder with the exact initjni invocation rather than silently
calling JNI_OnLoad via FalsoJNI. This gives the user control over when
and with which JVM (fake or real ART) initialization happens — important
for RE work where JNI_OnLoad can have side-effects or require a real JVM.
New REPL command: autojni [on|off]
on — restores the old automatic behavior (FalsoJNI init + JNI_OnLoad
called immediately whenever a qualifying library is loaded)
off — reminder-only mode (default)
(no arg) — print current setting
parseInitJNI continues to suppress the notify hook around getOrLoadLibrary
so that a manual initjni never double-calls JNI_OnLoad even when autojni
is on.
https://claude.ai/code/session_01J3u2WQxDNsfyDFs5uDaiay
Fields* structs hold primitive JNI values (jboolean, jbyte, jint, etc.), not pointers. NULL (void*) was rejected by -Werror on the Android toolchain. Methods* structs hold function pointers so NULL remains correct there. https://claude.ai/code/session_01J3u2WQxDNsfyDFs5uDaiay
cliffi_unit_tests uses the same main.c and library_manager.c as cliffi, so it needs the same Android JNI sources when building for Android or the linker cannot resolve parseInitJNI, parseAutoJNI, and jni_notify_library_loaded. https://claude.ai/code/session_01J3u2WQxDNsfyDFs5uDaiay
Upstream FalsoJNI casts pointer types (jclass, jobject) to int in debug log format strings. This is harmless but trips -Werror on 64-bit Android targets. Suppress only that warning on the vendored files rather than patching upstream code. https://claude.ai/code/session_01J3u2WQxDNsfyDJs5uDaiay
FalsoJNI was written for 32-bit PS Vita where sizeof(int)==sizeof(void*). On 64-bit Android it has systemic pointer<->int cast warnings throughout (void*, jclass*, jmethodID*, jfieldID* all cast to int in log statements, and int cast back to jfieldID* in ImplBridge macros). -Wno-void-pointer- to-int-cast only suppressed a subset; -w suppresses all warnings on these vendored-only files without touching our own code's -Werror coverage. Verified all four files compile cleanly with aarch64-linux-android24-clang. https://claude.ai/code/session_01J3u2WQxDNsfyDJs5uDaiay
FalsoJNI was written for the PS Vita (32-bit, int == pointer), so it contained widespread direct pointer-to-int casts that truncate silently on 64-bit Android (ARM64). Fix each cast properly: - jfieldID/jmethodID comparisons: (int)(uintptr_t) to make the intentional narrowing to a small integer explicit and safe - Log format args for pointer-typed values: cast to (uintptr_t) and use PRIxPTR format specifier from <inttypes.h> - GetPrimitiveArrayRegion / SetPrimitiveArrayRegion macros: fix (int)array -> (uintptr_t)array and bare buffer -> (uintptr_t)(void*)buffer CMakeLists.txt: replace blanket -w on vendored files with targeted flags: -Wno-unknown-pragmas (CLion IDE pragmas) -Wno-unused-parameter (stub implementations) -Wno-sign-compare (int vs size_t loop indices) Pointer-cast warnings are no longer suppressed since the casts are fixed. https://claude.ai/code/session_01J3u2WQxDNsfyDFs5uDaiay
Ports and restructures the android_jni/fakeffi branch work into master's
src/ layout under src/android/.
Two initialization paths:
github.com/v-atamanenko/FalsoJNI. Sufficient for calling through JNI_OnLoad
to register native methods, then invoking those methods via cliffi's FFI.
Stubs log any Java callbacks the native side makes.
libandroid_runtime.so. Lets native libs round-trip into real Java classes.
Works on Android < 8 or custom/rooted devices; falls back to FalsoJNI with
a clear error message on modern Android where ART restricts this.
Auto-detection: library_manager calls jni_notify_library_loaded after every
dlopen. If the lib exports JNI_OnLoad, FalsoJNI is auto-initialized and
JNI_OnLoad is called immediately so native methods are registered without
any manual setup needed.
REPL command: initjni <vm_var> <env_var> [--real [jvm_opts...]]
Files:
by default; populate for specific Java interop)