隨著 JDK 19 在未來幾周*內(nèi)發(fā)布,是時(shí)候討論巴拿馬(Panama)項(xiàng)目了,更具體地說,是新的外部函數(shù)和內(nèi)存 API,它簡化了 Java 和本機(jī)代碼之間的互操作性。
本文使用一個(gè)簡單的基于 Java 的“Hello World”應(yīng)用程序調(diào)用一些 C 本機(jī)代碼來介紹外部函數(shù)和內(nèi)存 API。
準(zhǔn)備
要使用 Foreign Function & Memory API 和示例代碼,請先下載 JDK 19(build 24 或更高版本)。
項(xiàng)目概述
巴拿馬項(xiàng)目旨在為 JVM 和用其他語言(如 C/C++)編寫的本機(jī)代碼之間搭建橋梁。包含以下 3 個(gè)部分:
外部函數(shù)和內(nèi)存 API:JEP 424
Jextract 工具
Vector API:JEP 338
外部函數(shù)和內(nèi)存 API 提供一些重要的抽象:
內(nèi)存段及其地址:一組 API 類,用于處理本機(jī)內(nèi)存和指向它的指針;
內(nèi)存布局和描述符:用于模擬外部類型(結(jié)構(gòu)、原語)和函數(shù)描述符的 API;
內(nèi)存會(huì)話:管理一個(gè)或多個(gè)內(nèi)存資源生命周期的抽象;
鏈接器和符號查找:一組用于執(zhí)行向下和向上調(diào)用的 API 類;
段分配器:一種用于在內(nèi)存會(huì)話中分配內(nèi)存段的 API。
Hello World 程序
對巴拿馬了解得越深,就越會(huì)發(fā)現(xiàn)擁有一個(gè)好的介紹是至關(guān)重要的,這樣就不會(huì)錯(cuò)過重要的概念、技術(shù)和方法。
本文將介紹鏈接器(Linker),并簡要介紹 SymbolLookup 方法和本機(jī)內(nèi)存管理 ( MemorySession )。上面描述的這三個(gè)主要組件是構(gòu)建塊,用于更深入地開發(fā)由 Java 和本機(jī)代碼組成的程序。
鏈接器
從技術(shù)角度來看,鏈接器是兩個(gè)二進(jìn)制接口之間的橋梁:JVM 和 C/C++ 本機(jī)代碼,也稱為 C ABI。
JDK 19 為所有流行的平臺(tái)提供了一組 C ABI 實(shí)現(xiàn):
publicstaticLinkergetSystemLinker(){ returnswitch(CABI.current()){ caseWin64->Windowsx64Linker.getInstance(); caseSysV->SysVx64Linker.getInstance(); caseLinuxAArch64->LinuxAArch64Linker.getInstance(); caseMacOsAArch64->MacOsAArch64Linker.getInstance(); }; }
在 JDK 術(shù)語中,鏈接器是特定于平臺(tái)的 C ABI 實(shí)現(xiàn)的一個(gè)實(shí)例。鏈接器提供一組方法來執(zhí)行向下調(diào)用和向上調(diào)用,其中:
downcall 是從高級子系統(tǒng)發(fā)起的事件。在我們的例子中是 JVM 到較低級別的子系統(tǒng),如操作系統(tǒng)內(nèi)核或者一些 Java 代碼調(diào)用一些本機(jī)代碼。稍后將通過外部函數(shù)和內(nèi)存 API 說明這一點(diǎn)。
upcall 例如一些本機(jī)代碼調(diào)用一些 Java 代碼。
雖然鏈接器就像電話一樣,想打電話給誰,只需撥入正確的電話號碼即可。符號查找方法就像通訊錄,只需提供要打電話的人正確的信息即可。
要執(zhí)行向下調(diào)用,需要提供調(diào)用的(本機(jī))函數(shù)的描述符、通過符號查找分配的本機(jī)地址,以及用于創(chuàng)建調(diào)用本機(jī)函數(shù)的方法句柄對應(yīng)的鏈接器。
從 Java 實(shí)現(xiàn)經(jīng)典的 C 風(fēng)格的 Hello World:
intprintf(constchar*__restrict,...)
Java 中的 C 語言風(fēng)格的“Hello World”
要編寫使用本機(jī) printf 函數(shù)的基于 Java 的“Hello World”應(yīng)用程序,我們需要:
1. 找到 native 函數(shù)的地址
首先,我們需要搜索 printf 函數(shù)的本機(jī)內(nèi)存地址:
Linkerlinker=Linker.nativeLinker(); SymbolLookuplinkerLookup=linker.defaultLookup(); SymbolLookupsystemLookup=SymbolLookup.loaderLookup(); SymbolLookupsymbolLookup=name-> systemLookup.lookup(name).or(()->linkerLookup.lookup(name)); OptionalprintfMemorySegment=symbolLookup.lookup("printf");
從技術(shù)上講,查找可能會(huì)失敗,因此需要提供適當(dāng)?shù)腻e(cuò)誤處理。
2. 構(gòu)建正在調(diào)用的函數(shù)的描述符
一旦知道了 C printf 所在的位置,就需要定義由結(jié)果類型和接受的參數(shù)組成的 printf 描述符。值得一提的是,像 printf 這樣的本機(jī)函數(shù)稱為可變參數(shù)函數(shù)。在 Java 中,接受可變參數(shù)集的方法稱為具有可變參數(shù)的方法。
為了簡化,我們可以為 printf 定義 FunctionDescriptor 的簡化版本:
FunctionDescriptorprintfDescriptor=FunctionDescriptor.of(JAVA_INT,ADDRESS);
注意 :從 Java 運(yùn)行時(shí)的角度來看,C 指針背后的值類型無關(guān)緊要,因?yàn)?C 指針的內(nèi)存布局不保存類型,而是平臺(tái)固定的 32/64 位值。
一個(gè)描述符定義了一個(gè)返回值類型為 int 的函數(shù),它的參數(shù)是一個(gè)指針。假設(shè)一個(gè)描述符幾乎對應(yīng)于它在 stdio.h 中的 C 定義,因?yàn)樗x了一個(gè)標(biāo)準(zhǔn)函數(shù),而 printf 是一個(gè)可變參數(shù)函數(shù)。
通過值布局(Value Layout)在 Java 中對 C 類型建模
在 Java 中,值布局用于對與基本數(shù)據(jù)類型的值關(guān)聯(lián)的內(nèi)存布局建模,例如整數(shù)類型(有符號或無符號)和浮點(diǎn)類型。JAVA_INT 和 ADDRESS 都是對應(yīng)的 C 類型的值布局。
JAVA_INT :
//ValueLayout.OfInt.class OfIntJAVA_INT=newOfInt(ByteOrder.nativeOrder()).withBitAlignment(32);
這是值布局的一個(gè)實(shí)例,它的載體是 int.class。通過這種布局,鏈接器被指示在 C int32和具有運(yùn)營商類 int.class 的相應(yīng) Java int 類型之間創(chuàng)建橋梁。
ADDRESS:
//ValueLayout.OfAddress.class OfAddressADDRESS=newOfAddress(ByteOrder.nativeOrder()) .withBitAlignment(ValueLayout.ADDRESS_SIZE_BITS);
ADDRESS 是一個(gè)值布局,其中對應(yīng)的 C 類型是一個(gè)指向變量的指針,載體是MemoryAddress.class。
3. 從函數(shù)的本機(jī)內(nèi)存地址構(gòu)建方法句柄
使用 C printf 本機(jī)地址及其函數(shù)描述符,我們現(xiàn)在可以為 C printf 創(chuàng)建一個(gè)方法句柄:
MethodHandleprintfMethodHandle=symbolLookup.lookup("printf").map( addr->linker.downcallHandle(addr,printfDescriptor) ).orElse(null);
上面的代碼創(chuàng)建了 C print 的可執(zhí)行引用,簡而言之:一個(gè)方法句柄,來自 printf 的本機(jī)內(nèi)存地址及其函數(shù)描述符。
注意 :方法句柄是對底層方法、構(gòu)造函數(shù)、字段或類似低級操作的類型化、可執(zhí)行引用,具有參數(shù)或返回值的可選轉(zhuǎn)換。
現(xiàn)在已經(jīng)解釋了必要的概念,我們可以擴(kuò)展 downcalls 和 upcalls 的定義:
downcall 是通過由本機(jī)函數(shù)地址及其 Java 版本的函數(shù)描述符形成的 MethodHandle調(diào)用本機(jī)函數(shù)。
upcall 是通過 MethodHandle 調(diào)用一些用 Java 編寫的代碼,該 MethodHandle 轉(zhuǎn)換為本機(jī)內(nèi)存段,然后可以將其作為函數(shù)指針傳遞給本機(jī)函數(shù)。
4. 分配本機(jī)內(nèi)存
我們需要以某種方式將 Java 對象綁定到本機(jī)內(nèi)存段,以確保 C printf 可以訪問它們。
C 中的內(nèi)存分配和釋放內(nèi)存都很痛苦,因?yàn)殚_發(fā)人員可能會(huì)忘記分配或釋放內(nèi)存,這會(huì)導(dǎo)致程序泄漏或因分段錯(cuò)誤而崩潰。
另一方面,Java 依靠垃圾收集器來分配和釋放內(nèi)存。但是巴拿馬的外部函數(shù)和內(nèi)存 API 是在堆外分配內(nèi)存,有助于分配堆外內(nèi)存,這是任何本機(jī)互操作的關(guān)鍵部分!
外部函數(shù)和內(nèi)存 API 允許開發(fā)人員分配和訪問內(nèi)存段、它們的地址以及位于堆上或堆外的連續(xù)內(nèi)存區(qū)域的形狀。所有分配的內(nèi)存段都綁定到特定的內(nèi)存會(huì)話 ( MemorySession )。內(nèi)存會(huì)話的實(shí)例提供一組 API 來分配本機(jī)內(nèi)存段??紤]一個(gè)內(nèi)存會(huì)話,就像一個(gè)統(tǒng)一的內(nèi)存分配工具,比如 C malloc。MemorySession 實(shí)現(xiàn)了 AutoClosable 接口,它使用 try-with-resources 結(jié)構(gòu)極大地簡化了取消分配。
外部函數(shù)和內(nèi)存 API 提供了不止一種分配內(nèi)存段的正確方法。一種可能的本機(jī)內(nèi)存分配方法是 SegmentAllocator,它類似于 MemorySession:
try(varmemorySession=MemorySession.openConfined()){ SegmentAllocatorallocator=SegmentAllocator.newNativeArena(memorySession); varcStringFromAllocator=allocator.allocateUtf8String("HelloWorld"+" "); varcStringFromSession=memorySession.allocateUtf8String("HelloWorld"+" "); }
簡單起見,這個(gè)“Hello World”應(yīng)用程序?qū)⑹褂?MemorySession 作為內(nèi)存段分配工具。
最后,要調(diào)用 C printf,我們需要使用 MemorySession 在內(nèi)存會(huì)話中分配 const char * 內(nèi)存段,并將其傳遞給 C printf 函數(shù):
MemorySegmentcString=memorySession.allocateUtf8String(str+" ");
使用分配的內(nèi)存段,我們可以調(diào)用函數(shù):
privatestaticintprintf(Stringstr,MemorySessionmemorySession)throwsThrowable{ Objects.requireNonNull(printfMethodHandle); varcString=memorySession.allocateUtf8String(str+" "); return(int)printfMethodHandle.invoke(cString); } publicstaticvoidmain(String[]args)throwsThrowable{ varstr="HelloWorld"; try(varmemorySession=MemorySession.openConfined()){ System.out.println(printf(str,memorySession)); } }
5. 小結(jié)
到目前為止,我們了解到內(nèi)存會(huì)話 ( MemorySession ) 或段分配器 ( SegmentAllocator ) 是執(zhí)行內(nèi)存分配的關(guān)鍵 API。應(yīng)使用 try-with-resources 聲明內(nèi)存會(huì)話以實(shí)現(xiàn)隱式內(nèi)存釋放。分配內(nèi)存段有多種選擇——通過段分配器或直接通過內(nèi)存會(huì)話。鏈接器、符號查找對象、值和內(nèi)存布局以及方法句柄都是靜態(tài)對象。
總結(jié)
本文概述了外部函數(shù)和內(nèi)存 API,并研究了如何從 Java 調(diào)用簡單的 C 函數(shù)。
好消息是開發(fā)人員可以依靠 jextract 工具來處理大部分外部函數(shù)和內(nèi)存機(jī)制。
使用外部函數(shù)和內(nèi)存 API 從 Java 調(diào)用本機(jī)代碼時(shí)需要解決幾個(gè)問題:
獲取本機(jī)庫及其對應(yīng)的頭文件。
在 Java 中構(gòu)建函數(shù)描述符 ( FunctionDescriptor )。
查找函數(shù)符號的本機(jī)內(nèi)存地址,并為其創(chuàng)建方法句柄。
創(chuàng)建一個(gè)相關(guān)的方法句柄并確認(rèn)它已經(jīng)正確創(chuàng)建(例如,如果本機(jī)庫不在系統(tǒng)路徑中,查找將失敗并且返回一個(gè)方法句柄將為空)。
決定應(yīng)用程序?qū)⑷绾畏峙鋬?nèi)存段:通過段分配器或內(nèi)存會(huì)話。確保內(nèi)存分配技術(shù)在應(yīng)用程序的整個(gè)代碼庫中保持一致。
代碼清單
packagecom.java_devrel.samples.panama.part_1; importjava.lang.foreign.*; importjava.lang.invoke.MethodHandle; importjava.util.Objects; importstaticjava.lang.foreign.ValueLayout.ADDRESS; importstaticjava.lang.foreign.ValueLayout.JAVA_INT; publicclassPrintfSimplified{ privatestaticfinalLinkerlinker=Linker.nativeLinker(); privatestaticfinalSymbolLookuplinkerLookup=linker.defaultLookup(); privatestaticfinalSymbolLookupsystemLookup=SymbolLookup.loaderLookup(); privatestaticfinalSymbolLookupsymbolLookup=name->systemLookup.lookup(name).or(()->linkerLookup.lookup(name)); privatestaticfinalFunctionDescriptorprintfDescriptor=FunctionDescriptor.of(JAVA_INT.withBitAlignment(32),ADDRESS.withBitAlignment(64)); privatestaticfinalMethodHandleprintfMethodHandle=symbolLookup.lookup("printf").map(addr->linker.downcallHandle(addr,printfDescriptor)).orElse(null); privatestaticintprintf(Stringstr,MemorySessionmemorySession)throwsThrowable{ Objects.requireNonNull(printfMethodHandle); varcString=memorySession.allocateUtf8String(str+" "); return(int)printfMethodHandle.invoke(cString); } publicstaticvoidmain(String[]args)throwsThrowable{ varstr="helloworld"; try(varmemorySession=MemorySession.openConfined()){ System.out.println(printf(str,memorySession)); } } }
審核編輯:劉清
-
JAVA
+關(guān)注
關(guān)注
19文章
2977瀏覽量
105227 -
JVM
+關(guān)注
關(guān)注
0文章
158瀏覽量
12273 -
C++語言
+關(guān)注
關(guān)注
0文章
147瀏覽量
7040 -
printf函數(shù)
+關(guān)注
關(guān)注
0文章
31瀏覽量
5928
原文標(biāo)題:巴拿馬項(xiàng)目:打通 JVM 與 Native 代碼
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
C語言函數(shù)調(diào)用過程中的內(nèi)存變化解析
C語言使用函數(shù)調(diào)用的知識點(diǎn)
如何查看及更改函數(shù)/函數(shù)塊的調(diào)用環(huán)境
![如何查看及更改<b class='flag-5'>函數(shù)</b>/<b class='flag-5'>函數(shù)</b>塊的<b class='flag-5'>調(diào)用</b>環(huán)境](https://file1.elecfans.com/web2/M00/AE/C0/wKgaomVWvV2ANCozAAAzJenX8j8177.png)
將外部代碼放入一個(gè)正確運(yùn)行地程序里,外部代碼里引用了一些外部API函數(shù),系統(tǒng)報(bào)錯(cuò)是什么原因?
CCS 編程接口 請問CCS有沒有給用戶一些可以調(diào)用的外部接口?
CodeViz--一款分析C/C++源代碼中函數(shù)調(diào)用關(guān)系的調(diào)用
python代碼示例之基于Python的日歷api調(diào)用代碼實(shí)例
![python<b class='flag-5'>代碼</b>示例之基于Python的日歷<b class='flag-5'>api</b><b class='flag-5'>調(diào)用</b><b class='flag-5'>代碼</b>實(shí)例](https://file.elecfans.com/web1/M00/63/17/o4YBAFuQy8-AO90pAAAei-DUxgU163.png)
在LabVIEW中使用外部代碼的詳細(xì)資料說明
![在LabVIEW中使用<b class='flag-5'>外部</b><b class='flag-5'>代碼</b>的詳細(xì)資料說明](https://file.elecfans.com/web1/M00/B1/30/pIYBAF3zLlWAJytvAABh3P_1z5s773.png)
評論