JEP 389:外国链接器API(孵化器)

所有者毛里齐奥·西马达摩尔
类型功能
范围JDK公司
状态关闭 / 已交付
发布16
组件核样物质
讨论openjdk dot-java dot-net上的巴拿马破折号开发
努力L(左)
持续时间L(左)
与相关JEP 393:外国内存访问API(第三孵化器)
JEP 412:外部功能和内存API(孵化器)
审核人Brian Goetz、Jorn Vernee、Paul Sandoz
创建2020/07/20 11:19
已更新2022年3月2日17:08
问题8249755

总结

引入一个API,它提供对本机代码的静态类型的pure-Java访问。此API以及Foreign-Memory API(JEP 393号机组),将大大简化绑定到本机库的过程,否则容易出错。

历史

Foreign-Memory Access API是由JEP 370型并于2019年末以Java 14为目标孵化API,然后由刷新JEP 383型JEP 393号机组分别针对Java 15和16。Foreign-Memory Access API和Foreign Linker API共同构成了巴拿马计划.

目标

非目标

这不是一个目标:

动机

Java支持通过Java本机接口(JNI)自Java1.1以来,这条路一直很难走。用JNI包装本机函数需要开发多个工件:Java API、C头文件和C实现。即使有工具帮助,Java开发人员也必须跨多个工具链工作,以保持多个平台相关工件的同步。对于稳定的API来说,这已经够难的了,但是当尝试跟踪正在进行的API时,每次API发展时更新所有这些工件都是一个很大的维护负担。最后,JNI主要与代码有关,但代码总是交换数据,而JNI在访问本机数据方面几乎没有提供什么帮助。因此,开发人员经常求助于变通方法(例如直接缓冲区或周日。不安全的)这使得应用程序代码更难维护,甚至更不安全。

多年来,出现了许多框架来填补JNI留下的空白,包括JNA公司,JNR公司JavaCPP(JavaCPP).JNA和JNR从用户定义的接口声明动态生成包装器;JavaCPP生成由JNI方法声明上的注释静态驱动的包装器。虽然这些框架通常是对JNI体验的显著改进,但情况仍然不太理想,尤其是与提供一流本地互操作的语言相比。例如,Python的C型包可以动态包装本机函数,而无需任何粘合代码。其他语言,例如生锈,提供从C/C++头文件机械派生本机包装器的工具。

最终,Java开发人员应该能够(大部分)只需使用任何被认为对特定任务有用的本地库&我们已经看到了现状如何阻碍实现这一点。这个JEP通过引入一个高效且受支持的API(Foreign Linker API)来纠正这种不平衡,该API提供了外部函数支持,而不需要任何干预JNI粘合代码。它通过将外部函数公开为可以在纯Java代码中声明和调用的方法句柄来实现这一点。这大大简化了编写、构建和分发依赖于外国库的Java库和应用程序的任务。此外,Foreign Linker API和Foreign-Memory Access API为第三方本机互操作框架(无论是现在还是将来)提供了可靠的基础。

描述

在本节中,我们将深入探讨如何使用Foreign Linker API实现本机互操作。本节中描述的各种抽象将作为培养箱模块命名jdk.孵化器.国外,位于与现有外部内存访问API并排的同名包中。

符号查找

任何外部函数支持的第一个要素是在本地库中查找符号的机制。在传统的Java/JNI场景中,这是通过系统::loadLibrary系统::加载方法,它在内部映射为对的调用dlopen(dlopen)。Foreign Linker API通过库查找类(类似于method-handle查找),它提供了在给定的本机库中查找命名符号的功能。我们可以通过三种不同的方式获取库查找:

获取查找后,客户端可以使用查找(字符串)方法。此方法返回一个新的库查找。符号,它只是内存地址和名称的代理。

例如,以下代码查找clang_getClang版本由提供的功能叮当作响图书馆:

LibraryLookup libclang=LibraryLockup.ofLibrary(“clang”);库查找。Symbol clangVersion=libclang.lookup(“clang_getClangVersion”);

Foreign Linker API和JNI的库加载机制之间的一个关键区别是,加载的JNI库与类加载器相关联。此外,为了保存类装入器完整性,不能将同一JNI库加载到多个类加载器中。这里描述的外部函数机制更原始:foreign Linker API允许客户端直接以本机库为目标,而无需任何干预JNI代码。至关重要的是,Java对象永远不会通过Foreign Linker API与本机代码相互传递。因此,库通过加载库查找没有绑定到任何类加载器,可以根据需要多次(重新)加载。

C链接器

这个C链接器接口是API对外函数支持的基础。

接口CLinker{MethodHandle downcallHandle(LibraryLookup.Symbol函数,MethodType类型,FunctionDescriptor函数);MemorySegment upcallStub(MethodHandle目标,FunctionDescriptor函数);}

这种抽象起着双重作用。首先,针对叫停(例如,从Java调用本地代码)下降手柄方法可用于将本机函数建模为普通函数方法句柄物体。其次,对于向上调用(例如,从本机返回到Java代码的调用)upcall存根方法可用于转换现有方法句柄(可能指向某些Java方法)转换为内存段,然后可以将其作为函数指针传递给本机函数。请注意,虽然C链接器抽象主要关注于为C语言提供互操作支持,此抽象中的概念足够通用,将来可以应用于其他外语。

两者都有向下调用句柄upcall存根拿一个功能描述符实例,它是用于完整描述外部函数签名的内存布局的集合。这个C链接器接口定义了许多布局常量,每个常量对应一个主C基元类型。这些布局可以使用功能描述符描述C函数的签名。例如,我们可以使用字符*并返回一个长的使用以下描述符:

FunctionDescriptor函数=(CLinker.C_LONG,CLinker.C_POINTER)的FunctionDescriptor;

此示例中的布局映射到适用于基础平台的布局,因此这些布局依赖于平台:长(_LONG)例如,在Windows上是32位的值布局,而在Linux上是64位的值。要以特定平台为目标,可以使用特定的平台相关布局常量集(例如。,C链接器。Win64.C_LONG窗口).

中定义的布局C链接器类很方便,因为它们为我们想要使用的C类型建模。它们还包含,通过布局属性,外部链接器用来计算与给定函数描述符关联的调用序列的隐藏信息。例如,两种C类型整数浮动可能共享类似的内存布局(它们都是32位值),但通常使用不同的处理器寄存器传递。中附加到C特定布局的布局属性C链接器类确保以正确的方式处理参数和返回值。

两者都有向下调用句柄upcall存根也接受(直接或间接)方法类型实例。方法类型描述了客户端在与生成的向下调用句柄或向上调用存根交互时将使用的Java签名。中的参数和返回类型方法类型实例根据相应的布局进行验证。例如,链接器运行时检查与给定参数/返回值关联的Java载体的大小是否等于相应布局的大小。原始布局到Java载体的映射可能因平台而异(例如。,长(_LONG)映射到长的在Linux/x64上,但到整数在Windows上),但指针布局(指针(_POINTER))始终与内存地址载体和结构(其布局由组布局)始终与内存段承运人。

Downcalls公司

假设我们想调用标准C库中定义的以下函数:

size_t字符串(常量字符*s);

为此,我们必须:

下面是一个如何做到这一点的示例:

MethodHandle strlen=CLinker.getInstance().downcallHandle(LibraryLookup.ofDefault().lookup(“strlen”),MethodType.MethodType(long.class,MemoryAddress.class),(C_LONG,C_POINTER)的FunctionDescriptor);

这个斯特伦函数是标准C库的一部分,该库与VM一起加载,所以我们可以只使用默认查找来查找它。剩下的很简单。唯一棘手的细节是我们如何建模尺寸_t-通常,此类型具有指针大小,因此我们可以使用长(_LONG)在Linux上,但我们必须使用C_LONG_LONG(长_长)在Windows上。在Java端,我们对尺寸_t使用长的指针使用内存地址参数。

一旦我们获得了向下调用本机方法句柄,我们就可以将其用作任何其他方法句柄:

try(MemorySegment str=CLinker.toCString(“你好”)){long len=strlen.invokeExact(str.address());//5}

这里我们使用中的一个helper方法C链接器将Java字符串转换为包含无效的终止C字符串。然后将该段传递给方法句柄并将结果存储在Java中长的.

注意,所有这一切都是可能的,没有任何干预的本机代码——所有互操作代码都可以用(低级)Java表示。

向上呼叫

有时将Java代码作为函数指针传递给某个本机函数很有用。我们可以通过使用对upcall的外国链接器支持来实现这一点。为了证明这一点,请考虑标准C库中定义的以下函数:

void qsort(void*base,size_t nmemb,size-t size,int(*compar)(常量无效*,常量无效*);

这是一个可以使用自定义比较器函数对数组的内容进行排序的函数,比较,它作为函数指针传递。为了能够呼叫快速排序我们首先要为Java函数创建一个向下调用本机方法句柄:

MethodHandle qsort=CLinker.getInstance().downcallHandle(LibraryLookup.ofDefault().lookup(“qsort”),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第一个指针参数(数组指针)和最后一个参数(函数指针)。

这一次,为了调用快速排序降级处理,我们需要一个函数指针作为最后一个参数传递。这就是外部链接器抽象的向上调用支持非常有用的地方,因为它允许我们从现有的方法句柄创建函数指针。首先,我们编写了一个静态方法,可以比较作为指针传递的两个int元素:

类Qsort{静态int qsortCompare(内存地址addr1,内存地址addr 2){return MemoryAccess.getIntAtOffset(NativeRestricted()的内存段,addr1.toRawLongValue())-MemoryAccess.getIntAtOffset(NativeRestricted()的内存段,addr2.toRawLongValue());}}

然后我们创建一个指向上述比较器函数的方法句柄:

MethodHandle比较句柄=MethodHandles.lookup().findStatic(Qsort.class,“qsortCompare”,MethodType.MethodType(int.class,内存地址类,MemoryAddress.class));

现在我们有了Java比较器的方法句柄,我们可以创建一个函数指针。与向下调用一样,我们使用C链接器类别:

内存分段比较功能=CLinker.getInstance().upcallStub(comparHandle,(C_INT,指针(_POINTER),C_POINTER)););

我们终于有了一个内存段,比较函数,其基址指向可用于调用Java比较器函数的存根,因此现在我们已经具备了调用快速排序向下调用句柄:

try(MemorySegment数组=MemorySegram.allocateNative(4*10)){array.copyFrom(数组的MemorySegment.ofArray(新的int[]{0,9,3,4,6,5,1,8,2,7}));qsort.invokeExact(array.address(),10L,4L,comparFunc.address));int[]排序=数组.toIntArray();//[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]}

此代码创建一个非堆数组,将Java数组的内容复制到其中,然后将数组传递给快速排序处理我们从外部链接器获得的比较器函数。副作用是,调用后,堆外数组的内容将根据我们用Java编写的比较器函数进行排序。然后,我们从包含排序元素的段中提取一个新的Java数组。

这个高级示例展示了外部链接器抽象的全部功能,以及跨Java/本机边界的代码和数据的完全双向互操作。

选择

继续使用JNI或其他第三方本地互操作框架。

风险和假设

依赖关系