建立C函式庫及使用JNI寫程式


建立C共享函式庫(Shared Library or Dynamic Library)及使用JNI寫程式

第一章、建立自己的C函式庫

  程式在執行時,會自動去連結libc.so,這是基本的c函式庫,也就是compile時不用加上-lc,就會自動進行連結。共享函式庫和靜態函式庫的不同在於,不論多少程序呼叫共享函式庫,它只使用一份記憶體,因此非常節省記憶體空間;並且在修改函式庫後,使用該函式庫的程式並不用重新編譯,只需要編譯函式庫本身即可。共享函式庫的代價就是程式的複雜度提高,若二進位檔搬去別的電腦執行時找不到共享函式庫,則程式會無法執行。

  以下是建立"自己"的共享函式庫的步驟。

  1. 建立hello.c和hello.h

    #include <stdio.h>
    
    void helloworld() {
            printf("hello world!\n");
    }

  2. gcc -fPIC -Wall -c hello.c //編譯時依照機器來產生hello.o,拿到別的機器上hello.o即無效
  3. gcc -shared -o libhello.so hello.o //編譯成libhello.so,加入hello.o
  4. 建立caller.c

    #include "hello.h"
    #include <stdio.h>
    
    int main() {
            printf("call hello.a\n");
            helloworld();
            return 1;
    }

  5. gcc caller.c -o caller -lhello -L. //編譯時指定連結libhello.so,並且指定目錄在.
  6. 接下來有兩種方法可以讓二進位執行檔找到libhello.so:
    1. cp libhello.so /lib/ //直接copy檔案到預設library的目錄下,程式執行時會自動去該目錄找尋
    2. LD_LIBRARY_PATH=`pwd` ./caller //在執行前設定LD_LIBRARY_PATH變數,程式執行時就會去找該目錄下的lib
  7. 完成自製的共享函式庫了。

補註:如果想要在一個lib中放入多個object檔案,只要在第三步時,將多個.o檔加在參數之後即可。
ex:gcc -shared -o libhello.so hello.o hello2.o hello3.o

第二章、使用Java Native Interface呼叫C程式
  如何讓JAVA程式呼叫以前寫好的C程式?JNI可以在JAVA程式中呼叫不同語言寫成的程式。以下是以C語言為例:
  1. 建立JAVA程式,其中宣告native function的格式為:public native void functionname()

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

  2. javac HelloWorld.java //編譯JAVA程式
  3. javah -jni HelloWorld //產生對應C程式的標頭檔,內容中粗體為C的函式宣告

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class HelloWorld */
    
    #ifndef _Included_HelloWorld
    #define _Included_HelloWorld
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     HelloWorld
     * Method:    helloworld
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_HelloWorld_helloworld
      (JNIEnv *, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

  4. 實作C函式的內容於HelloWorld.c,以下為其內容:
    注意其中兩個參數JNIEnv *jobject都是預設就會幫你傳的參數,故在java中呼叫native時並不用去理會這兩個參數。

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

  5. gcc -fPIC HelloWorld.c -c -I/usr/local/j2sdk1.4.2_06/include -I/usr/local/j2sdk1.4.2_06/include/linux //編譯HelloWorld.c成Object file
  6. gcc -shared -o libhello.so HelloWorld.o //產生hello library,命名為libhello.so
  7. LD_LIBRARY_PATH=`pwd` java HelloWorld //執行java HelloWorld之前必需要設定LD_LIBRARY_PATH後才能讓JNI找到libhello.so
  8. 完成
第三章、JNI進階一-從Java中傳參數給C程式
  只能呼叫C的程式並不能滿足我們的需求,當我們要傳入參數給C的函式時,我們可以傳入Java中定義的原生型態,並且在C函式中直接使用這些原生型態,以下是原生形態的對應列表:
Java Type Native Type Size
boolean jboolean 8
byte jbyte 8
char jchar 16
short jshort 16
int jint 32
long jlong 64
float jfloat 32
double jdouble 64
void void n/a


   若要使用Object作為參數的話,參數就會是jobject,JNI定義了一些方法來幫助我們轉換jobject來讓C程式使用,這裡以String和Array兩種物件為例子。

  1. 建立JAVA程式,其中傳入參數為String,其餘和第二章中相同。
    public native void helloworld(String param);
  2. javac StringHW.java //編譯JAVA程式
  3. javah -jni StringHW //產生對應C程式的標頭檔,內容中粗體為C的函式宣告
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class StringHW */
    
    #ifndef _Included_StringHW
    #define _Included_StringHW
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     StringHW
     * Method:    helloworld
     * Signature: (Ljava/lang/String;)V
     */
    JNIEXPORT void JNICALL Java_StringHW_helloworld
      (JNIEnv *, jobject, jstring);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    
  4. 開始實作C函式的內容。這裡使用JNIEnv中的函式來處理jstring
    #include "StringHW.h"
    #include <stdio.h>
    
    JNIEXPORT void JNICALL Java_StringHW_helloworld
      (JNIEnv *env, jobject obj, jstring param)
    {
            int str_len;
            char *buf;
    		//get string length
            str_len = (*env)->GetStringLength(env, param);
    		//get UTF-8 encoding char array
            buf = (*env)->GetStringUTFChars(env, param, 0);
            printf("UTF param:%s[%d]\n",buf,str_len);
    		//after using jstring object you must release it
            (*env)->ReleaseStringUTFChars(env, param, buf);  
    
    		//get unicode encoding char array
            buf = (*env)->GetStringChars(env, param, 0);   
            printf("Char param:%s[%d]\n",buf,str_len);
    		//release string
            (*env)->ReleaseStringChars(env, param, buf);  
    }
    
  5. 其餘同第二章的步驟。
第四章、JNI進階二-由C函式中存取Java中的變數
  要達到Java和C程式之間的互動,C程式必需也可以去修改Java物件中的成員變數。JNIEnv提供了一組函式讓C程式可以順利地取存Java中的變數。
   Java定義的成員變數中,每個都有signature(簽章)。在native code中要存取成員變數時,必須要先得知變數的簽章。以下指令是用來取得變數簽章:javap -s -p classname。該指令會列出所有成員變數的簽章,其內容大約如下:
Compiled from "TMC_GUIWarrant.java"
public class TMC_GUI.TMC_GUIWarrant extends javax.swing.JFrame{
java.lang.String ExpDate_str;
  Signature: Ljava/lang/String;
java.lang.String UpperWarrantBin_str;
  Signature: Ljava/lang/String;
java.lang.String OriSignerCertPath_str;
  Signature: Ljava/lang/String;
java.lang.String ProxySignerCertPath_str;
  Signature: Ljava/lang/String;
      .
	    .
	    .
static javax.swing.JLabel access$000(TMC_GUI.TMC_GUIWarrant);
  Signature: (LTMC_GUI/TMC_GUIWarrant;)Ljavax/swing/JLabel;
static javax.swing.JLabel access$100(TMC_GUI.TMC_GUIWarrant);
  Signature: (LTMC_GUI/TMC_GUIWarrant;)Ljavax/swing/JLabel;
static javax.swing.JLabel access$200(TMC_GUI.TMC_GUIWarrant);
  Signature: (LTMC_GUI/TMC_GUIWarrant;)Ljavax/swing/JLabel;
static javax.swing.JLabel access$300(TMC_GUI.TMC_GUIWarrant);
  Signature: (LTMC_GUI/TMC_GUIWarrant;)Ljavax/swing/JLabel;
static {};
  Signature: ()V
}

<未完>

第五章、FAQ[常見問題解答]

Q1:在JAVA程式執行時會出現java.lang.UnsatisfiedLinkError,是什麼問題?
A1:表示在System.loadLibrary時找不到要載入的動態函式庫,請檢查LD_LIBRARY_PATH環境變數是否已經設定,或是在執行java的命令列中加入-Djava.library.path=[PathToLibrary]參數,讓JVM知道去哪裡找這些函式庫。
後來我又發現一種問題會造成java.lang.UnsatisfiedLinkError,那是libXXX.so中如果有函式名稱無法連結的話,亦會造成java無法載入該libXXX.so。但是gcc在編譯library時不會顯示這些函式名稱無法連結的錯誤,例如像warning: implicit declaration of function xxx(在動態函式庫中,找不到函式名稱是很正常的事,因為函式可能在別的library中有定義),所以強烈建議在編譯時加上-Wall的參數,gcc才會顯示這些警告。

Q2:將java的類別放到package中後,可以載入動態函式庫,但是沒辦法使用native function了 ?
A2:這是因為header改變,必需重新產生header file,修改函式庫原始碼後,再重新編譯函式庫。注意的是,產生header file的指令必須加上package name,例如:javah -jni [PACKAGE].[CLASSNAME]。

 

參考文件:
Unix Programming Frequently Asked Questions : Use of tools
The Java™ Tutorial : Lesson: Writing a Java Program with Native Methods
Trail: Java Native Interface
註:JNIEnv和jobject這兩個變數是無法傳到native中別的thread或process,也就是你不能在不同thread中使用env和obj這兩個變數。


Good Luck.
Update 03/21/2005

回上頁
enijmax@cyber.cs.ntou.edu.tw