Skip to content

Add Android JNI support with FalsoJNI + real ART fallback - #8

Open
YSaxon wants to merge 17 commits into
masterfrom
feature/android-jni-support
Open

Add Android JNI support with FalsoJNI + real ART fallback#8
YSaxon wants to merge 17 commits into
masterfrom
feature/android-jni-support

Conversation

@YSaxon

@YSaxon YSaxon commented Apr 19, 2026

Copy link
Copy Markdown
Owner

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 <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)

claude added 3 commits April 19, 2026 13:21
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/library_manager.c
Comment on lines +52 to 54
#ifdef __ANDROID__
jni_notify_library_loaded(handle, libraryPath);
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 on dlopen and REPL command initjni.
  • 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.

Comment thread src/android/jni_impl.c Outdated

#ifdef __ANDROID__

#include "FalsoJNI/FalsoJNI_Impl.h"

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#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).

Suggested change
#include "FalsoJNI/FalsoJNI_Impl.h"
#include "FalsoJNI_ImplBridge.h"

Copilot uses AI. Check for mistakes.
Comment thread src/android/jni_support.c Outdated
Comment on lines +28 to +29
#include "FalsoJNI/FalsoJNI.h"
#include "FalsoJNI/FalsoJNI_Impl.h"

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
#include "FalsoJNI/FalsoJNI.h"
#include "FalsoJNI/FalsoJNI_Impl.h"
#include "FalsoJNI.h"
#include "FalsoJNI_ImplBridge.h"

Copilot uses AI. Check for mistakes.
Comment thread src/android/jni_support.c
Comment on lines +115 to +116
typedef bool (*SpecialSignalHandlerFn)(int, siginfo_t*, void*);

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>).

Copilot uses AI. Check for mistakes.
Comment thread src/android/jni_support.c
Comment on lines +232 to +235
// 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));

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
* of the MIT license. See the LICENSE file for details.
*/

#include "FalsoJNI_Impl.h"

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
#include "FalsoJNI_Impl.h"
#if defined(__has_include)
# if __has_include("FalsoJNI_Impl.h")
# include "FalsoJNI_Impl.h"
# endif
#endif

Copilot uses AI. Check for mistakes.
Comment thread src/android/jni_impl.c Outdated
Comment on lines +27 to +50
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[] = {};

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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 } };

Copilot uses AI. Check for mistakes.
claude and others added 14 commits April 19, 2026 14:10
- 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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants