Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I load my own Java class in C on Android?

I am trying to call some Java code that I wrote from C using the Android NDK. The application is a NativeActivity application. I have to access some functionality that is only available in Java, and the functionality requires you to subclass another class, so I can't just directly do the calls from C. Thus, I have Java code like this:

// src/com/example/my/package/SubClass.java
package com.example.my.package;

import android.foo.TheSuperClass;

public class SubClass extends TheSuperClass {
  public SubClass() {
    setImportantProperty(true);
  }
}

I also have C code like this:

// Some file.c
void setThatImportantJavaProperty() {
  JavaVM *vm = AndroidGetJavaVM(); // This returns a valid JavaVM object
  JNIEnv* env;
  (*vm)->AttachCurrentThread(vm, &env, 0);

  jclass theSubClass = (*env)->FindClass(env, "com/example/my/package/SubClass");
  jthrowable exception = (*env)->ExceptionOccurred(env);
  if (exception) {
    (*env)->ExceptionDescribe(env);
    // This gives me: "java.lang.NoClassDefFoundError: [generic]".
    // Also, theSubClass is null, so the next line causes a segfault.
  }
  jmethodID theSubClassConstructor = (*env)->GetMethodID(env, theSubClass, "<init>", "()V");
  jobject theSubClassObject = (*env)->NewObject(env, theSubClass, theSubClassConstructor);

  (*env)->DeleteLocalRef(env, theSubClass);
  (*env)->DeleteLocalRef(env, theSubClassConstructor);
  (*env)->DeleteLocalRef(env, theSubClassObject);

  (*vm)->DetachCurrentThread(vm);

}

As the inline comments say, running this gives me a "java.lang.NoClassDefFoundError: [generic]" error. When I unpack my apk, it shows me the classes.dex file, which appears to have my class in it. My guess is that there is some subtlety that I am missing regarding classpaths, but I am unable to resolve it thus far.

Incidentally, I am able to make similar calls to standard Android libraries from C without a problem (in the same C function above). Specifically, I tested calling Log.v and that works and prints the output correctly.

It seems that all the examples that I am finding only show how to call normal Java libraries from C, not Java libraries you have written yourself, so I haven't even found an example project to compare against.

like image 289
itfische Avatar asked Jul 27 '11 01:07

itfische


2 Answers

The subtlety of my question, and why the documentation linked to by Klaimmore and Winston does not quite resolve the issue, stems from the fact that I am writing an app using the NativeActivity class. This means that there is never a Java stack with a local class loader available to me. There is no JNI_OnLoad call, there is no Java method calling into my native function, and there is no other way (that I am aware of) of getting ahold of a local ClassLoader object. Unfortunately, most of the Android JNI documentation is simply not written with NativeActivity in mind.

However, there is a straightforward solution to this problem that can make your life much easier when writing apps using NativeActivity. Simply subclass NativeActivity in Java. This allows you to write and access arbitrary Java code from the beginning of your NativeActivity's instantiation, while still doing everything else in C as NativeActivity allows. It also allows you to set up your JNI calls from C in the manner described in those documents. Doing this looks something like the following:

package com.example.my.package;

import android.app.NativeActivity;
import android.util.Log;

public class MyNativeActivity extends NativeActivity {
  static {
    System.loadLibrary("my_ndk_lib");  
  }

  private static String TAG = "MyNativeActivity";

  public MyNativeActivity() {
    super();
    Log.v(TAG, "Creating MyNativeActivity");
  }

  public static void MyUsefulJavaFunction() {
    doSomethingAwesome();
  }
}

And in your C library:

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
  JNIEnv* env;
  if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_6) != JNI_OK)
    return -1;

  globalMyNativeActivityClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/example/my/package/MyNativeActivity"));

  return JNI_VERSION_1_6;
}

Then at some point in C, you can do:

// Some file.c
void doSomethingAwesomeInJava() {
  JavaVM *vm = AndroidGetJavaVM(); // This returns a valid JavaVM object
  JNIEnv* env;
  (*vm)->AttachCurrentThread(vm, &env, 0);

  jmethodID myUsefulJavaFunction = (*env)->GetStaticMethodID(env, globalMyNativeActivityClass, "MyUsefulJavaFunction", "()V");
  (*env)->CallStaticVoidMethod(env, theActivityClass, myUsefulJavaFunction);

  (*env)->DeleteLocalRef(env, myUsefulJavaFunction);

  (*vm)->DetachCurrentThread(vm);
}

And that is the way I found to incorporate my own new Java classes with a NativeActivity app. Hopefully this will be useful to someone besides me.

like image 161
itfische Avatar answered Oct 04 '22 03:10

itfische


Here is what I have abstracted from this link as mentioned by Klaimmore.

There are a few ways to work around this:

Do your FindClass lookups once, in JNI_OnLoad, and cache the class references for later use. Any FindClass calls made as part of executing JNI_OnLoad will use the class loader associated with the function that called System.loadLibrary (this is a special rule, provided to make library initialization more convenient). If your app code is loading the library, FindClass will use the correct class loader.*

Pass an instance of the class into the functions that need it, by declaring your native method to take a Class argument and then passing Foo.class in.

Cache a reference to the ClassLoader object somewhere handy, and issue loadClass calls directly. This requires some effort.

like image 28
Wei Avatar answered Oct 04 '22 03:10

Wei