Java Project Panama指南

2023/07/04

1. 概述

在本教程中,我们将介绍Project Panama组件。我们将首先探讨外部函数和内存API。然后,我们将了解JExtract工具如何促进其使用。

2. 什么是Project Panama?

Project Panama旨在简化Java与外部(非Java)API之间的交互,即用C、C++等编写的本机代码。

到目前为止,使用Java本机接口(JNI)是从Java调用外部函数的解决方案,但是JNI有一些缺点,Project Panama通过以下方式解决了这些缺点:

  • 无需在Java中编写中间本机代码包装器
  • 用更面向未来的内存API替换ByteBuffer API
  • 引入一种与平台无关、安全且内存高效的方法来从Java调用本机代码

为实现其目标,Panama提供了一组API和工具:

  • 外部函数和内存API:用于分配和访问堆外内存以及直接从Java代码调用外部函数
  • Vector API:使高级开发人员能够用Java表达复杂的数据并行算法
  • JExtract:一种从一组本机标头中机械地派生Java绑定的工具

3. 先决条件

要使用外部函数和内存API,让我们下载Project Panama Early-Access Build。在撰写本文时,我们使用的是Build 19-panama+1-13(2022/1/18)。接下来,我们根据使用的系统设置JAVA_HOME

由于外部函数和内存API是预览API,我们必须在启用预览功能的情况下编译和运行我们的代码,即通过向java和javac添加–enable-preview标志。

4. 外部函数和内存API

外部函数和内存API可帮助Java程序与Java运行时之外的代码和数据进行互操作。

它通过有效地调用外部函数(即JVM外部的代码)和安全地访问外部内存(即不受JVM管理的内存)来实现这一点。

它结合了两个早期的孵化API:外部内存访问API外部链接器API

API提供了一组类和接口来执行这些操作:

  • 使用MemorySegment、MemoryAddress和SegmentAllocator分配外部内存
  • 通过MemorySession控制外来内存的分配和释放
  • 使用MemoryLayout操作结构化外部内存
  • 通过VarHandles访问结构化外部内存
  • 借助Linker、FunctionDescriptor和SymbolLookup调用外部函数

4.1 外部内存分配

首先,让我们探讨一下内存分配。在这里,主要的抽象是MemorySegment。它对位于堆外或堆上的连续内存区域进行建模,MemoryAddress是段内的偏移量。简单的说,一个内存段是由内存地址组成的,一个内存段可以包含其他的内存段。

此外,内存段绑定到它们封装的MemorySession并在不再需要时释放。MemorySession管理段的生命周期并确保它们在被多个线程访问时被正确释放。

让我们在内存段中的连续偏移处创建4个字节,然后将浮点值设置为6:

try (MemorySession memorySession = MemorySession.openConfined()) {
    int byteSize = 4;
    int index = 0;
    float value = 6;
    MemorySegment segment = MemorySegment.allocateNative(byteSize, memorySession);
    segment.setAtIndex(JAVA_FLOAT, index, value);
    float result = segment.getAtIndex(JAVA_FLOAT, index);
    System.out.println("Float value is:" + result);
}

在上面的代码中,confined内存会话限制对创建会话的线程的访问,而shared内存会话允许从任何线程进行访问。

此外,JAVA_FLOAT ValueLayout指定取消引用操作的属性:类型映射的正确性和要取消引用的字节数。

SegmentAllocator抽象定义了分配和初始化内存段的有用操作。当我们的代码管理大量堆外段时,它会非常有用:

String[] greetingStrings = {"hello", "world", "panama", "tuyucheng"};
SegmentAllocator allocator = SegmentAllocator.implicitAllocator();
MemorySegment offHeapSegment = allocator.allocateArray(ValueLayout.ADDRESS, greetingStrings.length);
for (int i = 0; i < greetingStrings.length; i++) {
    // Allocate a string off-heap, then store a pointer to it
    MemorySegment cString = allocator.allocateUtf8String(greetingStrings[i]);
    offHeapSegment.setAtIndex(ValueLayout.ADDRESS, i, cString);
}

4.2 外部内存操作

接下来,我们深入研究内存布局的内存操作。MemoryLayout描述段的内容。它对操作本机代码的高级数据结构很有用,例如结构体、指针和指向结构体的指针。

让我们使用GroupLayout在堆外分配一个表示具有x和y坐标的点的C结构体

GroupLayout pointLayout = structLayout(
    JAVA_DOUBLE.withName("x"),
    JAVA_DOUBLE.withName("y")
);

VarHandle xvarHandle = pointLayout.varHandle(PathElement.groupElement("x"));
VarHandle yvarHandle = pointLayout.varHandle(PathElement.groupElement("y"));

try (MemorySession memorySession = MemorySession.openConfined()) {
    MemorySegment pointSegment = memorySession.allocate(pointLayout);
    xvarHandle.set(pointSegment, 3d);
    yvarHandle.set(pointSegment, 4d);
    System.out.println(pointSegment.toString());
}

值得注意的是,不需要计算偏移量,因为不同的VarHandle用于初始化每个点坐标。

我们还可以使用SequenceLayout构造数据数组。以下是获取5点列表的方法:

SequenceLayout ptsLayout = sequenceLayout(5, pointLayout);

4.3 来自Java的本机函数调用

外部函数API允许Java开发人员在不依赖第三方包装器的情况下使用任何本机库。它严重依赖方法句柄并提供三个主要类:Linker、FunctionDescriptor和SymbolLookup。

让我们考虑通过调用C printf()函数来打印“Hello world”消息:

#include <stdio.h>
int main() {
    printf("Hello World from Project Panama Tuyucheng Article");
    return 0;
}

首先,我们在标准库的类加载器中查找函数:

Linker nativeLinker = Linker.nativeLinker();
SymbolLookup stdlibLookup = nativeLinker.defaultLookup();
SymbolLookup loaderLookup = SymbolLookup.loaderLookup();

Linker是两个二进制接口之间的桥梁:JVM和C/C++本机代码,也称为C ABI

下面,我们需要描述函数原型:

FunctionDescriptor printfDescriptor = FunctionDescriptor.of(JAVA_INT, ADDRESS);

值布局JAVA_INT和ADDRESS分别对应C printf()函数的返回类型和输入:

int printf(const char * __restrict, ...)

接下来,我们得到方法句柄

String symbolName = "printf";
String greeting = "Hello World from Project Panama Tuyucheng Article";
MethodHandle methodHandle = loaderLookup.lookup(symbolName)
    .or(() -> stdlibLookup.lookup(symbolName))
    .map(symbolSegment -> nativeLinker.downcallHandle(symbolSegment, printfDescriptor))
    .orElse(null);

Linker接口支持向下调用(从Java代码调用本机代码)和向上调用(从本机代码调用回Java代码)。最后,我们调用本机函数:

try (MemorySession memorySession = MemorySession.openConfined()) {
    MemorySegment greetingSegment = memorySession.allocateUtf8String(greeting);
    methodHandle.invoke(greetingSegment);
}

5. JExtract

使用JExtract,无需直接使用大部分外部函数和内存API抽象。让我们重新打印上面的“Hello World”示例。

首先,我们需要从标准库头文件生成Java类:

jextract --source --output src/main -t foreign.c -I c:\mingw\include c:\mingw\include\stdio.h

stdio的路径必须更新为目标操作系统中的路径。接下来,我们可以简单地从Java中导入本机printf()函数:

import static foreign.c.stdio_h.printf;

public class Greetings {

    public static void main(String[] args) {
        String greeting = "Hello World from Project Panama Tuyucheng Article, using JExtract!";
        try (MemorySession memorySession = MemorySession.openConfined()) {
            MemorySegment greetingSegment = memorySession.allocateUtf8String(greeting);
            printf(greetingSegment);
        }
    }
}

运行代码会将问候语打印到控制台:

java --enable-native-access=ALL-UNNAMED --enable-preview --source 19 .\src\main\java\cn\tuyucheng\taketoday\java\panama\jextract\Greetings.java

6. 总结

在本文中,我们了解了Project Panama的主要功能。

首先,我们探索了使用外部函数和内存API进行本机内存管理。然后我们使用MethodHandles调用外部函数。最后,我们使用JExtract工具来隐藏隐藏外部函数和内存API的复杂性。

Project Panama还有很多值得探索的地方,特别是从本机代码调用Java、调用第三方库和Vector API

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章