2009年6月25日 星期四

[java] Java Native Interface(JNI) 簡介與教學

睡前來統整一下以前找過跟做過的資料,方便給隊友們參考,當然有其他不足或錯誤的地方也請大家指正。我主要是針對幾個常用的case來介紹,並不打算深入探討細節的部份。

JNI 是用來讓Java跟別種語言溝通的函式庫,如果我們舉C/C++ 為例,便分為C call JavaJava call C

Java call C

這段是參考[1]的第二章。

1. 建立一個 Java class (HelloWord.java)。在這個class裡宣告一個native method (print),然後在 main() 裡呼叫這個 method。此時,我們呼叫 printf() 時,它的實體是用 C/C++ 所寫的。

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

上例中的System.loadLibrary("HelloWorld")會去找你程式目錄下的HelloWorld.dllHelloWorld.so,這之後會提到。

2. Compile HelloWorld.java,用javac指令。

3. 產生一個 native method 的 header file。

指令: javah -jni HelloWorld

這裡,我們用javah可以產生 .h 檔,接著就是實作這個 .h 檔的 .cpp,然後就可以 compile 成 .dll 或 .so 了。cpp 的範例如下:

 #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 的目錄。如果是用VS系列的話,專案新增時選DLL版本,compile過後就會輸出HelloWorld.dll。

4. 最後,將HelloWorld.class跟HelloWorld.dll放在一起後,執行 java HelloWorld 就可以看到結果囉。

註: HelloWorld.dll 其實也不一定要跟 HelloWorld.class 放在一起,有個環境變數叫LD_LIBRARY_PATH,它便是用來設定 native library path。還有一種方式,就是利用java指令來指定路徑。下方是將路徑設為當前目錄。

java -Djava.library.path=. HelloWorld

C call Java

這段是參考[1]的第七章與其他收集的資料。

首先,我們用java寫一個 class Prog,請他印出些字,然後用javac去compile出 .class。

 public class Prog {
    public static void main(String[] args) {
         System.out.println("Hello World " + args[0]);
    }
}

基本上這段程式是可以直接用java指令去執行的,但我們的目標是要產生一個 .c 檔來呼叫它,其流程大致如下:

  1. 喚醒 Java VM
  2. 載入指定class path下的所有 .class 們
  3. 找到你想要執行的class及其 method ID。
  4. 呼叫 method。

對照第七章的範例來看的話,步驟一加二的程式如下。舊版的JNI 有提供JNI_GetDefaultJavaVMInitArgs,新版的還是請大家自己乖乖指定好 class path 跟其他資訊。JNI_CreateJavaVM 會提供兩個重要的指標,jvm 是指向一個新開的JavaVM,env則是待會讓你用來對java class做怪怪事的介面。

     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);

步驟三加四如下。

     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);

比較可能看不懂的地方是在GetStaticMethodID的第四個參數,這種詭異的寫法稱為JNI signature,當我們呼叫 Java method 時需要寫一些 signature,用來檢查你指定的型態是否與該 method 一樣。舉些例子好了。

Java constructor:

String s

對應到的signature為:

(Ljava/lang/String;)V

Java method:

String toString()

對應到的signature為:

()Ljava/lang/String;

Java method:

long myMethod(int n, String s, int[] arr)

對應到的signature為:

(ILjava/lang/String;[I)J

看了那麼多一定還看不懂對吧?基本上,一個函式會有參數值與回傳值,signature 的規則便是先用 () 描述參數值型態,後面再接回傳值型態。比較特別的,是如果有用到某個 package,除了要打出 package path 外還要加個分號!

下面列出其對應的所有規則,比較一下就可以知道了。

  • B=byte
  • C=char
  • D=double
  • F=float
  • I=int
  • J=long
  • S=short
  • V=void
  • Z=boolean
  • Lfully-qualified-class=fully qualified class
  • [type=array of type>
  • (argument types)return type=method type. If no arguments, use empty argument types: (). If return type is void (or constructor) use (argument types)V.

如果 method signature 不對的話,還會出現類似以下的錯誤訊息。

#
# An unexpected error has been detected by Java Runtime Environment:
#
#  Internal Error (sharedRuntime.cpp:552), pid=7304, tid=2492
#  Error: guarantee(cb != 0,"exception happened outside interpreter, nmethods and vtable stubs (1)")
#
# Java VM: Java HotSpot(TM) Client VM (10.0-b19 mixed mode, sharing windows-x86)
# If you would like to submit a bug report, please visit:
#   http://java.sun.com/webapps/bugreport/crash.jsp
#

最後,你可能會想問:我要如何知道一個 Java class 裡的所有 method 到底需要哪幾種 signature ? 除了自己一個個看以外,J2SDK 有提供神奇指令來幫助我們!

javap -s -p classname

Reference:

[1] Java Native Interface: Programmer's Guide and Specification

[2] Invocation API of JNI Enhancement

[3] Creating a JVM from a C Program

[4] JNI Spec

[5] JNI Functions

6 則留言:

匿名 提到...

謝謝你,這對我有幫助。

87showmin 提到...

不客氣,已經是五年前的文章,資訊可能已有落差了。:D

匿名 提到...

Hi 你好 看了你的文章也照了JAVA call C的步驟做,一切都沒甚麼問題直到最後跑java HelloWorld的指令出現了錯誤:

Exception in thread "main" java.lang.UnsatisfiedLinkError: HelloWorld.print()V

請問是甚麼問題呢?

Unknown 提到...

你有給java介面參數嗎?

87showmin 提到...

我在網路上找到這則留言,參考看看。

It seems that when using JNI in Windows it looks for a function starting with _Java_ while in every other platform it looks for Java_. Why this is the case and not written in the documentation I don't know but it make everything work perfectly!

When javah creates the header file each function is named like Java_package_Class_method. The odd thing is that when compiled like this, JNI cannot find the right function for the native method and spits out errors, but if an underscore was added before Java then JNI would be able to find the function.

提到...

JNIEXPORT void JNICALL Java_HelloJavaWorld_print(JNIEnv *env, jobject obj)
我在.c檔這一行寫成小寫 java_HelloJavaWorld_print 發出了一樣的錯誤訊息哈哈,僅供參考--如果還需要參考的話