0. JNI
.dll和.so就不用介紹了吧, 不知道的也應該不需要看本文, 就是庫檔案. JNI是啥, JNI全稱Java Native Interface, 如果你有跟過Java的原始碼, 反正我覺得最惡心的就是一路跟到一個簽名帶native的方法, 你什么都看不到. 當然其實也不是什么都看不到要是想知道更加內部的原理還是有辦法的, 比如下載hotspot的原始碼.JNI就是其它語言的代碼和Java之間的一個橋, 這個其它語言基本也就是C++了.
JNI的原理就沒必要在這里探討, 就寫個hello world怎么使用. 先閑聊幾句, 一開始學習Java的時候就有人說Java跨平臺的, 一次編譯到處運行, 其實了解的越多就有更多疑惑, 首先Java肯定是跨平臺的沒錯, 然后其實也不那么完全的跨平臺, 某些Java代碼在不同的平臺其實結果會有一點差別, 就我所知socket的某些屬性設定, 在linux和windows上結果是不一樣的.再其次其實Java所謂的跨平臺, 嗯, “你以為歲月靜好,其實有人在為你負重前行”, 那個負重前行的就是JVM.
主要分兩種動態注冊和靜態注冊
1. 靜態注冊
- 第一步撰寫一個包含native方法的java類
package aa.bb; public class TestJNI{ public native String hello(); } - 編譯這個類生成.class檔案
嗯, 這個java程式應該不需要教吧, 還是說一下:-
在TestJNI的同級包目錄執行
javac TestJNI.java會生成TestJNI.class檔案 -
退到src根目錄執行
javah aa.bb.TestJNI會在根目錄同級生成一個aa_bb_TestJNI.h, 這里解釋一下, 如果你的路徑是src/aa/bb/TestJNI, 而TestJNI.java的包路徑是aa.bb那么就要到src目錄下執行javah. 看一下生成的.h檔案#include <jni.h> #ifndef _Included_aa_bb_TestJNI #define _Included_aa_bb_TestJNI #ifdef __cplusplus extern "C" { #endif JNIEXPORT void JNICALL Java_aa_bb_TestJni_hello (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif需要解釋一下內容嗎? 首先jni.h頭檔案在你的jdk檔案目錄下的include檔案夾下, 這里面定義了一些c/c++資料型別到java資料型別的映射之類的宏變數. 下面兩行是防止重復包含頭檔案, 然后第一個__cplusplus到與之配對的#endif那一行, 這還有一點不好解釋, 簡單說就是識別是否是通過g++等c++的編譯器, 如果是就在中間的
JNIEXPORT void JNICALL Java_aa_bb_TestJni_hello (JNIEnv *, jobject);這句代碼前后添加點內容, 變成extern "C" {JNIEXPORT void JNICALL Java_com_waxxd_TestJni_hello (JNIEnv *, jobject);}至于為什么要這樣那就說來話長了, 不過懂的自然懂, 不懂的好像也沒必要懂. 大致就是在靜態注冊的時候上面寫的TestJNI類的hello方法會通過Java_com_waxxd_TestJni_hello這個函式名稱去動態庫找相應的函式呼叫, 但是c++是實作了函式多載的, Java_com_waxxd_TestJni_hello這個c++函式在編譯后并不叫這個名, 也可以說不完全叫這名, 前后加了些東西. extern "C"就是告訴編譯器這個函式按照c的方式(c沒有實作多載, 所以不改名)編譯, 如果不加按照c++的編譯是找不到這個函式的.
然后說說Java_com_waxxd_TestJni_hello 這個函式的兩個引數:- JNIEnv 這個是當前的java環境, 可以通過這個指標呼叫java中很多有用的方法
- jobject 這是個物件, 如果native方法是靜態的, 就是當前物件的Class物件實體(為啥是Class物件呢? 這得從類加載說起), 如果不是靜態的這個物件就是當前物件的實體. 上面的示例TestJNI的hello方法顯然是當前物件實體.
至于JNIEXPORT 和JNICALL 這兩個宏在windows和linux環境是不一樣的, JNICALL在linux環境沒啥實意, 在windows環境下表示__stdcall, JNIEXPORT點到定義處, 如果知曉linux或者windows動態庫撰寫就沒啥可以講的了.
-
- 撰寫動態庫
動態庫撰寫其實沒什么特別的可以說的, 這屬于另外一門課程了, 簡單分別說兩個基于linux和windows hello world級別的動態庫撰寫.再次提一句jni.h頭檔案在你jdk目錄的include的下// TestJNI.cpp #include "aa_bb_TestJni.h" #include <iostream> JNIEXPORT void JNICALL Java_aa_bb_TestJni_hello (JNIEnv *, jobject) { std::cout << "helloqqqq world" << std::endl; return; }- linux, linux相對簡單. 將include目錄復制到你專案路徑下. 撰寫上述的TestJNI.cpp. 在TestJNI.cpp同級目錄下編譯g++ -I 頭檔案路徑 -shared -o libTestJNI.so. 筆者編譯命令如下
g++ -I /home/waxxd/jdk/include -shared -o libTestJNI.so, 如果沒有g++ 可以百度安裝一下,因為筆者的jdk在/home/waxxd/jdk下. 然后就可以使用了. - windows, 筆者基于vs演示. 首先新建一個空專案, 進入后在解決方案視圖右鍵專案-配置屬性-常規-配置型別改為動態庫確定, 同樣配置屬性-c/c++常規附加包含目錄, 里面將你在windows平臺的jdk環境下的include路徑和include/win32路徑設定進去. 在源檔案新建TestJNI.cpp, 將aa_bb_TestJNI.h復制到頭檔案去.點擊編譯就完畢了, 如果不報錯就可以在專案路徑相應的下級目錄下找到專案名稱.dll檔案.
- linux, linux相對簡單. 將include目錄復制到你專案路徑下. 撰寫上述的TestJNI.cpp. 在TestJNI.cpp同級目錄下編譯g++ -I 頭檔案路徑 -shared -o libTestJNI.so. 筆者編譯命令如下
- 使用
不出意外你就成功了.public class Main { public static void main(String[] args) { System.load("動態庫檔案所在的全路徑"); String test = new TestJni().hello(); System.out.println(test); } }
2. 動態注冊
其實可以發現剛才的代碼挺繁瑣的, 在java native方法hello要呼叫動態庫, 動態庫的方法必須使用Java_aa_bb_TestJni_hello這樣的方法名稱, 很長并且當動態庫和java代碼開發人員不同的時候, 協作很不方便, 所以就有了動態注冊. 動態注冊省略了javah生成頭檔案這一步, 其它和靜態庫類似. 在java 加載動態庫的時候會自動執行JNI_OnLoad所以, 由動態庫開發人員在動態庫中完成映射.
新建一個TestJNID檔案代碼如下:
#include "jni.h"
#include <iostream>
jstring stringTest(JNIEnv *env, jobject thiz)
{
std::string hello = "hello wwww+++";
return env->NewStringUTF(hello.c_str());
}
/*
typedef struct {
char *name; 在java中native方法的名稱
char *signature; 方法簽名括號內表示引數, 右側表示回傳值.
void *fnPtr; 在c/c++中的函式
} JNINativeMethod;
*/
static const JNINativeMethod nativeMethods[] = {
{"hello", "()Ljava/lang/String;", (jstring *)stringTest}};
// 這個函式會在加載的時候執行.
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
std::cout << "load TestJNID ... " << std::endl;
JNIEnv *env = NULL;
// 檢查環境
if (vm->GetEnv((void **)&env, JNI_VERSION_1_4) != JNI_OK)
{
std::cout << "env wrong" << std::endl;
return -1;
}
// 加載撰寫了需要映射的class物件
jclass clazz = env->FindClass("aa/bb/TestJni");
if (!clazz)
{
std::cout << "can not get class" << std::endl;
return -1;
}
// 這一步就是注冊了
if(env->RegisterNatives(clazz, nativeMethods,
sizeof(nativeMethods)/sizeof(nativeMethods[0]))) {
std::cout << "register fail" << std::endl;
return -1;
}
// 這個回傳值不可少, 當然也可以回傳更高的版本號
return JNI_VERSION_1_4;
}
java到c++有具體的型別映射, 這就是深入的內容了, 這里簡單的入門helloworld就不介紹了.
3. 總結
入門JNI的hello world實際就介紹完了. 但其實在日常作業JNI呼叫還是比較麻煩的, 有一個很大的缺陷就是, Java和動態庫開發人員必須配合作業, 在實際的作業情況下我們更多的是呼叫已有的動態庫, 這個時候我們還需要撰寫一個中間的支持JNI的庫, 進行函式或者資料型別相關的映射轉化操作, 略顯復雜, 并且不少Java開發人員是不具有c/c++動態庫的撰寫能力, 讀代碼也比較費勁, 實際的作業中, 反正筆者一般用的是另外一個基于JNI封裝的庫JNA, JNA可以自動進行Java型別資料結構和C/C++型別資料結構的映射, 但運行效率比JNI低不少, 并且JNI可以呼叫Java代碼, JNA貌似不可以. 改天有時間寫一個JNA的介紹.
其實在更高更高更高的JDK版本已經有新的方式代替JNI/JNA了, 筆者瞄過一眼JDK16新特性就有一個叫Foreign Linker API的東西, 代替現在復雜的呼叫本地方法的機制.JDK8以后的版本帶來了很多變化 ,可惜大都還在用JDK8.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/273619.html
標籤:其他
上一篇:歸并排序(重要)
