Step By Step_Java通过JNI调C程序执行
文章為本人編纂,轉載請聯系作者并注明出處。
在日常項目中,我們可能會遇到需要用Java去命令行執行命令或執行shell腳本的情況,但有時可能又會因為某些環境或者權限等無法排查的原因調用失敗,這時候就可以通過一個中間介質C來執行。尤其是在對某些項目代碼(已經過廣泛測試或需要訪問特定設備)進行重寫,Java恐怕有些力不從心,而Sun公司定義的JNI規范,規定了Java對本地方法的調用規則,這就大可不必廢棄舊有代碼。
以下將以一個實際例子展示Java通過JNI調用C打印“Hello World!”主要記錄實現的過程和方法,對其中的一些原理和規范不做具體展開。想深入了解的可以參考Oracle的官方文檔,貼上地址:
JNI Interface Functions and Pointers
環境介紹
操作系統:Ubuntu Gnome 16.04 LTS
Java:Java 1.8.0_111
C:gcc version 5.4.0
實現步驟
Hello World
1、定義一個Java類——JavaCallC.java
首先定義一個Java類JavaCallC.java,在類中實現一個SayHello方法,并用關鍵字native為本地方法編寫本地聲明;
public native void SayHello();然后在類中的靜態代碼塊顯示地加載本地代碼庫;
static {System.loadLibrary("hello"); //加載本地共享庫 }再加上main方法和一些必要的異常處理程序,就生成以下源文件(當然,也可以將本地方法放在另外一個單獨的類中)。
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());}}}2、生成 Java 本地接口頭文件
P.S. 如果沒有使用IDE的,需先用 javac 將類編譯為 .class 文件。
要為以上定義的類生成 Java 本地接口頭文件,需使用 javah,Java 編譯器的 javah 功能將根據 JavaCallC 類生成必要的聲明,此命令將生成一個 .h 后綴的頭文件,我們在共享庫的代碼中要包含它。在工程項目的編譯文件 bin 目錄(也可能是build)下執行如下命令():
執行命令后生成了一個 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 #endif3、創建共享庫C文件
在與 com_jni_c_JavaCallC.h 相同的路徑下創建一個 .c 文件 hello.c ,在C文件中引入該頭文件 ,并使用和頭文件中一致的方法來聲明函數。內容如下:
記得要為 JNIEnv * 指針和 jobject 對象定義變量,習慣上將這兩個變量定義為 env 和 obj 。
env 指針是任意一個本地方法的第一個參數,它指向一個函數指針表。jobject 指向在此 Java 代碼中實例化的 Java 對象 LocalFunction 的一個句柄,相當于 this 指針。
4、編譯生成共享庫文件
編譯文件時,需要告知 GCC 編譯器在何處查找Java本地方法的支持文件 jni.h 和 jni_md.h ,這兩個文件一般是在 ../jdk/include 和 ../jdk/include/linux 兩個目錄下(AIX在 ../jdk/include/aix 目錄;Windows在 ../jdk/include/win32 目錄),在我的環境中按如下過程編譯。
先生成 hello.o :
再生成 libhello.so :
(共享庫 .so 的文件名必須是 lib+文件名)
拷貝 libhello.so 到共享庫目錄:
(共享庫目錄一般為 ../jre/lib/amd64/server)
5、運行Java程序
由于我未配置 $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官方文檔,方法大體上是一致的。
1、定義本地方法參數
先在聲明的本地方法中定義參數:
public native void SayHello(String strName1, String strName2);然后在 main 方法中調用它并傳遞參數:
public static void main(String[] args) {JavaCallC call = new JavaCallC();call.SayHello("Info", "Xiaosong"); }2、編譯并生成頭文件
生成頭文件的方法同上,這時候看一下生成的頭文件有何區別。
/* 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 參數。
其他數值型參數和數組型參數對照如下:
3、創建共享庫C文件
同樣地,在編寫 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; }4、編譯生成共享庫文件
方法和操作同上
5、運行Java程序
以下是調用 call.SayHello("Information", "Xiaosong"); 執行的結果:
以下是調用 call.SayHello("Information", ""); 執行的結果:
至此,Java通過JNI調C的例子全部結束,當中如有什么不足或錯誤還請指正。
總結
以上是生活随笔為你收集整理的Step By Step_Java通过JNI调C程序执行的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《剑指offer》-斐波那契数列
- 下一篇: CSS中常见的长度单位