文章爲本人編纂,轉載請聯繫做者並註明出處。html
在平常項目中,咱們可能會遇到須要用Java去命令行執行命令或執行shell腳本的狀況,但有時可能又會由於某些環境或者權限等沒法排查的緣由調用失敗,這時候就能夠經過一箇中間介質C來執行。尤爲是在對某些項目代碼(已通過普遍測試或須要訪問特定設備)進行重寫,Java恐怕有些力不從心,而Sun公司定義的JNI規範,規定了Java對本地方法的調用規則,這就大可沒必要廢棄舊有代碼。java
如下將以一個實際例子展現Java經過JNI調用C打印「Hello World!」主要記錄實現的過程和方法,對其中的一些原理和規範不作具體展開。想深刻了解的能夠參考Oracle的官方文檔,貼上地址:
JNI Interface Functions and Pointerslinux
Ubuntu Gnome 16.04 LTS
Java 1.8.0_111
gcc version 5.4.0
首先定義一個Java類JavaCallC.java
,在類中實現一個SayHello
方法,並用關鍵字native
爲本地方法編寫本地聲明;shell
public native void SayHello();
而後在類中的靜態代碼塊顯示地加載本地代碼庫;數組
static { System.loadLibrary("hello"); //加載本地共享庫 }
再加上main
方法和一些必要的異常處理程序,就生成如下源文件(固然,也能夠將本地方法放在另一個單獨的類中)。oracle
package com.jni.c; public class JavaCallC { /** * java經過JNI調用C * @author xiaosong 2017-04-03 */ public static void main(String[] args) { JavaCallC call = new JavaCallC(); call.SayHello(); } /** * 加載共享庫的本地方法 */ public native void SayHello(); static { try { System.loadLibrary("hello"); //加載本地共享庫 }catch(UnsatisfiedLinkError e) { System.err.println("沒法加載共享庫:" + e.toString()); } } }
P.S. 若是沒有使用IDE的,需先用 javac
將類編譯爲 .class
文件。
要爲以上定義的類生成 Java 本地接口頭文件,需使用 javah
,Java 編譯器的 javah
功能將根據 JavaCallC
類生成必要的聲明,此命令將生成一個 .h
後綴的頭文件,咱們在共享庫的代碼中要包含它。在工程項目的編譯文件 bin
目錄(也多是build
)下執行以下命令():ide
javah -jni [package.class]
執行命令後生成了一個 com_jni_c_JavaCallC.h
頭文件,頭文件的內容以下:函數
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_jni_c_JavaCallC */ #ifndef _Included_com_jni_c_JavaCallC #define _Included_com_jni_c_JavaCallC #ifdef __cplusplus extern "C" { #endif /* * Class: com_jni_c_JavaCallC * Method: SayHello * Signature: ()V */ JNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
在與 com_jni_c_JavaCallC.h
相同的路徑下建立一個 .c
文件 hello.c
,在C文件中引入該頭文件 ,並使用和頭文件中一致的方法來聲明函數。內容以下:測試
記得要爲
JNIEnv *
指針和jobject
對象定義變量,習慣上將這兩個變量定義爲env
和obj
。env
指針是任意一個本地方法的第一個參數,它指向一個函數指針表。jobject
指向在此 Java 代碼中實例化的 Java 對象LocalFunction
的一個句柄,至關於this
指針。ui
#include "com_jni_c_JavaCallC.h" #include <stdio.h> JNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello (JNIEnv * env, jobject obj) { printf("Hello World! \n"); return; }
編譯文件時,須要告知 GCC 編譯器在何處查找Java本地方法的支持文件 jni.h
和 jni_md.h
,這兩個文件通常是在 ../jdk/include
和 ../jdk/include/linux
兩個目錄下(AIX在 ../jdk/include/aix
目錄;Windows在 ../jdk/include/win32
目錄),在個人環境中按以下過程編譯。
先生成 hello.o
:
gcc -fPIC -I/usr/lib/jdk1.8.0_111/include -I/usr/lib/jdk1.8.0_111/include/linux -c hello.c
再生成 libhello.so
:
(共享庫 .so
的文件名必須是 lib+文件名
)
gcc -shared hello.o -o libhello.so
拷貝 libhello.so
到共享庫目錄:
(共享庫目錄通常爲 ../jre/lib/amd64/server
)
sudo cp libhello.so /usr/lib/jdk1.8.0_111/jre/lib/amd64/server
因爲我未配置 $LD_LIBRARY_PATH
環境變量,因此程序沒法加載到共享庫 hello
,於是我改寫成經過全路徑的方式來加載共享庫。
//System.loadLibrary("hello"); //加載本地共享庫 System.load("/usr/lib/jdk1.8.0_111/jre/lib/amd64/server/libhello.so");
運行結果以下:
接下來看一下Java如何經過JNI向C傳遞參數。本文中僅以 String
字符串爲例,其餘類型的參數的處理可參考文首提供的Oracle官方文檔,方法大致上是一致的。
先在聲明的本地方法中定義參數:
public native void SayHello(String strName1, String strName2);
而後在 main
方法中調用它並傳遞參數:
public static void main(String[] args) { JavaCallC call = new JavaCallC(); call.SayHello("Info", "Xiaosong"); }
生成頭文件的方法同上,這時候看一下生成的頭文件有何區別。
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_jni_c_JavaCallC */ #ifndef _Included_com_jni_c_JavaCallC #define _Included_com_jni_c_JavaCallC #ifdef __cplusplus extern "C" { #endif /* * Class: com_jni_c_JavaCallC * Method: SayHello * Signature: (Ljava/lang/String;Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello (JNIEnv *, jobject, jstring, jstring); #ifdef __cplusplus } #endif #endif
能夠看到函數聲明裏多了兩個 jstring
,這就對應於咱們要傳遞的兩個 String
參數。
其餘數值型參數和數組型參數對照以下:
一樣地,在編寫 hello.c
文件時,咱們須要爲傳遞的兩個參數定義變量;
JNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello (JNIEnv * env, jobject obj, jstring instring1, jstring instring2)
對於字符串型參數,由於在本地代碼中不能直接讀取 Java 字符串,而必須將其轉換爲 C /C++ 字符串或 Unicode。此處C的寫法和C++的寫法略微不一樣;
/** * C 寫法 */ //從instring字符串取得指向字符串UTF編碼的指針; const char *info = (*env)->GetStringUTFChars(env, instring1, 0);
/** * C++ 寫法 */ const char *info = env->GetStringUTFChars(instring1, 0);
//通知虛擬機本地代碼再也不須要經過 info
訪問Java字符串;
/** * C 寫法 */ (*env)->ReleaseStringUTFChars(env, instring1, info);
/** * C++ 寫法 */ env->ReleaseStringUTFChars(instring1, info);
再加上一些簡單的異常處理,完整的含參的 hello.c
以下:
#include "com_jni_c_JavaCallC.h" #include <stdio.h> #include <string.h> JNIEXPORT void JNICALL Java_com_jni_c_JavaCallC_SayHello (JNIEnv * env, jobject arg, jstring instring1, jstring instring2) { //從instring字符串取得指向字符串UTF編碼的指針 const char *info = (*env)->GetStringUTFChars(env, instring1, 0); const char *name = (*env)->GetStringUTFChars(env, instring2, 0); if (strlen(info)==0 || strlen(name)==0) { printf("參數缺失!\n"); }else { printf("%s : Hello %s \n", info, name); }; //通知虛擬機本地代碼再也不須要經過str訪問java字符串 (*env)->ReleaseStringUTFChars(env, s1, str); (*env)->ReleaseStringUTFChars(env, s2, user); return; }
方法和操做同上
如下是調用 call.SayHello("Information", "Xiaosong");
執行的結果:
如下是調用 call.SayHello("Information", "");
執行的結果:
至此,Java經過JNI調C的例子所有結束,當中若有什麼不足或錯誤還請指正。