Linker
是 Java 平台的预览 API。
外部函数通常驻留在可以按需加载的库中。每个库都符合特定的ABI(应用程序二进制接口)。ABI是与构建库的编译器、操作系统和处理器相关联的一组调用约定和数据类型。例如,在Linux/x64上,C编译器通常构建符合SystemV ABI的库。
链接器详细了解特定ABI使用的调用约定和数据类型。对于符合该ABI的任何库,链接器可以在Java代码运行在JVM中和库中的外部函数之间进行调解。具体来说:
- 链接器允许Java代码通过downcall方法句柄链接到外部函数;和
- 链接器允许外部函数通过生成upcall存根调用Java方法句柄。
libc
和libm
。这些库中的函数通过符号查找公开。
调用本地函数
本地链接器可用于链接到C库(本地函数)中定义的函数。假设我们希望从Java向标准C库中定义的strlen
函数进行downcall:
size_t strlen(const char *s);
strlen
的downcall方法句柄如下:
Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").orElseThrow(),
FunctionDescriptor.of(JAVA_LONG, ADDRESS)
);
strlen
本地函数的地址。然后,将该地址与一个平台相关的函数签名描述一起传递给本地链接器的downcallHandle(MemorySegment, FunctionDescriptor, Option...)
方法。然后,获取的downcall方法句柄如下调用:
try (Arena arena = Arena.ofConfined()) {
MemorySegment str = arena.allocateUtf8String("Hello");
long len = (long) strlen.invokeExact(str); // 5
}
描述C签名
与本地链接器交互时,客户端必须提供C函数签名的平台相关描述。这个描述,一个函数描述符
预览,定义了与C函数的参数类型和返回类型(如果有)相关联的布局。
诸如bool
、int
之类的标量C类型被建模为适当载体的值布局预览。标量类型与其对应布局之间的映射取决于本地链接器实现的ABI。例如,C类型long
在Linux/x64上映射到布局常量ValueLayout.JAVA_LONG
预览,但在Windows/x64上映射到布局常量ValueLayout.JAVA_INT
预览。类似地,C类型size_t
在64位平台上映射到布局常量ValueLayout.JAVA_LONG
预览,但在32位平台上映射到布局常量ValueLayout.JAVA_INT
预览。
复合类型被建模为组布局预览。具体来说,C struct
类型映射到结构布局预览,而C union
类型映射到联合布局
预览。在定义结构或联合布局时,客户端必须注意C中相应复合类型定义的大小和对齐约束。例如,两个结构字段之间的填充必须显式建模,通过向生成的结构布局添加足够大小的填充布局预览成员。
最后,诸如int**
和int(*)(size_t*, size_t*)
之类的指针类型被建模为地址布局预览。当指针类型的空间边界在静态情况下已知时,地址布局可以与目标布局预览关联。例如,已知指向C int[2]
数组的指针可以建模为一个地址布局,其目标布局是一个元素计数为2的序列布局,其元素类型是ValueLayout.JAVA_INT
预览。
以下表格显示了一些Linux/x64中C类型如何建模的示例:
C类型 布局 Java类型 bool
ValueLayout.JAVA_BOOLEAN
预览boolean
char
ValueLayout.JAVA_BYTE
预览byte
short
ValueLayout.JAVA_SHORT
预览short
int
ValueLayout.JAVA_INT
预览int
long
ValueLayout.JAVA_LONG
预览long
long long
ValueLayout.JAVA_LONG
预览long
float
ValueLayout.JAVA_FLOAT
预览float
double
ValueLayout.JAVA_DOUBLE
预览double
size_t
ValueLayout.JAVA_LONG
预览long
char*
,int**
,struct Point*
ValueLayout.ADDRESS
预览MemorySegment
预览int (*ptr)[10]
ValueLayout.ADDRESS.withTargetLayout( MemoryLayout.sequenceLayout(10, ValueLayout.JAVA_INT) );MemorySegment
预览struct Point { int x; long y; };
MemoryLayout.structLayout( ValueLayout.JAVA_INT.withName("x"), MemoryLayout.paddingLayout(32), ValueLayout.JAVA_LONG.withName("y") );MemorySegment
预览union Choice { float a; int b; }
MemoryLayout.unionLayout( ValueLayout.JAVA_FLOAT.withName("a"), ValueLayout.JAVA_INT.withName("b") );MemorySegment
预览
所有本机链接器实现都在一组内存布局上操作。更正式地说,如果布局L
是本机链接器NL
支持的,则布局L
是:
L
是值布局V
,并且V.withoutName()
等于以下布局常量之一:ValueLayout.JAVA_BOOLEAN
预览,ValueLayout.JAVA_BYTE
预览,ValueLayout.JAVA_CHAR
预览,ValueLayout.JAVA_SHORT
预览,ValueLayout.JAVA_INT
预览,ValueLayout.JAVA_LONG
预览,ValueLayout.JAVA_FLOAT
预览,ValueLayout.JAVA_DOUBLE
预览L
是地址布局A
,并且A.withoutTargetLayout().withoutName()
等于ValueLayout.ADDRESS
预览L
是序列布局S
,并且满足以下所有条件:S
的对齐约束设置为其自然对齐,并且S.elementLayout()
是链接器NL
支持的布局。
L
是组布局G
,并且满足以下所有条件:G
的对齐约束设置为其自然对齐;G
的大小是其对齐约束的倍数;G.memberLayouts()
中的每个成员布局都是填充布局或链接器NL
支持的布局;G
不包含除了严格要求对齐其非填充布局元素或满足(2)之外的填充。
函数指针
有时,将Java代码作为函数指针传递给某些本机函数是有用的;这可以通过使用上调存根来实现。为了演示这一点,让我们考虑来自C标准库的以下函数:void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
qsort
函数可用于对数组内容进行排序,使用自定义比较器函数作为函数指针(compar
参数)。为了能够从Java调用qsort
函数,我们必须首先为其创建一个下调方法句柄,如下所示:
Linker linker = Linker.nativeLinker();
MethodHandle qsort = linker.downcallHandle(
linker.defaultLookup().find("qsort").orElseThrow(),
FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS)
);
ValueLayout.JAVA_LONG
预览来映射C类型size_t
类型,以及ValueLayout.ADDRESS
预览用于第一个指针参数(数组指针)和最后一个参数(函数指针)。
要调用上面获取的qsort
下调句柄,我们需要将函数指针作为最后一个参数传递。也就是说,我们需要从现有方法句柄创建函数指针。首先,让我们编写一个Java方法,可以比较作为指针传递的两个int元素(即作为内存段预览):
class Qsort {
static int qsortCompare(MemorySegment elem1, MemorySegment elem2) {
return Integer.compare(elem1.get(JAVA_INT, 0), elem2.get(JAVA_INT, 0));
}
}
FunctionDescriptor comparDesc = FunctionDescriptor.of(JAVA_INT,
ADDRESS.withTargetLayout(JAVA_INT),
ADDRESS.withTargetLayout(JAVA_INT));
MethodHandle comparHandle = MethodHandles.lookup()
.findStatic(Qsort.class, "qsortCompare",
comparDesc.toMethodType());
int[]
数组元素的指针,我们可以指定 ValueLayout.JAVA_INT
预览 作为两个参数的地址布局的目标布局。这将允许比较器方法访问要比较的数组元素的内容。然后,我们将该函数描述符 转换预览 为适当的 方法类型,然后使用它来查找比较器方法句柄。现在我们可以创建一个指向该方法的上调存根,并将其作为函数指针传递给 qsort
下调句柄,如下所示:
try (Arena arena = Arena.ofConfined()) {
MemorySegment comparFunc = linker.upcallStub(comparHandle, comparDesc, arena);
MemorySegment array = arena.allocateArray(JAVA_INT, 0, 9, 3, 4, 6, 5, 1, 8, 2, 7);
qsort.invokeExact(array, 10L, 4L, comparFunc);
int[] sorted = array.toArray(JAVA_INT); // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
}
qsort
方法句柄。调用后,堆外数组的内容将根据我们在Java中编写的比较器函数进行排序。然后我们从段中提取一个新的Java数组,其中包含排序后的元素。
返回指针的函数
在与本机函数交互时,这些函数通常会分配一块内存区域并返回该区域的指针。让我们考虑来自C标准库的以下函数:void *malloc(size_t size);
malloc
函数分配给定大小的内存区域,并返回指向该内存区域的指针,稍后使用C标准库中的另一个函数 free
对该区域进行释放。在本节中,我们将展示如何与这些本机函数交互,目的是提供一个 安全 的分配API(下面概述的方法当然可以推广到除了 malloc
和 free
之外的其他分配函数)。
首先,我们需要为 malloc
和 free
创建下调方法句柄,如下所示:
Linker linker = Linker.nativeLinker();
MethodHandle malloc = linker.downcallHandle(
linker.defaultLookup().find("malloc").orElseThrow(),
FunctionDescriptor.of(ADDRESS, JAVA_LONG)
);
MethodHandle free = linker.downcallHandle(
linker.defaultLookup().find("free").orElseThrow(),
FunctionDescriptor.ofVoid(ADDRESS)
);
malloc
)时,Java运行时对返回的指针的大小或生命周期一无所知。考虑以下代码:
MemorySegment segment = (MemorySegment)malloc.invokeExact(100);
malloc
下调方法句柄返回的段的大小为 零。此外,返回段的范围是一个始终存活的新范围。为了安全地访问该段,我们必须不安全地将段调整为所需的大小(在本例中为100)。还可以将段附加到某个现有 arena预览,以便自动管理支持该段的内存区域的生命周期,就像从Java代码直接创建的任何其他本机段一样。这两个操作都是使用受限方法 MemorySegment.reinterpret(long, Arena, Consumer)
预览 完成的,如下所示:
MemorySegment allocateMemory(long byteSize, Arena arena) throws Throwable {
MemorySegment segment = (MemorySegment) malloc.invokeExact(byteSize); // size = 0, scope = always alive
return segment.reinterpret(byteSize, arena, s -> {
try {
free.invokeExact(s);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}); // size = byteSize, scope = arena.scope()
}
allocateMemory
方法接受两个参数:大小和arena。该方法调用 malloc
下调方法句柄,并不安全地重新解释返回的段,给它一个新大小(传递给 allocateMemory
方法的大小)和一个新范围(提供的arena的范围)。该方法还指定了一个 清理操作,在提供的arena关闭时执行。不足为奇,清理操作将段传递给 free
下调方法句柄,以释放底层内存区域。我们可以如下使用 allocateMemory
方法:
try (Arena arena = Arena.ofConfined()) {
MemorySegment segment = allocateMemory(100, arena);
} // 'free' called here
allocateMemory
获取的段与受限arena管理的任何其他段一样。具体来说,获取的段具有所需的大小,只能由单个线程访问(创建受限arena的线程),其生命周期与周围的 try-with-resources 块相关联。
可变参数函数
可变参数函数是可以接受可变数量和类型参数的C函数。它们声明如下:- 在形式参数列表的末尾带有省略号(
...
),例如:void foo(int x, ...);
- 使用空形式参数列表,称为无原型函数,例如:
void foo();
...
或空形式参数列表替换为固定数量和类型的 可变参数 来 特化 为多个非可变参数函数的模板。
值得注意的是,在C中,作为可变参数传递的值会进行默认参数提升。例如,会应用以下参数提升:
_Bool
->unsigned int
[signed] char
->[signed] int
[signed] short
->[signed] int
float
->double
本地链接器仅支持链接可变参数函数的专门形式。可变参数函数在其专门形式下可以使用描述专门形式的函数描述符进行链接。此外,必须提供Linker.Option.firstVariadicArg(int)
预览链接器选项,以指示参数列表中的第一个可变参数。在专门函数描述符中,对应的参数布局(如果有的话)以及所有后续的参数布局被称为可变参数布局。对于无原型的函数,传递给Linker.Option.firstVariadicArg(int)
预览的索引应始终为0
。
本地链接器将拒绝尝试链接具有任何对应于将受默认参数提升影响的C类型的可变参数布局的专门函数描述符(如上所述)。将被拒绝的布局是平台特定的,但以Linux/x64为例:布局ValueLayout.JAVA_BOOLEAN
预览、ValueLayout.JAVA_BYTE
预览、ValueLayout.JAVA_CHAR
预览、ValueLayout.JAVA_SHORT
预览和ValueLayout.JAVA_FLOAT
预览将被拒绝。
一个众所周知的可变参数函数是C标准库中定义的printf
函数:
int printf(const char *format, ...);
printf("%d plus %d equals %d", 2, 2, 4);
(char*, int, int, int)
,因为格式字符串接受三个整数参数。然后,我们需要使用一个链接器选项预览来指定所提供函数描述符中第一个可变布局的位置(从0开始)。在这种情况下,由于第一个参数是格式字符串(非可变参数),因此第一个可变索引需要设置为1,如下所示:
Linker linker = Linker.nativeLinker();
MethodHandle printf = linker.downcallHandle(
linker.defaultLookup().find("printf").orElseThrow(),
FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT, JAVA_INT, JAVA_INT),
Linker.Option.firstVariadicArg(1) // 第一个整数是可变的
);
try (Arena arena = Arena.ofConfined()) {
int res = (int)printf.invokeExact(arena.allocateUtf8String("%d plus %d equals %d"), 2, 2, 4); //打印"2 plus 2 equals 4"
}
安全注意事项
创建下行调用方法句柄本质上是不安全的。外部库中的符号通常不包含足够的签名信息(例如外部函数参数的数量和类型)。因此,链接器运行时无法验证链接请求。当客户端与通过无效链接请求获得的下行调用方法句柄交互时(例如通过指定包含太多参数布局的函数描述符),此类交互的结果是未指定的,可能导致JVM崩溃。当将上行调用存根传递给外部函数时,如果外部代码将与上行调用存根关联的函数指针转换为与上行调用存根类型不兼容的类型,然后尝试通过生成的函数指针调用函数,则可能导致JVM崩溃。此外,如果与上行调用存根关联的方法句柄返回一个内存段预览,客户端必须确保此地址在上行调用完成后不会变为无效。这可能导致未指定的行为,甚至导致JVM崩溃,因为上行调用通常在下行调用方法句柄调用的上下文中执行。
- 实现要求:
- 此接口的实现是不可变的、线程安全的,并且是基于值的。
- 自 JDK 版本:
- 19
-
Nested Class Summary
Modifier and TypeInterfaceDescriptionstatic interface
预览。链接器选项用于为链接请求提供附加参数。 -
Method Summary
Modifier and TypeMethodDescription返回一组常用库中符号的符号查找。downcallHandle
(FunctionDescriptorPREVIEW function, Linker.OptionPREVIEW... options) 创建一个方法句柄,用于使用给定签名调用外部函数。downcallHandle
(MemorySegmentPREVIEW address, FunctionDescriptorPREVIEW function, Linker.OptionPREVIEW... options) 创建一个方法句柄,用于使用给定签名和地址调用外部函数。返回与底层本机平台关联的 ABI 的链接器。upcallStub
(MethodHandle target, FunctionDescriptorPREVIEW function, ArenaPREVIEW arena, Linker.OptionPREVIEW... options) 创建一个上调存根,可以将其作为函数指针传递给其他外部函数,并与给定的区域关联。
-
Method Details
-
nativeLinker
返回与底层本机平台关联的 ABI 的链接器。底层本机平台是 Java 运行时当前执行的操作系统和处理器的组合。- API 注释:
- 目前无法获取不同操作系统和处理器组合的链接器。
- 实现注释:
-
由返回的链接器关联的默认查找公开的库是在 Java 运行时当前执行的进程中加载的本机库。例如,在 Linux 上,这些库通常包括
libc
、libm
和libdl
。 - 返回:
- 与底层本机平台关联的 ABI 的链接器
- 抛出:
-
UnsupportedOperationException
- 如果不支持底层本机平台。
-
downcallHandle
MethodHandle downcallHandle(MemorySegmentPREVIEW address, FunctionDescriptorPREVIEW function, Linker.OptionPREVIEW... options) 创建一个方法句柄,用于使用给定签名和地址调用外部函数。调用此方法等效于以下代码:
linker.downcallHandle(function).bindTo(symbol);
此方法是受限制的。受限制的方法是不安全的,如果使用不正确,可能会导致 JVM 崩溃,甚至更糟的是导致内存损坏。因此,客户端应避免依赖受限制的方法,尽可能使用安全且受支持的功能。
- 参数:
-
address
- 目标外部函数的地址的本机内存段,其基地址预览是目标外部函数的地址。 -
function
- 目标外部函数的函数描述符。 -
options
- 与此链接请求相关联的链接器选项。 - 返回:
- 一个下调方法句柄。
- 抛出:
-
IllegalArgumentException
- 如果提供的函数描述符不受此链接器支持。 -
IllegalArgumentException
- 如果!address.isNative()
,或者address.equals(MemorySegment.NULL)
。 -
IllegalArgumentException
- 如果给定了无效的链接器选项组合。 -
IllegalCallerException
- 如果调用者所在的模块未启用本机访问。 - 参见:
-
downcallHandle
创建一个方法句柄,用于使用给定签名调用外部函数。返回的方法句柄关联的 Java 方法类型是从函数描述符中的参数和返回布局派生预览而来,但具有额外的类型为
MemorySegment
预览的前导参数,从中派生目标外部函数的地址。此外,如果函数描述符的返回布局是组布局,则生成的下调方法句柄接受额外的类型为SegmentAllocator
预览的前导参数,该参数由链接器运行时用于分配由下调方法句柄返回的结构关联的内存区域。在调用下调方法句柄时,链接器为任何类型为
MemorySegment
预览且对应布局为地址布局预览的参数A
提供以下保证:A.scope().isAlive() == true
。否则,调用将抛出IllegalStateException
;- 调用发生在线程
T
中,使得A.isAccessibleBy(T) == true
。否则,调用将抛出WrongThreadException
;以及 A
在调用期间保持活动。例如,如果使用共享区域预览获取了A
,则在下调方法句柄仍在执行时尝试关闭预览该区域将导致IllegalStateException
。
此外,如果提供的函数描述符的返回布局是地址布局预览,则调用返回的方法句柄将返回与始终处于活动状态的新作用域关联的本机段。在正常情况下,返回段的大小为
0
。但是,如果函数描述符的返回布局具有目标布局预览T
,则返回段的大小设置为T.byteSize()
。如果表示外部函数目标地址的
MemorySegment
预览是MemorySegment.NULL
预览地址,则返回的方法句柄将抛出IllegalArgumentException
。如果传递给它的任何参数为null
,返回的方法句柄还将抛出NullPointerException
。此方法是受限制的。受限制的方法是不安全的,如果使用不正确,可能会导致 JVM 崩溃,甚至更糟的是导致内存损坏。因此,客户端应避免依赖受限制的方法,尽可能使用安全且受支持的功能。
- 参数:
-
function
- 目标外部函数的函数描述符。 -
options
- 与此链接请求相关联的链接器选项。 - 返回:
- 一个下调方法句柄。
- 抛出:
-
IllegalArgumentException
- 如果提供的函数描述符不受此链接器支持。 -
IllegalArgumentException
- 如果给定了无效的链接器选项组合。 -
IllegalCallerException
- 如果调用者所在的模块未启用本机访问。
-
upcallStub
MemorySegmentPREVIEW upcallStub(MethodHandle target, FunctionDescriptorPREVIEW function, ArenaPREVIEW arena, Linker.OptionPREVIEW... options) 创建一个上调用存根,可以将其作为函数指针传递给其他外部函数,并与给定的区域关联。从外部代码调用这样的函数指针将导致执行提供的方法句柄。返回的内存段的地址指向新分配的上调用存根,并与提供的区域关联。因此,返回的上调用存根段的生存期由提供的区域控制。例如,如果提供的区域是一个受限区域,则当提供的受限区域被关闭时,返回的上调用存根段将被释放。
一个上调用存根参数,其对应的布局是一个地址布局,是与始终存活的新作用域关联的本机段。在正常情况下,此段参数的大小为0。但是,如果地址布局具有目标布局T,则段参数的大小设置为T.byteSize()。
目标方法句柄不应抛出任何异常。如果目标方法句柄确实抛出异常,JVM将会突然终止。为了避免这种情况,客户端应该将目标方法句柄中的代码放在try/catch块中以捕获任何意外异常。可以使用MethodHandles.catchException(MethodHandle, Class, MethodHandle)方法句柄组合器来实现这一点,并在相应的catch块中根据需要处理异常。
此方法是受限的。受限方法是不安全的,如果使用不当,可能会导致JVM崩溃,甚至更糟的是导致内存损坏。因此,客户端应该避免依赖受限方法,并在可能的情况下使用安全且受支持的功能。
- 参数:
-
target
- 目标方法句柄。 -
function
- 上调用存根函数描述符。 -
arena
- 与返回的上调用存根段关联的区域。 -
options
- 与此链接请求关联的链接器选项。 - 返回:
- 一个地址为上调用存根地址的零长度段。
- 抛出:
-
IllegalArgumentException
- 如果提供的函数描述符不受此链接器支持。 -
IllegalArgumentException
- 如果target
的类型与从function
派生的类型不兼容。 -
IllegalArgumentException
- 如果确定目标方法句柄可能会抛出异常。 -
IllegalStateException
- 如果arena.scope().isAlive() == false
-
WrongThreadException
- 如果arena
是受限区域,并且此方法是从除了区域所有者线程之外的线程T
调用的。 -
IllegalCallerException
- 如果调用者位于未启用本机访问的模块中。
-
defaultLookup
SymbolLookupPREVIEW defaultLookup()返回一组常用库中符号的符号查找。每个Linker负责选择在支持Linker的OS和处理器组合上广泛认可为有用的库。因此,符号查找公开的确切符号集是未指定的;它在一个Linker到另一个Linker之间变化。
- 实现注意:
-
强烈建议defaultLookup()的结果公开一组随时间稳定的符号。如果先前由符号查找公开的符号不再公开,则defaultLookup()的客户端可能会失败。
如果实现者为多个OS和处理器组合提供Linker实现,则强烈建议defaultLookup()的结果尽可能在所有OS和处理器组合上公开一致的符号集。
- 返回:
- 一组常用库中符号的符号查找。
-
Linker
。