总结
引入一个API,通过它,Java程序可以与Java运行时之外的代码和数据进行互操作。通过有效地调用外部函数(即JVM外部的代码),并通过安全地访问外部内存(即不由JVM管理的内存),API使Java程序能够调用本机库并处理本机数据,而不会出现JNI的脆弱性和危险性。
历史
本JEP中提出的API是两个正在酝酿中的API的演变:Foreign-Memory Access API和Foreign Linker API。Foreign-Memory Access API最初由提出JEP 370型并于2019年末以Java 14为目标孵化API; 它被重新吸收JEP 383型Java 15和JEP 393号机组Java 16。Foreign Linker API最初由提出第389页并于2020年末针对Java 16孵化API.
目标
-
易用性-替换Java本机接口(JNI公司)具有卓越的纯Java开发模型。
-
性能-提供与现有API(如JNI和sun.misc公司。不安全的
.
-
概述-提供在不同类型的外部内存(例如,本机内存、持久内存和托管堆内存)上操作的方法,并随着时间的推移,适应其他平台(例如,32位x86)和用C以外的语言编写的外部函数(例如,C++、Fortran)。
-
安全-默认情况下禁用不安全操作,仅允许在应用程序开发人员或最终用户显式操作后才允许这些操作。
非目标
这不是一个目标
- 在此API之上重新实现JNI,或者以任何方式更改JNI;
- 重新实现遗留Java API,例如
sun.misc公司。不安全的
,位于此API之上;
- 提供从本机代码头文件机械生成Java代码的工具;或
- 更改与本机库交互的Java应用程序的打包和部署方式(例如,通过多平台JAR文件)。
动机
Java平台始终为希望超越JVM并与其他平台交互的库和应用程序开发人员提供了丰富的基础。Java API以方便可靠的方式公开非Java资源,无论是访问远程数据(JDBC)、调用web服务(HTTP客户端)、服务远程客户端(NIO通道)还是与本地进程(Unix域套接字)通信。不幸的是,Java开发人员在访问一种重要的非Java资源(即与JVM在同一台机器上但在Java运行时之外的代码和数据)时仍然面临着巨大的障碍。
外部存储器
存储在Java运行时外部内存中的数据称为非堆存储数据。(堆Java对象所在的位置-堆上访问堆外数据对于流行的Java库(例如Tensorflow公司,点燃,Lucene公司、和Netty公司这主要是因为它可以避免与垃圾收集相关的成本和不可预测性。它还允许通过将文件映射到内存来序列化和反序列化数据结构,例如。,甲基丙烯酸甲酯
然而,Java平台目前还不能为访问堆外数据提供令人满意的解决方案。
-
这个字节缓冲区
美国石油学会允许创建直接的字节缓冲区是在堆外分配的,但其最大大小为2GB,并且不会立即释放。这些和其他限制源于以下事实:字节缓冲区
API不仅设计用于堆外内存访问,还设计用于字符集编码/解码和部分I/O操作等领域的批量数据的生产者/消费者交换。在这种情况下,不可能满足多年来提出的许多堆外增强请求(例如。,4496703,6558368,4837564、和5029431).
-
这个sun.misc公司。不安全的
美国石油学会公开堆内数据的内存访问操作,这些操作也适用于堆外数据。使用不安全的
是高效的,因为其内存访问操作被定义为HotSpot JVM内部函数,并由JIT编译器进行优化。但是,使用不安全的
是危险的,因为它允许访问任何内存位置。这意味着Java程序可以通过访问一个已经空闲的位置使JVM崩溃;出于这个和其他原因不安全的
一直都是强烈劝阻.
-
使用JNI调用本机库,然后访问堆外数据是可能的,但性能开销很少使其适用:从Java到本机比访问内存慢几个数量级,因为JNI方法调用无法从许多常见的JIT优化(如内联)中受益。
总之,在访问堆外数据时,Java开发人员面临着一个两难境地:他们是否应该选择一条安全但效率低下的路径(字节缓冲区
)或者他们应该放弃安全而追求性能(不安全的
)? 他们所需要的是一个受支持的API,用于访问堆外数据(即外部内存),该API从头开始就设计为安全的,并考虑到JIT优化。
国外职能
自Java1.1以来,JNI一直支持调用本机代码(即外部函数),但由于许多原因,它是不够的。
-
JNI涉及几个乏味的工件:Java API(本地的
方法),从Java API派生的C头文件,以及调用感兴趣的本地库的C实现。Java开发人员必须跨多个工具链工作,以保持与平台相关的工件同步,当本机库快速发展时,这尤其繁重。
-
JNI只能与用语言(通常是C和C++)编写的库进行互操作,这些语言使用为其构建JVM的操作系统和CPU的调用约定。A类本地的
方法不能用于调用使用不同约定的语言编写的函数。
-
JNI不协调Java类型系统和C类型系统。Java中的聚合数据是用对象表示的,但C中的聚集数据是用结构表示的,因此任何传递给本地的
方法必须由本机代码费力地解包。例如,考虑一个记录类人
在Java中:传递人
对象到本地的
方法将要求本机代码使用JNI的C API提取字段(例如。,名字
和姓氏
)来自对象。因此,Java开发人员有时会将数据扁平化为单个对象(例如,字节数组或直接字节缓冲区),但更常见的是,由于通过JNI传递Java对象的速度较慢,他们使用不安全的
用于分配堆外内存并将其地址传递给本地的
方法作为长的
-这使得Java代码非常不安全!
多年来,出现了许多框架来填补JNI留下的空白,包括JNA公司,JNR公司和JavaCPP(JavaCPP)虽然这些框架通常是对JNI的显著改进,但情况仍然不太理想,尤其是与提供一流本地互操作的语言相比。例如,Python的C型包可以在本地库中动态包装函数,而无需任何粘合代码。其他语言,例如生锈,提供从C/C++头文件机械派生本机包装器的工具。
最终,Java开发人员应该有一个受支持的API,让他们能够直接使用任何被认为对特定任务有用的本机库,而不必像JNI那样冗长乏味。一个很好的构建抽象是方法句柄在Java 7中引入,以支持JVM上的快速动态语言。通过方法句柄公开本机代码将从根本上简化编写、构建和分发依赖本机库的Java库的任务。此外,能够建模外部函数(即本机代码)和外部内存(即堆外数据)的API将为第三方本机互操作框架提供坚实的基础。
描述
外部函数和内存API(FFMAPI)定义类和接口,以便库和应用程序中的客户端代码可以
- 分配外部内存
(内存段
,内存地址
、和分段分配器
),
- 操作和访问结构化外部存储器
(内存布局
,内存句柄
、和内存访问
),
- 管理外国资源的生命周期(
资源范围
)、和
- 调用外部函数(
符号查找
和C链接器
).
FFM API驻留在jdk.孵化器.国外
的包jdk.孵化器.国外
模块。
例子
作为使用FFMAPI的一个简短示例,下面是获得C库函数的方法句柄的Java代码基数排序
然后使用它对Java数组中开始生命的四个字符串进行排序(省略了一些细节):
// 1. 在C库路径上查找外部函数MethodHandle radixSort=CLinker.getInstance().downcall句柄(CLinker.systemLookup().lookup(“基数排序”),…);// 2. 分配堆内存以存储四个字符串String[]javaStrings={“鼠标”,“猫”,“狗”,“汽车”};// 3. 分配堆外内存以存储四个指针MemorySegment offHeap=内存段.allocateNative(序列的内存布局(javaStrings.length,C链接器。C_POINTER),…);// 4. 将字符串从堆上复制到堆外for(int i=0;i<javaStrings.length;i++){//从堆外分配字符串,然后存储指向它的指针MemorySegment cString=CLinker.toCString(javaStrings[i],newImplicitScope());MemoryAccess.setAddressAtIndex(offHeap,i,cString.address());}// 5. 通过调用外部函数对堆外数据进行排序radidSort.invoke(offHeap.address(),javaStrings.length,MemoryAddress)。空,“\0”);// 6. 将(重新排序的)字符串从堆外复制到堆内for(int i=0;i<javaStrings.length;i++){MemoryAddress cStringPtr=MemoryAccess.getAddressAtIndex(offHeap,i);javaStrings[i]=CLinker.toJavaStringRestricted(cStringPtr);}assert Arrays.equals(javaStrings,new String[]{“car”,“cat”,”dog“,”mouse“});//真的
这段代码比任何使用JNI的解决方案都要清晰得多,因为隐式转换和内存取消引用是隐藏在后面的本地的
方法调用现在直接用Java表示。也可以使用现代Java习语;例如,流可以允许多个线程并行地在堆内和堆外内存之间复制数据。
内存段
A类存储器段是对位于堆外或堆上的连续内存区域进行建模的抽象。内存段可以是
- 本地段,在本机内存中从头开始分配(例如,通过
malloc公司
),
- 已映射段,环绕映射的本机内存区域(例如,via
甲基丙烯酸甲酯
),或
- 阵列或缓冲器段,分别包装在与现有Java数组或字节缓冲区关联的内存中。
所有内存段都提供了空间、时间和线程约束保证,这些保证都得到了严格执行,以确保内存去引用操作的安全。例如,以下代码在堆外分配100字节:
MemorySegment段=MemorySegrament.allocateNative(100,newImplicitScope());
这个空间界限确定与段关联的内存地址的范围。上述代码中段的边界由基址 b条
,表示为内存地址
实例,以及字节大小(100),导致从b条
到b条
+99(含)。
这个时间界限段的长度决定了段的生存期,即段将被释放的时间。段的生存期和线程限制状态由资源范围
抽象,已讨论在下面。上述代码中的资源范围是一个新的隐性的作用域,确保当内存段
垃圾收集器认为对象不可访问。隐式作用域还确保可以从多个线程访问内存段。
换句话说,上面的代码创建了一个段,其行为与字节缓冲区
分配给分配直接
工厂。FFM API还支持确定性内存释放和其他线程限制选项,如前所述在下面.
取消引用内存段
通过获取var句柄是Java9中引入的数据访问抽象。特别是,段被取消引用内存访问变量句柄。这种var句柄使用一对访问坐标:
- 坐标类型
内存段
-要取消引用其内存的段,以及
- 坐标类型
长的
-从段的基地址开始的偏移量,在该偏移量处发生解引用。
内存访问var句柄是通过内存句柄
类。例如,此代码获取一个memory-accessvar句柄,该句柄可以编写整数
值写入本机内存段,并使用它以连续偏移量写入25个四字节值:
MemorySegment段=MemorySegrament.allocateNative(100,newImplicitScope());VarHandle intHandle=内存句柄.VarHandle(int.class,ByteOrder.nativeOrder());对于(int i=0;i<25;i++){intHandle.set(段,/*偏移量*/i*4,/*写入值*/i);}
通过使用由内存句柄
类。通过这些,客户端可以重新排序给定内存访问变量句柄的坐标,删除一个或多个坐标,并插入新坐标。这允许创建内存访问变量句柄,该句柄将一个或多个逻辑索引接受到由平坦的堆外内存区域支持的多维数组中。
为了使FFM API更容易接近内存访问
类提供静态访问器来取消引用内存段,而无需构造memory-access-var句柄。例如,有一个访问器来设置整数
给定偏移量段中的值,允许将上面的代码简化为:
MemorySegment段=MemorySegrament.allocateNative(100,newImplicitScope());对于(int i=0;i<25;i++){MemoryAccess.setIntAtOffset(段,i*4,i);}
内存布局
为了减少对内存布局进行繁琐计算的需要(例如。,i*4个
在上述示例中)内存布局
可用于以更具声明性的方式描述内存段的内容。例如,可以以下列方式描述上述示例中本机内存段的所需布局:
数组布局中的序列布局=MemoryLayout.sequenceLayout(25,MemoryLayout.valueLayout(32,ByteOrder.nativeOrder());
这将创建一个序列存储器布局其中32位价值布局(描述单个32位值的布局)重复25次。给定内存布局,我们可以避免计算代码中的偏移量,并简化内存分配和内存访问变量句柄的创建:
MemorySegment段=MemorySegram.allocateNative(intArrayLayout,newImplicitScope());VarHandle索引元素句柄=intArrayLayout.varHandle(int.class,PathElement.sequenceElement());for(int i=0;i<intArrayLayout.elementCount().getAsLong();i++){indexedElementHandle.set(段,(长)i,i);}
这个intArray布局
对象通过创建布局路径,用于从复杂布局表达式中选择嵌套布局。这个intArray布局
对象还驱动本机内存段的分配,这基于从布局导出的大小和对齐信息。前面示例中的循环常数,25
已替换为序列布局的元素计数。
资源作用域
在前面的示例中看到的所有内存段都使用了非确定性释放:一旦内存段实例变得无法访问,与这些段关联的内存将由垃圾收集器释放。我们说这样的部分是隐式释放.
在某些情况下,客户端可能希望控制内存释放的发生时间。例如,假设使用内存段::映射
。客户端可能更喜欢在不再需要该段时释放(即取消映射)与该段关联的内存,而不是等待垃圾回收器这样做,因为等待可能会对应用程序的性能产生不利影响。
内存段支持确定性释放资源作用域。资源范围对与一个或多个相关联的生命周期进行建模资源,例如内存段。新创建的资源范围位于活着的状态,这意味着它管理的所有资源都可以安全访问。根据客户端的请求,资源范围可以是关闭,这意味着不再允许访问范围管理的资源。这个资源范围
类实现自动关闭
接口,以便资源范围与尝试使用资源声明:
try(ResourceScope scope=ResourceScope.newConfigedScope()){MemorySegments1=MemorySegrament.map((“someFile”)的路径,0,100000,MapMode(地图模式)。READ_WRITE,范围);MemorySegment s2=MemorySegrament.allocateNative(100,范围);...}//此处发布了两个段
此代码创建受限制的资源范围并将其用于创建两个段:映射段(第1页
)和本机段(s2秒
). 这两个段的生命周期与资源范围的生命周期有关,因此在try-with-resources语句完成后访问这些段(例如,使用memory-access-var句柄取消对它们的引用)将导致抛出运行时异常。
除了管理内存段的生存期外,资源作用域还可以控制哪些线程可以访问该段。受限资源范围限制对创建范围的线程的访问,而共享资源范围允许从任何线程进行访问。
资源范围,无论是受限的还是共享的,都可以与java.lang.ref.清理器
对象,该对象负责在资源范围对象在关闭
方法由客户端调用。
一些资源范围,称为隐性的资源作用域,不支持显式释放-调用关闭
将失败。隐式资源范围始终使用保洁员
。可以使用资源范围::newImplicitScope
工厂,如前面的示例所示。
段分配器
当客户端使用堆外内存时,内存分配通常会成为瓶颈。FFM API包括分段分配器
抽象,它定义了分配和初始化内存段的有用操作。段分配器是通过分段分配器
接口。例如,下面的代码创建了一个基于arena的分配器,并使用它分配一个内容从Java初始化的段整数
数组:
try(ResourceScope scope=ResourceScope.newConfigedScope()){SegmentAllocator分配器=SegmentAllocator.arenaAllocator(scope);对于(int i=0;i<100;i++){MemorySegments=分配器.allocateArray(C_INT,new INT[]{1,2,3,4,5});...}...}//此处释放分配的所有内存
此代码创建一个受限的资源范围,然后创建一个无界竞技场分配器与该范围关联。这个分配器将分配特定大小的内存块,并通过返回预先分配的内存块的不同片来响应分配请求。如果板没有足够的空间来容纳新的分配请求,则会分配新的板。如果与竞技场分配器相关联的资源作用域被关闭,则与分配器创建的段相关联的所有内存(即,在对于
循环)以原子方式释放。此习惯用法结合了确定性释放的优点,由资源范围
抽象,具有更灵活和可扩展的分配方案。它在编写管理大量堆外段的代码时非常有用。
不安全的内存段
到目前为止,我们已经看到了内存段、内存地址和内存布局。取消引用操作只能在内存段上进行。由于内存段具有空间和时间边界,Java运行时始终可以确保与给定段关联的内存被安全地解除引用。然而,在某些情况下,客户端可能只有内存地址
实例,这是与本机代码交互时经常出现的情况。由于Java运行时无法知道与内存地址关联的空间和时间边界,因此FFM API禁止直接取消引用内存地址。
要取消引用内存地址,客户端有两个选项。
-
如果已知地址位于内存段中,则客户端可以执行重新设置基址操作通过内存地址::段偏移量
。重定基操作重新解释地址相对于段基址的偏移量,以生成一个新的偏移量。该偏移量可以应用于现有段,然后可以安全地取消引用。
-
或者,如果不存在这样的段,则客户端可以创建一个不安全的,使用内存地址::as段
工厂。此工厂有效地将新的空间和时间边界附加到原始内存地址,以便允许取消引用操作。此工厂返回的内存段为不安全的:原始内存地址可能与10字节长的内存区域相关联,但客户端可能意外高估了该区域的大小,并创建了100字节长的不安全内存段。这可能导致稍后尝试取消引用与不安全段关联的内存区域边界之外的内存,这可能会导致JVM崩溃,或者更糟糕的是,导致静默内存损坏。因此,创建不安全段被视为限制性操作,默认情况下已禁用(请参阅更多信息在下面).
查找外部函数
对外部函数的任何支持的第一个要素是加载本机库的机制。使用JNI,可以使用系统::loadLibrary
和系统::加载
方法,它在内部映射为对的调用dlopen(dlopen)
或其等效物。使用这些方法加载的库总是与类加载器(即调用系统
方法)。库和类装入器之间的关联至关重要,因为它控制生命周期加载的库的:只有当类加载器不再可访问时,才能卸载其所有库安全地.
FFM API没有提供加载本机库的新方法。开发人员使用系统::loadLibrary
和系统::加载
方法加载将通过FFM API调用的本机库。库和类加载器之间的关联得到了保留,因此库将以与JNI相同的可预测方式卸载。
与JNI不同,FFMAPI提供了在加载的库中查找给定符号地址的功能。此功能由符号查找
对象,对于将Java代码链接到外部函数至关重要(请参见在下面). 有两种方法可以获得符号查找
对象:
符号查找::loaderLookup
返回一个符号查找,它可以查看当前类加载器加载的所有库中的所有符号。
CLinker::systemLookup(链接器::系统查找)
返回特定于平台的符号查找,该查找可以查看标准C库中的符号。
给定符号查找,客户端可以使用符号查找::查找(字符串)
方法。如果命名函数出现在符号查找所看到的符号中,则该方法返回一个内存地址
它指向函数的入口点。例如,以下代码加载OpenGL库(使其与当前类加载器相关联)并查找其地址glGetString(全局获取字符串)
功能:
System.loadLibrary(“GL”);SymbolLookup loaderLookup=SymbolLookup.loaderLookup();MemoryAddress clangVersion=loaderLookup.lookup(“glGetString”).get();
将Java代码链接到外部函数
这个C链接器
接口是Java代码与本机代码互操作的核心。而C链接器
专注于提供Java和C库之间的互操作,接口中的概念非常通用,足以支持未来的其他非Java语言。该接口支持叫停(从Java代码到本机代码的调用)和向上调用(从本机代码调用回Java代码)。
接口CLinker{MethodHandle downcallHandle(MemoryAddress函数,MethodType类型,FunctionDescriptor函数);MemoryAddress upcallStub(MethodHandle目标,FunctionDescriptor函数,ResourceScope范围);}
对于叫停向下调用句柄
方法获取外部函数的地址-通常是内存地址
从库查找中获取-并将外部函数公开为downcall方法句柄稍后,Java代码通过调用其调用精确
方法,并运行外部函数。传递给方法句柄的任何参数invokeExact(调用精确)
方法传递给外部函数。
对于upcallsupcall存根
该方法接受一个方法句柄(通常指的是Java方法,而不是downcall方法句柄),并将其转换为内存地址。稍后,当Java代码调用downcall方法句柄时,内存地址作为参数传递。实际上,内存地址充当函数指针。(有关向上调用的更多信息,请参阅在下面.)
假设我们希望从Java向下调用斯特伦
标准C库中定义的函数:
size_t字符串(常量字符*s);
公开的downcall方法句柄斯特伦
可以通过以下方式获得(详细信息方法类型
和功能描述符
稍后将进行介绍):
MethodHandle strlen=CLinker.getInstance().downcall句柄(CLinker.systemLookup().lookup(“strlen”).get(),MethodType.MethodType(long.class,MemoryAddress.class),(C_LONG,C_POINTER)的FunctionDescriptor);
调用downcall方法句柄将运行斯特伦
并使其结果在Java中可用。对于参数斯特伦
,我们使用helper方法将Java字符串转换为非堆内存段,并传递该段的地址:
MemorySegment str=CLinker.toCString(“Hello”,newImplicitScope());long len=strlen.invokeExact(str.address());//5
方法句柄可以很好地用于公开外部函数,因为JVM已经优化了方法句柄的调用,一直到本机代码。当方法句柄引用班
文件,调用方法句柄通常会导致目标方法被JIT编译;随后,JVM解释调用的Java字节码MethodHandle::invokeExact
通过将控制权转移到为目标方法生成的汇编代码。因此,调用传统方法句柄已经是准外部调用;以C库中的函数为目标的向下调用方法句柄只是一种更加国际化的方法句柄。方法句柄还具有一个名为特征多态性它允许使用原始参数进行无框调用。总之,方法句柄使C链接器
以自然、高效和可扩展的方式公开外部函数。
用Java描述C类型
为了创建向下调用方法句柄,FFM API要求客户端提供目标C函数的双面视图:使用不透明的Java对象(内存地址
,内存段
)和使用的低级签名透明的Java对象(内存布局
). 依次获取每个签名:
-
高级签名,a方法类型
,用作向下调用方法句柄的类型。每个方法句柄都是强类型的,这意味着它对可以传递给它的参数的数量和类型都很严格invokeExact(调用精确)
方法。例如,创建一个方法句柄以获取内存地址
无法通过调用参数invokeExact(<MemoryAddress>,<MemoryAddress>)
或通过invokeExact(“你好”)
因此方法类型
描述了客户端在调用downcall方法句柄时必须使用的Java签名。它实际上是C函数的Java视图。
-
低级签名功能描述符
,包括内存布局
物体。这使得C链接器
对C函数参数的精确理解,以便能够正确地排列它们,如下所述。客户通常有内存布局
对象,以便取消引用外部内存中的数据,并且此类对象可以在此处作为外部函数签名重用。
例如,获得C函数的downchall方法句柄整数
并返回一个长的
将需要以下内容方法类型
和功能描述符
参数到向下调用句柄
:
MethodType mtype=MethodType.MethodType(long.class,int.class);FunctionDescriptor fdesc=(C_LONG,C_INT)的函数描述符;
(此示例针对Linux/x64和macOS/x64,其中Java类型长的
和整数
与预定义的C链接器
布局长(_LONG)
和C_INT(_IN)
分别是。Java类型与内存布局的关联因平台而异;例如,在Windows/x64上,Java长的
与关联C_LONG_LONG(长_长)
布局。)
作为另一个示例,为空隙
接受指针的C函数需要以下内容方法类型
和功能描述符
:
MethodType mtype=MethodType.MethodType(void.class,MemoryAddress.class);FunctionDescriptor fdesc=无效函数描述符(C_POINTER);
(C中的所有指针类型表示为内存地址
Java中的对象;相应的布局(其大小取决于当前平台)为指针(_POINTER)
客户不区分,例如。,整数*
和字符**
,因为传递给C链接器
共同包含足够的信息,以便将Java参数正确传递给C函数。)
最后,与JNI不同的是C链接器
支持将结构化数据传递给外部函数。获取的downcall方法句柄空隙
接受结构的C函数需要以下内容方法类型
和功能描述符
:
MethodType mtype=MethodType.MethodType(void.class,MemorySegment.class);MemoryLayout SYSTEMTIME=结构的内存布局(C_SHORT.withName(“wYear”),C_SHORT withName(”wMonth“),C_SHORT.withName(“wDayOfWeek”),C_SHORT withName(”wDay“),C_SHORT.withName(“wHour”),C_SHORT withName((“wMinute”)),C_SHORT.withName(“wSecond”),C_SHORT withName((“wMilliseconds”));FunctionDescriptor fdesc=无效的功能描述符(SYSTEMTIME);
(对于高层方法类型
签名,Java客户端始终使用不透明类型内存段
其中C函数需要通过值传递的结构。对于低层功能描述符
签名,与C结构类型关联的内存布局必须是一个复合布局,该布局定义了C结构中所有字段的子布局,包括可能由本机编译器插入的填充。)
如果C函数返回由低级签名表示的by-value结构,那么必须在堆外分配一个新内存段并将其返回给Java客户端。为了实现这一点向下调用句柄
需要额外的分段分配器
参数,FFM API使用该参数分配内存段以保存C函数返回的结构。
打包C函数的Java参数
不同语言之间的交互需要呼叫约定指定一种语言中的代码如何调用另一种语言的函数,如何传递参数,以及如何接收任何结果。这个C链接器
该实现了解几种现成的调用约定:Linux/x64、Linux/AArch64、macOS/x64和Windows/x64。它是用Java编写的,比JNI更容易维护和扩展,JNI的调用约定被硬连接到HotSpot的C++代码中。
考虑上面显示的SYSTEMTIME(系统时间)
结构和布局。给定JVM运行的操作系统和CPU的调用约定C链接器
使用函数描述符推断当使用内存段
参数。对于一个呼叫约定C链接器
可以安排分解传入内存段,使用通用CPU寄存器传递前四个字段,并传递C堆栈上的其余字段。对于不同的调用约定C链接器
可以安排FFM API通过分配内存区域间接传递结构,将传入内存段的内容批量复制到该区域,并将指向该内存区域的指针传递给C函数。这种最低级别的参数打包是在幕后进行的,不需要客户机代码的监督。
向上呼叫
有时将Java代码作为函数指针传递给某个外部函数很有用。我们可以通过使用C链接器
支持upcalls。在本节中,我们逐一构建了一个更复杂的示例,该示例演示了C链接器
,跨Java/本机边界实现代码和数据的完全双向互操作。
考虑标准C库中定义的以下函数:
void qsort(void*base,size_t nmemb,size-t size,int(*compar)(const void*,const void*));
拨打电话快速排序
在Java中,我们首先需要创建一个downcall方法句柄:
MethodHandle qsort=CLinker.getInstance().downcallHandle(CLinker.systemLookup().lookup(“qsort”).get(),MethodType.MethodType(void.class、MemoryAddress.class和long.class,long.class,MemoryAddress.class),Void的函数描述符(C_POINTER、C_LONG、C_LON、C_POINTER));
和以前一样,我们使用长(_LONG)
和长类
映射C尺寸_t
类型,我们使用内存地址.class
第一个指针参数(数组指针)和最后一个参数(函数指针)。
快速排序
使用自定义比较器功能对数组的内容进行排序,比较
,作为函数指针传递。因此,要调用下调用方法句柄,我们需要一个函数指针作为方法句柄的最后一个参数传递调用精确
方法。CLinker::upcallStub
帮助我们使用现有的方法句柄创建函数指针,如下所示。
首先,我们写一个静止的
Java中比较两个长的
值,间接表示为内存地址
物体:
类Qsort{静态int qsortCompare(内存地址addr1,内存地址addr 2){return MemoryAccess.getIntAtOffset(MemorySegment.globalNativeSegment(),将1.toRawLongValue())-MemoryAccess.getIntAtOffset(MemorySegment.globalNativeSegment(),addr2.toRawLongValue());}}
其次,我们创建了一个指向Java comparator方法的方法句柄:
MethodHandle比较句柄=MethodHandles.lookup().findStatic(Qsort.class,“qsortCompare”,MethodType.MethodType(int.class,内存地址类,MemoryAddress.class));
第三,现在我们有了Java比较器的方法句柄,可以使用CLinker::upcallStub
。与向下调用一样,我们使用中的布局描述函数指针的签名C链接器
类别:
内存地址比较功能=CLinker.getInstance().upcallStub(comparHandle,(C_INT,指针(_POINTER),C_POINTER),newImplicitScope()););
我们终于有了一个内存地址,比较函数
,它指向一个可用于调用Java比较器函数的存根,因此现在我们已经具备了调用快速排序
下拉手柄:
内存段数组=MemorySegment.allocateNative(4*10,newImplicitScope());array.copyFrom(数组的MemorySegment.ofArray(新的int[]{0,9,3,4,6,5,1,8,2,7}));qsort.invokeExact(array.address(),10L,4L,comparFunc);int[]排序=数组.toIntArray();//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
此代码创建一个非堆数组,将Java数组的内容复制到其中,然后将数组传递给快速排序
处理从C链接器
。调用后,将根据用Java编写的比较器函数对堆外数组的内容进行排序。然后,我们从包含排序元素的段中提取一个新的Java数组。
安全
从根本上讲,Java代码和本机代码之间的任何交互都可能损害Java平台的完整性。链接到预编译库中的C函数本质上是不可靠的,因为Java运行时无法保证函数的签名与Java代码的预期相符,甚至无法保证C库中的符号实际上是一个函数。此外,如果链接了合适的函数,则实际调用该函数可能会导致低级失败,例如分段错误,最终导致VM崩溃。Java运行时无法防止此类故障,Java代码也无法捕获此类故障。
使用JNI函数的本机代码尤其危险。这样的代码可以在不使用命令行标志(例如。,--附加开关
),通过使用以下函数获取静态字段
和调用虚拟方法
。它还可以更改最终的
字段初始化后很长时间。允许本机代码绕过应用于Java代码的检查会破坏JDK中的每个边界和假设。换句话说,JNI本质上是不安全的。
无法禁用JNI,因此无法确保Java代码不会调用使用危险JNI函数的本机代码。这是对平台完整性的风险,应用程序开发人员和最终用户几乎看不到这一风险,因为这些功能的99%使用通常来自夹在应用程序和JDK之间的第三方、第四方和第五方库。
大多数FFM API在设计上是安全的。过去许多需要使用JNI和本机代码的场景都可以通过调用FFM API中的方法来完成,这不会影响Java平台。例如,JNI的一个主要用例,即灵活的内存分配,通过一个简单的方法得到支持,内存段::allocateNative
,它不涉及本机代码,始终返回由Java运行时管理的内存。一般来说,使用FFM API的Java代码不会使JVM崩溃。
然而,FFM API的一部分本质上是不安全的。与交互时C链接器
,Java代码可以通过指定与底层C函数不兼容的参数类型来请求downcall方法句柄。在Java中调用downchall方法句柄将导致与调用本地的
JNI中的方法。FFM API还可能产生不安全的段,即其空间和时间边界由用户提供,无法由Java运行时验证的内存段(请参阅内存地址::as段
).
FFM API中的不安全方法不会带来与JNI功能相同的风险;例如,他们不能更改最终的
Java对象中的字段。另一方面,FFMAPI中的不安全方法很容易从Java代码调用。因此,在FFM API中使用不安全的方法是受限制的:默认情况下禁用对不安全方法的访问,因此调用此类方法会引发非法访问例外
。要为某些模块M中的代码启用对不安全方法的访问,请指定java--enable-native-access=M
在命令行上。(在逗号分隔的列表中指定多个模块;指定全部未修订
以启用类路径上所有代码的访问。)FFMAPI的大多数方法都是安全的,Java代码可以使用这些方法,无论--启用主动访问
给出了。
我们在此不建议限制JNI的任何方面。仍然可以拨打本地的
Java中的方法,以及本地代码调用不安全的JNI函数。然而,我们可能会在未来的版本中以某种方式限制JNI。例如,不安全的JNI函数,例如新DirectByte缓冲区
默认情况下可以禁用,就像FFM API中的不安全方法一样。更广泛地说,JNI机制是如此危险,以至于我们希望库将更喜欢pure-Java FFM API来进行安全和不安全的操作,以便我们能够及时禁用所有JNI。这与更广泛的Java路线图一致,即让平台安全、现成,要求最终用户选择不安全的活动,例如破坏强封装或链接到未知代码。
我们不建议在这里改变sun.misc公司。不安全的
无论如何。FFMAPI对堆外内存的支持是对包装器的一个很好的替代malloc公司
和自由的
在里面sun.misc公司。不安全的
,即分配内存
,设置内存
,复制内存
、和空闲内存
。我们希望需要堆外存储的库和应用程序采用FFM API,以便我们能够及时弃用并最终删除这些API周日。不安全的
方法。
选择
继续使用java.nio。字节缓冲区
,周日。不安全的
、JNI和其他第三方框架。
风险和假设
创建一个API以安全高效的方式访问外部内存是一项艰巨的任务。由于前面章节中描述的空间和时间检查需要在每次访问时执行,因此JIT编译器能够通过将这些检查提升到热循环之外来优化这些检查是至关重要的。JIT实现可能需要一些工作来确保API的使用与现有API的使用一样高效和可优化,例如字节缓冲区
和不安全的
。JIT实现还需要进行工作,以确保从API检索的本机方法句柄的使用至少与现有JNI本机方法的使用一样高效和可优化。
依赖关系