IntelliJ IDEA平臺下JNI編程(一)—HelloWorld篇

轉載請註明出處:【huachao1001的專欄:http://blog.csdn.net/huachao1001/article/details/53906237】

JNI(Java Native Interface),出於學習JNI的目的,爲了能夠更方便快速地運行程序。本文的是在IDEA中進行,而不在AndroidStudio,這樣能夠對NDK的工作過程有個更深刻的認識,同時也能對JNI的原理有更深的理解。雖然本文是HelloWorld篇,但是其中涉及到很多內容。博主將遇到的坑都記錄下來了,希望能夠幫到大家。這篇文章可能是2016年的最後一篇文章了,接下來JNI相關係列文章明年推出,歡迎大家關注。

1. 搭建GCC編譯環境

既然使用的了JNI,那就不可避免地需要將C/C++文件編譯成dll(windows)so(Linux)文件。因爲我是在Windows平臺下開發,可以有如下選擇:

  1. 使用VC(或VS)編譯成dll
  2. 使用GCC編譯成dll

因爲開發Android應用肯定是需要編譯成Linux平臺的so文件,因此,爲了後面開發Android程序的兼容,使用GCC編譯器比較好。而Windows平臺下的GCC又可以有如下選擇:

  1. 使用MinGW
  2. 使用Cygwin

這裏我選擇了MinGW,不管選擇哪個,只要能讓本地有GCC編譯環境即可。

注意:搭建GCC編譯環境時,一定要選擇正確的GCC編譯版本(32位和64位)。如果你本地安裝的JDK是64位的,那麼選擇64位GCC,否則選擇32位。這是爲了使得編譯後的庫文件跟JVM的位一致,否則後面JVM無法調用dll(或so)。

1.1 安裝MinGW

安裝MinGW的方法很多,可以前往https://sourceforge.net/projects/mingw/files/MinGW/Base/gcc/ 中自己選擇需要的包。老實說,我也不是很清楚哪些包需要下載,沒花時間研究,感興趣的自己研究一些各個包的功能。不過這裏有個安裝教程https://github.com/cpluspluscom/ChessPlusPlus/wiki/MinGW-Build-Tutorial按照這裏的方法安裝就可以。

另外,針對64位的,這裏https://nuwen.net/mingw.html提供了完整的壓縮包,直接下載https://nuwen.net/files/mingw/mingw-14.1.exe運行。如下:

指定解壓路徑

其本質也是解壓縮,把需要的MinGW庫和程序解壓到指定目錄。如果懶的去看英文,我這裏將我解壓後的重新壓縮了下,大家去下載並且解壓即可直接使用。

鏈接: https://pan.baidu.com/s/1slpQrrJ
密碼: fykw

解壓完成後,剛纔指定的解壓目錄中的bin加入到path環境變量中。例如上面圖中解壓到E:\MinGw,那麼應當將E:\MinGw\bin加入到環境變量path中。注意,請確保bin目錄確實在E:\MinGw中,如果不在,可能在更深一層目錄中,自行確定bin的目錄。

做完後,打開控制檯,輸入:gcc -v,如下:

C:\Users\NetLab\Desktop>gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=e:/mingw/bin/../libexec/gcc/x86_64-w64-mingw32/6.3.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../src/configure --enable-languages=c,c++ --build=x86_64-w64-mingw32 --host=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 --disable-multilib --prefix=/c/temp/gcc/dest --with-sysroot=/c/temp/gcc/dest --disable-libstdcxx-pch --disable-nls --disable-shared --disable-win32-registry --enable-checking=release --with-tune=haswell
Thread model: win32
gcc version 6.3.0 (GCC)

2. 開始編碼

2.1 編寫Java文件

新建一個Java Project,創建包com.huachao.java,如下:

創建項目

在包com.huachao.java下編寫HelloJNI類:

package com.huachao.java;

/**
 * Created by HuaChao on 2016/12/29.
 */
public class HelloJNI {
    static {
        // hello.dll (Windows) or libhello.so (Unixes)
        System.loadLibrary("hello");
    }

    private native void sayHello();

    public static void main(String[] args) {

        new HelloJNI().sayHello();  // invoke the native method
    }

}

函數System.loadLibrary()是加載dll(windows)或so(Linux)庫,只需名稱即可,無需加入文件名後綴(.dll或.so)。native關鍵字將函數sayHello()聲明爲本地函數,由C/C++實現。具體的實現就在hello.dll(Windows平臺)或hello.so(Linux平臺)中

2.2 生成JNI頭文件

2.2.1 手動輸入javah指令

JNI生成頭文件是通過JDK中提供的javah來完成,javah在 {JDKHome}/bin目錄中。用法如下:

javah -jni -classpath (搜尋類目錄) -d (輸出目錄) (類名)

例如,將E:\Porject\out\com\huachao\java目錄中的HelloJNI.class生成頭文件,並放入到E:\Project\jni中:

javah -jni -classpath  E:\Porject\out\com\huachao\java -d E:\Project\jni  com.huachao.java.HelloJNI.java

需要注意的是,使用javah來生成頭文件(.h)時,-classpath指定的是編譯後的java文件(.class)的目錄,而不是源文件(.java)的目錄,因此在使用javah指令之前,先build一下項目(或直接運行一下)。此時會生稱out目錄,所有編譯後的文件都會存放在這個目錄中。

out目錄

接下來,直接在IDEA的Terminal窗口運行javah:

執行javah指令

此時在jni目錄中生成了頭文件com_huachao_java_HelloJNI.h

自動生成頭文件

內容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_huachao_java_HelloJNI */

#ifndef _Included_com_huachao_java_HelloJNI
#define _Included_com_huachao_java_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_huachao_java_HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_huachao_java_HelloJNI_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

接下來我們只需實現Java_com_huachao_java_HelloJNI_sayHello(JNIEnv *, jobject)即可。仔細觀察就會發現這個函數名稱是有規律的,即Java_<包>_<類名>_<函數名>,JNIEXPORT和JNICALL這兩個宏定義暫時不用管。JNIEnv 和jobject後面系列文章會詳細介紹,這裏暫時不理會。

2.2.2 一鍵生成頭文件

在2.2.1小節中,介紹了輸入javah生成頭文件方法。但是如果目錄層次很深,或者是有多個需要生成頭文件的class文件,這工作量太大了,當然你可以通過寫個小程序來實現。但是這裏有個更便捷的方法。點擊File>Settings>Tools>External Tools

External Tools

添加一個先的External Tools:

添加一個先的External Tools

在HelloJNI.java文件中點擊右鍵>External Tools>Generate Header File

點擊生成頭文件

點擊生成,可以看到Terminal窗口會自動運行指令。跟2.2.1小節的指令一模一樣。

3. 編寫C文件並編譯成dll(或so)文件

3.1 手動輸入命令生成

在jni目錄中新建HelloJNI.c文件,如下:

新建C文件

編輯HelloJNI.c如下:

#include<jni.h>
#include <stdio.h>
#include "com_huachao_java_HelloJNI.h"

JNIEXPORT void JNICALL Java_com_huachao_java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
   printf("Hello World!\n");
   return;
}

接下來就是使用GCC對HelloJNI.c編譯,在Terminal窗口輸入如下:

E:\workspace\StudyJNI>gcc -c jni/HelloJNI.c
jni/HelloJNI.c:1:17: fatal error: jni.h: No such file or directory
 #include <jni.h>

發現報錯,找不到jni.h頭文件,將JDK目錄中的include目錄加入,即爲:

E:\workspace\StudyJNI>gcc -c -I"E:\JDK\include" jni/HelloJNI.c
In file included from jni/HelloJNI.c:1:0:
E:\JDK\include/jni.h:45:20: fatal error: jni_md.h: No such file or directory
compilation terminated.

又報找不到jni_md.h錯誤,繼續將JDK目錄中的include/win32加入,即:

gcc -c -I"E:\JDK\include" -I"E:\JDK\include\win32" jni/HelloJNI.c

完成編譯。此時在項目中會生成HelloJNI.o文件。接下來是將HelloJNI.o轉爲HelloJNI.dll,即轉爲windows平臺下的動態鏈接庫。在Terminal中輸入如下:

gcc -Wl,--add-stdcall-alias -shared -o hello.dll HelloJNI.o

此時項目目錄中生成了hello.dll文件:

生成dll

3.2 一鍵生成dll

有了前面使用External Tools一鍵生成頭文件的經驗後,我們可以將編譯成dll的過程命令也加入到External Tools中。前面將c文件編譯鏈接成dll文件分了2個命令,這裏我們直接通過一個命令來完成:

gcc -Wl,--add-stdcall-alias -I"E:\JDK\include" -I"E:\JDK\include\win32" -shared -o ./lib/hello.dll ./jni/HelloJNI.c

這樣就將c文件編譯成了dll,在這裏把生成的dll文件加入到了lib目錄中,而不是像之前那直接放到項目底下。因此在java.library.path應該指定目錄爲lib。

有了上面的命令後,可以很輕鬆的加入到External Tools中了。按照前面的方法,點擊File>Settings>Tools>External Tools>+,輸入內容如下:

name:Generate DLL
Program:<GCC路徑>
Parameters:-Wl,--add-stdcall-alias -I"$JDKPath$\include" -I"$JDKPath$\include\win32" -shared -o ./lib/$FileNameWithoutExtension$.dll ./jni/$FileNameWithoutExtension$.c
Working Directory:$ProjectFileDir$

添加External Tools

在HelloJNI.c中點擊右鍵,選擇External Tools>Generate DLL。此時,在lib目錄中會得到dll文件。

4. 運行

運行HelloJNI.java類後,如下:

運行結果

5 可能出現的錯誤

5.1 java.library.path找不到dll的錯誤

此時,點擊直接運行HelloJNI.java類時,依然還會有錯誤:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no hello in java.library.path
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
    at java.lang.Runtime.loadLibrary0(Runtime.java:870)
    at java.lang.System.loadLibrary(System.java:1122)
    at com.huachao.java.HelloJNI.<clinit>(HelloJNI.java:8)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:123)

Process finished with exit code 1

即找不到我們生成的dll文件。因爲在Windows中JVM的java.library.path屬性即爲環境變量Path指定的目錄,而我們生成的dll並未放入到Path指定的任何一個目錄中,因此我們需要告訴JVM,dll文件在哪個目錄中。點擊Run > Edit Configurations...,如下:

修改Edit Configurations

在VM options中加入java.library.path,指定dll(或so)文件所在的目錄,比如本文中dll放在項目目錄中的lib中,如下:

-Djava.library.path=E:\workspace\StudyJNI\lib

如下圖所示:

修改VM options

5.2 無法識別__int64類型錯誤

錯誤如下:

 error: unknown type name '__int64'
 typedef __int64 jlong;
         ^

出現這個錯誤的人一般是使用Cygwin GCC的人,這是因爲Cygwin GCC不認識__int64類型,找到<JDK_HOME>/include/win32/jni_md.h,找到typedef __int64 jlong;並修改爲:

#ifdef __GNUC__
typedef long long jlong;
#else
typedef __int64 jlong;
#endif

或者是編譯時將__int64加入,如下:

> gcc-3 -D __int64="long long" -mno-cygwin -Wl,--add-stdcall-alias 
  -I"<JAVA_HOME>\include" -I"<JAVA_HOME>\include\win32" -shared -o hello.dll HelloJNI.c

5.3 64-bit mode not compiled

錯誤如下:

HelloJNI.c:1:0: sorry, unimplemented: 64-bit mode not compiled in
 #include <jni.h>

出現這個錯誤是因爲,JDK版本是64位,而GCC編譯器編譯出的dll(或so)是32位,只需換個64位版本的GCC即可。

參考資料

https://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html#zz-3.
http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章