Module java.base

Interface Linker


public sealed interface Linker
Linker 是 Java 平台的预览 API。
仅当启用预览功能时,程序才能使用 Linker
预览功能可能会在将来的版本中被移除,或升级为 Java 平台的永久功能。
链接器提供了从Java代码访问外部函数以及从外部函数访问Java代码的功能。

外部函数通常驻留在可以按需加载的库中。每个库都符合特定的ABI(应用程序二进制接口)。ABI是与构建库的编译器、操作系统和处理器相关联的一组调用约定和数据类型。例如,在Linux/x64上,C编译器通常构建符合SystemV ABI的库。

链接器详细了解特定ABI使用的调用约定和数据类型。对于符合该ABI的任何库,链接器可以在Java代码运行在JVM中和库中的外部函数之间进行调解。具体来说:

此外,链接器提供了一种查找符合ABI的库中的外部函数的方法。每个链接器选择一组在与ABI相关联的操作系统和处理器组合上常用的库。例如,Linux/x64的链接器可能选择两个库:libclibm。这些库中的函数通过符号查找公开。

调用本地函数

本地链接器可用于链接到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)
);
请注意,本地链接器还通过其默认查找提供访问,以访问由Java运行时加载的C库中定义的本地函数。在上面的示例中,使用默认查找来搜索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函数的参数类型和返回类型(如果有)相关联的布局。

诸如boolint之类的标量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类型
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是:

本机链接器仅支持其参数/返回布局为该链接器支持的布局且不是序列布局的函数描述符。

函数指针

有时,将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());
首先,我们为函数指针类型创建一个函数描述符。由于我们知道传递给比较器方法的参数将是指向C 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 ]
}
这段代码创建了一个堆外数组,将Java数组的内容复制到其中,然后将数组与从本机链接器获取的比较器函数一起传递给 qsort 方法句柄。调用后,堆外数组的内容将根据我们在Java中编写的比较器函数进行排序。然后我们从段中提取一个新的Java数组,其中包含排序后的元素。

返回指针的函数

在与本机函数交互时,这些函数通常会分配一块内存区域并返回该区域的指针。让我们考虑来自C标准库的以下函数:
void *malloc(size_t size);
malloc 函数分配给定大小的内存区域,并返回指向该内存区域的指针,稍后使用C标准库中的另一个函数 free 对该区域进行释放。在本节中,我们将展示如何与这些本机函数交互,目的是提供一个 安全 的分配API(下面概述的方法当然可以推广到除了 mallocfree 之外的其他分配函数)。

首先,我们需要为 mallocfree 创建下调方法句柄,如下所示:

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函数。它们声明如下:
  1. 在形式参数列表的末尾带有省略号(...),例如:void foo(int x, ...);
  2. 使用空形式参数列表,称为无原型函数,例如:void foo();
传递给省略号位置的参数,或传递给无原型函数的参数称为 可变参数。可变参数函数本质上是可以通过将 ... 或空形式参数列表替换为固定数量和类型的 可变参数特化 为多个非可变参数函数的模板。

值得注意的是,在C中,作为可变参数传递的值会进行默认参数提升。例如,会应用以下参数提升:

  • _Bool -> unsigned int
  • [signed] char -> [signed] int
  • [signed] short -> [signed] int
  • float -> double
源类型的有符号性与提升类型的有符号性相对应。完整的默认参数提升过程在C规范中有描述。实际上,这些提升限制了可变参数函数的特化形式,因为特化形式的可变参数将始终具有提升类型。

本地链接器仅支持链接可变参数函数的专门形式。可变参数函数在其专门形式下可以使用描述专门形式的函数描述符进行链接。此外,必须提供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);
为了使用下行调用方法句柄执行等效调用,我们必须创建一个描述我们想要调用的C函数的专门签名的函数描述符。该描述符必须包括我们打算提供的每个可变参数的附加布局。在这种情况下,C函数的专门签名是(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
  • Method Details

    • nativeLinker

      static LinkerPREVIEW nativeLinker()
      返回与底层本机平台关联的 ABI 的链接器。底层本机平台是 Java 运行时当前执行的操作系统和处理器的组合。
      API 注释:
      目前无法获取不同操作系统和处理器组合的链接器。
      实现注释:
      由返回的链接器关联的默认查找公开的库是在 Java 运行时当前执行的进程中加载的本机库。例如,在 Linux 上,这些库通常包括 libclibmlibdl
      返回:
      与底层本机平台关联的 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

      MethodHandle downcallHandle(FunctionDescriptorPREVIEW function, Linker.OptionPREVIEW... options)
      创建一个方法句柄,用于使用给定签名调用外部函数。

      返回的方法句柄关联的 Java 方法类型是从函数描述符中的参数和返回布局派生预览而来,但具有额外的类型为MemorySegment预览的前导参数,从中派生目标外部函数的地址。此外,如果函数描述符的返回布局是组布局,则生成的下调方法句柄接受额外的类型为SegmentAllocator预览的前导参数,该参数由链接器运行时用于分配由下调方法句柄返回的结构关联的内存区域。

      在调用下调方法句柄时,链接器为任何类型为MemorySegment预览且对应布局为地址布局预览的参数A提供以下保证:

      此外,如果提供的函数描述符的返回布局是地址布局预览,则调用返回的方法句柄将返回与始终处于活动状态的新作用域关联的本机段。在正常情况下,返回段的大小为0。但是,如果函数描述符的返回布局具有目标布局预览T,则返回段的大小设置为T.byteSize()

      如果表示外部函数目标地址的MemorySegment预览MemorySegment.NULL预览地址,则返回的方法句柄将抛出IllegalArgumentException。如果传递给它的任何参数为null,返回的方法句柄还将抛出NullPointerException

      此方法是受限制的。受限制的方法是不安全的,如果使用不正确,可能会导致 JVM 崩溃,甚至更糟的是导致内存损坏。因此,客户端应避免依赖受限制的方法,尽可能使用安全且受支持的功能。

      参数:
      function - 目标外部函数的函数描述符。
      options - 与此链接请求相关联的链接器选项。
      返回:
      一个下调方法句柄。
      抛出:
      IllegalArgumentException - 如果提供的函数描述符不受此链接器支持。
      IllegalArgumentException - 如果给定了无效的链接器选项组合。
      IllegalCallerException - 如果调用者所在的模块未启用本机访问。
    • upcallStub

      创建一个上调用存根,可以将其作为函数指针传递给其他外部函数,并与给定的区域关联。从外部代码调用这样的函数指针将导致执行提供的方法句柄。

      返回的内存段的地址指向新分配的上调用存根,并与提供的区域关联。因此,返回的上调用存根段的生存期由提供的区域控制。例如,如果提供的区域是一个受限区域,则当提供的受限区域被关闭时,返回的上调用存根段将被释放。

      一个上调用存根参数,其对应的布局是一个地址布局,是与始终存活的新作用域关联的本机段。在正常情况下,此段参数的大小为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和处理器组合上公开一致的符号集。

      返回:
      一组常用库中符号的符号查找。