轉載請注明出處:【huachao1001的簡書:http://www.lxweimin.com/users/0a7e42698e4b/latest_articles】
JNI(Java Native Interface),出于學習JNI的目的,為了能夠更方便快速地運行程序。本文的是在IDEA中進行,而不在AndroidStudio,這樣能夠對NDK的工作過程有個更深刻的認識,同時也能對JNI的原理有更深的理解。雖然本文是HelloWorld篇,但是其中涉及到很多內容。博主將遇到的坑都記錄下來了,希望能夠幫到大家。這篇文章可能是2016年的最后一篇文章了,接下來JNI相關系列文章明年推出,歡迎大家關注。
1. 搭建GCC編譯環境
既然使用的了JNI
,那就不可避免地需要將C/C++
文件編譯成dll(windows)
或so(Linux)
文件。因為我是在Windows
平臺下開發,可以有如下選擇:
- 使用VC(或VS)編譯成dll
- 使用GCC編譯成dll
因為開發Android
應用肯定是需要編譯成Linux
平臺的so
文件,因此,為了后面開發Android
程序的兼容,使用GCC
編譯器比較好。而Windows
平臺下的GCC
又可以有如下選擇:
- 使用
MinGW
- 使用
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目錄,所有編譯后的文件都會存放在這個目錄中。
接下來,直接在IDEA的Terminal窗口運行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:
在HelloJNI.java文件中點擊右鍵>External Tools>Generate Header File
,
點擊生成,可以看到Terminal窗口會自動運行指令。跟2.2.1小節的指令一模一樣。
3. 編寫C文件并編譯成dll(或so)文件
3.1 手動輸入命令生成
在jni目錄中新建HelloJNI.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文件:
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$
在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...
,如下:
在VM options中加入java.library.path,指定dll(或so)文件所在的目錄,比如本文中dll放在項目目錄中的lib中,如下:
-Djava.library.path=E:\workspace\StudyJNI\lib
如下圖所示:
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