有段時間沒有寫文件了,剛好最近學弟妹問到這個問題,做完實驗後來整理一下心得。其實上次使用JNI已經很久一段時間,後來每當有學弟來問我相關問題時我都得重新翻一次以前做的文章才能恢復記憶,程式這東西果然還是要常用啊!
噢!不過他們問我的問題當然不是標題寫的那樣,大家關心的是如何讓java與native c++的資料互相傳遞及溝通,其中最直覺的方法便是透過函式的參數列傳遞,在簡介與教學一文中已有介紹,可是但我有很多筆資料要傳時參數列不就要寫一整排?況且又沒有彈性。其實在變數的溝通上還有其他方式,今天先介紹一種:Fields Accessing。
這個方法的觀念很簡單,就是:java native function既然會屬於某一個class,理論上它就可以存取這個class裡所有的member fields。因此,這個class中所有的程式,無論在java還是c++,都可以對同一組資料做存取而無需透過參數傳遞。
例子:有個StructPass class,裡面成員有
private String name; private int age;
Constructor
public StructPass(String n, int a) { name = n; age = a; }
兩個native funciton
public native void toNativeStruct(); public native void fromNativeStruct();
現在,我希望在toNativeStruct裡可抓出在java中初始化好的fields,重點便是在於GetObjectField這個function。當他取得field後再做適當的型態轉換 (jstring, for example),便可以使用。
jfieldID fid; /* store the field ID */ jstring jstr; const char *str; /* Get a reference to obj's class */ jclass cls = env->GetObjectClass(obj); printf("In C:\n"); /* Look for the instance field name in cls */ fid = env->GetFieldID(cls, "name", "Ljava/lang/String;"); if (fid == NULL) { return; /* failed to find the field */ } /* Read the instance field name */ jstr = (jstring) env->GetObjectField(obj, fid); str = env->GetStringUTFChars(jstr, NULL); if (str == NULL) { return; /* out of memory */ } printf(" c.name = \"%s\"\n", str); env->ReleaseStringUTFChars(jstr, str);
之後,我們希望fromNativeStruct函式中可改變name這個field,相信聰明的大家一定猜到是使用SetObjectField,當然前面的GetObjectClass, GetFieldID仍免不了囉。
jfieldID fid; /* store the field ID */ jstring jstr; /* Get a reference to obj's class */ jclass cls = env->GetObjectClass( obj); /* Look for the instance field s in cls */ fid = env->GetFieldID( cls, "name", "Ljava/lang/String;"); if (fid == NULL) { return; /* failed to find the field */ } /* Create a new string and overwrite the instance field */ jstr = env->NewStringUTF("123"); if (jstr == NULL) { return; /* out of memory */ } env->SetObjectField(obj, fid, jstr);
在java的main function如下,執行後原本初始化的字串"kerker"就會變成"123"了。
public static void main(String[] args) { StructPass hello = new StructPass(new String("kerker"), 10); hello.toNativeStruct(); hello.fromNativeStruct(); System.out.println("In Java:"); System.out.println(" hello.name = \"" + hello.name + "\""); }
好大一個分隔線
以上介紹的是傳單一primitive type variable的方法,如果是要傳的是primitive array呢?用法一樣!只是在native c++中只靠GetObjectField所取得的值,其型態為jxxxArray (xxx may be int, float, double, …etc),需搭配GetxxxArrayElements才能取出每個array中的值。
例子:成員
private float [] float_array;
Constructor 初始化
public StructPass(String n, int a) { for(int i = 0 ; i < 10 ; i++) { float_array[i] = i + 0.5f; } }
在toNativeStruct (c++)中
/* Look for the instance field s in cls */ fid = env->GetFieldID(cls, "float_array", "[F"); if (fid == NULL) { return; /* failed to find the field */ } jfloatArray jfarray = (jfloatArray) env->GetObjectField(obj, fid); jsize len = env->GetArrayLength(jfarray); jfloat *body = env->GetFloatArrayElements(jfarray, 0); for(int i = 0 ; i < len ; i++) { printf("float[%d] = %f\n", i, body[i]); // modify some of them if (i%2 == 0) body[i] = 99.99; } // ReleaseIntArrayElements enables the JNI to copy back and free the memory env->ReleaseFloatArrayElements(jfarray, body, 0);
這裡特別需要注意的是,一旦你更改了field的陣列值,在呼叫ReleaseFloatArrayElements後不但可以釋放JNI中使用的這塊記憶體,還可以把你改過的值更新回java的field array中。因此,這個例子就不需要再寫額外的code在fromNativeStruct裡面。
程式原始碼下載:點我點我
後記
本來隔天想要實作在網路上找的另外兩個方法,一個是將java的物件利用ByteBuffer包裝成連續的記憶體然後傳到native這邊,不過文章中有人回應說這個方法的變數很多,跟compiler, variable format, byte order等都有關。另一個方法就是用XML-like serialization來傳。不過後來想想,只要把想要傳的資料包成一個class (java沒有structure),然後用本文教的方法來傳應該就可以解決問題了,因此java或native c++的愛恨交織暫時就告一段落啦!
reference
[1] doc chap 4 : Fields and Methods
[3] JNI functions