操作步驟
用 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
相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/13163105.html