最近更新: 2010-07-26

Java Native Interface with C tutorial, part 2

延續第一篇的教學。示範下列項目的 JNI 實現方式:

  • 在原生方法中,呼叫 Java 的方法。
  • 原生方法回傳新的參考型別資料。
  • 在原生方法中處理參考型別陣列。
  • 如何寫入資料到託管陣列(managed array)。
  • 由原生方法擲出 Java 例外。

增加 Hello.java 的原生方法

本文在 Hello.java 中新增三個原生方法: put(), concat(), fileGetContent().

blog/rock/Hello.java
package blog.rock;
import java.util.*;

class Hello {
    //匯入此類別時,一併載入包含原生方法的共享函數庫。

    static {
        //此例的共享函數庫文件名稱是 libHello.so,故此處給的參數是 "Hello".

        System.loadLibrary("Hello");
    }

    //tutorial 2

    private HashMap<String, Integer> hash;
    public native void put(String k, Integer v);

    public native static String concat(String[] msgs);

    public native static byte[] fileGetContent(String filepath)
        throws java.io.FileNotFoundException, java.io.IOException;

    //這只是用來測試的方法。

    public static void main(String[] args) {
        Hello h = new Hello();

        h.hash = new HashMap<String, Integer>();
        h.put("xyz", new Integer(100));
        System.out.println("h.hash.size: " + h.hash.size());
        System.out.println("h.hash("xyz"): " + h.hash.get("xyz"));

        s = Hello.concat(new String[]{"hello-", "world;", "石頭成"});
        System.out.println("concat: " + s);

        try {
            byte[] b = Hello.fileGetContent("libHello.so");
            System.out.println("File size: " + b.length);
            System.out.println("Byte[0]: " + b[0]);
        }
        catch (java.io.IOException e) {
        }
    }
}

產生 C 標頭文件與程式碼骨架

將 hello-glue.h 中的函數原型宣告複製起來,貼到 C 程式碼中 (hello-native.c),再補上函數的參數名稱。透過上述操作,我們將得到最基本的程式碼骨架。

hello-native.c
#include "hello-glue.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <memory.h>
#include <errno.h>

/*
 * Class:     blog_rock_Hello
 * Method:    put
 * Signature: (Ljava/lang/String;Ljava/lang/Integer;)V
 */
JNIEXPORT void JNICALL Java_blog_rock_Hello_put
  (JNIEnv *env, jobject this, jstring k, jobject v)
{
    return;
}

/*
 * Class:     blog_rock_Hello
 * Method:    concat
 * Signature: ([Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_blog_rock_Hello_concat
  (JNIEnv *env, jclass Hello, jobjectArray msgs)
{
    return NULL;
}

/*
 * Class:     blog_rock_Hello
 * Method:    fileGetContent
 * Signature: (Ljava/lang/String;)[B
 */
JNIEXPORT jbyteArray JNICALL Java_blog_rock_Hello_fileGetContent
  (JNIEnv *env, jclass Hello, jstring filepath)
{
    return NULL;
}

實作原生方法的內容

接下來,我們就要開始填空,實現原生方法的內容。

put() - invoke method of instance.

實作一個實體方法,接受一個 key 和一個 value ,將它們加入實體的 hash 欄位內。hash 是一個 HashMap ,提供 put 方法。實作時,我們要先透過反射方法向 this 取得 hash 欄位的參考內容,再透過反射方法向 hash 取得 put 方法的參考內容,最後就可以呼叫這個方法。在 java.lang.reflect 亦有相對應的操作。

查詢方法時,需要給予方法描述符號(方法簽名),詳情查看 The Java Native Interface Programmer's Guide and Specification - Method descriptor。此外,雖然 HashMap 是一個泛型類別,但是因為 JVM 執行時已經丟棄了泛型資訊,所以全部的泛型化參數,在 JNI 中都視為 java.lang.Object (jobject) 資料。

/*
 * Class:     blog_rock_Hello
 * Method:    put
 * Signature: (Ljava/lang/String;Ljava/lang/Integer;)V
 */
JNIEXPORT void JNICALL Java_blog_rock_Hello_put
  (JNIEnv *env, jobject this, jstring k, jobject v)
{
    jclass cls = (*env)->GetObjectClass(env, this);
    jclass hash_cls = (*env)->FindClass(env, "java/util/HashMap");
    jfieldID fid;
    jobject hash;
    jmethodID mid;

    fid = (*env)->GetFieldID(env, cls, "hash", "Ljava/util/HashMap;");
    hash = (*env)->GetObjectField(env, this, fid);

    // Java: mid = hash_cls.getMethod("put", Object.class, Object.class);
    // method descriptor:
    // see http://java.sun.com/docs/books/jni/html/types.html#65751
    //
    // prototype: V java.util.HashMap.put(K key, V value);
    // JVM already drops information of generic type.
    // All generic arguments think as java.lang.Object.
    mid = (*env)->GetMethodID(env, hash_cls, "put",
        "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
    if (mid == NULL) {
        printf("could not found method\n");
        goto end;
    }

    // Java: mid.invoke(k, v);
    (*env)->CallObjectMethod(env, hash, mid, k, v);

  end:
    // In this case, those are not necessarily.
    (*env)->DeleteLocalRef(env, cls);
    (*env)->DeleteLocalRef(env, hash_cls);
    (*env)->DeleteLocalRef(env, hash);
    return;
}

In most cases, you do not have to worry about freeing local references when implementing a native method. The Java virtual machine frees them for you when the native method returns to the caller.
The Java Native Interface Programmer's Guide and Specification - 5.2.1 Freeing Local References
concat() - access array of reference type data and return new String.

實作一個靜態方法,接受一個字串陣列,將它們的字串合併成一個新的字串回傳。

字串陣列被視為一個包含參考型別資料的陣列,所以要用 GetObjectArrayElement() 一一取出陣列中的元素內容。複製字串內容時,則應注意 UTF-8 字元與 C 字元的差異,必須使用 GetStringUTFLength() 才能得到以 byte 為計算單位的長度,從而分配足夠的記憶體空間。

/*
 * Class:     blog_rock_Hello
 * Method:    concat
 * Signature: ([Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_blog_rock_Hello_concat
  (JNIEnv *env, jclass Hello, jobjectArray msgs)
{
    typedef struct {
        jsize length;
        jstring msg;
    } jmsg_t;
    jmsg_t *j_msgs = NULL;
    jsize msgs_len, i, total_len;
    jstring result = NULL;
    const char *c_msg;
    char *buf = NULL, *pb = NULL;

    msgs_len = (*env)->GetArrayLength(env, msgs);

    j_msgs = (jmsg_t*) malloc(msgs_len * sizeof(jmsg_t));
    //alloc an array to cache jstring object of msgs.

    total_len = 0;
    for (i = 0; i < msgs_len; ++i) {
        j_msgs[i].msg = (*env)->GetObjectArrayElement(env, msgs, i);
        j_msgs[i].length = (*env)->GetStringUTFLength(env, j_msgs[i].msg);
        total_len += j_msgs[i].length;
        //printf("[%d] %d\n", i, j_msgs[i].length);
    }
    //printf("total len: %d\n", total_len);

    buf = (char*) malloc(total_len + 1);
    pb = buf;
    for (i = 0; i < msgs_len; ++i) {
        c_msg = (*env)->GetStringUTFChars(env, j_msgs[i].msg, NULL);
        memcpy(pb, c_msg, j_msgs[i].length);
        pb += j_msgs[i].length;
        (*env)->ReleaseStringUTFChars(env, j_msgs[i].msg, c_msg);
    }
    *pb = '\0';
    //printf("result: %s\n", buf);

    result = (*env)->NewStringUTF(env, buf);
    // Java: return new String(buf);

    for (i = 0; i < msgs_len; ++i) {
        (*env)->DeleteLocalRef(env, j_msgs[i].msg);
    }

    free(j_msgs);
    free(buf);
    return result;
}

fileGetContent() - return a new array or throws exception.

實體一個靜態方法,它將讀取指定的檔案內容,將其內容儲存於 byte[] 中回傳。當檔案不存在或是發生 IO 錯誤時,它將會擲出 Java 的例外 (IOException, FileNotFoundException)。

第一個實作版本,先用 C 函數配置未受 Java 託管的陣列(unmanaged array),將檔案的資料內容讀入該未託管空間。再使用 NewByteArray() 配置一個被託管的陣列(managed array),將檔案的資料內容從未託管空間複製到託管空間中。當我們需要寫入資料到託管陣列時,我們要用 GetPrimitiveArrayCritical() 取得指向該託管空間的指標。

擲出例外的方式,請查看 The Java Native Interface Programmer's Guide and Specification - 6. Exceptions

/*
 * see http://java.sun.com/docs/books/jni/html/exceptions.html#26050
 */
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);
}

/*
 * Class:     blog_rock_Hello
 * Method:    fileGetContent
 * Signature: (Ljava/lang/String;)[B
 */
JNIEXPORT jbyteArray JNICALL Java_blog_rock_Hello_fileGetContent
  (JNIEnv *env, jclass Hello, jstring filepath)
{
    const char *c_filepath;
    int fd;
    struct stat file_stat;
    unsigned char *buf = NULL, *bp = NULL, *pcontent = NULL;
    jbyteArray content = NULL;

    size_t nbr; //number of bytes readed
    ssize_t rc;

    c_filepath = (*env)->GetStringUTFChars(env, filepath, NULL);

    if ((fd = open(c_filepath, O_RDONLY)) == -1) {
        // errno
        (*env)->ReleaseStringUTFChars(env, filepath, c_filepath);
        if (errno == ENOENT)
            JNU_ThrowByName(env, "java/io/FileNotFoundException", "file not found");
        else
            JNU_ThrowByName(env, "java/io/IOException", "open error");
        return NULL;
    }

    (*env)->ReleaseStringUTFChars(env, filepath, c_filepath);

    if (fstat(fd, &file_stat) == -1) {
        // errno
        close(fd);
        JNU_ThrowByName(env, "java/io/IOException", "stat error");
        return NULL;
    }

    buf = (unsigned char *) malloc( file_stat.st_size );

    bp = buf;
    nbr = file_stat.st_size;
    
    while ( (rc = read(fd, bp, nbr)) < nbr) {
        if ( rc > 0 ) {
            bp += rc;
            nbr -= rc;
        }
        else if (errno != EINTR ) {
            //errno
            close(fd);
            free(buf);
            JNU_ThrowByName(env, "java/io/IOException", "read error");
            return NULL;
        }
    }
    content = (*env)->NewByteArray(env, file_stat.st_size);

    //if you want to write back data to managed array, use GetPrimitiveArrayCritical()
    pcontent = (*env)->GetPrimitiveArrayCritical(env, content, NULL);

    memcpy(pcontent, buf, file_stat.st_size);

    (*env)->ReleasePrimitiveArrayCritical(env, content, pcontent, 0);

    close(fd);
    free(buf);
    return content;
}

fileGetContent() 第二個實作版本,不將資料先行寫入到 malloc() 配置的未託管空間,而是直接將資料寫進託管空間,節省記憶體用量。

JNIEXPORT jbyteArray JNICALL Java_blog_rock_Hello_fileGetContent
  (JNIEnv *env, jclass Hello, jstring filepath)
{
    const char *c_filepath;
    int fd;
    struct stat file_stat;
    unsigned char *buf = NULL, *bp = NULL;
    jbyteArray content = NULL;

    size_t nbr; //number of bytes readed
    ssize_t rc;

    c_filepath = (*env)->GetStringUTFChars(env, filepath, NULL);

    if ((fd = open(c_filepath, O_RDONLY)) == -1) {
        // errno
        (*env)->ReleaseStringUTFChars(env, filepath, c_filepath);
        if (errno == ENOENT)
            JNU_ThrowByName(env, "java/io/FileNotFoundException", "file not found");
        else
            JNU_ThrowByName(env, "java/io/IOException", "open error");
        return NULL;
    }

    (*env)->ReleaseStringUTFChars(env, filepath, c_filepath);

    if (fstat(fd, &file_stat) == -1) {
        // errno
        close(fd);
        JNU_ThrowByName(env, "java/io/IOException", "stat error");
        return NULL;
    }

    content = (*env)->NewByteArray(env, file_stat.st_size);
    //if you want to write back data to managed array, use GetPrimitiveArrayCritical()
    buf = (*env)->GetPrimitiveArrayCritical(env, content, NULL);

    bp = buf;
    nbr = file_stat.st_size;
    
    while ( (rc = read(fd, bp, nbr)) < nbr) {
        if ( rc > 0 ) {
            bp += rc;
            nbr -= rc;
        }
        else if (errno != EINTR ) {
            //errno
            close(fd);
            (*env)->ReleasePrimitiveArrayCritical(env, content, buf, 0);
            JNU_ThrowByName(env, "java/io/IOException", "read error");
            return NULL;
        }
    }
    (*env)->ReleasePrimitiveArrayCritical(env, content, buf, 0);

    close(fd);
    return content;
}

執行

執行結果當如下列所示:

rock@rock-desktop:~/workspace/jni-tutorial$ java -Djava.library.path=. blog.rock.Hello
h.hash.size: 1
h.hash('xyz'): 100
concat: hello-world;石頭成
File size: 13296
Byte[0]: 127
樂多舊網址: http://blog.roodo.com/rocksaying/archives/13196157.html