|
| 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 | + |
| 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