使用JNI连接java和C/C++
(我就懂java的基本语法,所以后面很多翻译可能有错 )
在本章我们将学习以下东西:
1.在java与本地代码之间传递与接收基本类型、对象、数组
2.在本地代码里面处理java对象
3.从本地代码中引起异常
Jni是一项巨大的高度技术性的科目,如果要深入研究需要一整本书来详细讲解。然而我们在这章将集中了解在他是怎么连接
java和c++的。
与java基本类型的联系
你可能已经迫不及待的想学更多的东西,而不是前面几章里面简单的MyProject:传递参数,接收返回值,产生异常,追踪客体目标。通过这章我们将会完成不同数据类型的值的存储,以基本数据类型和字符串开始。
一个简单的Java GUI是由一个关键字(一个特殊的字符串),类型(整形,字符串等等)和一个与所选类型有关的值所定义的接口。接口被插入或者修改的数据将会驻留在本地代码里面。接口也可以被Java客户端回调。下面的图片展示了一个程序大概会被怎样构架:
![](https://img-my.csdn.net/uploads/201211/05/1352104281_1378.png)
动手时间——建立一个本地值/关键字Store
首先实现Java方面的代码:
1.建立一个Java/C++的混合项目
命名为Store
主包为com.packtpub
主Activity为StoreActivity
不要忘了在根目录下面创建Jni文件夹
java方面将会包含三个源文件 Store.java StoreType.java 和 StoreActivity.java
2.建立一个新的Store类导入与本地library同名的库,定义我们store将要听过的函数。Store是我们本地代码 的前端。它仅仅支持整型和字符串型
public class Store { static { System.loadLibrary(“store”); } public native int getInteger(String pKey); public native void setInteger(String pKey, int pInt); public native String getString(String pKey); public native void setString(String pKey, String pString); }
3.以enum建立StoreType.java支持特定的几种数据类型
public enum StoreType { Integer, String }
4.在res/layout/main.xml文件里面按照下面的图片建立Java GUI。你可以是用ADT图形输出来设计,或者更简单的从Store_part3-1里面直接复制过去。
5.应用程序的GUI和Store需要被绑定在一起。这就是StoreActivity类将要实现的。当activity被创建,创建GUI的元素:类型容器就会与StoreType枚举类绑定在一起。GetValue和SetValue按钮就会触发私有函数onGetValue()和onSetValue()最后,初始化一个新的store的对象:
public class StoreActivity extends Activity { private EditText mUIKeyEdit, mUIValueEdit; private Spinner mUITypeSpinner; private Button mUIGetButton, mUISetButton; private Store mStore; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Initializes components and binds buttons to handlers. ... mStore = new Store(); }
6.定义onGetValue()函数,根据GUI里面选择的StoreType类型获得一个返回值
private void onGetValue() { String lKey = mUIKeyEdit.getText().toString(); StoreType lType = (StoreType) mUITypeSpinne.getSelectedItem(); switch (lType) { case Integer: mUIValueEdit.setText(Integer.toString(mStore.getInteger(lKey))); break; case String: mUIValueEdit.setText(mStore.getString(lKey)); break; } }
7.在StoreActivity里面添加onSetValue()函数来插入或者更新一个值。Entry的值需要根据他的类型来被描述
如果值的格式不正确,将会有一个提示信息显示:... private void onSetValue() { String lKey = mUIKeyEdit.getText().toString(); String lValue = mUIValueEdit.getText().toString(); StoreType lType = (StoreType) mUITypeSpinner.getSelectedItem(); try { switch (lType) { case Integer: mStore.setInteger(lKey, Integer.parseInt(lValue)); break; case String: mStore.setString(lKey, lValue); break; } } catch (NumberFormatException eNumberFormatException) { displayError(“Incorrect value.”); } } private void displayError(String pError) { Toast.makeText(getApplicationContext(), pError, Toast.LENGTH_LONG).show(); } }
Java方面的代码已经完成,也定义了本地方法的原型,我们将写本地代码。你可以查看最终代码获取帮助
8.在Jni目录下面,创建定义了数据结构体的Store.h。创建一个StoreType枚举出匹配java方面的类型。
同时创建Store。StoreEntry包含了一个关键字(C字符串),类型和一个值。StoreValue就是一个包含所有可能值
的集合体:#ifndef _STORE_H_ #define _STORE_H_ #include “jni.h” #include <stdint.h> #define STORE_MAX_CAPACITY 16 typedef enum { StoreType_Integer, StoreType_String } StoreType; typedef union { int32_t mInteger; char* mString; } StoreValue; typedef struct { char* mKey; StoreType mType; StoreValue mValue; } StoreEntry; typedef struct { StoreEntry mEntries[STORE_MAX_CAPACITY]; int32_t mLength; } Store; ...
9.最后声明一个函数来创建,找到,回收entry来结束Store.h。JNIEnv和jstring类型已经在jni.h的头文件里面定义了
所有这些函数都在jni/Store.c里面,isEntryValid()仅仅检查一个entry是否被分配以及类型是否正确:... int32_t isEntryValid(JNIEnv* pEnv, StoreEntry* pEntry,StoreType pType); StoreEntry* allocateEntry(JNIEnv* pEnv, Store* pStore, jstring pKey); StoreEntry* findEntry(JNIEnv* pEnv, Store* pStore, jstring pKey,int32_t* pError); void releaseEntryValue(JNIEnv* pEnv, StoreEntry* pEntry);
#include “Store.h” #include <string.h> int32_t isEntryValid(JNIEnv* pEnv, StoreEntry* pEntry,StoreType pType) { if ((pEntry != NULL) && (pEntry->mType == pType)) { return 1; } return 0; } ...
10.findEntry()函数用来比较所有传递进来的值,直到找到一个正确的。他接收的是一个jstring格式的参数,这个参数是java用来代替C代码对应格式的string,而不是标准的C语言string格式。jstring不能直接在本地代码中使用。事实上,Java和C的string是完全不同的两种概念。在Java中,String是一个有着成员方法的实体对象,而在C中,string只是一组字符数组。
为了从Java String中获取对应的C string,可以使用JNI API函数 GetStringUTFChars()来获取一个零时字符缓存。然后就能使用标准的C流程来处理。使用了GetStringUTFChars()就必须使用对应的ReleaseStringUTFChars()来释放零时的缓冲空间:StoreEntry* findEntry(JNIEnv* pEnv, Store* pStore, jstring pKey,Int32_t* pError) { StoreEntry* lEntry = pStore->mEntries; StoreEntry* lEntryEnd = lEntry + pStore->mLength; const char* lKeyTmp = (*pEnv)->GetStringUTFChars(pEnv, pKey,NULL); if (lKeyTmp == NULL) { if (pError != NULL) { *pError = 1; } return; } while ((lEntry < lEntryEnd)&& (strcmp(lEntry->mKey, lKeyTmp) != 0)) { ++lEntry; } (*pEnv)->ReleaseStringUTFChars(pEnv, pKey, lKeyTmp); return (lEntry == lEntryEnd) ? NULL : lEntry; } ...
11.在Store.c里面,实现allocateEntry()函数,它既能用来创建一个新的entry,当值存在的时候也能用来返回一个已经存在的。如果entry是新的,在函数外面的内存里面转换成C的格式
StoreEntry* allocateEntry(JNIEnv* pEnv, Store* pStore, jstring pKey) { Int32_t lError = 0; StoreEntry* lEntry = findEntry(pEnv, pStore, pKey, &lError); if (lEntry != NULL) { releaseEntryValue(pEnv, lEntry); } else if (!lError) { if (pStore->mLength >= STORE_MAX_CAPACITY) { return NULL; } lEntry = pStore->mEntries + pStore->mLength; const char* lKeyTmp = (*pEnv)->GetStringUTFChars (pEnv, pKey, NULL); if (lKeyTmp == NULL) { return; } lEntry->mKey = (char*) malloc(strlen(lKeyTmp)); strcpy(lEntry->mKey, lKeyTmp); (*pEnv)->ReleaseStringUTFChars(pEnv, pKey, lKeyTmp); ++pStore->mLength; } return lEntry; } ...
12.Store.c代码中最后一个函数是releaseEntryValue(),用来释放为一个值分配的内存空间。目前只有string是被动态分配和需要被回收的
void releaseEntryValue(JNIEnv* pEnv, StoreEntry* pEntry) { int i; switch (pEntry->mType) { case StoreType_String: free(pEntry->mValue.mString); break; } } #endif
13.使用javah像前面第2章里面为com.packtpub.Store生成JNI的头文件。jni/com_packtpub_Store.h应该被生成
14.现在我们的功能函数和JNI头文件已经生成了。我们需要写JNI的资源文件。com_packtpub_Store.c。唯一的Store线程被保存在一个静态的变量里面当library被装载的时候才创建。#include “com_packtpub_Store.h” #include “Store.h” #include <stdint.h> #include <string.h> static Store gStore = { {}, 0 };
15.在生成的JNI头文件的帮助下,完成getInteger()和setInteger()函数(在com_packtpub_Store.c里面)
第一个函数用来在store里面查找已经确认过的关键字,并返回其对应的值。如果发生什么异常,就会返回一个默认值。
第二个函数用来分配一个新的entry并保存新的integer值在里面(新建一个或者重新只用已有的)。注意这里C的int类型可以直接转换成Java的jint,反过来也一样。事实上他们是一样的类型:JNIEXPORT jint JNICALL Java_com_packtpub_Store_getInteger(JNIEnv* pEnv, jobject pThis, jstring pKey) { StoreEntry* lEntry = findEntry(pEnv, &gStore, pKey, NULL); if (isEntryValid(pEnv, lEntry, StoreType_Integer)) { return lEntry->mValue.mInteger; } else { return 0.0f; } } JNIEXPORT void JNICALL Java_com_packtpub_Store_setInteger(JNIEnv* pEnv, jobject pThis, jstring pKey, jint pInteger) { StoreEntry* lEntry = allocateEntry(pEnv, &gStore, pKey); if (lEntry != NULL) { lEntry->mType = StoreType_Integer; lEntry->mValue.mInteger = pInteger; } }
16.处理String的时候需要更加留心。Java的string并不是真正的原始数据,在第11步里面的jstring和char *是不能互换的。
使用NewStringUTF()创建Java String. 第2个函数setString()用GetStringUTFChars()和SetStringUTFChars()把Java String转换成C stringJNIEXPORT jstring JNICALL Java_com_packtpub_Store_getString(JNIEnv* pEnv, jobject pThis, jstring pKey) { StoreEntry* lEntry = findEntry(pEnv, &gStore, pKey, NULL); if (isEntryValid(pEnv, lEntry, StoreType_String)) { return (*pEnv)->NewStringUTF(pEnv, lEntry->mValue.mString); } else { return NULL; } } JNIEXPORT void JNICALL Java_com_packtpub_Store_setString(JNIEnv* pEnv, jobject pThis, jstring pKey, jstring pString) { const char* lStringTmp = (*pEnv)->GetStringUTFChars(pEnv,pString, NULL); if (lStringTmp == NULL) { return; } StoreEntry* lEntry = allocateEntry(pEnv, &gStore, pKey); if (lEntry != NULL) { lEntry->mType = StoreType_String; jsize lStringLength = (*pEnv)->GetStringUTFLength(pEnv,pString); lEntry->mValue.mString =(char*) malloc(sizeof(char) * (lStringLength + 1)); strcpy(lEntry->mValue.mString, lStringTmp); } (*pEnv)->ReleaseStringUTFChars(pEnv, pString, lStringTmp); }
17.最后按照下面的格式写Android.mk。库名为store.以及列出的两个C文件。在项目的根目录使用ndk-build来编译C代码:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_CFLAGS := -DHAVE_INTTYPES_H LOCAL_MODULE := store LOCAL_SRC_FILES := com_packtpub_Store.c Store.c include $(BUILD_SHARED_LIBRARY)
刚刚完成了什么?
运行程序,使用不同的值以及类型保存值。然后从本地方法里面取得输入的值。我们已经完成了C和Java之间的传递与接收int类型的值。这些值被store以string类型保存着。输入的值可以根据他们的值或者类型取回。整型在native调用之间有几种不同的状态,首先在Java里面是int,然后在传进/传出java代码的时候是jint格式。最后在本地代码中是int/int32_t。显然我们可以保存JNI的代表类型jint在本地代码里面因为两种类型是相等的。更普遍的,原始数据型不同语言只有有对应代替
另一方面,Java string需要一个固定的转换方法转换到C string这样可以允许使用标准C方法来处理字符串。事实上jstring并不是一个典型的char *数组仅仅是一个Java String对象的引用。仅仅只能在java方使用。
转换是由JNI方法GetStringUTFChars()实现的同时必须调用ReleaseStringUTFChars()来释放。本质上,这个转转分配了一个新的字符串缓冲区。结果的C string是用UTF-8编码的允许使用标准C处理。改良的UTF-8可以代表标准的ASCII字符串,并且可以延伸成几种扩展类型。这种格式与java的UTF-16是不同的。在获取本地strings的时候为了避免内部的转换,JNI同样也提供了GetStringChars()和ReleaseStringCharset(),返回UTF-16。与GetStringLength()连接的时候强烈建议使用它(因为GetStringUTFLength()可以使用标准函数strlen()(改良的UTF-8格式)来代替)