Java Native Interface with C tutorial
操作步驟
- 用 Java 設計一個類別,將你想要用 C/C++ 實作的方法用修飾子(modifier) native 宣告為原生方法(native method)。原生方法除了不用在 Java 程式碼中寫出程式內容外,其他都與一般方法無異,一樣可以使用 public, protected, private, static, final, synchronized 等修飾子。
- 使用 javac 編譯你剛剛設計的 Java 類別,產生 class 文件。我們將會需要透過這份 class 文件,產生撰寫原生方法所需的 C/C++ 標頭文件。
- 使用 javah -jni 讀取 class 文件,產生原生方法的 C/C++ 標頭文件。標頭文件中含有原生方法的 C/C++ 函數原型宣告。
- 以 C/C++ 實作原生方法的程式內容。我們要先自前一步驟產生的 C/C++ 標頭文件,複製原生方法的 C/C++ 函數原型宣告到 C/C++ 程式碼中。
- 使用 C/C++ 編譯器編譯 C/C++ 程式文件,建立一個共享函數庫文件 (dll/so)。
- 最後,你可以用 Java 撰寫其他程式,調用這個 Java 類別與其中的原生方法。
參考: The Java Native Interface Programmer's Guide and Specification - Chapter 2 Getting Started
基本工具
-
javah -jni -o output_header_file java_class_name
根據指定的 Java 類別,產生撰寫原生方法所需的 C/C++ 標頭文件。如果該 Java 類別沒有宣告任何原生方法,則它產生的標頭文件中不會包含任何函數原型宣告。選項 -o 可指定產出的標頭文件名稱;建議使用此選項固定標頭文件名稱。 -
javap -s -p java_class_name
列出指定的 Java 類別的內部型態簽名,這是 JVM 用於標註符號型態的記號。原生方法需要這些簽名資訊,才能透過 JNI 的反射方法,存取 Java 類別和實體的屬性與方法。 -
gcc -I$(JNI_INCLUDE) -fPIC -c
編譯 C/C++ 程式碼。我們需要透過 -I 指示 jni.h 所在路徑,例如 -I/usr/lib/jvm/default-java/include;需要選項 -fPIC 產生可共享的目的碼。 -
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 。 - 如果你只有一個 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
- Java Native Interface with C tutorial, part 2
- The Java Native Interface Programmer's Guide and Specification