之前寫的兩個項目中,有使用過java調用dll,之前一直使用jni進行調用。最近在了解jdk21時,其中有個更新讓我感興趣,JEP 442[Foreign Function & Memory API (Third Preview)],是對外部函數和堆外內存訪問的API更新。并且在檢索發現,jdk17孵化的版本和jdk21的三次預覽的版本的api還是不太一樣的。
Memory segments 和 arenas
Memory segments(內存段)是由位于堆外或堆上的連續內存區域在java中的抽象。
可以使用Arena來申請內存段,每個段都提供存儲空間,并且為了安全這個空間的使用有時間界限的。
Arena在申請內存時,可以定義該內存的使用期限。創建一個100bytes的native連續內存段。其中Arena定義的是內存段的生命周期。
-
global提供了無限的生命周期。這段內存的申請永遠不會被釋放掉。
MemorySegment data = Arena.global().allocate(100);
-
ofAuto,直到jvm的垃圾收集器檢測到該內存段不可訪問。該內存就會被釋放。
void processData() { MemorySegment data = Arena.ofAuto().allocateNative(100); } 方法結束后,data被釋放
-
try-with-resource方式釋放內存
MemorySegment input = null, output = null; try (Arena processing = Arena.ofConfined()) { input = processing.allocate(100); ... set up data in 'input' ... output = processing.allocate(100); ... process data from 'input' to 'output' ... ... calculate the ultimate result from 'output' and store it elsewhere ... } // 內存段在這里被釋放
Dereferencing內存段
Dereferencing內存段(不知道這個翻譯成什么)。大概理解成,在對引用字段申請內存時:
- 需要知道申請的內存總大小。
- 要使每個值的字段地址對齊。
- 要明確存儲的java類型和順序
對官方用例做了一些修改來明確他的說法。
用例中將長度為25的int數組寫入內存中,申請內存時使用java_int的內存對齊大小*申請數量。
MemorySegment會在調用setAtIndex時自動對齊。
public static void dereferenceSegments() throws Throwable {
long byteAlignment = JAVA_INT.byteAlignment();
int arraySize = 25;
MemorySegment segment
= Arena.ofAuto().allocate(byteAlignment * arraySize, byteAlignment);
//寫入
for (int i = 0; i < arraySize; i++) {
segment.setAtIndex(ValueLayout.JAVA_INT, i, i);
//等價于 segment.setAtIndex(ValueLayout.JAVA_INT, i * byteAlignment, i);
}
//讀出
for (int i = 0; i < arraySize; i++) {
int i1 = segment.get(JAVA_INT, i * byteAlignment);
System.out.println(i1);
int i2 = segment.getAtIndex(JAVA_INT, i);
System.out.println(i2);
}
}
Memory layouts與結構體
使用Memory layouts(內存布局)來定義結構體
struct Point {
int x;
int y;
} pts[10];
如果使用dereference的方式去寫入該結構體,雖然已經對齊結構體,但在設值時還要進行字段對齊。
MemorySegment segment
= Arena.ofAuto().allocate(2 * ValueLayout.JAVA_INT.byteSize() * 10, // size
ValueLayout.JAVA_INT.byteAlignment); // alignment
for (int i = 0; i < 10; i++) {
segment.setAtIndex(ValueLayout.JAVA_INT,
/* index */ (i * 2),
/* value to write */ i); // x
segment.setAtIndex(ValueLayout.JAVA_INT,
/* index */ (i * 2) + 1,
/* value to write */ i); // y
}
可以使用MemoryLayout創建內存布局。
定義結構體內存布局structLayout,利用sequenceLayout創建10個重復的struct相當的內存,并對齊結構體和字段。
創建變量內存訪問句柄去訪問字段值,根據布局路徑,先使用sequence index篩選結構體,再使用group name篩選字段。
StructLayout structLayout = MemoryLayout.structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y"));
SequenceLayout ptsLayout = MemoryLayout.sequenceLayout(10, structLayout);
VarHandle xHandle
= ptsLayout.varHandle(PathElement.sequenceElement(),
PathElement.groupElement("x"));
VarHandle yHandle
= ptsLayout.varHandle(PathElement.sequenceElement(),
PathElement.groupElement("y"));
MemorySegment segment = Arena.ofAuto().allocate(ptsLayout);
for (int i = 0; i < ptsLayout.elementCount(); i++) {
xHandle.set(segment,
/* index */ (long) i,
/* value to write */ i); // x
yHandle.set(segment,
/* index */ (long) i,
/* value to write */ i); // y
}
內存段分配器
內存段也能從Segment allocators中獲得。與直接使用Arena分配不同的是,Segment allocators可以提前分配比較大的內存段,在向他申請內存時,他會返回提前分配的一部分內存來響應分配請求。以下代碼,涉及的native內存的分配只有一次(應該是為了提高allocate效率)
MemorySegment segment = Arena.ofAuto().allocate(100);
SegmentAllocator allocator = SegmentAllocator.slicingAllocator(segment);
for (int i = 0 ; i < 10 ; i++) {
MemorySegment s = allocator.allocateArray(JAVA_INT,
1, 2, 3, 4, 5);
}
查找外部函數
SymbolLookup::libraryLookup(String, Arena):加載指定lib的symbols,作用在指定arena內
SymbolLookup::loaderLookup():查找指定 System::loadLibrary
and System::load
相同的symbol
Linker::defaultLookup():查找系統自帶的c/c++標準庫
Linker linker = Linker.nativeLinker();
SymbolLookup defaultLookup = linker.defaultLookup();
SymbolLookup symbolLookup = SymbolLookup.libraryLookup("src\\main\\resources\\MathLibrary.dll", Arena.global());
鏈接到外部函數
interface Linker {
MethodHandle downcallHandle(MemorySegment address,
FunctionDescriptor function);
MemorySegment upcallStub(MethodHandle target,
FunctionDescriptor function,
Arena arena);
}
對于向下調用,該downcallHandle
方法獲取外部函數的地址(通常是MemorySegment
從庫查找中獲得的地址)并將外部函數公開為向下調用方法句柄MethodHandle
,通過調用句柄invoke執行。
對于向上調用,該upcallStub
方法采用一個方法句柄(通常是指 Java 方法,而不是向下調用方法句柄)并將其轉換為實例MemorySegment
。隨后,當 Java 代碼調用向下調用方法句柄時,內存段將作為參數傳遞。實際上,內存段充當函數指針。
向下調用
在調用外部函數前,了解一下函數描述對象的構造方法,第一個為返回值內存布局,剩余為傳入參數內存布局。
假設我們希望從 Java 向下strlen
調用標準 C 庫中定義的函數:
size_t strlen(const char *s);
Linker linker = Linker.nativeLinker();
SymbolLookup defaultLookup = linker.defaultLookup();
MethodHandle strlenHandle = linker.downcallHandle(
defaultLookup.find("strlen").orElseThrow(),
FunctionDescriptor.of(JAVA_LONG, ADDRESS)
);
try (Arena offHeap = Arena.ofConfined()) {
MemorySegment pointers = offHeap.allocateUtf8String("Hello world!");
System.out.println(strlenHandle.invoke(pointers)); //11
}
更復雜的嘗試,我們希望定義一個結構體Point,并且傳入Point數組,鏈接到C函數找到x和y相加最大的Point,定義以下DLL
// MathLibrary.h - Contains declarations of math functions
#pragma once
#ifdef MATHLIBRARY_EXPORTS
#define MATHLIBRARY_API __declspec(dllexport)
#else
#define MATHLIBRARY_API __declspec(dllimport)
#endif
struct Point {
int x;
int y;
};
extern "C" MATHLIBRARY_API Point test_point(Point points[], long count);
// MathLibrary.cpp : Defines the exported functions for the DLL.
#include "pch.h"
#include <utility>
#include <limits.h>
#include "MathLibrary.h"
#include <iostream>
// cpp文件內容
Point test_point(Point points[],long count)
{
if (count <= 0) {
// Return a default Point with x and y set to 0
Point defaultPoint = { 0, 0 };
return defaultPoint;
}
Point maxPoint = points[0];
int maxSum = maxPoint.x + maxPoint.y;
for (int i = 1; i < count; ++i) {
int currentSum = points[i].x + points[i].y;
if (currentSum > maxSum) {
maxSum = currentSum;
maxPoint = points[i];
}
}
int x = maxPoint.x;
int y = maxPoint.y;
std::cout << "x = " << x << ", y = " << y << std::endl;
return maxPoint;
}
用前面了解到的方法,利用內存布局創建基于結構體的序列布局,基于函數名和函數描述對象現在函數句柄,創建變量內存訪問句柄去設置字段值。
public static void dereferenceSegmentsStruct() throws Throwable {
StructLayout structLayout = MemoryLayout.structLayout(
JAVA_INT.withName("x"),
JAVA_INT.withName("y"));
MethodHandle test_point = linker.downcallHandle(
symbolLookup.find("test_point").orElseThrow(),
FunctionDescriptor.of(structLayout, ADDRESS, JAVA_LONG)
);
SequenceLayout ptsLayout = MemoryLayout.sequenceLayout(10, structLayout);
VarHandle xHandle
= ptsLayout.varHandle(PathElement.sequenceElement(),
PathElement.groupElement("x"));
VarHandle yHandle
= ptsLayout.varHandle(PathElement.sequenceElement(),
PathElement.groupElement("y"));
MemorySegment segment = Arena.ofAuto().allocate(ptsLayout);
for (int i = 0; i < ptsLayout.elementCount(); i++) {
xHandle.set(segment, (long) i, i);
yHandle.set(segment, (long) i, i);
}
SegmentAllocator allocator = SegmentAllocator.slicingAllocator(Arena.ofAuto().allocate(structLayout.byteSize()));
MemorySegment result = (MemorySegment) test_point.invoke(allocator, segment, ptsLayout.elementCount());
result = result.reinterpret(structLayout.byteSize());
VarHandle resultX
= structLayout.varHandle(PathElement.groupElement("x"));
VarHandle resultY
= structLayout.varHandle(PathElement.groupElement("y"));
System.out.println(StringTemplate.STR. "\{ resultX.get(result) }:\{ resultY.get(result) }" );
}
在創建MethodHandle時,注意描述符的正確性,其特殊性在于:
傳入Point數組時,需要使用地址布局傳入(對象內存已初始化賦值完畢)
返回Point對象時,需要使用結構體布局作為返回,并且需要使用內存段分配器為結構體分配內存(對象內存未初始化)。
向上調用
使java代碼作為函數指針傳遞到某個外部函數中調用。
考慮到標準c庫中有函數qsort。用于對數組進行快速排序。這個函數接受以下參數:
-
void* base
:指向待排序數組的指針,數組的每個元素的大小為size
字節。 -
size_t nmemb
:數組中元素的數量。 -
size_t size
:每個元素的大?。ㄒ宰止潪閱挝唬?。 -
int (*compar)(const void*, const void*)
:一個函數指針,用于比較數組中的兩個
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
所使用的函數指針可以用java定義一個Qsort類
public class Qsort {
static int qsortCompare(MemorySegment elem1, MemorySegment elem2) {
return Integer.compare(elem1.get(JAVA_INT, 0), elem2.get(JAVA_INT, 0));
}
}
現在,我們可以使用Linker根據方法句柄獲取到方法的內存段,將他和其他參數一同傳遞給已鏈接的外部函數。如下:
public static void lookingUpForeignFunctions() throws Throwable {
MethodHandle qsort = linker.downcallHandle(
defaultLookup.find("qsort").orElseThrow(),
FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS)
);
MethodHandle comparHandle
= MethodHandles.lookup()
.findStatic(Qsort.class, "qsortCompare",
MethodType.methodType(int.class,
MemorySegment.class,
MemorySegment.class));
MemorySegment comparFunc
= linker.upcallStub(comparHandle,
/* A Java description of a C function
implemented by a Java method! */
FunctionDescriptor.of(JAVA_INT,
ADDRESS.withTargetLayout(JAVA_INT),
ADDRESS.withTargetLayout(JAVA_INT)),
Arena.ofAuto());
try (Arena arena = Arena.ofConfined()) {
MemorySegment array
= arena.allocateArray(ValueLayout.JAVA_INT,
0, 9, 3, 4, 6, 5, 1, 8, 2, 7);
qsort.invoke(array, 10L, ValueLayout.JAVA_INT.byteSize(), comparFunc);
int[] sorted = array.toArray(JAVA_INT); // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
System.out.println(Arrays.toString(sorted));
}
}
零長度內存段
外部函數通常分配一個內存區域并返回指向該區域的指針。FFM API 將從外部函數返回的指針表示為零長度內存段。段的地址是指針的值,段的大小為零。類似地,當從內存段讀取指針時,則返回零長度內存段。
零長度段不具有空間,因此訪問此類段都會失敗并拋出IndexOutOfBoundsException
。我們可以通過MemorySegment::reinterpret
將零長度內存段轉換成其內存段的真實大小,就像我們在向下調用結構體
中代碼片段result = result.reinterpret(structLayout.byteSize());
一樣。但這可能會嘗試引用該區域邊界之外的內存,這可能會導致 JVM 崩潰或無提示內存損壞。