I am modifying an existing library written in native C to get it to work on Android. To access some hardware elements, I need to use some Java functions. So, I'm using JNI to call Java functions from native C.
My approach is the the following:
Use JNI_OnLoad to get a reference to the Java VM.
Get the JNI environment (JNIEnv)
Find the Java class using FindClass
Create an instance of the Java class using NewObject
Create a reference to the Java method I need to call.
Later on, I call a different function which actually calls the Java method using the previously stored reference. All objects and references are stored in global variables in order to initialize them on JNI_OnLoad and use them at the additional function.
The problem is that when I call the actual Java method I always get 0 (it should return an int). If I check for exceptions, it seems there is one but but I cannot see exactly what is creating the exception.
If I call the method from the JNI_OnLoad function, then it works. This makes me think that some reference is lost when the JNI_OnLoad function exits. I am converting all objects to global references but there must be still something getting lost.
Here is a minimal example of my setup. Any help will be highly appreciated.
Thanks!
JC
Native C:
JNIEnv *jni_env = NULL;
jclass jni_cls = NULL;
jobject jni_obj = NULL;
jmethodID jni_myJavaFunc= NULL;
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
(void)reserved;
jint ver = JNI_VERSION_1_6;
if (vm == NULL)
return JNI_ERR;
if ((*vm)->GetEnv(vm, (void **)&jni_env, ver) != JNI_OK)
return JNI_ERR;
jni_cls = (jclass)(*jni_env)->NewGlobalRef(jni_env, (*jni_env)->FindClass(jni_env, "org/java/MyJavaClass"));
if (jni_cls == NULL)
return JNI_ERR;
jmethodID init = (*jni_env)->GetMethodID(jni_env, jni_cls, "<init>", "()V");
if (init == NULL)
return JNI_ERR;
jni_obj = (*jni_env)->NewGlobalRef(jni_env, (*jni_env)->NewObject(jni_env, jni_cls, init));
if (jni_obj == NULL)
return JNI_ERR;
jni_myJavaFunc = (*jni_env)->GetMethodID(jni_env, jni_cls, "myJavaFunc", "()I");
return ver;
}
int myNativeFunc()
{
if (jni_myJavaFunc != NULL) {
(*jni_env)->ExceptionClear(jni_env);
int n = (*jni_env)->CallIntMethod(jni_env, jni_obj, jni_myJavaFunc);
if (!(*jni_env)->ExceptionCheck(jni_env))
return n;
else
return -1;
}
}
Java class:
public class MyJavaClass extends Activity
{
public MyJavaClass()
{
}
public int myJavaFunc()
{
return 1;
}
}
Related
I have a native code and I want to call some Java methods from it using JNI, my main problem is FindClass method of JVM env always returns null in case of my classes although I write it correctly but if any Java class like String it returns a valid pointer, I got a sample code for loading the ClassLoader and let it loads my class but unfortunately I got NULL.
this is the code:
unsigned char* pixels = new unsigned char[3 * width * height];
glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels);
std::string imgURL(dataPath+"/Levels/screenshot.png");
stbi_write_png(imgURL.c_str(), width, height, 3, pixels, width * 3);
stbi_image_free(pixels);
JNIEnv *lJNIEnv;
manager.activity->vm->AttachCurrentThread(&lJNIEnv, NULL);
jobject lNativeActivity = manager.activity->clazz;
jclass ClassNativeActivity = lJNIEnv->FindClass("android/app/NativeActivity");
jclass contextClass = lJNIEnv->FindClass("android/content/Context");
if(contextClass == 0)
return;
jmethodID startActivityMethodId = lJNIEnv->GetMethodID(contextClass, "startActivity", "(Landroid/content/Intent;)V");
if(startActivityMethodId == 0)
return;
jclass intentClass = lJNIEnv->FindClass("android/content/Intent");
if(intentClass == 0)
return;
jmethodID intentConstructorMethodId = lJNIEnv->GetMethodID(intentClass, "<init>", "()V");
if(intentConstructorMethodId == 0)
return;
jmethodID intentSetActionMethodId = lJNIEnv->GetMethodID(intentClass, "setAction", "(Ljava/lang/String;)Landroid/content/Intent;");
if(intentSetActionMethodId == 0)
return;
jmethodID getClassLoader = lJNIEnv->GetMethodID(ClassNativeActivity,"getClassLoader", "()Ljava/lang/ClassLoader;");
if(getClassLoader == 0)
return;
jobject cls = lJNIEnv->CallObjectMethod(lNativeActivity, getClassLoader);
if(cls == 0)
return;
jclass classLoader = lJNIEnv->FindClass("java/lang/ClassLoader");
if(classLoader == 0)
return;
jmethodID findClass = lJNIEnv->GetMethodID(classLoader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
if(findClass == 0)
return;
jstring intentString = lJNIEnv->NewStringUTF("com/game/Share");
if(intentString == 0)
return;
jobject intentObject = lJNIEnv->NewObject(intentClass,intentConstructorMethodId);
if(intentObject == 0)
return;
//Here I got NULL although the package and class are correct
jclass marketActivityClass = (jclass)lJNIEnv->CallObjectMethod(cls, findClass, intentString);
if(marketActivityClass == 0)
return;
lJNIEnv->CallVoidMethod(intentObject, intentSetActionMethodId,intentString);
lJNIEnv->CallVoidMethod(lNativeActivity, startActivityMethodId, intentObject);
manager.activity->vm->DetachCurrentThread();
any help?
You have been bitten by a known Android JNI limitation:
You can get into trouble if you create a thread yourself (perhaps by calling pthread_create and then attaching it with AttachCurrentThread). Now there are no stack frames from your application. If you call FindClass from this thread, the JavaVM will start in the "system" class loader instead of the one associated with your application, so attempts to find app-specific classes will fail.
Following the advice listed in the article should get you unstuck:
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.
So I'm developing a small project with Cocos2Dx but I'm trying to add Bluetooth functionality, and that implies calling a non-static method to be able to access the Main Activity's association to the Android API. Almost everything that I've seen tells me to follow this procedure:
- Create an instance of the main activity (environment->NewGlobalRef is the one I'm using)
- Get method from activity and execute it (environment->GetObjectClass)
And here's the code. In java we have the following (omitting logical stuff like onCreate, onResume, etc):
public class TSP extends Cocos2dxActivity{
public void CnxAttempt(){
Log.e("TSP_BT","aTTEMPTING!");
}
}
That's it! Just for now, I only want to show a Log message, confirming that the function is executed. Now, the fun part is at C++:
static JNIEnv* getJNIEnv(void){
JNIEnv *env = 0;
// get jni environment
if (gJavaVM->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK){
CCLog("Failed to get the environment using GetEnv()");
}
if (gJavaVM->AttachCurrentThread(&env, 0) < 0){
CCLog("Failed to get the environment using AttachCurrentThread()");
}
return env;
}
typedef struct JniMethodInfo_{
JNIEnv * env; // The environment
jclass classID; // classID
jmethodID methodID; // methodID
} JniMethodInfo; // Struct that stores most of the important information to relate to Java code
static bool getMethodInfo(JniMethodInfo &methodinfo, const char *methodName, const char *paramCode){
jmethodID methodID = 0;
JNIEnv *pEnv = 0;
jobject methodObject = NULL;
bool bRet = false;
do {
pEnv = getJNIEnv();
if (! pEnv){
CCLog("getMethodInfo -- pEnv false");
break;
}
jclass localRef = pEnv->FindClass("org/cocos2dx/tsp/TSP");
if (localRef == NULL) {
CCLog("getMethodInfo -- localRefCls false");
break; // exception thrown
}
gCallbackObject = pEnv->NewGlobalRef(localRef);
if (gCallbackObject == NULL){
CCLog("getMethodInfo -- CallbackOBJ false");
break;
}
jclass classID = pEnv->GetObjectClass(methodObject);
if (!classID){
CCLog("getMethodInfo -- classID false");
break;
}
methodID = pEnv->GetMethodID(classID, methodName, paramCode);
if (!methodID){
CCLog("getMethodInfo -- methodID false");
break;
}
methodinfo.classID = classID;
methodinfo.env = pEnv;
methodinfo.methodID = methodID;
CCLog("getMethodInfo -- methodinfo created");
bRet = true;
} while(0);
return bRet;
}
void CnxAttempt(){
JniMethodInfo methodInfo; // Creating a JniMethodInfo object to store all the data
if (! getMethodInfo(methodInfo, "CnxAttempt", "()V")){
CCLog("getMethodInfo is FALSE :(");
return;
}
methodInfo.env->CallVoidMethod(methodObject,methodInfo.methodID);
methodInfo.env->DeleteLocalRef(methodInfo.classID);
}
And that's it! While calling CnxAttempt on C++, it goes BOOM because it doesn't recognise the method within the Java class and can't get to it...
Can someone give me a hand? If something is not clear please let me know. Thanks a bunch in advance!!
Creating a new global reference does does not create a new object. The difference between local and global references (from the docs) is:
Local references are valid for the duration of a native method call, and are automatically freed after the native method returns. Global references remain valid until they are explicitly freed.
If you want to call a non-static method to an object you need to either pass the object to the native method (if it exists - shouldn't the main activity already exist?), create a new one using the NewObject* functions, or by calling some factory method.
Then get the class object of the object, get the methodID and then call the method.
I have native Android app using Google's native_app_glue wrapper. I would like to obtain a less than full-screen surface for rendering GLES into. In GLES apps using java layer derived from Activity this is accomplished by getWindow().setLayer() in Java layer. However, my project situation doesn't allow me to use this solution.
With nativeActivtiy and native_app_glue layer I can use JNI to get the Java classes and callback into Java, but not modify the View hierarchy. When calling back to setLayers() from my C code via JNI, I get this error since the NativeActivity is not in the same thread as the View hierarchy was created in.
E/AndroidRuntime(21503): android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
And here is my code to do this:
// Call Java to set Window size
//-----------------------------------------------------------------------------
int CallJavaWindowSize(struct android_app* state, jint width, jint height)
//-----------------------------------------------------------------------------
{
JNIEnv *env;
jclass nativeActivityClass;
jobject nativeActivityObj;
jmethodID mid;
jobject windowObj;
bool didAttachment = false;
int ret = -1;
JavaVMAttachArgs JVMAttachArgs;
jint result = state->activity->vm->GetEnv((void**) &env, JNI_VERSION_1_6);
if (!env && result == JNI_EDETACHED)
{
JVMAttachArgs.version = JNI_VERSION_1_6;
JVMAttachArgs.name = "NativeThread";
JVMAttachArgs.group = NULL;
if (state->activity->vm->AttachCurrentThread(&env, NULL) < 0)
{
__android_log_print(ANDROID_LOG_ERROR, "PowerLift", "CallJavaWindowSize() Failed to attach to thread");
return ret;
}
__android_log_print(ANDROID_LOG_DEBUG, "PowerLift", "CallJavaWindowSize() attached to Thread");
didAttachment = true;
}
else if (result < 0)
{
__android_log_print(ANDROID_LOG_ERROR, "PowerLift", "CallJavaWindowSize() Failed to GetEnv()");
return ret;
}
// retrieves NativeActivity class
nativeActivityObj = state->activity->clazz;
//nativeActivityClass = env->FindClass("android/app/NativeActivity");
nativeActivityClass = env->GetObjectClass(nativeActivityObj);
if (!nativeActivityClass)
{
__android_log_print(ANDROID_LOG_ERROR, "PowerLift", "CallJavaWindowSize() Failed to Find NativeActivity class");
return ret;
}
//Run getWindow().setLayout(width,height)
mid = env->GetMethodID(nativeActivityClass, "getWindow", "()Landroid/view/Window;");
if (mid == 0)
{
__android_log_print(ANDROID_LOG_ERROR, "PowerLift", "CallJavaWindowSize() Failed to get method getWindow() with signature = ()Landroid/view/Window;");
return ret;
}
windowObj = env->CallObjectMethod(nativeActivityObj, mid);
if (windowObj == 0)
{
__android_log_print(ANDROID_LOG_ERROR, "PowerLift", "CallJavaWindowSize() Failed to CallObjectMethod for mid getWindow()");
return ret;
}
jclass classWindow = env->FindClass("android/view/Window");
mid = env->GetMethodID(classWindow, "setLayout", "(II)V");
env->CallVoidMethod(windowObj, mid, width, height);
if (didAttachment)
state->activity->vm->DetachCurrentThread();
return 0;
}
A solution some of you may suggest is to use glViewport() to draw to less than full-screen. This solution works visually but is poor performance as EGL driver is still handling fullscreen surfaces.
I am wondering if this approach is the best solution as it is architecturally quite a change from using native app wrapper:
a) ditch native app glue wrapper and run native code (or at least a portion of it) in same thread as JVM
b) derive from NativeActivity a Java class that creates the View hierarchy via setContentView()
c) in Native code that is running in same thread as Java use JNI to call setLayout()
d) rest of native code can be running in a different thread as needed
I am not sure if the above approach is feasible of if I will run into a roadblock with this.
In case you're tring to render out from a pixel buffer, you might want to use glSubTexImage2D().
I am using native code to create an object which I then pass as an argument to a method I call.
Nothing appears to go wrong in native code, but when I get the call in Java, the object seems to have nulled values.
Here's the code of the java object I create in native:
package org.test;
import java.util.ArrayList;
public class JNITeam {
public int mTeamID;
public String mTeamName;
private ArrayList<String> mMembers;
public JNITeam(int id, String name) {
mTeamID = id;
mTeamName = name;
}
public void addMember(String name) {
mMembers.add(name);
}
}
Here's the native code used to create an instance of the class and pass it up to the Java method "onGetTeam", which takes an instance of the above class as a parameter. It is run from a thread created in Native code, hence I have to attach the thread.
JNIEnv* jenv = 0;
clientHandle->runningJVM->AttachCurrentThread(&jenv,0);
if (!jenv)
__android_log_print(ANDROID_LOG_INFO, ANDROID_DEBUG_TAG, "jenv is null");
jclass cls = jenv->GetObjectClass(clientHandle->job);
if (!cls)
__android_log_print(ANDROID_LOG_INFO, ANDROID_DEBUG_TAG, "cls is null");
jmethodID constructor = jenv->GetMethodID(clientHandle->JNITeamCls, "<init>", "(ILjava/lang/String;)V");
jint teamID = 2;
jstring js = jenv->NewStringUTF("test");
jobject dataObject = jenv->NewObject(clientHandle->JNITeamCls, constructor, teamID, js);
if (!dataObject)
__android_log_print(ANDROID_LOG_INFO, ANDROID_DEBUG_TAG, "dataobject is null");
if (jenv && cls && dataObject) {
jmethodID mid = jenv->GetMethodID(cls,"onGetTeam","(Lorg/test/JNITeam;)V");
if (mid) {
jenv->CallVoidMethod(clientHandle->job,mid);
}
else {
__android_log_print(ANDROID_LOG_INFO, ANDROID_DEBUG_TAG, "mid is null");
}
}
I do not want the object to be persistent; I want it to ony be valid during the call to Java, then it can be garbage-collected. However its data fields - that are set in the constructor - are just null when I call it, Why?
You're not passing the object you constructed to the method (or indeed doing anything with it).
Instead of
jenv->CallVoidMethod(clientHandle->job,mid);
don't you want
jenv->CallVoidMethod(clientHandle->job,mid,dataObject);
?
Also you're not checking whether that call succeeded or not.
I have an issue with the NDK.
In my JNI_OnLoad method, I cache the JavaVm pointer, the class that called the method, and a method id which I use later on:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved){
JNIEnv *env;
cachedJVM = jvm;
if((*jvm)->GetEnv(jvm, (void**)&env, JNI_VERSION_1_6)){
LOG_ERROR("Could not get JNIEnv*");
return JNI_ERR;
}
javaClass = (*env)->FindClass(env, "org/test/opensl/AudioProcessor");
if(javaClass == NULL){
LOG_ERROR("Could not get java class");
return JNI_ERR;
}
javaCallbackMID = (*env)->GetMethodID(env, javaClass, "enqueueAudio", "([B)V");
if(javaCallbackMID == NULL){
LOG_ERROR("Could not get method identifier");
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
I have a small utility method defined as follows that should get me a pointer to the JNIEnv:
JNIEnv* JNU_GetEnv(){
JNIEnv* env;
(*cachedJVM)->GetEnv(cachedJVM, (void**)&env, JNI_VERSION_1_6);
return env;
}
And finally, I have a callback from an OpenSL ES SLAndroidSimpleBufferQueueItf which I want to handle the recorded audio from a SLRecordItf:
void recorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context){
SLresult result;
JNIEnv* env;
recorderContext* thisContext = (recorderContext*)context;
env = JNU_GetEnv();
if(env == NULL){
LOG_ERROR("Could not get JNIEnv*");
return;
}
jbyteArray data = (*env)->NewByteArray(env, MAX_PACKET_SIZE);
if(data == NULL){
LOG_ERROR("No memory could be allocated for buffer");
return;
}
(*env)->SetByteArrayRegion(env, data, 0, MAX_PACKET_SIZE, recorderBuffer);
(*env)->CallByteMethodA(env, thisContext->caller, javaCallbackMID, data);
(*env)->DeleteLocalRef(env, data);
result = (*bq)->Enqueue(bq, recorderBuffer,
RECORDER_FRAMES * sizeof(jbyte));
checkError(result, "Unable to enqueue new buffer");
}
Where the context parameter for the callback method only holds a reference to the object that called the native method. It is a self defined struct like this:
typedef struct recorderContext{
jobject caller;
} recorderContext;
However, every time I try to run this, I get the "Could not get JNIEnv*" error message from the callback method.
My question basically comes down to this: Why can I get a pointer to the JNIEnv in the JNI_OnLoad method, but not in the recorderCallback, as both use the same Java VM pointer to get the JNIEnv?
I need this callback to pass the recorded Audio back up to my Java layer for further processing...
Why can I get a pointer to the JNIEnv
in the JNI_OnLoad method, but not in
the recorderCallback, as both use the
same Java VM pointer to get the
JNIEnv?
Because the callback happens on some native thread, different from the VM thread which loads the library. The JNI implementation maintains a JNIEnv per thread, and puts the pointer in thread-local storage. It is only initialized for native threads which are attached to the VM. You need to call AttachCurrentThread() (or more probably AttachCurrentThreadAsDaemon()) inside the callback to get a JNIEnv pointer valid for that thread. This attaches the thread to the VM on the first call and is a nop thereafter.
See http://download.oracle.com/javase/1.5.0/docs/guide/jni/spec/invocation.html
Caveat: This answer assumes proper Java. The problems you're seeing suggest that Dalvik behaves the same as the JVM.