標籤:
JNI是Java native interface的簡寫,可以譯作Java原生介面。Java可以通過JNI調用C/C++的庫,這對於那些對效能要求比較高的Java程式無疑是一個 福音。
使用JNI也是有代價。大家都知道JAVA程式是運行在JVM之上的,可以做到平台無關。但是如果Java程式通過JNI調用了原生的代碼(比如 c/c++等),則Java程式就喪失了平台無關性。最起碼需要重新編譯原生代碼部分。所以應用JNI需要好好權衡,不到萬不得已,請不要選擇JNI,可 以選擇替代方案,比如TCP/IP進行進程間通訊等等。這也是為什麼Google的Android平台的底層雖然用JNI實現,但是他不建議開發人員用JNI來開 發Android上面的應用的原因。將會喪失Android上面的應用程式平台無關性。
下面是在linux下java jni調用C語言動態連結程式庫的具體操作步驟。
1、建立一個Java程式(Hello.java)定義原生的c/c++函數。
2、用javac編譯Hello.java產生Hello.class。
3、用javah帶-jni參數編譯Hello.class產生Hello.h檔案,該檔案中 定義了c的函數原型。在實現c函數的時候需要。
4、建立Hello.c,實現Hello.h定義的函數。
5、編譯Hello.c產生libHello.so。
6、在java虛擬機器運行java程式Hello。
第一步,定義一個 Java 類 -- Hello. 它提供SayHello方法:
此時應注意兩點:
1.為要使用的每個本地方法編寫本地方法聲明,其聲明方式與普通 Java 方法介面沒什麼不同,只是必須指定 native 關鍵字,如下所示:
public native void SayHello(String strName);
在這個函數中,我們將根據傳進的人名,向某人問好。
2.必須顯式地載入本地程式碼程式庫。當然要調用System.loadLibrary("hello");注 意此時不要lib,也不要.so!; 我們需在類的一個靜態塊中載入這個庫:
static
{
System.loadLibrary("hello");
}
再加上必要的異常處理就產生如下源檔案Hello.java:
public class Hello
{
static
{
System.loadLibrary("hello");
}
//聲明的本地方法
public staitc native void sayHello(String strName);
}
運行命令 javac Hello.java 產生Hello.class檔案。
第二步,產生本地連結庫。具體過程如下:
1. 要為以上定義的類產生 Java 本地介面標頭檔,需使用javah,Java 編譯器的 javah 功能將根據 Hello類產生必要的聲明,此命令將產生Hello.h 檔案,我們在共用庫的代碼中要包含它,javah不使預設內部命令,需要指明路徑,它在JDK的bin目錄下,在我的Linux環境下命令如下:
javah Hello
但是出現如下錯誤:
error: cannot access Hello
class file for Hello not found
javadoc: error - Class Hello not found.
Error: No classes were specified on the command line. Try -help.
原因是CLASS_PATH沒有把目前的目錄加入其中。所以必須指定classpath 為目前的目錄。或者在系統CLASS_PATH加入當前路徑。執行如下命令:
javah -classpath . Hello
產生的Hello.h 檔案內容的第一句子為 #include <jni.h>
但是gcc裡面預設環境可不知道jni.h是什麼東西,jni.h在jdk 的$JAVA_HOME/include下面,可進去查看一下~
2.在與Hello.h相同的路徑下建立一個CPP檔案Hello.cpp。注意,自動產生的那個函數名字很長,並且 開頭的 Java是大寫的,大小寫很致命一定要注意。內容如下:
#include "Hello.h"
#include <stdio.h>
//與Hello.h中函式宣告相同
JNIEXPORT void JNICALL Java_Hello_sayHello (JNIEnv * env, jclass arg, jstring instring)
{
//從instring字串取得指向字串UTF編碼的指標
const jbyte *str =
(const jbyte *)env->GetStringUTFChars( instring, JNI_FALSE );
printf("Hello,%s/n",str);
//通知虛擬機器本地代碼不再需要通過str訪問Java字串。
env->ReleaseStringUTFChars( instring, (const char *)str );
return;
}
所有的JNI調用都使用了JNIEnv *類型的指標,習慣上在CPP檔案中將這個變數定義為env,它是任意一個本地方法的第一個參數。env指標指向一個函數指標表,在VC中可以直接 用"->"操作符訪問其中的函數。
jobject 指向在此 Java 代碼中執行個體化的 Java 對象 LocalFunction的一個控制代碼,相當於this指標。
後續的參數就是本地調用中有Java程式傳進的參數,本例中只有一個String型參數。 對於字串型參數,因為在本地代碼中不能直接讀取 Java 字串,而必須將其轉換為 C /C++字串或 Unicode。以下是三個我們經常會用到的字串類型處理的函數:
const char* GetStringUTFChars(jstring string,jboolean* isCopy)
3.編譯產生共用庫。
使用GCC時,必須通知編譯器在何處尋找此 Java 本地方法的支援檔案,並且顯式通知編譯器產生位置無關的代碼,在我的環境中按如下過程編譯:
g++ -I /usr/lib/jvm/java-6-sun/include/linux/ -I /usr/lib/jvm/java-6-sun/include/ -fPIC -c Hello.cpp
產生Hello.o
g++ -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0 Hello.o
產生libhello.so.1.0
接下來將產生的共用庫拷貝為標準檔案名稱
cp libhello.so.1.0 libhello.so
或者使用:
g++ -I /usr/lib/jvm/java-6-sun/include/linux/ -I /usr/lib/jvm/java-6-sun/include/ -fPIC -shared -o libLexical.so Lexical.cpp
注意在linux下,動態連結程式庫的名字 必須是 lib****.so,必須以lib開頭!
4.編寫一個簡單的Java程式來測試我們的本地方法。
將如下源碼存為ToSay.java:
public class ToSay
{
public static void main(String argv[])
{
Hello.sayHello("John");
}
}
用javac編譯ToSay.java命令javac -cp . ToSay.java,產生ToSay.class。
向執行普通Java程式一樣使用java ToSay,
java ToSay
Exception in thread "main" java.lang.NoClassDefFoundError: ToSay
Caused by: java.lang.ClassNotFoundException: ToSay
at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:248)
Could not find the main class: ToSay. Program will exit.
原因是classpath沒有當前路徑。改正後執行如下命令:
java -cp . ToSay
Exception in thread "main" java.lang.UnsatisfiedLinkError: no hello in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1734)
at java.lang.Runtime.loadLibrary0(Runtime.java:823)
at java.lang.System.loadLibrary(System.java:1028)
at Hello.<clinit>(Hello.java:6)
at ToSay.main(ToSay.java:5)
依然報錯。java.lang.UnsatisfiedLinkError: no HelloNative in java.library.path。這個錯誤很經典,原因:是java找不到庫路徑~: 顯然: libhello.so放在當前路徑 ".",只linux執行的時候卻不知道在當前路徑找。
a. linux下面java.library.path 和環境變臉 jdk/bin的那個個PATH不是一回事情,有另外一個預設變數 LD_LIBRARY_PATH來儲存他的資訊。而windows下,首先java會找目前的目錄,其次,它會去環境變數的地址找!
b。 由於linux的路徑特殊,所以,解決方案 1-可以調用sysout(System.getProperty("java.library.path")); 來查看! 然後把 libXXXX.so拷貝到那裡面的目錄下去
2 設定環境變數 export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
3 可以單次執行時候指定library位置:
java -Djava.library.path=. -cp . ToSay
我們會看到在螢幕上出現Hello,John。
附:gcc 參數解釋(轉載):
最主要的是GCC命令列的一個選項:
-shared 該選項指定產生動態串連庫(讓連接器產生T類型的匯出符號表,有時候也產生弱串連W類型的匯出符號),不用該標誌外部程式無法串連。相當於一個可執行檔
l -fPIC:表示編譯為位置獨立的代碼,不用此選項的話編譯後的代碼是位置相關的所以動態載入時是通過代碼拷貝的方式來滿足不同進程的需要,而不能達到真 正程式碼片段共用的目的。
l -L.:表示要串連的庫在目前的目錄中
l -ltest:編譯器尋找動態串連庫時有隱含的命名規則,即在給出的名字前面加上lib,後面加上.so來確定庫的名稱
l LD_LIBRARY_PATH:這個環境變數指示動態連接器可以裝載動態庫的路徑。
l 當然如果有root許可權的話,可以修改/etc/ld.so.conf檔案,然後調用 /sbin/ldconfig來達到同樣的目的,不過如果沒有root許可權,那麼只能採用輸出LD_LIBRARY_PATH的方法了。
4、注意
調用動態庫的時候有幾個問題會經常碰到,有時,明明已經將庫的標頭檔所在目錄 通過 “-I” include進來了,庫所在檔案通過 “-L”參數引導,並指定了“-l”的庫名,但通過ldd命令察看時,就是死活找不到你指定連結的so檔案,這時你要作的就是通過修改 LD_LIBRARY_PATH或者/etc/ld.so.conf檔案來指定動態庫的目錄。通常這樣做就可以解決庫無法連結的問題了。
Linux平台下Java調用C函數