Reputation: 383
I am just starting out with JNI and I have a following problem.
I have a C++ library that has a simple class. I have three JNI methods called from the Java Android project that, instatiate said class, call a method on the instantiated class, and destroy it, respectively. I keep a global reference to this object, so it would be available for me in the other two JNI methods.
I suspect that I cannot do this. When I run the app, I get a runtime error (used reference stale), and I suspect that this is because the global refernce is invalid at subsequent calls to other JNI methods.
Is the only way to achieve what I want (have the object live across multiple JNI calls), to actually pass back the pointer to the instantiated class back to Java, keep it around there, and then pass it back to the JNI functions? If so, that's fine, I want to make sure I can't do it with a global reference, and I'm not just missing something.
I have read the documentation and the chapters about global/local references in JNI, but it seems like that only applies to Java classes, and not my own, native C++ classes, or am I wrong.
Here is the code if my description is not clear (summarizing, I am wondering if this mechanism of persisting objects will work at all):
Java:
package com.test.ndktest;
import android.app.Activity;
import android.os.Bundle;
import android.app.AlertDialog;
public class NDKTestActivity extends Activity {
static {
System.loadLibrary("ndkDTP");
}
private native void initializeTestClass();
private native void destroyTestClass();
private native String invokeNativeFunction();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
initializeTestClass();
String hello = invokeNativeFunction();
destroyTestClass();
new AlertDialog.Builder(this).setMessage(hello).show();
}
}
JNI header:
extern "C" {
jstring Java_com_test_ndktest_NDKTestActivity_initializeTestClass(JNIEnv* env, jobject javaThis);
jstring Java_com_test_ndktest_NDKTestActivity_destroyTestClass(JNIEnv* env, jobject javaThis);
jstring Java_com_test_ndktest_NDKTestActivity_invokeNativeFunction(JNIEnv* env, jobject javaThis);
};
JNI body:
#include <string.h>
#include <jni.h>
#include <ndkDTP.h> //JNI header
#include <TestClass.h> //C++ header
TestClass *m_globalTestClass;
void Java_com_test_ndktest_NDKTestActivity_initializeTestClass(JNIEnv* env, jobject javaThis) {
m_globalTestClass = new TestClass(env);
}
void Java_com_test_ndktest_NDKTestActivity_destroyTestClass(JNIEnv* env, jobject javaThis) {
delete m_globalTestClass;
m_globalTestClass = NULL;
}
jstring Java_com_test_ndktest_NDKTestActivity_invokeNativeFunction(JNIEnv* env, jobject javaThis) {
jstring testJS = m_globalTestClass->getString();
return testJS;
}
C++ header:
class TestClass
{
public:
jstring m_testString;
JNIEnv *m_env;
TestClass(JNIEnv *env);
jstring getString();
};
C++ body:
#include <jni.h>
#include <string.h>
#include <TestClass.h>
TestClass::TestClass(JNIEnv *env){
m_env = env;
m_testString = m_env->NewStringUTF("TestClass: Test string!");
}
jstring TestClass::getString(){
return m_testString;
}
Thanks
Upvotes: 10
Views: 10301
Reputation: 841
Not really sure from the question, whether this meats your use case:
Regularly, you want to control C++ objects by java calls. C++ objects can outlive a JNI calls, when instantiated as static variables or on the heap.
With a single object, this is easy: You have JNI-methods like create/modify/delete which do the job on that object.
When you want to control a dynamic set of objects, you need to distinguish them by a unique reference number. I.e. the JNI-create method would
On further calls the reference number is passed to the C++ context,
The delete method in third step instead will remove the entry from the reference map.
Upvotes: 0
Reputation: 3697
I couldn't find a good answer on SO on this topic, so here's my solution to keep objects alive on C++ in order to reference them from multiple JNI calls:
Java
On the Java side, I am creating a class with a long
pointer to keep a reference to the C++ object. Wrapping the C++ methods in a Java class, allows us to use the C++ methods in multiple activities. Notice that I am creating the C++ object on the constructor, and I am deleting the object on cleanup. This is very important in order to prevent memory leaks:
public class JavaClass {
// Pointer (using long to account for 64-bit OS)
private long objPtr = 0;
// Create C++ object
public JavaClass() {
createCppObject();
}
// Delete C++ object on cleanup
public void cleanup() {
deleteCppObject();
this.objPtr = 0;
}
// Native methods
public native void createCppObject();
public native void workOnCppObject();
public native void deleteCppObject();
// Load C++ shared library
static {
System.loadLibrary("CppLib");
}
}
C++
On the C++ side, I am defining functions to create, modify and delete the object. It's important to mention that we have to use new
and delete
to store the object in the HEAP memory to keep it alive throughout the lifecycle of the Java class instances. I am also storing the pointer to CppObject
straight in the JavaClass
, using getFieldId
, SetLongField
, and GetLongField
:
// Get pointer field straight from `JavaClass`
jfieldID getPtrFieldId(JNIEnv * env, jobject obj)
{
static jfieldID ptrFieldId = 0;
if (!ptrFieldId)
{
jclass c = env->GetObjectClass(obj);
ptrFieldId = env->GetFieldID(c, "objPtr", "J");
env->DeleteLocalRef(c);
}
return ptrFieldId;
}
// Methods to create, modify, and delete Cpp object
extern "C" {
void Java_com_test_jnitest_JavaClass_createCppObject(JNIEnv *env, jobject obj) {
env->SetLongField(obj, getPtrFieldId(env, obj), (jlong) new CppObject);
}
void Java_com_test_jnitest_JavaClass_workOnCppObject(JNIEnv *env, jobject obj) {
CppObject* cppObj = (CppObject*) env->GetLongField(obj, getPtrFieldId(env, obj));
// Write your code to work on CppObject here
}
void Java_com_test_jnitest_JavaClass_deleteCppObject(JNIEnv *env, jobject obj) {
CppObject* cppObj = (CppObject*) env->GetLongField(obj, getPtrFieldId(env, obj));
delete cppObj;
}
}
NOTES:
delete
.GetFieldID
, SetLongField
, and GetLongField
to store the object reference from C++, but you could also store the jlong
object pointer from Java as discussed on other answers.JavaObject
class as a Parcelable
in order to pass my class to multiple activities using Intent
with extras.Upvotes: 5
Reputation: 2862
The problem with your implementation is the jstring
data member. NewStringUTF()
creates a Java String
object to return from a JNI method. So it is a Java local reference. However, you are storing this inside a C++ object and try to use it across JNI calls.
You should keep a better distinction between C++ objects, Java and the JNI interface in between. In other words, the C++ should use a C++ way of storing strings (like std::string
). The JNI implementation of InvokeNativeFunction()
should convert that to a jstring
as return value.
PS: There are cases that require the C++ implementation to keep references to Java objects (or the other way around). But it makes the code more complex and prone to memory bugs if not done right. So you should only use it where it really adds value.
Upvotes: 5
Reputation: 310860
You can't do that. Object references, including class references, are not valid across JNI calls. You need to read the section of the JNI Specification about local and global references.
Upvotes: 0