最近更新: 2010-07-23

Java Native Interface with C tutorial

操作步驟

  1. 用 Java 設計一個類別,將你想要用 C/C++ 實作的方法用修飾子(modifier) native 宣告為原生方法(native method)。原生方法除了不用在 Java 程式碼中寫出程式內容外,其他都與一般方法無異,一樣可以使用 public, protected, private, static, final, synchronized 等修飾子。
  2. 使用 javac 編譯你剛剛設計的 Java 類別,產生 class 文件。我們將會需要透過這份 class 文件,產生撰寫原生方法所需的 C/C++ 標頭文件。
  3. 使用 javah -jni 讀取 class 文件,產生原生方法的 C/C++ 標頭文件。標頭文件中含有原生方法的 C/C++ 函數原型宣告。
  4. 以 C/C++ 實作原生方法的程式內容。我們要先自前一步驟產生的 C/C++ 標頭文件,複製原生方法的 C/C++ 函數原型宣告到 C/C++ 程式碼中。
  5. 使用 C/C++ 編譯器編譯 C/C++ 程式文件,建立一個共享函數庫文件 (dll/so)。
  6. 最後,你可以用 Java 撰寫其他程式,調用這個 Java 類別與其中的原生方法。

參考: The Java Native Interface Programmer's Guide and Specification - Chapter 2 Getting Started

基本工具

  1. javah -jni -o output_header_file java_class_name
    根據指定的 Java 類別,產生撰寫原生方法所需的 C/C++ 標頭文件。如果該 Java 類別沒有宣告任何原生方法,則它產生的標頭文件中不會包含任何函數原型宣告。選項 -o 可指定產出的標頭文件名稱;建議使用此選項固定標頭文件名稱。
  2. javap -s -p java_class_name
    列出指定的 Java 類別的內部型態簽名,這是 JVM 用於標註符號型態的記號。原生方法需要這些簽名資訊,才能透過 JNI 的反射方法,存取 Java 類別和實體的屬性與方法。
  3. gcc -I$(JNI_INCLUDE) -fPIC -c
    編譯 C/C++ 程式碼。我們需要透過 -I 指示 jni.h 所在路徑,例如 -I/usr/lib/jvm/default-java/include;需要選項 -fPIC 產生可共享的目的碼。
  4. gcc -L$(JNI_LIB) -shared -o shared_library_file object_files
    將目的碼連結為共享函數庫文件 (dll/so)。選項 -L 指示 JNI 連結所需函數庫的路徑(有些 JDK 實作品不需要),例如 -L/usr/lib/jvm/default-java/lib。選項 -shared 指示連結器產出 shared library 。
  5. 如果你只有一個 C/C++ 程式文件,你也可以省略編譯產生目的碼的動作,直接將 gcc 的編譯與連結動作合併在一起。如下: gcc -I$(JNI_INCLUDE) -L$(JNI_LIB) -fPIC -shared -o shared_library_file c_soure_files

選項 -shared, -fPIC 是 gcc 專用的選項。其他 C/C++ 編譯器並不相同。例如 Solaris 系統提供的 C 編譯器使用的選項是 -G。

根據本文所需的文件內容,我將上述的工具操作指令,寫入一份 Makefile ,如下列 (ps. 我不喜歡用 ant)。為了方便理解各工具的操作對象,我沒有使用巨集語法。

# Makefile: jni

JNI_INCLUDE=/usr/lib/jvm/default-java/include
JNI_LIB=/usr/lib/jvm/default-java/lib

build: libHello.so

blog/rock/Hello.class: blog/rock/Hello.java
	javac blog/rock/Hello.java

hello-glue.h: blog/rock/Hello.class
	javah -jni -o hello-glue.h blog.rock.Hello
	touch hello-glue.h

libHello.so: blog/rock/Hello.class hello-glue.h hello-native.c
	gcc -I$(JNI_INCLUDE) -L$(JNI_LIB) \

	    -fPIC -shared -o libHello.so hello-native.c

sig: blog/rock/Hello.class
	javap -s -p blog.rock.Hello

clean:
	rm -f blog/rock/Hello.class libHello.so hello-glue.h

文件的目錄結構如下:

.
|-- blog
|   `-- rock
|       |-- Hello.class
|       `-- Hello.java
|-- hello-glue.h
|-- hello-native.c
|-- libHello.so
`-- Makefile

第一步、設計你的 Java 類別

blog/rock/Hello.java
package blog.rock;

class Hello {
    //匯入此類別時,一併載入包含原生方法的共享函數庫。

    static {
        //此例的共享函數庫文件名稱是 libHello.so,故此處給的參數是 "Hello".

        System.loadLibrary("Hello");
    }

    private String message1; //Accessing Strings

    private char[] message2; //Accessing Arrays of Primitive Types

    private int x; //Accessing Primitive Type and Field.

    private static int y; //Class field.


    public native boolean say(String msg, int x);
    public native Hello setMessage1(String msg);
    public native String getMessage1();
    public native Hello setMessage2(char[] msg);
    public native char[] getMessage2();
    public native Hello setX(int v);
    public native int getX();
    public native static void setY(int v);
    public native static int getY();
    
    //Java 對照組

    public boolean jsay(String msg, int x) {
        System.out.println("[J] msg: " + msg);
        System.out.println("[J] x: " + x);
        return true;
    }

    //這只是用來測試的方法。

    public static void main(String[] args) {
        String s;
        char[] msg2 = {'c', 'h', 'a', 'r'};
        Hello h = new Hello();

        h.say("world", 1);
        h.jsay("world", 1);
        s = h.setMessage1("rock").getMessage1();
        System.out.println( "h.message1: " + s );
        h.setMessage2(msg2);
        System.out.println( h.getMessage2() );
        h.setX(11);
        System.out.println( "h.x: " + h.getX() );
        Hello.setY(21);
        System.out.println( "Hello.y: " + Hello.getY() );
    }
}

C/C++ 透過JNI的反射方法存取個體欄位與方法,無視存取等級,public/protect/private 皆無妨礙。

第二步、編譯 Java 程式

使用 javac 編譯剛剛設計的 Hello.java ,產生 Hello.class 文件。我們將會需要透過這份 class 文件,產生撰寫原生方法所需的 C/C++ 標頭文件。

第三步、產生 C 標頭文件

編譯前一步驟的 Java 類別之後,使用 javah 產生撰寫原生方法所需的標頭文件。

在本例中,指定了產出的 C 標頭文件名稱為 hello-glue.h。

第四步、實作原生方法的程式內容

將 hello-glue.h 中的函數原型宣告複製起來,貼到 C 程式碼中 (hello-native.c),再補上函數的參數名稱。

原生方法的第一個參數的慣用名稱是 env 。實體原生方法的第二個參數的慣用名稱是 this;類別原生方法的第二個參數的慣用名稱是 cls 或是類別名稱。env 代表 JVM 在 C/C++ 程式碼中的實體,C/C++ 程式碼必須透過它提供的 JNI 函數,才能存取 JVM 管理的資源 (類別、實體、欄位與方法等)。JNI 函數請查閱 The Java Native Interface Programmer's Guide and Specification - JNI Functions .

透過上述操作,我們將得到最基本的 C 程式碼骨架,見下列。

hello-native.c
#include "hello-glue.h"
#include <stdio.h>
#include <stdlib.h>

/*
 * Class:     blog_rock_Hello
 * Method:    say
 * Signature: (Ljava/lang/String;I)Z
 */
JNIEXPORT jboolean JNICALL Java_blog_rock_Hello_say
  (JNIEnv *env, jobject this, jstring msg, jint x)
{
}

/*
 * Class:     blog_rock_Hello
 * Method:    setMessage1
 * Signature: (Ljava/lang/String;)Lblog/rock/Hello;
 */
JNIEXPORT jobject JNICALL Java_blog_rock_Hello_setMessage1
  (JNIEnv *env, jobject this, jstring msg)
{
}

/*
 * Class:     blog_rock_Hello
 * Method:    getMessage1
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_blog_rock_Hello_getMessage1
  (JNIEnv *env, jobject this)
{
}

/*
 * Class:     blog_rock_Hello
 * Method:    setMessage2
 * Signature: ([C)Lblog/rock/Hello;
 */
JNIEXPORT jobject JNICALL Java_blog_rock_Hello_setMessage2
  (JNIEnv *env, jobject this, jcharArray msg)
{
}

/*
 * Class:     blog_rock_Hello
 * Method:    getMessage2
 * Signature: ()[C
 */
JNIEXPORT jcharArray JNICALL Java_blog_rock_Hello_getMessage2
  (JNIEnv *env, jobject this)
{
}

/*
 * Class:     blog_rock_Hello
 * Method:    setX
 * Signature: (I)Lblog/rock/Hello;
 */
JNIEXPORT jobject JNICALL Java_blog_rock_Hello_setX
  (JNIEnv *env , jobject this, jint v)
{
}

/*
 * Class:     blog_rock_Hello
 * Method:    getX
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_blog_rock_Hello_getX
  (JNIEnv *env, jobject this)
{
}

/*
 * Class:     blog_rock_Hello
 * Method:    setY
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_blog_rock_Hello_setY
  (JNIEnv *env, jclass Hello, jint v)
{
}

/*
 * Class:     blog_rock_Hello
 * Method:    getY
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_blog_rock_Hello_getY
  (JNIEnv *env, jclass Hello)
{
}

接下來,我們就要開始填空,實現原生方法的內容。不過在填空之前,先介紹一下 JNI 中的資料存取方式。

JNI 的資料存取概要

JNI 的資料存取方法,依資料型別分成三大類。基本型別資料(primitive type data)是一類;字串與陣列是一類;除此之外的所有參考型別資料(reference type data)歸於一類。

基本型別資料(primitive type data)

基本型別資料(primitive type data)共有八種,參考: The Java Native Interface Programmer's Guide and Specification - 12.1.1 Primitive Types.

Java Language Type Native Type Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits

屬於這八種基本型別的參數,都可以直接存取;基本型別的欄性,則透過 Get<Type>Field/Set<Type>Field, GetStatic<Type>Field/SetStatic<Type>Field 方法存取。使用時不必考慮記憶體管理的問題。

字串與陣列

字串(String)與陣列(array) 屬於參考型別,但是 JNI 針對這兩種型別,各提供了一組存取方法,這些方法負責字串與陣列的配置與釋放管理工作。在實作原生方法時,必須要特別注意它們的記憶體管理工作,以免出現記憶體漏洞。

字串相關方法 (The Java Native Interface Programmer's Guide and Specification - String Operations):

  • NewString / NewStringUTF
  • GetStringLength / GetStringLengthUTF
  • GetStringChars / ReleaseStringChars, GetStringUTFChars / ReleaseStringUTFChars
  • GetStringCritical / ReleaseStringCritical
  • GetStringRegion / GetStringUTFRegion

陣列相關方法 (The Java Native Interface Programmer's Guide and Specification - Array Operations ):

  • NewObjectArray
  • GetArrayLength
  • Get<Type>ArrayElements / Release<Type>ArrayElements
  • GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical
  • Get<Type>ArrayRegion and Set<Type>ArrayRegion
  • GetObjectArrayElement / SetObjectArrayElement
參考型別資料(Reference type data)

字串與陣列之外的所有參考型別 (所有的 Java 類別),在 JNI 中,都被視為 jobject 型別的資料。在 JNI 存取 jobject 型別資料時,一律要透過 JNI 提供的反射方法。反射取得指定的欄位或方法後,同樣透過反射方法調用那些欄位或方法。如果你曾經使用過 Java 的反射功能 (java.lang.reflect 套件),你會發覺 JNI 提供的這些方法之使用概念與 java.lang.reflect 套件相同;或者可說 java.lang.reflect 就是把 JNI 提供的這些原始方法包裝起來罷了。下列對照程式碼,清楚地說明了兩者的關係。

// C 
    jclass cls = (*env)->GetObjectClass(env, this);
    jfieldID fid = (*env)->GetFieldID(env, cls, "x", "I");
    jint value = (*env)->GetIntField(env, this, fid);

// Java
    Class cls = this.getClass();
    Field fid = cls.getField("x");
    int value = fid.getInt(this);

JNI 反射方法所使用的型別簽名資訊,可使用 javap -s -p 查詢。詳情請查看: The Java Native Interface Programmer's Guide and Specification - 12.3 Field Descriptors .

實作原生方法的內容
say() - access String and primitive type parameters.
/*
 * Class:     blog_rock_Hello
 * Method:    say
 * Signature: (Ljava/lang/String;I)Z
 */
JNIEXPORT jboolean JNICALL Java_blog_rock_Hello_say
  (JNIEnv *env, jobject this, jstring msg, jint x)
{
    const char *c_msg;

    //get String type data.
    //存取參考型別(Reference type)資料時,需要透過 JNI 提供的特定方法。
    c_msg = (*env)->GetStringUTFChars(env, msg, NULL); //need ReleaseStringUTFChars
    printf("[C] msg: %s\n", c_msg);

    (*env)->ReleaseStringUTFChars(env, msg, c_msg);

    //get Primitive type data.
    //存取基本型別資料時,可以直接存取。
    printf("[C] x: %d\n", x);

    return JNI_TRUE; 
}

JNI 提供了一些常用的常數定義,例如 JNI_TRUE, JNI_FALSE。詳見 The Java Native Interface Programmer's Guide and Specification - 12.4 Constants .

setMessage1()/getMessage1() - access field of String type.

存取欄位時,需要透過 JNI 提供的反射方法,反射取得指定的欄位ID,再透過反射方法存取欄位內容。型別的簽名資訊請查看 The Java Native Interface Programmer's Guide and Specification - 12.3 Field Descriptors 或以工具 javap 查詢。

/*
 * Class:     blog_rock_Hello
 * Method:    setMessage1
 * Signature: (Ljava/lang/String;)Lblog/rock/Hello;
 */
JNIEXPORT jobject JNICALL Java_blog_rock_Hello_setMessage1
  (JNIEnv *env, jobject this, jstring msg)
{
    jclass cls = (*env)->GetObjectClass(env, this);
    jfieldID fid; // store the field ID

    //use javap -s -p to see data type signature.
    fid = (*env)->GetFieldID(env, cls, "message1", "Ljava/lang/String;");
    (*env)->SetObjectField(env, this, fid, msg);
    return this;
}

/*
 * Class:     blog_rock_Hello
 * Method:    getMessage1
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_blog_rock_Hello_getMessage1
  (JNIEnv *env, jobject this)
{
    jclass cls = (*env)->GetObjectClass(env, this);
    jfieldID fid; // store the field ID
    jstring msg;
    
    fid = (*env)->GetFieldID(env, cls, "message1", "Ljava/lang/String;");
    msg = (*env)->GetObjectField(env, this, fid);
    return msg;  //return NULL;
}
setMessage2()/getMessage2() - access field of array of primitive type.

基本型別陣列可以對應的專屬方法 Get<Type>ArrayElements()/Release<Type>ArrayElements() 取得指向其資料內容的指標,但是該內容的記憶體空間不一定是原本的那一塊。所以我們可以透過指標讀取內容,但寫入時並不會影嚮到原本的陣列內容。想要將資料寫入陣列時,需改用 GetPrimitiveArrayCritical()/ReleasePrimitiveArrayCritical() 方法。

/*
 * Class:     blog_rock_Hello
 * Method:    setMessage2
 * Signature: ([C)Lblog/rock/Hello;
 */
JNIEXPORT jobject JNICALL Java_blog_rock_Hello_setMessage2
  (JNIEnv *env, jobject this, jcharArray msg)
{
    jclass cls = (*env)->GetObjectClass(env, this);
    jfieldID fid; // store the field ID

    jchar *c_msg;
    int len, i;
    len = (*env)->GetArrayLength(env, msg);
    printf("[C] length of msg: %d\n", len);
    
    c_msg = (*env)->GetCharArrayElements(env, msg, NULL);

    printf("[C] dump char array: ");
    for (i = 0; i < len; ++i) {
        printf("%c", c_msg[i]);
    }
    printf("\n");

    (*env)->ReleaseCharArrayElements(env, this, c_msg, 0);

    fid = (*env)->GetFieldID(env, cls, "message2", "[C");
    (*env)->SetObjectField(env, this, fid, msg);
    return this;
}

/*
 * Class:     blog_rock_Hello
 * Method:    getMessage2
 * Signature: ()[C
 */
JNIEXPORT jcharArray JNICALL Java_blog_rock_Hello_getMessage2
  (JNIEnv *env, jobject this)
{
    jclass cls = (*env)->GetObjectClass(env, this);
    jfieldID fid; // store the field ID
    jstring msg;
    
    fid = (*env)->GetFieldID(env, cls, "message2", "[C");
    msg = (*env)->GetObjectField(env, this, fid);
    return msg;  //return NULL;
}
setX()/getX() - access field of primitive type

基本型別的實體欄性,則透過 Get<Type>Field()/Set<Type>Field() 方法存取。

/*
 * Class:     blog_rock_Hello
 * Method:    setX
 * Signature: (I)Lblog/rock/Hello;
 */
JNIEXPORT jobject JNICALL Java_blog_rock_Hello_setX
  (JNIEnv *env , jobject this, jint v)
{
    jclass cls = (*env)->GetObjectClass(env, this);
    jfieldID fid; // store the field ID

    fid = (*env)->GetFieldID(env, cls, "x", "I");
    (*env)->SetIntField(env, this, fid, v);
    return this;
}

/*
 * Class:     blog_rock_Hello
 * Method:    getX
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_blog_rock_Hello_getX
  (JNIEnv *env, jobject this)
{
    jclass cls = (*env)->GetObjectClass(env, this);
    jfieldID fid; // store the field ID
    
    fid = (*env)->GetFieldID(env, cls, "x", "I");
    return (*env)->GetIntField(env, this, fid);
}
setY()/getY() - access class field

類別原生方法的第二個參數是類別本身,而不是個體。其他部份都與實體原生方法相同。

/*
 * Class:     blog_rock_Hello
 * Method:    setY
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_blog_rock_Hello_setY
  (JNIEnv *env, jclass Hello, jint v)
{
    jfieldID fid; // store the field ID

    fid = (*env)->GetStaticFieldID(env, Hello, "y", "I");
    (*env)->SetStaticIntField(env, Hello, fid, v);
    return;
}

/*
 * Class:     blog_rock_Hello
 * Method:    getY
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_blog_rock_Hello_getY
  (JNIEnv *env, jclass Hello)
{
    jfieldID fid; // store the field ID
    
    fid = (*env)->GetStaticFieldID(env, Hello, "y", "I");
    return (*env)->GetStaticIntField(env, Hello, fid);
}

第五步、建立共享函數庫

使用 C/C++ 編譯器編譯 hello-native.c,建立 libHello.so。

第六步、執行 Java 程式

本例的 Hello.java 自帶了一個 main() 方法,包含了原生方法的基本使用程式碼。所以我們直接執行它,就可以觀察我們實作的原生方法是否正確地運作。

在執行程式之前,我們要先確認原生方法函數庫被放置在正確的搜尋路徑上。在 POSIX 平台上,你可以使用環境變數 LD_LIBRARY_PATH 指定共享函數庫的搜尋路徑。在 Win32 平台上,則使用環境變數 PATH 指定搜尋路徑。

Java2 新增了一個系統屬性 java.library.path,我們也可以在啟動 java 時,以選項 -D 指定共享函數庫的搜尋路徑。詳情參閱: The Java Native Interface Programmer's Guide and Specification - 2.7 Run the Program.

執行結果當如下列所示:

rock-desktop:~/workspace/jni-tutorial$ java blog.rock.Hello
Exception in thread "main" java.lang.UnsatisfiedLinkError: no Hello in java.library.path
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1681)
	at java.lang.Runtime.loadLibrary0(Runtime.java:840)
	at java.lang.System.loadLibrary(System.java:1047)
	at blog.rock.Hello.(Hello.java:7)
Could not find the main class: blog.rock.Hello. Program will exit.

rock@rock-desktop:~/workspace/jni-tutorial$ java -Djava.library.path=. blog.rock.Hello
[C] msg: world
[C] x: 1
[J] msg: world
[J] x: 1
h.message1: rock
[C] length of msg: 4
[C] dump char array: char
char
h.x: 11
Hello.y: 21

References

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/13163105.html