關鍵詞:Java Java17
JNI 不安全還繁瑣,所以 Java 搞了一套新的 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 標準庫當中的函數,則應使用 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高級面試》,這門課涉及內容均非淺嘗輒止,除知識點講解外更注重培養高級工程師意識:
掃描二維碼即可進入課程啦!
