Skip to content

Commit 292ff8e

Browse files
Panama-memorylayout
1 parent e48b5f1 commit 292ff8e

File tree

3 files changed

+320
-0
lines changed

3 files changed

+320
-0
lines changed

SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* [Panama教程-0-MethodHandle介绍](panama/panama-tutorial-0-A.md)
2525
* [Panama教程-0-Varhandle介绍](panama/panama-tutorial-0-B.md)
2626
* [Panama教程-1-MemorySegment介绍](panama/panama-tutorial-1-MemorySegment.md)
27+
* [Panama教程-2-MemoryLayout介绍](panama/panama-tutorial-2-MemoryLayout.md)
2728
* [失去了Unsafe内存操作之后该何去何从](panama/afterUnsafe.md)
2829
* [Panama源码浅析](panama/Panama浅析.md)
2930

Loading
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
# Panama教程-2-MemoryLayout介绍
2+
3+
> 若无特别提及,这里的数据类型长度均为x86 64 linux上的长度
4+
>
5+
> 示例代码可以从这里找到
6+
>
7+
> https://github.com/dreamlike-ocean/Panama-tutorial
8+
9+
## 前言
10+
11+
之前我们介绍了MemorySegement的相关内容,那么在迈向FFI的介绍之前,我们先来看一个东西MemoryLayout,它用来描述与FFI交互的时候应该传入什么类型的参数,你可以简单理解为这就是一个c语言中的结构体
12+
13+
## 基础概念
14+
15+
我们先来看一下MemoryLayout的功能
16+
17+
蓝色框只是一些对与当前的MemoryLayout的元数据描述,以及一些可以给它打tag的api
18+
19+
红色的的部分则是MemoryLayout提供的各种方便操作内存的API——获取偏移量,获取操作偏移量的varhandle
20+
21+
![ecde3c69-6498-4205-bc34-342d2207112b](assets/ecde3c69-6498-4205-bc34-342d2207112b.jpeg)
22+
23+
## 基础类型
24+
25+
对于八大基础类型和指针类型均有对应的的MemoryLayout实现,你可以通过` ValueLayout.JAVA_INT`获取到一个基础类型的MemoryLayout对象
26+
27+
```java
28+
public sealed interface ValueLayout extends MemoryLayout
29+
permits ValueLayout.OfBoolean, ValueLayout.OfByte, ValueLayout.OfChar,
30+
ValueLayout.OfShort, ValueLayout.OfInt, ValueLayout.OfFloat,
31+
ValueLayout.OfLong, ValueLayout.OfDouble, AddressLayout
32+
```
33+
34+
这里有个小坑,对于c而言char的长度为一个字节,但是Java的char长度为2个字节,c的char对应的是java的byte
35+
36+
它的用法一般是这样的,通过某个偏移量从某个MemorySegment获取一个Java_Int的值,当你获取到一个varhandle时就可以通过varhandle api+偏移量形式获取对应位置的值
37+
38+
```java
39+
public static int getInt(MemorySegment memorySegment) {
40+
VarHandle varHandle = ValueLayout.JAVA_INT.varHandle();
41+
42+
return (int) varHandle.get(memorySegment,/*offset*/ 0);
43+
}
44+
45+
```
46+
47+
大部分情况下我们都是从0偏移量的地方进行获取,所以我们可以这样转换下Varhandle,正如我们之前文章讲到的,Varhandle都是不可变的所以这里转换并不会影响到原始的Varhandle,而是返回一个新的Varhandle
48+
49+
```java
50+
private static final VarHandle GET_INT_OFFSET_0 = MethodHandles.insertCoordinates(ValueLayout.JAVA_INT.varHandle(), 1, 0);
51+
52+
```
53+
54+
以上是针对于对齐的内存进行操作,所以若传入的内存未对齐,那么就会抛出一个异常
55+
56+
```java
57+
IllegalArgumentException illegalArgumentException = Assertions.assertThrows(IllegalArgumentException.class, () -> {
58+
var memorySegment1 = scope.allocate(ValueLayout.JAVA_INT.byteSize() + 1);
59+
alueLayout.JAVA_INT.varHandle().set(memorySegment1, 1, 2001);
60+
});
61+
//java.lang.IllegalArgumentException: Target offset 1 is incompatible with alignment constraint 4 (of i4) for segment MemorySegment{ address: 0x707c9442e020, byteSize: 5 }
62+
63+
```
64+
65+
如果你有确切把握你所在的平台支持未对齐读写,那么就可以使用对应的`ValueLayout.*_UNALIGNED`进行读写操作
66+
67+
```java
68+
var memorySegment1 = scope.allocate(ValueLayout.JAVA_INT.byteSize() + 1);
69+
Assertions.assertEquals(2001, ValueLayout.JAVA_INT_UNALIGNED.varHandle().get(memorySegment1, 1));
70+
```
71+
72+
注意未对齐读写可能存在不可预知的问题,甚至会导致jvm crash,请务必三思后行
73+
74+
## 数组类型
75+
76+
对于一个数组类型,它是n个具有相同MemoryLayout连续排列的一个连续内存布局,n为声明描述时制定的一个变量
77+
78+
参考以下代码 即可以获取到一个varhandle用来遍历这个数组
79+
80+
```java
81+
public static int sum(MemorySegment intArray) {
82+
int count = (int) (intArray.byteSize() / ValueLayout.JAVA_INT.byteSize());
83+
SequenceLayout sequenceLayout = MemoryLayout.sequenceLayout(/*count*/count, ValueLayout.JAVA_INT);
84+
//真实使用的时候务必将VarHandle const化
85+
VarHandle varHandle = sequenceLayout.varHandle(MemoryLayout.PathElement.sequenceElement());
86+
int sum = 0;
87+
for (int i = 0; i < count - 1; i++) {
88+
sum += (int) varHandle.get(intArray, 0, i);
89+
}
90+
//专门用来获取最后一个元素 同时给 VarHandle 插入基准偏移量 0
91+
VarHandle indexVarhandle = MethodHandles.insertCoordinates(sequenceLayout.varHandle(MemoryLayout.PathElement.sequenceElement(count - 1)), 1, 0);
92+
sum += (int) indexVarhandle.get(intArray);
93+
return sum;
94+
}
95+
```
96+
97+
## 填充类型
98+
99+
`MemoryLayout.paddingLayout`这是一个没有实际字段意义的的类型,只是用来填充对齐空间的,这里不展开,在结构体类型中经常使用
100+
101+
## 结构体/联合类型
102+
103+
### 声明结构体
104+
105+
与native打交道比较重要的类型就是结构体类型,下面这段Rust代码声明了一个c布局的结构体,第一个字段是32位的int类型,第二个是64位的int类型
106+
107+
```rust
108+
#[repr(C)]
109+
pub struct Person {
110+
pub a: i32,
111+
pub n: i64,
112+
}
113+
```
114+
115+
那么是否我们可以直接这样声明一个Java侧的结构体呢?
116+
117+
```java
118+
StructLayout structLayout = MemoryLayout.structLayout(
119+
ValueLayout.JAVA_INT.withName("a"),
120+
ValueLayout.JAVA_LONG.withName("n")
121+
);
122+
```
123+
124+
如果你真的这样写了就会发现他会抛出一个异常`java.lang.IllegalArgumentException: Invalid alignment constraint for member layout: j8(n)`
125+
126+
通过这个异常我们可以看出来Java这个structLayout的实现非常简单粗暴,只是单纯依次放置对应的MemoryLayout罢了
127+
128+
为了让其能够通过运行时检查我们需要这样写,在a和n之间插入一个四个字节长的填充类型
129+
130+
```java
131+
public static final MemoryLayout PERSON_LAYOUT = MemoryLayout.structLayout(
132+
ValueLayout.JAVA_INT.withName("a"),
133+
MemoryLayout.paddingLayout(4),
134+
ValueLayout.JAVA_LONG.withName("n")
135+
);
136+
```
137+
138+
### 声明联合(Union)
139+
140+
学过c的读者应该能意识到这个联合类型是并不需要struct那样的对齐策略
141+
142+
```java
143+
public static final MemoryLayout UNION_SAMPLE_STRUCT_LAYOUT = MemoryLayout.unionLayout(
144+
ValueLayout.JAVA_INT.withName("a"),
145+
ValueLayout.JAVA_LONG.withName("n")
146+
);
147+
```
148+
149+
此时这个union的长度为一个Java_Long的长度
150+
151+
### 操作字段
152+
153+
那么我们该如何操作对应的字段呢?
154+
155+
```java
156+
public static long getN(MemorySegment memorySegment, boolean useName) {
157+
VarHandle varHandle = useName
158+
//由于我们使用了ValueLayout.JAVA_LONG.withName("n") 所以可以通过名字获取varhandle
159+
? PERSON_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("n"))
160+
// 由于名字为n的字段是第三个布局元素(包含填充类型的布局) 所以这里是2
161+
: PERSON_LAYOUT.varHandle(MemoryLayout.PathElement.groupElement(2));
162+
//老样子习惯性插入基准偏移量
163+
varHandle = MethodHandles.insertCoordinates(varHandle, 1, 0);
164+
return (long) varHandle.get(memorySegment);
165+
}
166+
```
167+
168+
通过使用一组PathElement来指定对应的搜索路径以获取Varhandle,同理你也可以使用`PERSON_LAYOUT.byteOffset`获取某一字段的偏移量
169+
170+
### 稍微复杂一点的例子
171+
172+
下面我给一个包含嵌套结构体,数组的例子,为了方便展示我故意将全部的类型都设置为long类型,这样可以不需要计算对齐策略
173+
174+
其依次为一个long类型字段,一个长度为3的long数组,一个包含两个long类型的嵌套结构体,一个指向某个long数组的指针,一个指向long_and_long的指针
175+
176+
```rust
177+
#[repr(C)]
178+
pub struct long_and_long {
179+
pub a: i64,
180+
pub b: i64,
181+
}
182+
183+
#[repr(C)]
184+
pub struct Complex {
185+
pub a: i64,
186+
pub long_array: [i64; 3],
187+
pub sub_struct: long_and_long,
188+
pub long_array_ptr: *mut i64,
189+
pub long_and_long_ptr: *mut long_and_long,
190+
}
191+
```
192+
193+
### 布局声明
194+
195+
那么他该如何声明布局呢?
196+
197+
```java
198+
private static final MemoryLayout LONG_AND_LONG_LAYOUT = MemoryLayout.structLayout(
199+
ValueLayout.JAVA_LONG.withName("a"),
200+
ValueLayout.JAVA_LONG.withName("b")
201+
);
202+
public static final MemoryLayout COMPLEX_LAYOUT = MemoryLayout.structLayout(
203+
ValueLayout.JAVA_LONG.withName("a"),
204+
MemoryLayout.sequenceLayout(3, Integer.JAVA_LONG).withName("long_array"),
205+
LONG_AND_LONG_LAYOUT.withName("sub_struct"),
206+
ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(Long.MAX_VALUE, ValueLayout.JAVA_LONG)).withName("long_array_ptr"),
207+
ValueLayout.ADDRESS.withTargetLayout(LONG_AND_LONG_LAYOUT).withName("long_and_long_ptr")
208+
);
209+
```
210+
211+
### 获取long_array的第二个元素
212+
213+
还是通过一组PathElement灵活组织下就好了
214+
215+
```java
216+
public static long getLongArrayIndex1(MemorySegment memorySegment) {
217+
VarHandle varHandle = COMPLEX_LAYOUT.varHandle(
218+
MemoryLayout.PathElement.groupElement("long_array"),
219+
MemoryLayout.PathElement.sequenceElement(/*index*/ 1)
220+
);
221+
varHandle = MethodHandles.insertCoordinates(varHandle, 1, 0);
222+
return (long) varHandle.get(memorySegment);
223+
}
224+
```
225+
226+
### 获取嵌套结构体的B字段的值
227+
228+
```java
229+
public static long getSubStructFieldB(MemorySegment memorySegment) {
230+
VarHandle varHandle = COMPLEX_LAYOUT.varHandle(
231+
MemoryLayout.PathElement.groupElement("sub_struct"),
232+
MemoryLayout.PathElement.groupElement("b")
233+
);
234+
varHandle = MethodHandles.insertCoordinates(varHandle, 1, 0);
235+
return (long) varHandle.get(memorySegment);
236+
}
237+
```
238+
239+
### 通过指针获取指针指向数组的第五个元素
240+
241+
这里要使用dereferenceElement这个特殊的Path元素
242+
243+
```java
244+
public static long getLongArrayPtrIndex4(MemorySegment segment) {
245+
VarHandle varHandle = COMPLEX_LAYOUT.varHandle(
246+
MemoryLayout.PathElement.groupElement("long_array_ptr"),
247+
MemoryLayout.PathElement.dereferenceElement(),
248+
MemoryLayout.PathElement.sequenceElement(4)
249+
);
250+
//类似于
251+
//struct.long_array_ptr[4]
252+
varHandle = MethodHandles.insertCoordinates(varHandle, 1, 0);
253+
return (long) varHandle.get(segment);
254+
}
255+
```
256+
257+
### 获取某个结构体指针的指向的结构体的字段值
258+
259+
在此之前我们来回看一下上面的结构体声明,这里在声明long_and_long_ptr字段的的时候特意为这个address指定了withTargetLayout这个布局,这是我们能解指针的关键
260+
261+
```java
262+
ValueLayout.ADDRESS.withTargetLayout(LONG_AND_LONG_LAYOUT).withName("long_and_long_ptr")
263+
```
264+
265+
那么这个功能实现也很简单了
266+
267+
```java
268+
public static long getLongAndLongPtrFieldB(MemorySegment segment) {
269+
VarHandle varHandle = COMPLEX_LAYOUT.varHandle(
270+
MemoryLayout.PathElement.groupElement("long_and_long_ptr"),
271+
MemoryLayout.PathElement.dereferenceElement(),
272+
MemoryLayout.PathElement.groupElement("b")
273+
);
274+
//类似于
275+
//struct.long_and_long_ptr->b
276+
varHandle = MethodHandles.insertCoordinates(varHandle, 1, 0);
277+
return (long) varHandle.get(segment);
278+
}
279+
```
280+
281+
### 自动布局
282+
283+
虽然JDK并没有提供对应的自动布局API但是我们可以自己写一个
284+
285+
```java
286+
public static StructLayout calAlignLayout(MemoryLayout... memoryLayouts) {
287+
long size = 0;
288+
long align = 1;
289+
ArrayList<MemoryLayout> layouts = new ArrayList<>();
290+
for (MemoryLayout memoryLayout : memoryLayouts) {
291+
//当前布局是否与size对齐
292+
if (size % memoryLayout.byteAlignment() == 0) {
293+
size = Math.addExact(size, memoryLayout.byteSize());
294+
align = Math.max(align, memoryLayout.byteAlignment());
295+
layouts.add(memoryLayout);
296+
continue;
297+
}
298+
long multiple = size / memoryLayout.byteAlignment();
299+
//计算填充
300+
long padding = (multiple + 1) * memoryLayout.byteAlignment() - size;
301+
size = Math.addExact(size, padding);
302+
//添加填充
303+
layouts.add(MemoryLayout.paddingLayout(padding));
304+
//添加当前布局
305+
layouts.add(memoryLayout);
306+
size = Math.addExact(size, memoryLayout.byteSize());
307+
align = Math.max(align, memoryLayout.byteAlignment());
308+
}
309+
//尾部对齐
310+
if (size % align != 0) {
311+
long multiple = size / align;
312+
long padding = (multiple + 1) * align - size;
313+
size = Math.addExact(size, padding);
314+
layouts.add(MemoryLayout.paddingLayout(padding));
315+
}
316+
return MemoryLayout.structLayout(layouts.toArray(MemoryLayout[]::new));
317+
}
318+
```
319+

0 commit comments

Comments
 (0)