close

關鍵詞:Java Java17

JNI 不安全還繁瑣,所以 Java 搞了一套新的 API,結果把這事兒搞得更複雜了。。。

Java 17 更新(1):更快的 LTS 節奏
Java 17 更新(2):沒什麼存在感的 strictfp, 這回算是迴光返照了
Java 17 更新(3):隨機數生成器來了一波穩穩的增強
Java 17 更新(4):這波更新,居然利好 mac 用戶
Java 17 更新(5):歷史包袱有點兒大,JDK 也在刪代碼啦
Java 17 更新(6):制裁!我自己私有的 API 你們怎麼隨便一個人都想用?
Java 17 更新(7):模式匹配要支持 switch 啦
Java 17 更新(8):密封類終於轉正
Java 17 更新(9):Unsafe 不 safe,我們來一套 safe 的 API 訪問堆外內存

我們書接上回,接着聊 JEP 412: Foreign Function & Memory API (Incubator) 當中訪問外部函數的內容。

調用自定義 C 函數

新 API 加載 Native 庫的行為沒有發生變化,還是使用 System::loadLibrary 和 System::load 來實現。

相比之前,JNI 需要提前通過聲明 native 方法來實現與外部函數的綁定,新 API 則提供了直接在 Java 層通過函數符號來定位外部函數的能力:

System.loadLibrary("libsimple");SymbolLookuploaderLookup=SymbolLookup.loaderLookup();MemoryAddressgetCLangVersion=loaderLookup.lookup("GetCLangVersion").get();

對應的 C 函數如下:

intGetCLangVersion(){return__STDC_VERSION__;}

通過以上手段,我們直接獲得了外部函數的地址,接下來我們就可以使用它們來完成調用:

MethodHandlegetClangVersionHandle=CLinker.getInstance().downcallHandle(getCLangVersion,MethodType.methodType(int.class),FunctionDescriptor.of(C_INT));System.out.println(getClangVersionHandle.invoke());

運行程序的時候需要把編譯好的 Native 庫放到 java.library.path 指定的路徑下,例如我把編譯好的 libsimple.dll 放到了 lib/bin 目錄下,所以:

-Djava.library.path=./lib/bin

運行結果:

201112

可以看出來,我的 C 編譯器覺得自己的版本是 C11。

調用系統 C 函數

如果是加載 C 標準庫當中的函數,則應使用 CLinker::systemLookup,例如:

MemoryAddressstrlen=CLinker.systemLookup().lookup("strlen").get();MethodHandlestrlenHandle=CLinker.getInstance().downcallHandle(strlen,MethodType.methodType(int.class,MemoryAddress.class),FunctionDescriptor.of(C_INT,C_POINTER));varstring=CLinker.toCString("HelloWorld!!",ResourceScope.newImplicitScope());System.out.println(strlenHandle.invoke(string.address()));

程序輸出:

13結構體入參

對於比較複雜的場景,例如傳入結構體:

typedefstructPerson{longlongid;charname[10];intage;}Person;voidDumpPerson(Person*person){printf("Person%%%lld(id=%lld,name=%s,age=%d)\n",sizeof(Person),person->id,person->name,person->age);char*p=person;for(inti=0;i<sizeof(Person);++i){printf("%d,",*p++);}printf("\n");}

這種情況我們首先需要在 Java 當中構造一個 Person 實例,然後把它的地址傳給 DumpPerson,這個過程比較複雜,我們分步驟來介紹:

MemoryLayoutpersonLayout=MemoryLayout.structLayout(C_LONG_LONG.withName("id"),MemoryLayout.sequenceLayout(10,C_CHAR).withName("name"),MemoryLayout.paddingLayout(16),C_INT.withName("age"));

首先我們定義好內存布局,每一個成員我們可以指定一個名字,這樣在後面方便定位。注意,由於 Person 的 name 只占 10 個字節(我說我是故意的你信嗎),因此這裡還有內存對齊問題,根據實際情況設置對應大小的 paddingLayout。

接下來我們用這個布局來開闢堆外內存:

MemorySegmentperson=MemorySegment.allocateNative(personLayout,newImplicitScope());

下面就要初始化這個 Person 了:

VarHandleidHandle=personLayout.varHandle(long.class,MemoryLayout.PathElement.groupElement("id"));idHandle.set(person,1000000);varageHandle=personLayout.varHandle(int.class,MemoryLayout.PathElement.groupElement("age"));ageHandle.set(person,30);

使用 id 和 name 分別定位到對應的字段,並初始化它們,這兩個都比較簡單。

接下來我們看下如何初始化一個 char[]。

方法1,逐個寫入:

VarHandlenameHandle=personLayout.varHandle(byte.class,MemoryLayout.PathElement.groupElement("name"),MemoryLayout.PathElement.sequenceElement());

注意我們獲取 nameHandle 的方式,要先定位到 name 對應的布局,它實際上是個 sequenceLayout,所以要緊接着用 sequenceElement 來定位它。如果還有更深層次的嵌套,可以在 varHandle(...) 方法當中添加更多的參數來逐級定位。

byte[]bytes="bennyhuo".getBytes();for(inti=0;i<bytes.length;i++){nameHandle.set(person,i,bytes[i]);}nameHandle.set(person,bytes.length,(byte)0);

然後就是循環賦值,一個字符一個字符寫入,比較直接。不過,有個細節要注意,Java 的 char 是兩個字節,C 的 char 是一個字節,因此這裡要用 Java 的 byte 來寫入。

方法2,直接複製 C 字符串:

person.asSlice(personLayout.byteOffset(MemoryLayout.PathElement.groupElement("name"))).copyFrom(CLinker.toCString("bennyhuo",newImplicitScope()));

asSlice 可以通過內存偏移得到 name 這個字段的地址對應的 MemorySegment 對象,然後通過它的 copyFrom 把字符串直接全部複製過來。

兩種方法各有優缺點。

接下來就是函數調用了,與前面幾個例子基本一致:

MemoryAddressdumpPerson=loaderLookup.lookup("DumpPerson").get();MethodHandledumpPersonHandle=CLinker.getInstance().downcallHandle(dumpPerson,MethodType.methodType(void.class,MemoryAddress.class),FunctionDescriptor.ofVoid(C_POINTER));dumpPersonHandle.invoke(person.address());

結果:

Person%24(id=1000000,name=bennyhuo,age=30)64,66,15,0,0,0,0,0,98,101,110,110,121,104,117,111,0,0,0,0,30,0,0,0,

我們把內存的每一個字節都打印出來,在 Java 層也可以打印這個值,這樣方便我們調試:

for(byteb:person.toByteArray()){System.out.print(b+",");}System.out.println();

以上是單純的 Java 調用 C 函數的情形。

函數指針入參

很多時候我們需要在 C 代碼當中調用 Java 方法,JNI 的做法就是反射,但這樣會有些安全問題。新 API 也提供了類似的手段,允許我們把 Java 方法像函數指針那樣傳給 C 函數,讓 C 函數去調用。

下面我們給出一個非常簡單的例子,大家重點關注如何傳遞 Java 方法給 C 函數。

我們首先給出 C 函數的定義,它的功能實際上就是遍歷一個數組,調用傳入的函數 on_each。

typedefvoid(*OnEach)(intelement);voidForEach(intarray[],intlength,OnEachon_each){for(inti=0;i<length;++i){on_each(array[i]);}}

Java 層想要調用 ForEach 這個函數,最關鍵的地方就是構造 on_each 這個函數指針。接下來我們給出它的 Java 層的定義:

publicstaticvoidonEach(intelement){System.out.println("onEach:"+element);}

然後把 onEach 轉成函數指針,我們只需要通過 MethodHandles 來定位這個方法,得到一個 MethodHandle 實例:

MethodHandleonEachHandle=MethodHandles.lookup().findStatic(ForeignApis.class,"onEach",MethodType.methodType(void.class,int.class));

接着獲取這個函數的地址:

MemoryAddressonEachHandleAddress=CLinker.getInstance().upcallStub(onEachHandle,FunctionDescriptor.ofVoid(C_INT),newImplicitScope());

再調用 CLinker 的 upcallStub 來得到它的地址。

int[]originalArray=newint[]{1,2,3,4,5,6,7,8,9,10};MemorySegmentarray=MemorySegment.allocateNative(4*10,newImplicitScope());array.copyFrom(MemorySegment.ofArray(originalArray));MemoryAddressforEach=loaderLookup.lookup("ForEach").get();MethodHandleforEachHandle=CLinker.getInstance().downcallHandle(forEach,MethodType.methodType(void.class,MemoryAddress.class,int.class,MemoryAddress.class),FunctionDescriptor.ofVoid(C_POINTER,C_INT,C_POINTER));forEachHandle.invoke(array.address(),originalArray.length,onEachHandleAddress);

剩下的就是構造一個 int 數組,然後再調用 ForEach 這個 C 函數,這與前面調用其他 C 函數的方式是一致的。

運行結果顯而易見:

onEach:1onEach:2onEach:3onEach:4onEach:5onEach:6onEach:7onEach:8onEach:9onEach:10小結

這篇文章我們介紹了一下 Java 新提供的這套訪問外部函數的 API,相比之下它確實比過去有了更豐富的能力,不過用起來也並不輕鬆。將來即便正式發布,我個人覺得也需要一些工具來處理這些模板代碼的生成(例如基於註解處理器的代碼生成框架),以降低使用複雜度。

就目前的情況來講,其實我更願意用 JNI,不安全怎麼了,小心點兒不就行了嘛。算了,寫什麼垃圾 Java,直接寫 C++ 不香嗎?

C 語言是所有程序員應當認真掌握的基礎語言,不管你是 Java 還是 Python 開發者,歡迎大家關注我的新課 《C 語言系統精講》:

掃描二維碼即可進入課程啦!

Kotlin 協程對大多數初學者來講都是一個噩夢,即便是有經驗的開發者,對於協程的理解也仍然是懵懵懂懂。如果大家有同樣的問題,不妨閱讀一下我的新書《深入理解 Kotlin 協程》,徹底搞懂 Kotlin 協程最難的知識點:

如果大家想要快速上手 Kotlin 或者想要全面深入地學習 Kotlin 的相關知識,可以關注基於 Kotlin 1.3.50 的 《Kotlin 入門到精通》

掃描二維碼即可進入課程啦!

Android 工程師也可以關注下《破解Android高級面試》,這門課涉及內容均非淺嘗輒止,除知識點講解外更注重培養高級工程師意識:

掃描二維碼即可進入課程啦!

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()