5

How to run Java standalone app (with JNI) on Android without creating an apk

 9 months ago
source link: https://yrom.net/blog/2023/07/07/run-java-with-jni-app-on-android/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In this week, I found a great POC to run a pure Java standalone app (command line tool, no apk) on Android. But what about running a standalone application using JNI (with .so files) on Android like this?

Java app with JNI

Imagine there is a Java program that loads the JNI shared native library to run and use some Android APIs :

package com.example;
import android.os.Build;
import android.util.Log;
public class Helloworld {

static { System.loadLibrary("hello"); }
public static native String stringFromJNI();

public static void main(String[] args) {
Log.i("@@", "Hello world, " + Build.MANUFACTURER + " "+ Build.MODEL + "!");
Log.i("@@", stringFromJNI());
System.out.println(stringFromJNI());
System.out.println("DONE.");
}

public static String getBuildVersion() {
return Build.VERSION.RELEASE;
}
}

…the JNI source would be like:

// ... emit codes

JNIEXPORT jstring JNICALL
Java_com_example_Helloworld_stringFromJNI(JNIEnv *env,
jobject thiz)
{
// ... emit codes
jmethodID versionFunc = (*env)->GetStaticMethodID(env, clz, "getBuildVersion", "()Ljava/lang/String;");

jstring buildVersion = (*env)->CallStaticObjectMethod(env, clz, versionFunc);
const char *version = (*env)->GetStringUTFChars(env, buildVersion, NULL);

if (!version)
{
LOGE("Unable to get version string");
}
else
{
LOGI("Build Version - %s\n", version);
(*env)->ReleaseStringUTFChars(env, buildVersion, version);
}
(*env)->DeleteLocalRef(env, buildVersion);

return (*env)->NewStringUTF(env,
"Hello from JNI ! Compiled with ABI " ABI ".");
}
// ...

The working directory structure

.
├── Helloworld.java
└── hello-jni.c

Compile and deploy

Now we need to compile both the Java and C sources for Android.

Using javac and dx to compile for a jar file which Android can read:

export BUILD_DIR=$PWD/build
export JARFILE=helloworld.jar
export JAVAC_OPTS=-source 1.8 -target 1.8 -cp .:$ANDROID_HOME/platforms/android-30/android.jar
# Compile .java to .class
javac $JAVAC_OPTS -d $BUILD_DIR/classes Helloworld.java
# Convert .class file into a dex file and embedded in a jar file
$ANDROID_HOME/build-tools/30.0.2/dx --output=$BUILD_DIR/$JARFILE --dex ./$BUILD_DIR/classes

Cross-compile the C to Android shared native library via NDK:

# Using the prebuilt toolchain diretly
# See https://developer.android.com/ndk/guides/other_build_systems
export ANDROID_NDK_STANDALONE=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64
$ANDROID_NDK_STANDALONE/bin/clang \
--target=aarch64-none-linux-android21 \
--gcc-toolchain=$ANDROID_NDK_STANDALONE \
--sysroot $ANDROID_NDK_STANDALONE/sysroot \
-L${ANDROID_NDK_STANDALONE}/sysroot/usr/lib \
-shared -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables \
-fstack-protector-strong -no-canonical-prefixes -fno-addrsig -fPIC \
-Wl,-llog \
-Wl,-soname,libhello.so \
-o $BUILD_DIR/libhello.so hello-jni.c

The build directory looks like:

build
├── classes
│ └── com
│ └── example
│ └── Helloworld.class
├── helloworld.jar
└── libhello.so

Using the adb tool to deploy the helloworld.jar and libhello.so to Android device:

adb shell mkdir /data/local/tmp/helloworld
adb push $BUILD_DIR/helloworld.jar $BUILD_DIR/libhello.so /data/local/tmp/helloworld/

Run appliation on Android

Run helloworld.jar via app_process on Android:

adb shell CLASSPATH="/data/local/tmp/helloworld/helloworld.jar" \
LD_LIBRARY_PATH=/data/local/tmp/helloworld \
app_process \
/data/local/tmp/helloworld \
com.example.Helloworld
# output
Hello from JNI ! Compiled with ABI arm64-v8a.
DONE.

Logging in adb logcat:

07-06 07:57:38.911 21599 21599 D AndroidRuntime: >>>>>> START com.android.internal.os.RuntimeInit uid 2000 <<<<<<
07-06 07:57:38.915 21599 21599 I AndroidRuntime: Using default boot image
07-06 07:57:39.025 21599 21599 D AndroidRuntime: Calling main entry com.example.Helloworld
07-06 07:57:39.027 21599 21599 I @@ : Hello world, vivo V2219A!
07-06 07:57:39.027 21599 21599 I hello-jni: Build Version - 12
07-06 07:57:39.027 21599 21599 I @@ : Hello from JNI ! Compiled with ABI arm64-v8a.
07-06 07:57:39.027 21599 21599 I hello-jni: Build Version - 12
07-06 07:57:39.028 21599 21599 D AndroidRuntime: Shutting down VM

Startup shell script

We can create a startup shell script for this tool:

#!/system/bin/sh
HERE="$(cd "$(dirname "$0")" && pwd)"

export CLASSPATH=$HERE/helloworld.jar
export ANDROID_DATA=$HERE
export LD_LIBRARY_PATH="$HERE"

if [ -f "$HERE/libc++_shared.so" ]; then
# Workaround for https://github.com/android-ndk/ndk/issues/988.
export LD_PRELOAD="$HERE/libc++_shared.so"
fi

echo "try com.example.Helloworld with LD_LIBRARY_PATH=${LD_LIBRARY_PATH}"
cmd="app_process $HERE com.example.Helloworld $@"
echo "run: $cmd"
exec $cmd

Push the script to Android device:

adb push helloworld /data/local/tmp/helloworld/
# executable
adb shell chmod a+x /data/local/tmp/helloworld/helloworld

Execute the shell script via adb shell:

adb shell /data/local/tmp/helloworld/helloworld

Makefile

We can bundle the compile and deploy commands into a convenient Makefile :

BUILD_DIR=build

JAVAC_OPTS=-source 1.8 -target 1.8 -cp .:$(ANDROID_HOME)/platforms/android-30/android.jar

APP_PROCESS=app_process
JARFILE=helloworld.jar
ifeq ($(ARCH),arm)
TARGET=--target=armv7-none-linux-androideabi19 -march=armv7-a -mfpu=vfpv3-d16
APP_PROCESS=app_process32
else
TARGET=--target=aarch64-none-linux-android21
endif

$(BUILD_DIR)/$(JARFILE) : Helloworld.java
test -d $(BUILD_DIR) || mkdir $(BUILD_DIR)
$(JAVA_HOME)/bin/javac $(JAVAC_OPTS) -d $(BUILD_DIR)/classes Helloworld.java
$(ANDROID_HOME)/build-tools/30.0.2/dx --output=$(BUILD_DIR)/$(JARFILE) --dex ./$(BUILD_DIR)/classes

$(BUILD_DIR)/libhello.so : hello-jni.c
test -d $(BUILD_DIR) || mkdir $(BUILD_DIR)
$(ANDROID_NDK_STANDALONE)/bin/clang \
$(TARGET) --gcc-toolchain=$(ANDROID_NDK_STANDALONE) \
--sysroot $(ANDROID_NDK_STANDALONE)/sysroot \
-L$(ANDROID_NDK_STANDALONE)/sysroot/usr/lib \
-shared -g -DANDROID -fdata-sections -ffunction-sections -funwind-tables \
-fstack-protector-strong -no-canonical-prefixes -fno-addrsig -fPIC \
$(CFLAGS) -Wl,--exclude-libs,libgcc.a -Wl,--exclude-libs,libatomic.a \
-Wl,--build-id -Wl,--warn-shared-textrel \
-Wl,--no-undefined -Wl,--as-needed \
$(LINKFLAGS) -Wl,-llog \
-Wl,-soname,libhello.so \
-o $(BUILD_DIR)/libhello.so hello-jni.c

all: $(BUILD_DIR)/$(JARFILE) $(BUILD_DIR)/libhello.so helloworld
.PHONY : clean deploy
deploy : all
adb shell mkdir /data/local/tmp/helloworld
adb push --sync $(BUILD_DIR)/$(JARFILE) $(BUILD_DIR)/libhello.so helloworld /data/local/tmp/helloworld/
adb shell chmod a+x /data/local/tmp/helloworld/helloworld
$(info adb shell /data/local/tmp/helloworld/helloworld)
clean :
test -d $(BUILD_DIR) && rm -rf $(BUILD_DIR) || true
adb shell -n "test -d /data/local/tmp/helloworld && rm -rf /data/local/tmp/helloworld || true"

Using make for compile all targets:

ANDROID_NDK_STANDALONE=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64 \
make all

Complete codes

You can get the complete codes in my git repo: https://github.com/yrom/sample-android-java-standalone-tool

git clone https://github.com/yrom/sample-android-java-standalone-tool.git
cd sample-android-java-standalone-tool

ANDROID_NDK_STANDALONE=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/$HOST_TAG \
make all

…the HOST_TAG:

NDK hostHOST_TAG
macOSdarwin-x86_64
Linuxlinux-x86_64
64bit Windowswindows-x86_64

(from https://developer.android.com/ndk/guides/other_build_systems#overview)

Conclusion:

  • Compile Java sources to an Android dex jar file via javac and dx
  • Compile C (or C++) sources to shared native library via ndk
  • Push complete .jar file and .so file(s) to Android device under /data/local/tmp
  • Run the main class via app_prcoess on Android device from adb shell

References:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK