1. 程式人生 > >在Java程式中呼叫C函式--列印"HelloWorld"

在Java程式中呼叫C函式--列印"HelloWorld"

本文是將書中的第二章單獨抽出來,紅色部分為譯者注.

1.概述

這個列印的過程是用JDK或Java 2 SDK寫一個簡單的Java程式,程式會呼叫一個C函式列印"HelloWorld".這個過程將包括以下步驟:

  1. 建立一個Java類(HelloWorld.java),以及定義一個native方法.
  2. 使用javac去編譯這個HelloWorld原始檔,生成HelloWorld.class.
  3. 使用javah –jni 來生成C的標頭檔案(HelloWorld.h),這個標頭檔案包含native方法的實現原型.javah是JDK或者Java 2 SDK提供的一個工具.
  4. 編寫C的程式碼(HelloWorld.c),實現這個native方法.
  5. 把這個C的實現編譯到Java native庫中,建立HelloWorld.dll或者libHello-World.so.
  6. 使用Java執行時直譯器執行HelloWorld程式.無論是類檔案(HelloWorld.class),還是native庫(HelloWorld.dll或者libHelloWorld.so),都是在執行時載入.

剩下的部分將詳細介紹這些步驟:

2.Native方法宣告

用Java語言寫下面的程式.該程式定義的類名是HelloWorld,包含一個native方法,print.

class HelloWorld {
     private native void
print();
     public static void main(String[] args) {
         new HelloWorld().print();
     }
     static {
         System.loadLibrary("HelloWorld");
     }
 }

該類不在任何包中

HelloWorld的類定義用一個print的本地方法開始.其次就是main方法,它例項化了一個HelloWorld,並且呼叫這個例項的print的本地方法.類定義的最後部分是一個靜態程式碼塊,它載入了包含print本地方法實現的native庫.

在native方法(例如print)的定義和常規的Java語言方法的定義中有這樣的兩個差異.一個本地方法宣告必須包含native修飾符,native修飾符表明,該方法以另一種語言實現.此外,本地方法的定義是以分號結束,因為在類本身中,沒有實現本地方法,我們將在一個獨立的C檔案中實現這個print方法.

在呼叫本地方法print之前,native庫必須要載入print的實現.在這種情況下,我們在HelloWorld類的靜態程式碼塊中載入native庫,Java虛擬機器會自動執行靜態程式碼塊,並且在呼叫任何HelloWorld類的方法之前,以確保native庫在print本地方法呼叫之前就已經載入了.

我們定義一個main方法能去執行HelloWorld,HelloWorld.main呼叫本地方法列印,就像是呼叫常規方法一樣.

System.loadLibrary需要一個庫名,然後定位到符合這個名字的native庫中,並載入到應用程式的native庫.我們將在本書的後面討論具體的載入過程.現在只需要簡單地記住,為了System.loadLibrary("HelloWorld")成功,我們需要建立一個native庫,用來呼叫Win32上的HelloWorld.dll或者在Solaris上的libHelloWorld.so.

3.編譯HelloWorld

在你定義HelloWorld類之後,儲存原始碼為檔案HelloWorld.java,然後用javac 編譯器編譯原始碼:

javac HelloWorld.java

這個命令將在當前目錄生成一個HelloWorld.class檔案.

4.建立Native方法的標頭檔案

下一步,我們將使用javah工具來生成一個JNI風格(JNI-style)的標頭檔案.當在C 上面實現本地方法時很有用.你可以在HelloWorld.class上執行javah,就像下面一樣:

javah –jni HelloWorld

生成的標頭檔案的名字是在類名後面追加一個".h".上面顯示的命令將生成一個檔名為HelloWorld.h的檔案.我們將不在這裡列出生成的標頭檔案.這個標頭檔案最重要的部分Java_HelloWorld_print的函式原型,這是C 函式實現的HelloWorld.print方法:

JNIEXPORT void JNICALL

Java_HelloWorld_print (JNIEnv *, jobject);

現在忽略JNIEXPORT和JNICALL巨集.你可能已經注意到,本地方法C 的實現接收了兩個引數,然而實際上對應的native方法宣告不接收任何引數.對於每一個本地方法實現,第一個引數都是JNIEnv介面指標(JNIEnv interface pointer),第二個引數是一個指向HelloWorld物件本身(有點像C++的"this"指標)的引用.我們將在本書的後面討論如何使用JNIEnv介面指標和jobject.但是現在這個簡單的例子就忽略這兩個引數.

5.寫本地方法的實現

用javah生成的JNI風格的標頭檔案能夠幫助你去完成C/C++的本地方法實現.你寫的函式必須遵循生成的標頭檔案的原型.你能在C 檔案HelloWorld.c中實現HelloWorld.print方法,就像下面一樣:

#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h"
JNIEXPORT void JNICALL 
Java_HelloWorld_print(JNIEnv *env, jobject obj)
{
    printf("Hello World!/n");
    return;
}

該本地方法的實現很簡單,它使用了printf函式來顯示字串"HelloWorld!",然後返回.正如前面提到,兩個引數,JNIEnv指標和指向物件的引用,都被忽略了.

這個C 程式包含了三個標頭檔案:

  • jni.h--這個標頭檔案提供了原生代碼需要呼叫JNI函式的資訊.當寫本地方法的時候,你必須在你的C 或者 C++原始檔中,永遠包含這個檔案.
  • stdio.h--以上程式碼段也包含了stdio.h ,因為它使用了printf函式.
  • HelloWorld.h--這是你用javah生成的標頭檔案,它包含了Java_HelloWorld_print的函式原型.

6.編譯C 原始碼和建立native庫

請記住,當你在HelloWorld.java建立HelloWorld.class的時候,你包含了一行載入native庫的程式碼在你的程式裡面:

System.loadLibrary("HelloWorld");

現在所有必要的C 程式碼已經寫完了,你需要去編譯HelloWorld.c和建立這個native庫.

不同的作業系統支援不同的方法來建立native庫,在Solaris上面,下面的命令生成一個共享庫呼叫libHelloWorld.so:

cc -G -I/java/include -I/java/include/solaris
HelloWorld.c -o libHelloWorld.so

-G選項指示這個C 編譯器去生成一個共享庫,而不是一個普通的Solaris的可執行檔案.因為本書頁面寬度的限制,我把一行分成了兩行,你需要在一行輸入命令,或者把命令放在指令碼中.在Win32的系統上,下面的命令將使用Microsoft Visual C++ 編譯器建立一個動態連結庫(DLL)HelloWorld.dll:

cl -Ic:/java/include -Ic:/java/include/win32
-MD -LD HelloWorld.c -FeHelloWorld.dll

-MD選項將確保HelloWorld.dll已經連線到Win32的多執行緒C 庫當中.-LD選項指示C 編譯器去生成一個DLL,而不是一個普通的Win32可執行檔案.當然,在Solaris和Win32,你都需要放到include路徑中去,以反射你自己的安裝配置.

-I<dir>把指定路徑加到include的搜尋路徑中.
如果遇到"Cannot open include file:’jni.h’"的錯誤,則說明路徑指定地有問題.有兩種解決方案:
1.在你的JDK安裝目錄下有個include目錄,把裡面的jni.h和win32目錄下的jawt_md.h和jni.md.h複製到你的C 編譯器的include目錄中(可能是vc/include).
2.指定正確的搜尋路徑.比如我的JDK安裝在F:/Program Files/Java/jdk1.6.0_16/,那麼上面的引數就改為:
-I"F:/Program Files/Java/jdk1.6.0_16/include" -I"F:/Program Files/Java/jdk1.6.0_16/include/win32"
如果目錄中有空格要引起來.

7 執行程式

此時,你要準備好兩個元件去執行這個程式.類檔案(HelloWorld.class)呼叫本地方法,native庫(HelloWorld.dll)實現這個本地方法.

因為HelloWorld類包含了它自己的main方法,你可以在Solaris 或者是Win32的作業系統上執行這個程式:

java HelloWorld

你應該會看到下列的輸出:

Hello World!

設定好正確地native庫路徑對程式的執行是很重要的.native庫路徑是一個目錄列表,是Java虛擬機器裝載native庫時的搜尋列表.如果你沒有設定一個正確的native庫路徑,你就會看到類似於下列的錯誤:

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)

要確保native庫存在於native庫路徑的目錄中.如果你是在Solaris上執行,LD_LIBRARY_PATH的環境變數是用來定義native庫路徑.請確保它包含的目錄名稱中包含了libHelloWorld.so 檔案.如果libHelloWorld.so 檔案在當前目錄裡面,你可以在標準shell(sh)或者KornShell(ksh)上發出兩條命令去設定正確的LD_LIBRARY_PATH環境變數屬性:

LD_LIBRARY_PATH=.
export LD_LIBRARY_PATH

這與在C shell(csh或者tcsh)中的下列命令等效:

setenv LD_LIBRARY_PATH .

如果你是在Windows95或者Windows NT 計算機上執行,設法確保HelloWorld.dll是在當前目錄下,或者在PATH環境變數列出的目錄中.

在Java 2 SDK 1.2版本中,你也可以在java命令列中像系統屬性一樣指定native庫路徑,如下所述:

java -Djava.library.path=. HelloWorld

-D 命令列選項是設定Java平臺的系統屬性,設定java.library.path屬性成".",表示JVM要搜尋當前目錄下的native庫.

如果本文有任何問題,請及時指出,以免對後來者產生不必要的困擾,不勝感激!