jni詳解(摘自《jni詳解》)


本書涵蓋了 Java Native Interface(JNI)的內容,將探討如下問題:
• 在一個 Java 項目中集成一個 C/C++庫
• 在一個用 C/C++開發的項目中,嵌入 JavaVM
• 實現 Java VM
• 語言互操做性問題,特別是互操做過程當中的垃圾回收(GC, garbage collection)和並
發編程(multithreading)
譯註:
JNI(Java Native Interface)是 SUN 定義的一套標準接口,如 Dalvik, Apache Harmony
項目...等 Java 虛擬機,都會實現 JNI 接口,供本地(C/C++)應用與 Java VM 互調。
JNI: 能夠供 Java 代碼調用本地代碼,本地代碼也能夠調用 Java 代碼,即上文列出的第
4 條內容:語言互操做;因此,這是一套完善而功能強大的接口。
可能有朋友據說過 KNI,那是 J2ME VM(CLDC)中定義的一套東西,不如 JNI 強大。
此外,由於 C/C++在系統編程領域的地位,只要打通了與 C/C++的接口,就等因而天塹變
通途。
首先,經過本書,你會很容易的掌握 JNI 開發,並能瞭解到方方面面的關於 JNI 的知識。
本書詳盡的敘述,會帶給你你不少如何高效使用 JNI 的啓示。JNI 自 1997 年初發布以來,
Sun 的工程師們和 Java 社區使用 JNI 的經驗造就了本書。
第二,本書介紹了 JNI 的設計原理。這些原理,不只會使學術界感興趣,也是高效使用
JNI 的前提。
第三,本書的某些部分是 Java 2 平臺規範的最終版本。 JNI 程序員能夠此書做爲規範的
參考手冊,Java 虛擬機實現者必須遵循規範,以保證各平臺實現的一致性。
(...幾段不重要,未翻譯...)
CHAPTER 1
Introduction
JNI 是 Java 平臺中的一個強大特性。
應用程序能夠經過 JNI 把 C/C++代碼集成進 Java 程序中。經過 JNI,開發者在利用 Java 平
臺強大功能的同時,又沒必要放棄對原有代碼的投資;由於 JNI 是 Java 平臺定義的規範接口,
當程序員向 Java 代碼集成本地庫時,只要在一個平臺中解決了語言互操做問題,就能夠把
該解決方案比較容易的移植到其餘 Java 平臺中。
譯註:
好比爲 Dalvik 添加了一個本地庫,也能夠把這個本地庫很容易的移植到 J2SE 和 Apache
Harmony 中,由於在 Java 與 C/C++互操做方面,你們都遵循一套 API 接口,即 JNI。
本書由下列三個部分組成:
• Chapter 2 經過簡單示例介紹了 JNI 編程
• Chapter 3-10,對 JNI 各方面特性和功能作介紹,並給出示例(譯者:重要)
• Chapters 11-13, 羅列 JNI 全部的數據類型的定義
(...幾段不重要,未翻譯...)
1.1 The Java Platform and Host Environment
因本書覆蓋了 Java 和本地(C, C++, etc...)編程語言,讓咱們首先理一理這些編程語言
的適用領域。
Java 平臺(Java Platform)的組成:Java VM 和 Java API. Java 應用程序使用 Java 語言
開發,而後編譯成與平臺無關的字節碼(.class 文件)。 Java API 由一組預約義的類組成。
任何組織實現的 Java 平臺都要支持:Java 編程語言,虛擬機,和 API(譯者:Sun 對 Java 語
言、虛擬機和 API 有明確規範)。
平臺環境: 操做系統,一組本機庫,和 CPU 指令集。本地應用程序, 一般依賴於一個特定
的平臺環境, 用 C、C++等語言開發,並被編譯成平臺相關的二進制指令,目標二進制代碼
在不一樣 OS 間通常不具備可移植性。
Java 平臺(Java VM 和 Java API)通常在某個平臺下開發。 好比,Sun 的 Java Runtime
Environment(JRE)支持類 Unix 和 Windows 平臺. Java 平臺作的全部努力,都爲了使程序更
具可移植性。
1.2 Role of the JNI
當 Java 平臺部署到本地系統中,有必要作到讓 Java 程序與本地代碼協同工做。 部分是
因爲遺留代碼(保護原有的投資)的問題(一些效率敏感的代碼用 C 實現,但如今 JavaVM 的執
行效率徹底可信賴),工程師們很早就開始以 C/C++爲基礎構建 Java 應用,因此,C/C++代碼
將長時間的與 Java 應用共存。
JNI 讓你在利用強大 Java 平臺的同時,使你仍然能夠用其餘語言寫程序。 做爲 JavaVM 的
一部分,JNI 是一套雙向的接口,容許 Java 與本地代碼間的互操做。
如圖 1.1 所示
做爲雙向接口,JNI 支持兩種類型本地代碼:本地庫和本地應用。
• 用本地代碼實現 Java 中定義的 native method 接口,使 Java 調用本地代碼
• 經過 JNI 你能夠把 Java VM 嵌到一個應用程序中,此時 Java 平臺做爲應用程序的增
強,使其能夠調用 Java 類庫
好比,在瀏覽器中運行 Applet, 當瀏覽器遇到"Applet"標籤,瀏覽器就會把標籤中的內
容交給 Java VM 解釋執行,這個實現,就是典型的把 JavaVM 嵌入 Browser 中。
譯註:
JNI 不僅是一套接口,仍是一套使用規則。 Java 語言有"native"關鍵字,聲明哪些方法
是用本地代碼實現的. 翻譯的時候,對於"native method",根據上下文意思作了不一樣處理,
當 native method 指代 Java 中用"native"關鍵字修飾的那些方法時,不翻譯;而當代碼用
C/C++實現的部分翻譯成了本地代碼。
上述,在應用中嵌入 Java VM 的方法,是用最少的力量,爲應用作最強擴展的不二選擇,
這時你的應用程序能夠自由使用 Java API 的全部功能;你們有興趣能夠讀一讀瀏覽器是怎
麼擴展 Applet 的,或者讀一讀 Palm WebOS 的東西。
譯者最近一年都在作這件事,對這個強大的功能,印象特別深入. 咱們整個小組作了兩
個平臺的擴展,設計、編碼、測試和 debug 用了近一年半時間,代碼量在 14000 行左右,
作完擴展後,平臺功能空前加強。我感受作軟件,可貴不在編碼,難在開始的設計和後期
的測試、調試和優化,並最終商用,這就要求最終產品是一個強大而穩定的平臺,達到此
目標是個曠日持久的事. 看看 Java,Windows,Linux,Qt,WebKit 發展了多少年?
向全部軟件工程師致敬!
1.3Implicationsof Using the JNI
請記住,當 Java 程序集成了本地代碼,它將丟掉 Java 的一些好處。
首先,脫離 Java 後,可移植性問題你要本身解決,且需從新在其餘平臺編譯連接本地庫。
第二,要當心處理 JNI 編程中各方面問題和來自 C/C++語言自己的細節性問題,處理不當,
應用將崩潰。
通常性原則:作好應用程序架構,使 native methods 定義在儘量少的幾個類裏。
譯註:
學習 JNI 編程是個漫長的實踐過程,會碰到無數問題。
用 C/C++編程,常見問題有內存泄露,指針越界...,此外使用了 JNI,還要面對 JavaVM
的問題:
• 在本地代碼中 new 一個 Java 對象後指望在本地代碼中維護此對象的引用,如何避免
被 GC?
• Java 面嚮對象語言的封裝性被破壞了,Java 類中任何方法和屬性對 JNI 都是可見的,
無論它是 public 的,仍是 private/protected/package 的
• 對 LocalRef/GlobalRef 管理不善,會引起 Table OverflowException,致使應用崩

• 從 JNI 調用 Java 的過程不是很直觀,每每幾行 Java 代碼能搞定的事情,用 JNI 實現
卻要幾百行
雖然,有這樣多問題,逃避不了,你就認了吧。 通過一段時間的實踐,當你能熟練處理
這些問題時,就會,眉頭一皺,文思泉涌,指尖飛舞,瞬間幾百行代碼誕生了,一個 make
所有編譯經過,這時的你確定已經對 JNI 上癮了......
1.4 When to Use the JNI
當你準備在項目中使用 JNI 以前,請先考慮一下是否有其餘更合適的方案。 上節有關 JNI
缺點的介紹,應該引發你足夠的重視。
這裏介紹幾個不經過 JNI 與其餘語言交互的技術:
• IPC 或者 經過 TCP/IP 網絡方案 ( Android ASE)
• 數據庫方面,可使用 JDBC
• 使用 Java 的分佈式對象技術:Java IDL API
譯註:
IPC 與 TCP/IP 是經常使用的基於協議的信息交換方案. 能夠參考 Android 上的 Binder 和
ASE(Android Script Environment)。
一典型的解決方案是,Java 程序與本地代碼分別運行在不一樣的進程中. 採用進程分置最
大的好處是:一個進程的崩潰,不會當即影響到另外一個進程。
可是,把 Java 代碼與本地代碼置於一個進程有時是必要的。 以下:
• Java API 可能不支某些平臺相關的功能。好比,應用程序執行中要使用 Java API 不
支持的文件類型,而若是使用跨進程操做方式,即繁瑣又低效
• 避免進程間低效的數據拷貝操做
• 多進程的派生:耗時、耗資源(內存)
• 用本地代碼或彙編代碼重寫 Java 中低效方法
總之,若是 Java 必須與駐留同進程的本地代碼交互,請使用 JNI。
譯註:
寫代碼是技巧和藝術,看你想在設計上下多大功夫. 好比:Chrome,是多進程的典範,
她的簡潔、高效,使人歎服。
1.5Evolution of the JNI
關於 Java 應用程序如何與本地代碼互操做的問題,在 Java 平臺早期就被提了出來.
JDK1.0 包括了一套與本地代碼交互的接口。 當時許多 Java 方法和庫都依賴本地方法實現
(如 java.io, java.net)。
可是,JDKrelease 1.0 有兩個主要問題:
• Java 虛擬機規範未定義對象佈局,本地代碼訪問對象的成員是經過訪問 C 結構的成
員實現的
• 本地代碼能夠獲得對象在內存中的地址,因此,本地方法是 GC 相關的
爲解決上述問題對 JNI 作了從新設計,讓這套接口在全部平臺都容易獲得支持。
• 虛擬機實現者經過 JNI 支持大量的本地代碼
• 工具開發商不用處理不一樣種類的本地接口
• 全部 JNI 開發者面對的是操做 JavaVM 的規範 API
JNI 的首次支持是在 JDKrelease 1.1,但 1.1 內部 Java 與本地代碼的交互仍然使用原始
方式(JDK 1.0). 但這種局面,沒有持續好久,在 Java 2SDKrelease 1.2 中 Java 層與本
地代碼的交互部分用 JNI 重寫了。
做爲 JavaVM 規範的一部分,Java 層與本地代碼的交互,都應經過 JNI 實現。
1.6Example Programs
本書注重 JNI 編程,不涉及如何經過第三方工具簡化該過程。
(譯者:不重要,未翻譯)。
請從官網下載本書的示例代碼:http://java.sun.com/docs/books/jni/
CHAPTER 2
Getting Started
本章用 Hello World 示例帶你領略 JNI 編程。
2.1 Overview
準備過程:
1. 建立一個類(HelloWorld.java)
2. 使用 javac 編譯該類
3. 利用 javah -jni 產生頭文件
4. 用本地代碼實現頭文件中定義的方法
5. Run
譯註:
在一個特定環境中,寫本地實現的過程是不一樣的(如 Android)。
javah 主要是生成頭文件和函數簽名(每一個方法和成員都有簽名,後有詳細介紹),經過
javah 學習如何正確的寫法。
注意:如上述 HelloWorld.java,編譯後的文件爲 HelloWorld.class, 用
$javah HelloWorld
來產生頭文件,不要帶末尾的".class"。
2.2Declare the Native Method
HelloWorld.java
class HelloWorld {
private native void print();
public static void main(String[] args) {
new HelloWorld().print();
}
static {
System.loadLibrary("HelloWorld");
}
}
HelloWrold 類首先聲明瞭一個 private native print 方法. static 那幾行是本地庫。
在 Java 代碼中聲明本地方法必須有"native"標識符,native 修飾的方法,在 Java 代碼中
只做爲聲明存在。
在調用本地方法前,必須首先裝載含有該方法的本地庫. 如 HelloWorld.java 中所示,置
於 static 塊中,在 Java VM 初始化一個類時,首先執行這部分代碼,這可保證調用本地方
法前,裝載了本地庫。
裝載庫的機制,後有介紹。
2.3Compile the HelloWorld Class
$javac HelloWorld.java
2.4Create the Native Method Header File
$javah -jni HelloWorld
譯者:"-jni"爲默認參數,無關緊要.
上述命令,生成 HelloWorld.h 文件. 關鍵部分以下:
JNIEXPORTvoid JNICALL
Java_HelloWorld_print (JNIEnv *, jobject);
如今,請先忽略兩個宏:JNIEXPORT 和 JNICALL。 你會發現,該函數聲明,接受兩個參數,
而對應的 Java 代碼對該函數的聲明沒有參數。第一個參數是指向 JNIEnv 結構的指針; 第
二個參數,爲 HelloWorld 對象自身,即 this 指針。
譯註:
JNIEnv 是 JNI 核心數據之一,地位很是崇高,全部對 JNI 的調用都要經過此結構。
2.5 Write the Native Method Implementation
必須根據 javah 生成的本地函數聲明實現函數,以下:
#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h"
JNIEXPORT void JNICALL
Java_HelloWorld_print(JNIEnv *env, jobject obj)
{
printf("Hello World!\n");
return;
}
請注意:"jni.h"文件必須被包含,該文件定義了 JNI 全部的函數聲明和數據類型。
2.6Compile the C Source and Create a Native Library
請注意,生成的本地庫的名字,必須與 System.loadLibrary("HelloWorld");待裝載庫
的名字相同。
Solaris:
$cc -G -I/java/include -I/java/include/solaris HelloWorld.c -o libHelloWorld.so
-G: 生成共享庫
Win:
$cl -Ic:\java\include -Ic:\java\include\win32 -MD -LD HelloWorld.c
-FeHelloWorld.dll
-MD:保證與 Win32 多線程 C 庫鏈接(譯者:Win 上分靜態、動態、動態多線程...C 庫)
-LD: 生成動態連接庫
2.7 Run the Program
Solarisor Win:
$java HelloWorld
輸出:
Hello World!
運行前,必須保證鏈接器,能找到待裝載的庫,否則,將拋以下異常:
java.lang.UnsatisfiedLinkError: no HelloWorld in library path
at java.lang.Runtime.loadLibrary(Runtime.java)
at java.lang.System.loadLibrary(System.java)
at HelloWorld.main(HelloWorld.java)
如,Solaris, 經過 sh 或 ksh shell:
$LD_LIBRARY_PATH=.
$export LD_LIBRARY_PATH
C shell:
$setenv LD_LIBRARY_PATH.
在 Win 上,請保證待裝載庫在當前位置,或在 PATH 環境變量中。
你也能夠以下:
java -Djava.library.path=. HelloWorld
-D:設置 Java 平臺的系統屬性。 此時 JavaVM 能夠在當前位置找到該庫。
CHAPTER 3
Basic Types, Strings, and Arrays
JNI 編程中常被提到的問題是,Java 語言中的數據類型是如何映射到 c/c++本地語言中的。
實際編程中,向函數傳參和函數返回值是很廣泛的事情。 本章將介紹這方面技術,咱們
從基本類型(如 int)和通常對象(如 String 和 Array)開始介紹. 其餘內容將放在下一章介紹。
譯註:
JavaVM 規範中稱 int,char,byte 等爲 primitive types,譯者平時叫慣了基本類型,
因此翻譯時延用了習慣,不知合適否。
3.1 ASimple Native Method
擴充 HelloWorld.java,該例是先打印一串字符,而後等待用戶的輸入, 以下:
class Prompt {
// native method that prints a prompt and reads a line
private native String getLine(String prompt);
public static void main(String args[]) {
Prompt p = new Prompt();
String input = p.getLine("Type a line: ");
System.out.println("User typed: " + input);
}
static {
System.loadLibrary("Prompt");
}
}
Prompt.getLine 方法的 C 聲明以下:
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);
3.1.2Native Method Arguments
Java_Prompt_getLine 接收 3 個參數:JNIEnv 結構包括 JNI 函數表。
第二個參數的意義取決於該方法是靜態仍是實例方法(static or an instance method)。
當本地方法做爲一個實例方法時,第二個參數至關於對象自己,即 this. 當本地方法做爲
一個靜態方法時,指向所在類. 在本例中,Java_Prompt_getLine 是一個本地實例方法實現,
因此 jobject 指向對象自己。
譯註:
Java 語言中類與對象的聯繫與區別,概念很清晰,但在 JNI 和 VM 中,有一些問題須要說
明,後有專門文章闡述。
3.1.3Mapping of Types
在 native method 中聲明的參數類型,在 JNI 中都有對應的類型.
在 Java 中有兩類數據類型:primitive types,如,int, float, char;另外一種爲
reference types,如,類,實例,數組。
譯者:
數組,無論是對象數組仍是基本類型數組,都做爲 reference types 存在,並有專門的
JNI 方法取數組中每一個元素.
Java 與 JNI 基本類型的映射很直接,以下:
Java Native(jni.h)
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble
相比基本類型,對象類型的傳遞要複雜不少。 Java 層對象做爲 opaque references(指
針)傳遞到 JNI 層。 Opaque references 是一種 C 的指針類型,它指向 JavaVM 內部數據結構。
使用這種指針的目的是:不但願 JNI 用戶瞭解 JavaVM 內部數據結構。對 Opaque reference
所指結構的操做,都要經過 JNI 方法進行. 好比,"java.lang.String"對象,JNI 層對應的
類型爲 jstring,對該 opaque reference 的操做要經過 JNIEnv->GetStringUTFChars 進行。
譯註:
必定要按這種原則編程,千萬不要爲了效率或容易的取到某個值,繞過 JNI,直接操做
opaque reference.
JNI 是一套完善接口,全部需求都能知足。
在 JNI 中對象的基類即爲 jobject. 爲方便起見,還定義了 jstring,jclass,
jobjectArray 等結構,他們都繼承自 jobject。
3.2 Accessing Strings
以下使用方式是錯誤的,由於 jstring 不一樣於 C 中的 char *類型。
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
/* ERROR: incorrect use of jstring as a char* pointer */
printf("%s", prompt);
...
}
3.2.1Converting to Native Strings
使用對應的 JNI 函數把 jstring 轉成 C/C++字串。JNI 支持 Unicode/UTF-8 字符編碼互轉。
Unicode 以 16-bits 值編碼;UTF-8 是一種以字節爲單位變長格式的字符編碼,並與 7-bits
ASCII 碼兼容。UTF-8 字串與 C 字串同樣,以 NULL('\0')作結束符, 當 UTF-8 包含非 ASCII
碼字符時,以'\0'作結束符的規則不變。7-bit ASCII 字符的取值範圍在 1-127 之間,這些
字符的值域與 UTF-8 中相同。當最高位被設置時,表示多字節編碼。
以下,調用 GetStringUTFChars,把一個 Unicode 字串轉成 UTF-8 格式字串,若是你肯定
字串只包含 7-bit ASCII 字符。這個字串可使用 C 庫中的相關函數,如 printf.
如何操做 non-ASCII 字符,後面有介紹。
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
char buf[128];
const jbyte *str;
str = (*env)->GetStringUTFChars(env, prompt, NULL);
if (str == NULL) {
return NULL; /* OutOfMemoryError already thrown */
}
printf("%s", str);
(*env)->ReleaseStringUTFChars(env, prompt, str);
/* We assume here that the user does not type more than
* 127 characters */
scanf("%127s", buf);
return (*env)->NewStringUTF(env, buf);
}
記得檢測 GetStringUTFChars 的返回值,由於調用該函數會有內存分配操做,失敗後,該
函數返回 NULL,並拋 OutOfMemoryError 異常。
如何處理異常,後面會有介紹。JNI 處理異常,不一樣於 Java 中的 try...catch。在 JNI 中,
發生異常,不會改變代碼執行軌跡,因此,當返回 NULL,要及時返回,或立刻處理異常。
3.2.2 Freeing Native String Resources
調用 ReleaseStringUTFChars 釋放 GetStringUTFChars 中分配的內存(Unicode ->UTF-8
轉換的緣由)。
3.2.3Constructing NewStrings
使用 JNIEnv->NewStringUTF 構造 java.lang.String;若是此時沒有足夠的內存,
NewStringUTF 將拋 OutOfMemoryError 異常,同時返回 NULL。
3.2.4 Other JNI String Functions
除了 GetStringUTFChars, ReleaseStringUTFChars, 和 NewStringUTF, JNI 還支持其餘
操做 String 的函數供使用。
GetStringChars 是有 Java 內部 Unicode 到本地 UTF-8 的轉換函數,能夠調用
GetStringLength,得到以 Unicode 編碼的字串長度。也可使用 strlen 計算
GetStringUTFChars 的返回值,獲得字串長度。
const jchar *GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);
上述聲明中,有 isCopy 參數,當該值爲 JNI_TRUE,將返回 str 的一個拷貝;爲
JNI_FALSE 將直接指向 str 的內容。 注意:當 isCopy 爲 JNI_FALSE,不要修改返回值,不
然將改變 java.lang.String 的不可變語義。
通常會把 isCopy 設爲 NULL,不關心 Java VM 對返回的指針是否直接指向
java.lang.String 的內容。
通常不能預知 VM 是否會拷貝 java.lang.String 的內容,程序員應該假設 GetStringChars
會爲 java.lang.String 分配內存。在 JavaVM 的實現中,垃圾回收機制會移動對象,併爲對
象從新配置內存。一但 java.lang.String 佔用的內存暫時沒法被 GC 從新配置,將產生內存
碎片,過多的內存碎片,會更頻繁的出現內存不足的假象。
記住在調用 GetStringChars 以後,要調用 ReleaseStringChars 作釋放,無論在調用
GetStringChars 時爲 isCopy 賦值 JNI_TRUE 仍是 JNI_FALSE,因不一樣 JavaVM 實現的緣由,
ReleaseStringChars 可能釋放內存,也可能釋放一個內存佔用標記(isCopy 參數的做用,從
GetStringChars 返回一個指針,該指針直接指向 String 的內容,爲了不該指針指向的內
容被 GC,要對該內存作鎖定標記)。
3.2.5NewJNI String Function in Java 2SDK Release 1.2
爲儘量的避免內存分配,返回指向 java.lang.String 內容的指針,Java 2SDK
release 1.2 提供了:Get/RleaseStringCritical. 這對函數有嚴格的使用原則。
當使用這對函數時,這對函數間的代碼應被當作臨界區(critical region). 在該代碼區,
不要調用任何會阻塞當前線程和分配對象的 JNI 函數,如 IO 之類的操做。
上述原則,能夠避免 JavaVM 執行 GC。由於在執行 Get/ReleaseStringCritical 區的代碼
時,GC 被禁用了,若是因某些緣由在其餘線程中引起了 JavaVM 執行 GC 操做,VM 有死鎖的
危險:當前線程 A 進入 Get/RelaseStringCritical 區,禁用了 GC,若是其餘線程 B 中有 GC
請求,因 A 線程禁用了 GC,因此 B 線程被阻塞了;而此時,若是 B 線程被阻塞時已經得到了
一個 A 線程執行後續工做時須要的鎖;死鎖發生了。
能夠嵌套調用 GetStringCritical:
jchar *s1, *s2;
s1 = (*env)->GetStringCritical(env, jstr1);
if (s1 == NULL) {
... /* error handling */
}
s2 = (*env)->GetStringCritical(env, jstr2);
if (s2 == NULL) {
(*env)->ReleaseStringCritical(env, jstr1, s
... /* error handling */
}
...  /* use s1 and s2 */
(*env)->ReleaseStringCritical(env, jstr1, s1);
(*env)->ReleaseStringCritical(env, jstr2, s2);
GetStringCritical 因 VM 實現的緣由,會涉及內存操做,因此咱們須要檢查返回指. 好比,
對於 java.lang.String 來講,VM 內部並非連續存儲的,因此 GetStringCritical 要返回
一個連續的字符數組,必然要有內存操做。
爲避免死鎖,此時應儘可能避免調用其餘 JNI 方法,只容許調用
GetStringCritical/ReleaseStringCritical,Get/ReleasePrimitiveArrayCritical 因 VM 內
部 Unicode 編碼的緣故,因此 Get/ReleaseStringUTFCritical 這種涉及 Unicode->UTF8 轉
換要分配內存的函數不支持。
GetStringRegion/GetStringUTFRegion,向準備好的緩衝區賦值,以下:
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
/*assumethepromptstringanduserinputhaslessthan128
characters */
char outbuf[128], inbuf[128];
int len = (*env)->GetStringLength(env, prompt);
(*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf);
printf("%s", outbuf);
scanf("%s", inbuf);
return (*env)->NewStringUTF(env, inbuf);
}
GetStringUTFRegion 有兩個參數,starting index 和 length, 這兩個參數以 Unicode 編
碼計算. 該函數會作邊界檢查,因此可能拋出 StringIndexOutOfBoundsException。
由於該函數不涉及內存操做,因此較 GetStringUTFChars 使用要簡單。
譯註:
有兩個函數:GetStringLength/GetStringUTFLength,前者是 Unicode 編碼長度,後者
是 UTF 編碼長度。
GetStringUTFRegion 頗有用,由於你不能修改 GetStringUTFChars 返回值,因此須要另
外 malloc/strcpy 以後,再操做返回值,耗時費力,不如直接使用 GetStringUTFRegion 來
的簡潔、高效。
3.2.6Summaryof JNI String Functions
JNI Function Description Since
GetStringChars
ReleaseStringChars
Obtains or releases a pointer to the
contents of a string in Unicode
format.May return a copy of the
string.
JDK1.1
GetStringUTFChars
ReleaseStringUTFChars
Obtains or releases a pointer to the
contents of a string in UTF-8
format.
May return a copy of the string.
JDK1.1
GetStringLength
Returns the number of Unicode
characters in the string.
JDK1.1
GetStringUTFLength
Returns the number of bytes
needed(not including the trailing
0) to represent a string in the UTF-8 format.
JDK1.1
NewString
Creates a java.lang.String
instance that contains the same
sequence of characters as the given
Unicode C string.
JDK1.1
NewStringUTF
Creates a java.lang.String
instance that contains the same
sequence of characters as the given
UTF-8 encoded C string.
JDK1.1
GetStringCritical
ReleaseStringCritical
Obtains a pointer to the contents of
a string in Unicode format. May
return a copy of the string. Native
code must not block between a
pair of Get/
ReleaseStringCritical calls.
Java 2
SDK1.2
GetStringRegion
SetStringRegion
Copies the contents of a string to
or from a preallocated C buffer in
the Unicode format.
Java 2
SDK1.2
GetStringUTFRegion
SetStringUTFRegion
Copies the content of a string to or
from a preallocated C buffer in the
UTF-8 format.
Java 2
SDK1.2
3.2.7Choosing among the String Functions
該表給出了選擇字符串函數的策略:
若是你使用 JDK 1.1 或 JDK 1.2,你只能使用 Get/ReleaseStringChars 和
Get/ReleaseStringUTFChars。
對於小尺寸字串的操做,首選 Get/SetStringRegion 和 Get/SetStringUTFRegion,由於棧
上空間分配,開銷要小的多;並且沒有內存分配,就不會有 out-of-memoryexception。如
果你要操做一個字串的子集,本套函數的 starting index 和 length 正合要求。
GetStringCritical 必須很是當心使用。你必須確保不分配新對象和任何阻塞系統的操做,
以免發生死鎖。以下,因調用 fprintf, 該 c 函數要執行 IO 操做,因此是不安全的。
/* This is not safe! */
const char *c_str = (*env)->GetStringCritical(env, j_str, 0);
if (c_str == NULL) {
... /* error handling */
}
fprintf(fd, "%s\n", c_str);
(*env)->ReleaseStringCritical(env, j_str, c_str);
上述代碼,不安全的緣由: 當前線程執行了 GetStringCritical 後將禁用 GC. 假設,T 線
程正等待從 fd 讀取數據. 進一步假設,調用 fprintf 時使用的系統緩存將等待 T 讀取完畢
後設置. 咱們製造了一個死鎖情景:若是 T 在讀取數據時有內存分配需求,可能使
JavaVM 執行 GC. 而此時的 GC 請求將被阻塞,直到當前線程執行 ReleaseStringCritical,
不幸的時,這個操做必須等 fprintf 調用完畢後纔會執行。此時,死鎖發生。
因此,當你調用 Get/RleaseStringCritical 要時刻警戒死鎖。
3.3 Accessing Arrays
JNI 對每種數據類型的數組都有對應的函數。
class IntArray {
private native int sumArray(int[] arr);
public static void main(String[] args) {
IntArray p = new IntArray();
int arr[] = new int[10];
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
int sum = p.sumArray(arr);
System.out.println("sum = " + sum);
}
static {
System.loadLibrary("IntArray");
}
}
3.3.1 Accessing Arraysin C
以下直接操做數組是錯誤的:
/* This program is illegal! */
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
int i, sum = 0;
for (i = 0; i < 10; i++) {
sum += arr[i];
}
}
以下操做正確:
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint buf[10];
jint i, sum = 0;
(*env)->GetIntArrayRegion(env, arr, 0, 10, buf);
for (i = 0; i < 10; i++) {
sum += buf[i];
}
return sum;
}
JNI 中數組的基類爲 jarray,其餘如 jintArray 都繼承自 jarray。
3.3.2 Accessing Arraysof Primitive Types
上節示例中,使用 GetIntArrayRegion 拷貝數組內容到 buf 中,這裏沒有作越界異常檢測,
由於知道數組有 10 個,參數 3 爲待拷貝數組的起始位置,參數 4 爲拷貝元素的個數。
JNI 支持 SetIntArrayRegion 容許從新設置數組一個區域的值,其餘基本類型(boolean,
short, 和 float)也有對應的支持。
JNI 支持經過 Get/Release<Type>ArrayElemetns 返回 Java 數組的一個拷貝(實現優良的
VM,會返回指向 Java 數組的一個直接的指針,並標記該內存區域,不容許被 GC)。
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint *carr;
jint i, sum = 0;
carr = (*env)->GetIntArrayElements(env, arr, NULL);
if (carr == NULL) {
return 0; /* exception occurred */
}
for (i=0; i<10; i++) {
sum += carr[i];
}
(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
return sum;
}
GetArrayLength 返回數組元素個數。
Java 2SDKrelease 1.2 支持 Get/ReleasePrimitiveArrayCritical,該套函數的使用原
則與上述 String 部分相同。
3.3.3Summaryof JNI Primitive Array Functions
JNI Function  Description Since
Get<Type>ArrayRegion
Set<Type>ArrayRegion
Copies the contents of primitive
arrays to or from a preallocated
C buffer.
JDK1.1
Get<Type>ArrayElements
Release<Type>ArrayElements
Obtains a pointer to the contents
of a primitive array.May return a
copy of the array.
JDK1.1
GetArrayLength
Returns the number of elements
in the array.
JDK1.1
New<Type>Array
Creates an array with the
given length.
JDK1.1
GetPrimitiveArrayCritical
ReleasePrimitiveArrayCritica
l
Obtains or releases a pointer
to the contents of a primitive
array. May disable garbage
collection, or return a copy
of the array.
Java 2
SDK1.2
3.3.4Choosing among the Primitive Array Functions
使用原則,與上述 String 部分相同,請閱讀原文或回顧前面的內容。
3.3.5 Accessing Arraysof Objects
對於對象數組的訪問,使用 Get/SetObjectArrayElement,對象數組只提供針對數組的每
個元素的 Get/Set,不提供相似 Region 的區域性操做。
以下,二維數組示例,Java 部分
class ObjectArrayTest {
private static native int[][] initInt2DArray(int size);
public static void main(String[] args) {
int[][] i2arr = initInt2DArray(3);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
System.out.print(" " + i2arr[i][j]);
}
System.out.println();
}
}
static {
System.loadLibrary("ObjectArrayTest");
}
}
JNI 部分:
JNIEXPORT jobjectArray JNICALL
Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int
size)
{
jobjectArray result;
int i;
jclass intArrCls = (*env)->FindClass(env, "[I");
if (intArrCls == NULL) {
return NULL; /* exception thrown */
}
result = (*env)->NewObjectArray(env, size, intArrCls, NULL);
if (result == NULL) {
return NULL; /* out of memory error thrown */
}
for (i = 0; i < size; i++) {
jint tmp[256]; /* make sure it is large enough! */
int j;
jintArray iarr = (*env)->NewIntArray(env, size);
if (iarr == NULL) {
return NULL; /* out of memory error thrown */
}
for (j = 0; j < size; j++) {
tmp[j] = i + j;
}
(*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
(*env)->SetObjectArrayElement(env, result, i, iarr);
(*env)->DeleteLocalRef(env, iarr);
}
return result;
}
newInt2DArray 方法首先調用 FindClass 得到一個一維 int 數組. "[I"做爲 JNI 類描述符
等價於 Java int[]聲明。FindClass 當裝載類失敗,返回 NULL(多是沒找到類或內存不足)。
譯註:
類描述符,也能夠叫作"類簽名"。簽名的做用:爲了準確描述一件事物. Java Vm 定義
了類簽名,方法簽名;其中方法簽名是爲了支持方法重載。
FindClass 返回 NULL 的緣由:
• 提供了錯誤的類描述符
• 沒法在當前 ClassLoader 上下文中找到類
解決辦法:
• 認真檢查類描述符是否正確
• 以"/"做爲包分隔符, 即類描述符的形式爲"xxx/xxx/xxx",而非"xxx.xxx.xxx",也
可簡單記憶爲"/"用在本地形式(或虛擬機)中;"."分隔符,用在 Java(Java
Programming Language)環境中;而且類描述符末尾沒有".java",如
FindClass("java/lang/String")而非 FindClass("java/lang/String.java")
• 構造 ClassLoader,並利用 Class.forName(String name, boolean initialize,
ClassLoader loader)裝載類
其中第三個解決辦法比較複雜,涉及到 Java 的雙親委派模型,類與對象相容性斷定等問
題,將有專門文章闡述。
而後調用 NewObjectArray 分配一個對象數組。注意,"基本類型數組"這是個總體的概念,
它是一個對象。後面咱們要填充它。
注意,DeleteLocalRef 是釋放局部對象引用。
譯註:
Java 中有許多引用的概念,咱們只關心 GlobalRef 和 LocalRef 兩種。JNI 編程很複雜,
建議不要引入更多複雜的東西,正確、高效的實現功能就能夠了。好比對引用來講,最好
不要在 JNI 中考慮:虛引用和影子引用等複雜的東西。
GlobalRef: 當你須要在 JNI 層維護一個 Java 對象的引用,而避免該對象被垃圾回收時,
使用 NewGlobalRef 告訴 VM 不要回收此對象,當本地代碼最終結束該對象的引用時,用
DeleteGlobalRef 釋放之。
LocalRef: 每一個被建立的 Java 對象,首先會被加入一個 LocalRef Table,這個 Table 大
小是有限的,當超出限制,VM 會報 LocalRef OverflowException,而後崩潰. 這個問題
是 JNI 編程中常常碰到的問題,請引發高度警戒,在 JNI 中及時經過 DeleteLocalRef 釋放
對象的 LocalRef. 又,JNI 中提供了一套函數:Push/PopLocalFrame,由於 LocalRef
Table 大小是固定的,這套函數只是執行相似函數調用時,執行的壓棧操做,在 LocalRef
Table 中預留一部分供當前函數使用,當你在 JNI 中產生大量對象時,虛擬機仍然會因
LocalRef OverflowException 崩潰,因此使用該套函數你要對 LocalRef 使用量有準確估
計。
CHAPTER 4
Fieldsand Methods
本章介紹如何訪問對象成員,如何從本地代碼調用 Java 方法,即以 callback 方式從本地
代碼調用 Java 代碼;最後介紹一些優化技術。
4.1 Accessing Fields
Java 語言支持兩種成員(field):(static)靜態成員和實例成員. 在 JNI 獲取和賦值成員
的方法是不一樣的.
譯者:
Java 層的 field 和 method,無論它是 public,仍是 package、private 和 protected,從
JNI 均可以訪問到,Java 面向語言的封裝性不見了。
Java:
class InstanceFieldAccess {
private String s;
private native void accessField();
public static void main(String args[]) {
InstanceFieldAccess c = new InstanceFieldAccess();
c.s = "abc";
c.accessField();
System.out.println("In Java:");
System.out.println(" c.s = \"" + c.s + "\"");
}
static {
System.loadLibrary("InstanceFieldAccess");
}
}
JNI:
JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj)
{
jfieldID fid; /* store the field ID */
jstring jstr;
const char *str;
/* Get a reference to obj’s class */
jclass cls = (*env)->GetObjectClass(env, obj);
printf("In C:\n");
/* Look for the instance field s in cls */
fid = (*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");
if (fid == NULL) {
return; /* failed to find the field */
}
/* Read the instance field s */
jstr = (*env)->GetObjectField(env, obj, fid);
str = (*env)->GetStringUTFChars(env, jstr, NULL);
if (str == NULL) {
return; /* out of memory */
}
printf(" c.s = \"%s\"\n", str);
(*env)->ReleaseStringUTFChars(env, jstr, str);
/* Create a new string and overwrite the instance field */
jstr = (*env)->NewStringUTF(env, "123");
if (jstr == NULL) {
return; /* out of memory */
}
(*env)->SetObjectField(env, obj, fid, jstr);
}
輸出:
In C:
c.s = "abc"
In Java:
c.s = "123"
4.1.1Procedure for Accessing an Instance Field
訪問對象成員分兩步,首先經過 GetFieldID 獲得對象成員 ID, 以下:
fid =(*env)->GetFieldID(env, cls, "s", "Ljava/lang/String;");
示例代碼,經過 GetObjectClass 從 obj 對象獲得 cls.
這時,經過在對象上調用下述方法得到成員的值:
jstr =(*env)->GetObjectField(env, obj, fid);
示例中要獲得的是一個對象類型,因此用 GetObjectField. 此外 JNI 還提供
Get/SetIntField,Get/SetFloatField 訪問不一樣類型成員。
譯者:
經過 JNI 方法訪問對象的成員,JNI 對應的函數命名很是有規律,即 Get/Set<Return
Value Type>Field。
4.1.2 Field Descriptors
此章主要講述簽名問題,較繁瑣,能夠總結以下:
Type Signature Java Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double
Lfully-qualified-class ; fully-qualified-class
[type type[]
( arg-types) ret-type  method type
以下 Java 方法:
long f (int n, String s, int[]arr);
signature:  "(ILjava/lang/String;[I)J"
簽名是一種用參數個數和類型區分同名方法的手段,即解決方法重載問題。
其中要特別注意的是:
1. 類描述符開頭的'L'與結尾的';'必需要有
2. 數組描述符,開頭的'['必須有.
3. 方法描述符規則: "(各參數描述符)返回值描述符",其中參數描述符間沒有任何分隔
符號
描述符很重要,請爛熟於心. 寫 JNI,對於錯誤的簽名必定要特別敏感,此時編譯器幫不
上忙,執行 make 前仔細檢查你的代碼。
4.1.3 Accessing Static Fields
靜態成員訪問與實例成員相似。
Java:
class StaticFielcdAccess {
private static int si;
private native void accessField();
public static void main(String args[]) {
StaticFieldAccess c = new StaticFieldAccess();
StaticFieldAccess.si = 100;
c.accessField();
System.out.println("In Java:");
System.out.println(" StaticFieldAccess.si = " + si);
}
static {
System.loadLibrary("StaticFieldAccess");
}
}
JNI:
JNIEXPORT void JNICALL
Java_StaticFieldAccess_accessField(JNIEnv *env, jobject obj)
{
jfieldID fid; /* store the field ID */
jint si;
/* Get a reference to obj’s class */
jclass cls = (*env)->GetObjectClass(env, obj);
printf("In C:\n");
/* Look for the static field si in cls */
fid = (*env)->GetStaticFieldID(env, cls, "si", "I");
if (fid == NULL) {
return; /* field not found */
}
/* Access the static field si */
si = (*env)->GetStaticIntField(env, cls, fid);
printf(" StaticFieldAccess.si = %d\n", si);
(*env)->SetStaticIntField(env, cls, fid, 200);
}
輸出:
In C:
StaticFieldAccess.si = 100
In Java:
StaticFieldAccess.si = 200
請閱讀上述代碼,再也不敘述。
4.2Calling Methods
Java 中有三類方法:實例方法、靜態方法和構造方法。
class InstanceMethodCall {
private native void nativeMethod();
private void callback() {
System.out.println("In Java");
}
public static void main(String args[]) {
InstanceMethodCall c = new InstanceMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary("InstanceMethodCall");
}
}
JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj)
{
jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "()V");
if (mid == NULL) {
return; /* method not found */
}
printf("In C\n");
(*env)->CallVoidMethod(env, obj, mid);
}
輸出:
In C
In Java
4.2.1Calling Instance Methods
如上節示例,回調 Java 方法分兩步:
• 首先經過 GetMethodID 在給定類中查詢方法. 查詢基於方法名稱和簽名
• 本地方法調用 CallVoidMethod,該方法代表被調 Java 方法的返回值爲 void
譯者:
從 JNI 調用實例方法命名規則:Call<Return Value Type>Method
4.2.2 Formaing the Method Descriptor
一個方法描述(簽名)由各參數類型簽名和返回值簽名構成. 參數簽名在前,並用小括號括
起. 具體描述請參照上文 4.1.2
4.2.3Calling Static Methods
同實例方法,回調 Java 靜態方法分兩步:
• 首先經過 GetStaticMethodID 在給定類中查找方法
• 經過 CallStatic<ReturnValueType>Method 調用
靜態方法與實例方法的不一樣,前者傳入參數爲 jclass,後者爲 jobject
4.2.4Calling Instance Methodsof a Superclass
調用被子類覆蓋的父類方法:JNI 支持用 CallNonvirtual<Type>Method 知足這類需求:
• GetMethodID 得到 method ID
• 調用 CallNonvirtualVoidMethod, CallNonvirtualBooleanMethod
上述,等價於以下 Java 語言的方式:
super.f();
CallNonvirtualVoidMethod 能夠調用構造函數
4.3Invoking Constructors
你能夠像調用實例方法同樣,調用構造方法,只是此時構造函數的名稱叫作"<init>". 如
下構造 java.lang.String 對象(JNI 爲了方便有個對應的 NewString 作下面全部工做,這裏
只是作示例展現):
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
jclass stringClass;
jmethodID cid;
jcharArray elemArr;
jstring result;
stringClass = (*env)->FindClass(env, "java/lang/String");
if (stringClass == NULL) {
return NULL; /* exception thrown */
}
/* Get the method ID for the String(char[]) constructor */
cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V");
if (cid == NULL) {
return NULL; /* exception thrown */
}
/* Create a char[] that holds the string characters */
elemArr = (*env)->NewCharArray(env, len);
if (elemArr == NULL) {
return NULL; /* exception thrown */
}
(*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
/* Construct a java.lang.String object */
result = (*env)->NewObject(env, stringClass, cid, elemArr);
/* Free local references */
(*env)->DeleteLocalRef(env, elemArr);
(*env)->DeleteLocalRef(env, stringClass);
return result;
}
首先,FindClass 找到 java.lang.String 的 jclass. 接下來,用 GetMethodID 找到構造
函數 String(char[]chars)的 MethodID. 此時用 NewCharArray 分配一個 Char 數組對象。
NewObject 調用構造函數。
用 DeleteLocalRef 釋放資源。
注意 NewString 是個經常使用函數,因此在 JNI 中直接被支持了,而且該函數的實現要比咱們
實現的高效。
也可以使用 CallNonvirtualVoidMehtod 調用構造函數. 以下代碼:
result =(*env)->NewObject(env, stringClass, cid, elemArr);
可被替換爲:
result = (*env)->AllocObject(env, stringClass);
if (result) {
(*env)->CallNonvirtualVoidMethod(env, result, stringClass, cid,
elemArr);
/* we need to check for possible exceptions */
if ((*env)->ExceptionCheck(env)) {
(*env)->DeleteLocalRef(env, result);
result = NULL;
}
}
AllocObject 建立一個未初始化的對象,該函數必須在每一個對象上被調用一次並且只能是
一次。
有時你會發現先建立未初始化對象再調用構造函數的方法是有用的。
4.4Caching Field and Method IDs
得到 field 與 method IDs,須要作基於名稱和簽名的符號表查詢,此過程能夠被優化。
基本想法是: 只在第一次使用 ID 時查詢,而後緩存該值. 有兩個緩存時機:首次使用和
初始化類時。
4.4.1Caching at the Point of Use
以下,首次使用時,緩存的局部靜態變量中,避免每次調用計算。
JNIEXPORT void JNICALL
Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj)
{
static jfieldID fid_s = NULL; /* cached field ID for s */
jclass cls = (*env)->GetObjectClass(env, obj);
jstring jstr;
const char *str;
if (fid_s == NULL) {
fid_s = (*env)->GetFieldID(env, cls, "s",
"Ljava/lang/String;");
if (fid_s == NULL) {
return; /* exception already thrown */
}
}
printf("In C:\n");
jstr = (*env)->GetObjectField(env, obj, fid_s);
str = (*env)->GetStringUTFChars(env, jstr, NULL);
if (str == NULL) {
return; /* out of memory */
}
printf(" c.s = \"%s\"\n", str);
(*env)->ReleaseStringUTFChars(env, jstr, str);
jstr = (*env)->NewStringUTF(env, "123");
if (jstr == NULL) {
return; /* out of memory */
}
(*env)->SetObjectField(env, obj, fid_s, jstr);
}
如上,靜態變量 fid_s 保存了 InstanceFieldAccess.s 的 filed ID。初始化階段靜態變量
被賦值爲 NULL。第一調用 InstanceFieldAccess.accessField 時,緩存 fieldID 以待後用。
你可能會發現上述代碼有個競爭條件,當多個線程同時訪問此函數時,可能會同時計算一
個 field ID. 不要緊,此處的競爭是無害的,由於即便在多個線程中同時計算該 field
ID,各線程中的計算結果都是同樣的。
構造函數的 MethodID 也可被緩存,以下:
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
jclass stringClass;
jcharArray elemArr;
static jmethodID cid = NULL;
jstring result;
stringClass = (*env)->FindClass(env, "java/lang/String");
if (stringClass == NULL) {
return NULL; /* exception thrown */
}
/* Note that cid is a static variable */
if (cid == NULL) {
/* Get the method ID for the String constructor */
cid = (*env)->GetMethodID(env, stringClass,
"<init>", "([C)V");
if (cid == NULL) {
return NULL; /* exception thrown */
}
}
/* Create a char[] that holds the string characters */
elemArr = (*env)->NewCharArray(env, len);
if (elemArr == NULL) {
return NULL; /* exception thrown */
}
(*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);
/* Construct a java.lang.String object */
result = (*env)->NewObject(env, stringClass, cid, elemArr);
/* Free local references */
(*env)->DeleteLocalRef(env, elemArr);
(*env)->DeleteLocalRef(env, stringClass);
return result;
}
4.4.2Caching in the Defining Class'sInitializer
上述第一次使用緩存的方式,每次都有與 NULL 的判斷,而且可能有一個無害的競爭條件。
而初始化類時,同時初始化 JNI 層對該類成員的緩存,能夠彌補上述缺憾,以下 initIDs:
Java 代碼:
class InstanceMethodCall {
private static native void initIDs();
private native void nativeMethod();
private void callback() {
System.out.println("In Java");
}
public static void main(String args[]) {
InstanceMethodCall c = new InstanceMethodCall();
c.nativeMethod();
}
static {
System.loadLibrary("InstanceMethodCall");
initIDs();
}
}
JNI 代碼:
jmethodID MID_InstanceMethodCall_callback;
JNIEXPORT void JNICALL
Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls)
{
MID_InstanceMethodCall_callback = (*env)->GetMethodID(env, cls,
"callback", "()V");
}
譯註:
還能夠改進上述緩存策略的初始化時機,第一種方法的缺陷文中已經提了,而第二種需
要在 Java 代碼主動調用 JNI 做緩存。
改進:能夠在你的項目中加一套 Hash 表, 封裝 FindClass,GetMethodID,GetFieldID
等函數,查詢的全部操做,都對 Hash 表操做,如首次 FindClass 一個類,這時能夠把一個
類的全部成員緩存到 Hash 表中,用名字+簽名作鍵值。
譯者所作項目引入了這個優化,項目的執行效率有 100 倍的提升;當時還作過兩個權衡:
1. 用一個 Hash 表,仍是每一個類一個 Hash 表
2. 首次 FindClass 類時,一次緩存全部的成員,仍是用時緩存
最終作的選擇是:爲了下降衝突,每一個類一個 Hash 表,而且一次緩存一個類的全部成員。
固然,沒有盡善盡美的優化策略,咱們作到這個層次,已經達到預期目標,沒有繼續深
入。
4.4.3Comparison between the Two Approachesto Caching IDs
在對 Java 源碼無改動權時使用時緩存是一種合理的解決方案. 但有許多弊端:
• 無害的競爭條件和重複與 NULL 比較
• 在類沒被卸載時,MethodID 和 FieldID 一直有效. 因此你必須保證:當你的 JNI 代
碼依賴這些緩存值的聲明週期內,該類不會被卸載。而與另外一種優化策略,連同類的
初始化緩存 Method/Field ID,每當類再次被裝載,緩存值會被更新
因此,有條件的話,更安全的優化策略是:連同類的初始化緩存 Method/Field ID。
4.5Performance of JNI Field and Method Operations
在學習瞭如何緩存 field 和 method ID 的優化技術後,你可能會想:影響 JNI 回調性能的
關鍵性因素是什麼?在效率方面,JNI/Java 與 Java/JNI 和 Java/Java 間對比,是怎樣的?
這要看具體 VM 實現的 JNI 效率. 很難給出一個普適的性能關鍵指標. 取而代之,咱們將
分析在訪問類成員時的固有性能損失。
首先比較 Java/native 和 Java/Java,前者因下述緣由可能會比後者慢:
• Java/native 與 Java/Java 的調用約定不一樣. 因此,VM 必須在調用前,對參數和調用
棧作特殊準備
• 經常使用的優化技術是內聯. 相比 Java/Java 調用,Java/native 建立內聯方法很難
粗略估計:執行一個 Java/native 調用要比 Java/Java 調用慢 2-3 倍. 也可能有一些 VM 實
現,Java/native 調用性能與 Java/Java 至關。(此種虛擬機,Java/native 使用 Java/Java
相同的調用約定)。
native/Java 調用效率可能與 Java/Java 有 10 倍的差距,由於 VM 通常不會作 Callback 的
優化。
對於 field 的訪問,將沒什麼不一樣,只是經過 JNI 訪問某對象結構中某個位置的值。
譯註:
上述只是學術考慮. 用好緩存的優化策略,徹底可讓項目工做的絕對出色。
CHAPTER 5
Local and Global References
JNI 把 instance 和 array 類型的指針對外公佈爲 opaque reference. 本地代碼不直接操
做指針,而是經過 JNI 函數,因此本地代碼不用關心內存佈局. 關於 reference,這裏還有
更豐富的東西有待介紹:
• JNI 支持三種類型的 opaque reference:local references, global references,
和 weak global references
• Local 和 Global 引用有不一樣的生命週期. Local Ref 在 native method 執行完畢後被
JavaVM 自動釋放,而 GlobalRef,WeakRef 在程序員主動釋放前一直有效
• 各類引用都有使用範圍. 如 LocalRef 只能在當前線程的 native method 中使用
本章將詳細講述不一樣類型 Ref 的使用方法,正確管理 JNI 引用是程序健壯、空間佔用少的
關鍵。
5.1 Local and Global References
LocalRef 與 GlobalRef 的差別,將用幾個示例說明:
大部分 JNI 函數都會建立 LocalRef,如 NewObject 建立一個實例,並返回一個指向該實例
的 LocalRef。
LocalRef 只在本線程的 native method 中有效. 一但 native method 返回,LocalRef 將
被釋放。不要緩存一個 LocalRef,並企圖在下次進入該 JNI 方法時使用,以下:
/* This code is illegal */
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jclass stringClass = NULL;
jmethodID cid;
jcharArray elemArr;
jstring result;
if (stringClass == NULL) {
stringClass = (*env)->FindClass(env, "java/lang/String");
if (stringClass == NULL) {
return NULL; /* exception thrown */
}
}
/* It is wrong to use the cached stringClass here,
because it may be invalid. */
cid = (*env)->GetMethodID(env, stringClass, "<init>", "([C)V");
...
elemArr = (*env)->NewCharArray(env, len);
...
result = (*env)->NewObject(env, stringClass, cid, elemArr);
(*env)->DeleteLocalRef(env, elemArr);
return result;
}
上述代碼,企圖重複使用 FindClass(env, "java/lang/String")的返回值,這種方式不
對,由於 FindClass 返回的是一個 LocalRef. 請設想如下代碼:
JNIEXPORT jstring JNICALL
Java_C_f(JNIEnv *env, jobject this)
{
char *c_str = ...;
...
return MyNewString(c_str);
}
以下,兩次調用 f 這個本地方法.
...
... =C.f();// The first call is perhaps OK.
... =C.f();// This would use an invalid local reference.
...
第一次調用可能正確,而第二次將引用一個無效位置,由於第二次企圖使用存在靜態變量
中的 LocalRef。
有兩種方式讓 LocalRef 無效,一,native method 返回,JavaVM 自動釋放 LocalRef;二,
用 DeleteLocalRef 主動釋放。
既然 LocalRef 會被 JavaVM 自動釋放,爲何還要有 DeleteLocalRef?由於 LocalRef 是
阻止引用被 GC,但當你在本地代碼中操做大量對象時,而 LocalRefTable 又是有限的,及時
調用 DeleteLocalRef,會釋放 LocalRef 在 LocalRefTable 中所佔位置並使對象及時獲得回
收。
LocalRef 只在建立該對象的線程中有效,企圖把 LocalRef 存到全局變量中供其餘線程使
用的作法是錯誤的。
譯註:
注意這裏的提到的 native method 返回,返回是指回到 Java 層,若是從一個本地函數返
回到另外一個本地函數,LocalRef 是有效的。
5.1.2Global References
釋放 GlobalRef 前,你能夠在多個本地方法調用過程和多線程中使用 GlobalRef 所引對象。
與 LocalRef 相似,GlobalRef 的做用:防止對象被 GC(garbage collected, 垃圾回收)。
GlobalRef 與 LocalRef 不一樣的是,LocalRef 通常自動建立(返回值爲 jobject/jclass 等
JNI 函數),而 GlobalRef 必須經過 NewGlobalRef 由程序員主動建立。以下:
/* This code is OK */
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jclass stringClass = NULL;
...
if (stringClass == NULL) {
jclass localRefCls = (*env)->FindClass(env,
"java/lang/String");
if (localRefCls == NULL) {
return NULL; /* exception thrown */
}
/* Create a global reference */
stringClass = (*env)->NewGlobalRef(env, localRefCls);
/* The local reference is no longer useful */
(*env)->DeleteLocalRef(env, localRefCls);
/* Is the global reference created successfully? */
if (stringClass == NULL) {
return NULL; /* out of memory exception thrown */
}
}
...
}
該例作了修改,當 stringClass 爲 NULL 時,咱們建立了 java.lang.String 的
GlboalRef,並刪除了對應的 LocalRef,以待下次再進入此方法時,使用 stringClass。
5.1.3 Weak Global References
Weak Global Ref 用 NewGlobalWeakRef 於 DeleteGlobalWeakRef 進行建立和刪除,多個本
地方法調用過程當中和多線程上下文中使用的特性與 GlobalRef 相同,但該類型的引用不保證
不被 GC。
前述示例 MyNewString 中,對 java.lang.String 聲明 GlobalRef 或 GlobalWeakRef 效果相
同,由於 java.lang.String 是一個系統類不會被 GC。
Weak Global Ref 使用在容許被 GC 的場合,如內存緊張時。
JNIEXPORT void JNICALL
Java_mypkg_MyCls_f(JNIEnv *env, jobject self)
{
static jclass myCls2 = NULL;
if (myCls2 == NULL) {
jclass myCls2Local =
(*env)->FindClass(env, "mypkg/MyCls2");
if (myCls2Local == NULL) {
return; /* can’t find class */
}
myCls2 = NewWeakGlobalRef(env, myCls2Local);
if (myCls2 == NULL) {
return; /* out of memory */
}
}
... /* use myCls2 */
}
咱們假設,MyCls 與 MyCls2 有一樣的生命週期(並被一樣的 Class Loader 裝載),相似
MyCls 被卸載而 MyCls2 沒被卸載的狀況不考慮。若是發生這種狀況,咱們還須要檢測
myCls2 是否還執行的對象仍然有效。
5.1.4Comparing Reference
有兩個對象,用以下方法比較相容性:
(*env)->IsSameObject(env, obj1, obj2)
若是相容,返回 JNI_TRUE, 不然返回 JNI_FALSE。
與 NULL 的比較,LocalRef 與 GlobalRef 語義顯然, 前提是釋放了兩個引用,程序員從新
爲相應變量作了 NULL 初始化。
但對於 Weak Global Ref 來講,須要使用下述代碼斷定:
(*env)->IsSameObject(env, wobj, NULL)
由於,對於一個 Weak Global Ref 來講可能指向已經被 GC 的無效對象。
譯註:
上述的判斷,都是假設全部的類和對象都是在一個 Class Loader 下被裝載的. 關於
ClassLoader 的議題後有專門文章.
5.2 Freeing Reference
每一個 JNI 引用都會引用表中的一個位置. 做爲一個 JNI 程序員,你應該清楚程序某階段中
使用的引用數量。 如 LocalRef,若是你疏於 DeleteLocalRef 的話,在 JavaVM 運行限制內
你的應用程序工做正常,在極端狀況會崩潰。
5.2.1 Freeing Local References
譯註:
本章沒有翻譯.。
本章講了不少關於 LocalRef 的釋放原則,譯者認爲:考慮什麼時候釋放/什麼時候不釋放的問題,
不如認真審查代碼,嚴堵每一個泄露環節,盡最大努力提升程序的穩定性。 就像內存分配一
樣,雖然進程結束後,OS 自動釋放該進程分配的全部內存,但對於指望長期穩定運行的系
統來講,咱們但願杜絕內存泄露。
5.2.2Managing Local Referencesin Java 2SDK Release 1.2
譯註:
本章沒有翻譯。
因爲 LocalRef Table 大小是固定的,這套函數只是執行相似函數調用時,執行的壓棧操
做,並在執行 PopLocalFrame 後執行相似退棧操做, 在 LocalRef Table 中預留一部分供當
前函數使用,當你在 JNI 中產生大量對象時,虛擬機仍然會因 LocalRef Overflow
Exception 崩潰。
具體原則仍如上述,嚴堵每一個泄露環節;若是你能準確估計 LocalRef 用量,可使用
Push/PopLocalFrame。
5.2.3 Freeing Global References
當再也不使用 GlobalRef 所指對象,及時調用 DeleteGlobalRef 釋放對象. 不然,GC 將不回
收該對象。
對於 DeleteWeakGlobalRef 來講,不使用 WeakGlobalRef 時,也要及時釋放,由於即便 GC
會回收該對象內容,WeakGlobalRef 在 Table 中的位置還佔用着,即和尚都跑了,廟還在。
譯註:
綜上,無論何種類型引用,在不使用所引用對象後,及時調用對應指針類型的釋放函數。
5.3 Rulesfor Managing References
如今咱們概括一下管理 JNI 引用的原則. 看看如何減小內存使用、有效使用對象。
有兩類本地函數:功能函數和工具函數。
當寫 native method 的實現時,要認真處理循環中產生的 LocalRef. VM 規範中規定每一個
本地方法至少要支持 16 個 LocalRef 供自由使用並在本地方法返回後回收. 本地方法絕對不
能濫用 GlobalRef 和 WeakGlobalRef,由於此類型引用不會被自動回收。
工具函數,對 LocalRef 的使用更要提起警戒,由於該類函數調用上下文不肯定,並且會
被重複調用,每一個代碼路徑都要保證不存在 LocalRef 泄露。
因爲某些緩存機制,能夠在工具函數中建立 GlobalRef, WeakGlobalRef。
當工具函數返回對象時,要嚴格遵照引用約定,讓調用者在決定是否釋放時能做出準確判
斷, 以下:
while (JNI_TRUE) {
jstring infoString =GetInfoString(info);
... /* processinfoString */
???
/*
* we need to call DeleteLocalRef, DeleteGlobalRef,
*or DeleteWeakGlobalRef depending on the type of
*reference returned byGetInfoString.
*/
}
JNI 方法 NewLocalRef 總保證返回一個 LocalRef,以下:
jstring
MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jstring result;
/* wstrncmp compares two Unicode strings */
if (wstrncmp("CommonString", chars, len) == 0) {
/* refers to the global ref caching "CommonString" */
static jstring cachedString = NULL;
if (cachedString == NULL) {
/* create cachedString for the first time */
jstring cachedStringLocal = ... ;
/* cache the result in a global reference */
cachedString =
(*env)->NewGlobalRef(env, cachedStringLocal);
}
return (*env)->NewLocalRef(env, cachedString);
}
...
/* create the string as a local reference and store in
result as a local reference */
return result;
}
Push/PopLocalFrame 常被用來管理 LocalRef. 在進入本地方法時,調用一次
PushLocalFrame,並在本地方法結束時調用 PopLocalFrame. 此對方法執行效率很是高,建
議使用這對方法。
譯註:
你只要對當前上下文內使用的對象數量有準確估計,建議使用這對方法,在這對方法間,
沒必要調用 DeleteLocalRef,只要該上下文結尾處調用 PopLocalFrame 會一次性釋放全部
LocalRef。
必定保證該上下文出口只有一個,或每一個 return 語句都作嚴格檢查是否調用了
PopLocalFrame。
jobject f(JNIEnv *env, ...)
{
jobject result;
if ((*env)->PushLocalFrame(env, 10) < 0) {
/* frame not pushed, no PopLocalFrame needed */
return NULL;
}
...
result = ...;
if (...) {
/* remember to pop local frame before return */
result = (*env)->PopLocalFrame(env, result);
return result;
}
...
result = (*env)->PopLocalFrame(env, result);
/* normal return */
return result;
}
忘記調用 PopLocalFrame 可能會使 VM 崩潰。
CHAPTER 6
Exceptions
咱們已經碰到在調用 JNI 方法時出現異常的狀況. 本章將介紹如何檢查並處理異常。
本章只關注在調用 JNI 方法或 Java 方法時出現異常的處理辦法(Java 異常),不涉及本地
代碼自己(如本地代碼中的除 0 錯)或調用系統函數出現異常的處理方法。
6.1.1Caching and Throwing Exceptionsin Native Code
以下 Java 代碼展現如何聲明 JNI 可能拋出的異常。
class CatchThrow {
private native void doit()
throws IllegalArgumentException;
private void callback() throws NullPointerException {
throw new NullPointerException("CatchThrow.callback");
}
public static void main(String args[]) {
CatchThrow c = new CatchThrow();
try {
c.doit();
} catch (Exception e) {
System.out.println("In Java:\n\t" + e);
}
}
static {
System.loadLibrary("CatchThrow");
}
}
JNI 代碼:
JNIEXPORT void JNICALL
Java_CatchThrow_doit(JNIEnv *env, jobject obj)
{
jthrowable exc;
jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID mid =
(*env)->GetMethodID(env, cls, "callback", "()V");
if (mid == NULL) {
return;
}
(*env)->CallVoidMethod(env, obj, mid);
exc = (*env)->ExceptionOccurred(env);
if (exc) {
/* We don't do much with the exception, except that
we print a debug message for it, clear it, and
throw a new exception. */
jclass newExcCls;
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
newExcCls = (*env)->FindClass(env,
"java/lang/IllegalArgumentException");
if (newExcCls == NULL) {
/* Unable to find the exception class, give up. */
return;
}
(*env)->ThrowNew(env, newExcCls, "thrown from C code");
}
}
輸出:
java.lang.NullPointerException:
at CatchThrow.callback(CatchThrow.java)
at CatchThrow.doit(Native Method)
at CatchThrow.main(CatchThrow.java)
In Java:
java.lang.IllegalArgumentException:thrown from C code
callback 方法拋出 NullPointerException. 當 CallVoidMethod 把控制權返回給本地代碼,
本地代碼調用 ExceptionOccurred 檢查是否有異常發生. 咱們的處理方式是,當有異常發生,
調用 ExceptionDescribe 打印調用堆棧,而後用 ExceptionClear 清空異常,最後從新拋出
IllegalArgumentException。
譯者:
ExceptionOccurred 返回一個 jobject,注意結束處理時調用 DeleteLocalRef 刪除該返
回值。JNI 中還有一個 ExceptionCheck, 只是返回一個 jboolean 的布爾值, 更適合檢查異
常是否發生。
在 JNI 中產生的異常(經過調用 ThrowNew),與 Java 語言中異常發生的行爲不一樣,JNI 中
當前代碼路徑不會當即改變。在 Java 中發生異常,VM 自動把控制權轉向 try/catch 中匹配
的異常類型處理塊。VM 首先清空異常隊列,而後執行異常處理塊。相反,JNI 中必須顯式處
理 VM 的處理方式。
6.1.2 AUtility Function
JNI 中拋異常很經典:找異常類,調用 ThrowNew 拋出之;因此,能夠寫一個工具函數。
void
JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg)
{
jclass cls = (*env)->FindClass(env, name);
/* if cls is NULL, an exception has already been thrown */
if (cls != NULL) {
(*env)->ThrowNew(env, cls, msg);
}
/* free the local ref */
(*env)->DeleteLocalRef(env, cls);
}
本書中,JNU 前綴表示 JNI Utilities. JNU_ThrowByName 首先經過 FindClass 找到異常類。
若是 FindClass 找類失敗,將返回 NULL,並拋出 NoClassDefFoundError 異常。此狀況,
JNU_ThrowByName 將保留該異常,而後返回. 若是 FindClass 成功, 將調用 ThrowNew
拋出異常。因此無論哪一種狀況,調用該函數後,當前的 JNIEnv 環境裏總有個異常。
6.2Proper Exception Handling
JNI 程序員應對全部可能的異常作處理,這個要求雖然苛刻,但這是健壯軟件的保證。
6.2.1Checking for Exception
有兩種方式檢查是否有異常發生。
1. 大多數 JNI 函數用顯式方式代表當前線程是否有異常發生。
下述代碼判斷 GetFieldID 返回是否爲 NULL 以檢查是否發生異常:
/* a class in the Java programming language */
public class Window {
long handle;
int length;
int width;
static native void initIDs();
static {
initIDs();
}
}
JNI 代碼:
/* C code that implements Window.initIDs */
jfieldID FID_Window_handle;
jfieldID FID_Window_length;
jfieldID FID_Window_width;
JNIEXPORT void JNICALL
Java_Window_initIDs(JNIEnv *env, jclass classWindow)
{
FID_Window_handle =
(*env)->GetFieldID(env, classWindow, "handle", "J");
if (FID_Window_handle == NULL) { /* important check. */
return; /* error occurred. */
}
FID_Window_length =
(*env)->GetFieldID(env, classWindow, "length", "I");
if (FID_Window_length == NULL) { /* important check. */
return; /* error occurred. */
}
FID_Window_width =
(*env)->GetFieldID(env, classWindow, "width", "I");
/* no checks necessary; we are about to return anyway */
}
2. 若是返回值不能代表是否有異常發生,須要用 JNI 提供的 ExceptionOccurred 檢查但
前線程是否有未處理異常。
public class Fraction {
// details such as constructors omitted
int over, under;
public int floor() {
return Math.floor((double)over/under);
}
}
JNI 代碼:
/* Native code that calls Fraction.floor. Assume method ID
MID_Fraction_floor has been initialized elsewhere. */
void f(JNIEnv *env, jobject fraction)
{
jint floor = (*env)->CallIntMethod(env, fraction,
MID_Fraction_floor);
/* important: check if an exception was raised */
if ((*env)->ExceptionCheck(env)) {
return;
}
... /* use floor */
}
6.2.2 Handling Exception
本地代碼以兩種方式處理異常:
• 本地代碼能夠當即返回,並在調用者中處理異常
• 本地代碼能夠 ExceptionClear 清空異常,而後本身作從新拋出等策略
繼續執行後續代碼前,必須嚴格按着: 檢查->處理->清除的邏輯處理異常. 若是沒有預先
清空異常就調用一個 JNI 方法,行爲不可預料。有一些函數能夠在未清空異常前調用,但只
侷限於不多的幾個,並且可能是異常處理 JNI 函數。
在異常發生後,及時釋放資源很重要,以下異常發生時對 String 的處理:
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
{
const jchar *cstr = (*env)->GetStringChars(env, jstr);
if (c_str == NULL) {
return;
}
...
if (...) { /* exception occurred */
(*env)->ReleaseStringChars(env, jstr, cstr);
return;
}
...
/* normal return */
(*env)->ReleaseStringChars(env, jstr, cstr);
}
6.2.3Exceptionsin Utility Functions
寫工具函數對異常的處理要很注意,必定是針對調用者的,即無累積異常行爲,通常原則
以下:
• 工具函數應該提供返回值告訴調用者釋放發生異常,以簡化調用者的處理
• 工具函數要注意處理 LocalRef
以下示例:
jvalue
JNU_CallMethodByName(JNIEnv *env,
jboolean *hasException,
jobject obj,
const char *name,
const char *descriptor, ...)
{
va_list args;
jclass clazz;
jmethodID mid;
jvalue result;
if ((*env)->EnsureLocalCapacity(env, 2) == JNI_OK) {
clazz = (*env)->GetObjectClass(env, obj);
mid = (*env)->GetMethodID(env, clazz, name,
descriptor);
if (mid) {
const char *p = descriptor;
/* skip over argument types to find out the
return type */
while (*p != ')') p++;
/* skip ')' */
p++;
va_start(args, descriptor);
switch (*p) {
case 'V':
(*env)->CallVoidMethodV(env, obj, mid, args);
break;
case '[':
case 'L':
result.l = (*env)->CallObjectMethodV(
env, obj, mid, args);
break;
case 'Z':
result.z = (*env)->CallBooleanMethodV(
env, obj, mid, args);
break;
case 'B':
result.b = (*env)->CallByteMethodV(
env, obj, mid, args);
break;
case 'C':
result.c = (*env)->CallCharMethodV(
env, obj, mid, args);
break;
case 'S':
result.s = (*env)->CallShortMethodV(
env, obj, mid, args);
break;
case 'I':
result.i = (*env)->CallIntMethodV(
env, obj, mid, args);
break;
case 'J':
result.j = (*env)->CallLongMethodV(
env, obj, mid, args);
break;
case 'F':
result.f = (*env)->CallFloatMethodV(
env, obj, mid, args);
break;
case 'D':
result.d = (*env)->CallDoubleMethodV(
env, obj, mid, args);
break;
default:
(*env)->FatalError(env, "illegal descriptor");
}
va_end(args);
}
(*env)->DeleteLocalRef(env, clazz);
}
if (hasException) {
*hasException = (*env)->ExceptionCheck(env);
}
return result;
}
JNU_CallMethodByName, 經過對*hasException 指針賦值,來代表是否有異常發生。
CHAPTER 7
The Invocation Interface
本章介紹如何在你的應用中嵌入一個 JavaVM。JavaVM 被廣泛的實現爲一個庫。本地應用
能夠連接該庫,並經過 invocation interface 裝載 JavaVM. 命令行的 java 命令,實現原理
與上述相似。
7.1Creating the Java Virtual Machcine
爲演示如何經過 invocation interface 裝載 JavaVM 並執行 Java 代碼,以下示例:
public class Prog {
public static void main(String[] args) {
System.out.println("Hello World " + args[0]);
}
}
C 代碼:
#include <jni.h>
#define PATH_SEPARATOR ';' /* define it to be ':' on Solaris */
#define USER_CLASSPATH "." /* where Prog.class is */
main() {
JNIEnv *env;
JavaVM *jvm;
jint res;
jclass cls;
jmethodID mid;
jstring jstr;
jclass stringClass;
jobjectArray args;
#ifdef JNI_VERSION_1_2
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString =
"-Djava.class.path=" USER_CLASSPATH;
vm_args.version = 0x00010002;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = JNI_TRUE;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
#else
JDK1_1InitArgs vm_args;
char classpath[1024];
vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* Append USER_CLASSPATH to the default system class path */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
#endif /* JNI_VERSION_1_2 */
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
cls = (*env)->FindClass(env, "Prog");
if (cls == NULL) {
goto destroy;
}
mid = (*env)->GetStaticMethodID(env, cls, "main",
"([Ljava/lang/String;)V");
if (mid == NULL) {
goto destroy;
}
jstr = (*env)->NewStringUTF(env, " from C!");
if (jstr == NULL) {
goto destroy;
}
stringClass = (*env)->FindClass(env, "java/lang/String");
args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
if (args == NULL) {
goto destroy;
}
(*env)->CallStaticVoidMethod(env, cls, mid, args);
destroy:
if ((*env)->ExceptionOccurred(env)) {
(*env)->ExceptionDescribe(env);
}
(*jvm)->DestroyJavaVM(jvm);
}
該代碼用宏區分不一樣版本 JavaVM 處理參數的代碼。
當爲 1.2release 版本 JavaVM,C 代碼建立了 JavaVMInitArgs 結構. VM 初始化參數存在
JavaVMOption 數組中. 並設置 ignoreUnrecognized 爲 JNI_TRUE 使虛擬機忽略不支持的參數。
當 JavaVM 初始化完畢, 調用 JNI_CreateJavaVM 裝載 VM. 該函數填充了兩個結構:
• 指向新建 JavaVM 的 jvm
• 指向當前線程的 JNIEnv
JNI_CreateJavaVM 調用完畢,一切準備工做都作好了,就能夠調用 JNI 方法了。
最後,調用 DestroyJavaVM 卸載 JavaVM。
輸出:
Hello World from C!
7.2 Linking Native Applications with the Java Virtual Machine
如何連接,取決於你想把本地代碼與特定 VM 連接,仍是與任意廠商的 VM 連接.
7.2.1 Linking with a Known Java Virtual Machine
當你打算與特定虛擬機連接時,能夠直接指定 VM 庫路徑.
$cc -I<jni.h dir> -L<libjava.so dir> -lthread -ljava invoke.c
-lthread:使用本地線程支持
-ljava: 連接 Solaris 共享庫
Win32 與 JDK1.1release 連接:
$cl -I<jni.h dir> -MD invoke.c -link <javai.lib dir>\javai.lib
-MD: Win32 多線程 C 庫
Win32 與 JDK1.2release 連接:上述的庫要換爲 jvm.lib 或 jvm.dll
連接完畢,若是執行出錯,Log 顯示找不到庫,請設置 LD_LIBRARY_PATH(類 Unix 平臺),
或 PATH 環境變量(Win32)。
7.2.2 Linking with Unknown Java Virtual Machines
若是本地應用指望與多個版本/廠商 VM 共同工做,你就不能鏈接特定虛擬機。好比 JDK
release 1.1 的 JavaVM 動態庫爲 javai.dll,而 1.2 的動態庫爲 jvm.dll。
咱們可使用執行期裝載動態庫的方式解決,以下:
/* Win32 version */
void *JNU_FindCreateJavaVM(char *vmlibpath)
{
HINSTANCE hVM = LoadLibrary(vmlibpath);
if (hVM == NULL) {
return NULL;
}
return GetProcAddress(hVM, "JNI_CreateJavaVM");
}
LoadLibrary 和 GetProcAddress 爲 Win32 API 能夠接受"jvm"(PATH 環境變量中定義全路
徑)或"C:\\jdk1.2\\jre\\bin\\classic\\jvm.dll"的絕對路徑形式。
類 Unix 以下:
/*Solarisversion */
void *JNU_FindCreateJavaVM(char *vmlibpath)
{
void *libVM = dlopen(vmlibpath, RTLD_LAZY);
if (libVM == NULL) {
return NULL;
}
return dlsym(libVM, "JNI_CreateJavaVM");
}
dlopen/dlsym 爲動態連接庫函數。
7.3 Attaching Native Threads
假設你有一個多線程 Web Server,每來一個請求,將起一個線程爲之服務,並在每一個線
程中調用 JNI/Java 方法。
派生的線程較 JavaVM 生命期要短,咱們能夠 attach 一個本地線程到 JavaVM,在線程結束
時從 JavaVM detach 該線程。
譯者:
attach/detach 至今也沒有找到一個恰當的中文詞描繪。
以下示例:
/* Note: This program only works on Win32 */
#include <windows.h>
#include <jni.h>
JavaVM *jvm; /* The virtual machine instance */
#define PATH_SEPARATOR ';'
#define USER_CLASSPATH "." /* where Prog.class is */
void thread_fun(void *arg)
{
jint res;
jclass cls;
jmethodID mid;
jstring jstr;
jclass stringClass;
jobjectArray args;
JNIEnv *env;
char buf[100];
int threadNum = (int)arg;
/* Pass NULL as the third argument */
#ifdef JNI_VERSION_1_2
res = (*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
#else
res = (*jvm)->AttachCurrentThread(jvm, &env, NULL);
#endif
if (res < 0) {
fprintf(stderr, "Attach failed\n");
return;
}
cls = (*env)->FindClass(env, "Prog");
if (cls == NULL) {
goto detach;
}
mid = (*env)->GetStaticMethodID(env, cls, "main",
"([Ljava/lang/String;)V");
if (mid == NULL) {
goto detach;
}
sprintf(buf, " from Thread %d", threadNum);
jstr = (*env)->NewStringUTF(env, buf);
if (jstr == NULL) {
goto detach;
}
stringClass = (*env)->FindClass(env, "java/lang/String");
args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
if (args == NULL) {
goto detach;
}
(*env)->CallStaticVoidMethod(env, cls, mid, args);
detach:
if ((*env)->ExceptionOccurred(env)) {
(*env)->ExceptionDescribe(env);
}
(*jvm)->DetachCurrentThread(jvm);
}
main() {
JNIEnv *env;
int i;
jint res;
#ifdef JNI_VERSION_1_2
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString =
"-Djava.class.path=" USER_CLASSPATH;
vm_args.version = 0x00010002;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = TRUE;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
#else
JDK1_1InitArgs vm_args;
char classpath[1024];
vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* Append USER_CLASSPATH to the default system class path */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
#endif /* JNI_VERSION_1_2 */
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
for (i = 0; i < 5; i++)
/* We pass the thread number to every thread */
_beginthread(thread_fun, 0, (void *)i);
Sleep(1000); /* wait for threads to start */
(*jvm)->DestroyJavaVM(jvm);
}
此示例,會分別在 5 個線程中調用 Prog.main。 當派生完畢,主線程等待各子線程啓動,
該函數會在 5 個 thread 結束後返回。
譯者:
該示例只爲簡單展現 Attach/detach 機制,具體實現線程機制,請不要用 Sleep 之類的
函數在主線程中等待子線程啓動,應該使用信號量。
JNI_AttachCurrentThread 第三個參數爲 NULL,Java 2SDKrelease 1.2 中提供
JNI_ThreadAttachArgs 結構,容許傳參,如準備把當前線程 attach 到那個 thread group。
當線程執行完畢,調用 DetachCurrentThread 釋放全部當前線程的 LocalRef。
輸出:
Hello World from thread 1
Hello World from thread 0
Hello World from thread 4
Hello World from thread 2
Hello World from thread 3
在不一樣平臺輸出可能不一樣。
CHAPTER 8
Additional JNI Features
上一章介紹了應用程序中嵌入 JavaVM 和多線程應用的寫法,本章介紹其餘特性。
8.1JNI and Threads
JavaVM 支持多線程編程。
關於併發編程議題已超出本書範圍,請閱讀<Concurrent Programming in Java, Design
Principlesand Patterns>byDoug Lea(Addison-Wesley, 1997) (中譯本,<併發編程實
踐>)。
8.1.1Constraints
這裏有幾個在多線程環境中必須遵照的原則,這些原則可讓你的代碼安全的在多線程環
境下運行。
• JNIEnv 結構與線程綁定的,絕對不能在多線程中共享 JNIEnv 結構
• LocalRef 與本線程綁定的,絕對不能在多線程中共享 LocalRef
8.1.2Monitor Entryand Exit
monitor(監視器)是 Java 平臺原生(語言級別,語言自己支持的)的同步機制。每一個對象都
帶一個 monitor。
以下 Java 代碼的同步機制,JNI 提供等價機制達到同步的目的。
synchronized( obj) {
... //synchronized block
}
JavaVM 保證只有一個線程持有同步塊鎖。當另外一個線程執行到同步塊時將一直阻塞,直
到先進入同步塊的那個線程離開該塊(釋放鎖)。
JNI 代碼可用以下機制,獲得一樣語義:
if ((*env)->MonitorEnter(env, obj) != JNI_OK) {
... /* error handling */
}
... /* synchronized block */
if ((*env)->MonitorExit(env, obj) != JNI_OK) {
... /* error handling */
};
MonitorEnter/MonitorExit 必定要配對調用。否則,將致使死鎖。
8.1.3Monitor Wait and Notify
爲了同步須要,Java 提供了 Object.wait/Object.notify/Object.notifyAll。JNI 不提供
這些對應的 JNI 實現,但你能夠回調 Java 方法.
/* precomputed method IDs */
static jmethodID MID_Object_wait;
static jmethodID MID_Object_notify;
static jmethodID MID_Object_notifyAll;
void
JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout)
{
(*env)->CallVoidMethod(env, object, MID_Object_wait,
timeout);
}
void
JNU_MonitorNotify(JNIEnv *env, jobject object)
{
(*env)->CallVoidMethod(env, object, MID_Object_notify);
}
void
JNU_MonitorNotifyAll(JNIEnv *env, jobject object)
{
(*env)->CallVoidMethod(env, object, MID_Object_notifyAll);
}
8.1.4 Obtaining a JNIEnv Pointer in ArbitraryContexts
咱們前面已經作了說明:JNIEnv 與線程綁定。對於 native method 這不是問題,由於第一
個參數即爲 JNIEnv。但某些函數不是在 native method 下被調用的,沒有 JNIEnv 結構,比
如供 OS 回調的函數。
JavaVM *jvm;/*already set */
f()
{
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
... /*use env */
}
如上,只要 jvm 被賦值,就可經過 AttachCurrentThread 獲得 JNIEnv。JavaVM 結構能夠
在多線程間共享,有如下兩個 jvm 的賦值時機:
• JNI_GetCreatedJavaVM 建立虛擬機時
• JNI_OnLoad
此外,只要在該線程中事先調用過 AttachCurrentThread 後,JNIEnv->GetEnv 能夠返回
JNIEnv。java

相關文章
相關標籤/搜索