java高階用法之:無所不能的java,本地方法呼叫實況

flydean發表於2022-03-23

簡介

相信每個程式設計師都有一個成為C++大師的夢想,畢竟C++程式設計師處於程式設計師鄙視鏈的頂端,他可以俯視任何其他語言的程式設計師。

但事實情況是,無數的程式設計師從小白到放棄,鑑於C++的難度,最後都投入了java的懷抱。JAVA以他寬廣的胸懷接納了一眾無法登頂C++的程式設計師。

開個玩笑,C和C++的優勢在於和系統底層的互動和其執行的速度和效率,JAVA的優勢在與廣泛的應用框架,可以快速搭建所需的應用程式。兩者各有所長。

框架的好處就是降低了程式開發的難度,讓應用程式可以快速批量複製。

大家知道,JVM底層是使用C和C++來編寫的,而JAVA位元組碼適合JVM進行互動的,所以直觀上看來,JAVA是可以和底層的C++程式碼進行互動的。那麼如何互動呢?會不會很複雜?

今天本文帶大家一一揭曉。

JDK的本地方法

所謂本地方法就是呼叫作業系統或者其他底層庫的方法。這些方法屬於系統的外部介面,用於程式和作業系統之間進行互動。大家想一下,JDK中有哪些本地的方法呢?

第一個想到的應該就是檔案操作,因為檔案操作肯定需要依賴與系統底層提供的IO介面。我們先具體來看一下File的delete方法的實現:

    public boolean delete() {
        @SuppressWarnings("removal")
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkDelete(path);
        }
        if (isInvalid()) {
            return false;
        }
        return fs.delete(this);
    }

File的delete方法首先呼叫SecurityManager來進行許可權判斷,看是否可以刪除。如果可以刪除則繼續呼叫FileSystem的delete方法。

我們繼續檢視FileSystem的delete方法:

public abstract boolean delete(File f);

可以看到FileSystem中的delete方法是一個抽象方法,需要具體的實現。

而這個實現是和平臺有關係的,如果你是linux或者mac系統,那麼它的實現類是UnixFileSystem,它的delete方法如下:

    public boolean delete(File f) {
        if (useCanonCaches) {
            cache.clear();
        }
        if (useCanonPrefixCache) {
            javaHomePrefixCache.clear();
        }
        return delete0(f);
    }
    private native boolean delete0(File f);

可以看到,delete方法最終會呼叫delete0方法,而這個方法是一個native方法,表示該方法需要呼叫系統本地的方法。

JDK提供了一個JAVA呼叫本地系統方法的實現,叫做JNI,全稱是Java Native Interface,它是從JAVA1.1中引入的一項技術。它允許Java程式碼和其他語言寫的程式碼進行互動。

為了驗證JNI的可行性,我們接下來自己實現一個native的方法,並在java中呼叫,看看是否能夠成功。

自定義native方法

在JAVA中定義native方法很簡單,我們只需要在方法描述前面加上native關鍵字即可,這個方法並不需要任何實現。舉個具體的例子如下:

public class JNIUsage {

    public native void printMsg();

    public static void main(String[] args) {
        //載入C檔案
        System.loadLibrary("JNIUsage");
        JNIUsage jniUsage = new JNIUsage();
        jniUsage.printMsg();
    }
}

上面的例子中,我們定義了一個native的printMsg,然後在main中首先載入包含該實現的Library檔案,之後就可以像正常的JAVA方法一樣進行呼叫。

那麼這麼實現這個native方法呢?

不管熟悉還是不熟悉C++的朋友應該都聽過標頭檔案的概念,一般來說我們在標頭檔案中定義好要實現的方法,然後在具體的內容檔案中對標頭檔案中定義的方法進行實現。

所以標頭檔案中需要包含這個printMsg的方法,生成標頭檔案可以使用javah命令。

首先進入到JNIUsage原始檔的根目錄,執行下面的命令:

javah -classpath . -jni com.flydean.JNIUsage

該命令會在專案原始碼的根目錄中生成一個com_flydean_JNIUsage.h檔案。開啟看看,具體的內容如下:

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

#ifndef _Included_com_flydean_JNIUsage
#define _Included_com_flydean_JNIUsage
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_flydean_JNIUsage
 * Method:    printMsg
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_flydean_JNIUsage_printMsg
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

簡單點講,該head檔案中定義了一個需要實現的Java_com_flydean_JNIUsage_printMsg方法。

接下來,我們需要對這個標頭檔案進行實現。

這裡我們使用JetBrain公司的Clion開發工具,首先建立一個c++的專案:

注意,這個專案的type需要是shared型別。

然後將com_flydean_JNIUsage.h檔案拷貝到專案的根目錄下。

這時候是編譯不了的,你會發現很多依賴包的錯誤,我們還需要將JDK home目錄中include目錄下的jni.h檔案,和jni_md.h檔案(如果是windows平臺該檔案在win32目錄下,如果是mac平臺,該檔案在darwin目錄下),拷貝到專案的根目錄下。

這樣編譯的錯誤就不見了。

最後我們修改預設的library.cpp檔案,引入com_flydean_JNIUsage.h並實現其中的方法如下所示:

#include "com_flydean_JNIUsage.h"

#include <iostream>

JNIEXPORT void JNICALL Java_com_flydean_JNIUsage_printMsg
        (JNIEnv *, jobject){
    printf("this is www.flydean.com!");
}

目前為止,專案的程式碼結構應該如下圖所示:

<img src="https://img-blog.csdnimg.cn/ee0512f8cab94d47ae8a13fd6a062e92.png" style="zoom:50%"/>

接著build-->Build 'JNIUsage', 生成libJNIUsage.dylib檔案:

====================[ Build | JNIUsage | Debug ]================================
/Applications/CLion.app/Contents/bin/cmake/mac/bin/cmake --build /Users/flydean/data/git/cplus/JNIUsage/cmake-build-debug --target JNIUsage
[2/2] Linking CXX shared library libJNIUsage.dylib

Build finished

有了libJNIUsage.dylib,我們還需要將其加入JAVA專案中的path中:

<img src="https://img-blog.csdnimg.cn/f5b938e8a753482d86fc792e870b8fe7.png" style="zoom:50%"/>

選擇java-jni的module,在依賴中選擇JARs or Directories, 選擇剛剛的libJNIUsage.dylib 目錄。

儲存之後,就可以執行JAVA程式碼了,結果如下:

/Library/Java/JavaVirtualMachines/jdk-17.0.1.jdk/Contents/Home/bin/java -Djava.library.path=/Users/flydean/data/git/cplus/JNIUsage/cmake-build-debug -Dfile.encoding=UTF-8 -classpath /Users/flydean/data/git/learn-java-base-9-to-20/java-jni/target/classes: com.flydean.JNIUsage

this is www.flydean.com!

或者你可以在命令列中將libJNIUsage.dylib加入到java執行的classpath中即可。

總結

以上就是一個簡單的使用JAVA呼叫native方法的例子。大家可以看到,步驟還是挺複雜的,那麼有沒有其他更加簡單的方法,讓JAVA來呼叫native方法呢?有的,這就是JNA,我們會在後續的文章中深入進行介紹。

本文的程式碼可以參考https://github.com/ddean2009/learn-java-base-9-to-20.git

本文已收錄於 http://www.flydean.com/01-jni-overview/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章