Step By Step_Java經過JNI調C程序執行

文章爲本人編纂,轉載請聯繫做者並註明出處。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

Ubuntu Gnome 16.04 LTS

Java:Java 1.8.0_111

Java version

C:gcc version 5.4.0

gcc version 5.4.0

實現步驟

Hello World

一、定義一個Java類——JavaCallC.java

首先定義一個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());
        }
    }

}
二、生成 Java 本地接口頭文件

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
三、建立共享庫C文件

在與 com_jni_c_JavaCallC.h 相同的路徑下建立一個 .c 文件 hello.c ,在C文件中引入該頭文件 ,並使用和頭文件中一致的方法來聲明函數。內容以下:測試

記得要爲 JNIEnv * 指針和 jobject 對象定義變量,習慣上將這兩個變量定義爲 envobj
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.hjni_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

hello.o

  • 再生成 libhello.so
    (共享庫 .so 的文件名必須是 lib+文件名

gcc -shared hello.o -o libhello.so

libhello.so

  • 拷貝 libhello.so 到共享庫目錄:
    (共享庫目錄通常爲 ../jre/lib/amd64/server

sudo cp libhello.so /usr/lib/jdk1.8.0_111/jre/lib/amd64/server

五、運行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官方文檔,方法大致上是一致的。

一、定義本地方法參數

先在聲明的本地方法中定義參數:

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 參數。
其餘數值型參數和數組型參數對照以下:

三、建立共享庫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;
}
四、編譯生成共享庫文件

方法和操做同上

五、運行Java程序

如下是調用 call.SayHello("Information", "Xiaosong"); 執行的結果:

如下是調用 call.SayHello("Information", ""); 執行的結果:


至此,Java經過JNI調C的例子所有結束,當中若有什麼不足或錯誤還請指正。

相關文章
相關標籤/搜索