struct_arm64.go raw
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2024 The Ebitengine Authors
3
4 package purego
5
6 import (
7 "math"
8 "reflect"
9 "runtime"
10 "strconv"
11 stdstrings "strings"
12 "unsafe"
13
14 "github.com/ebitengine/purego/internal/strings"
15 )
16
17 func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
18 outSize := outType.Size()
19 switch {
20 case outSize == 0:
21 return reflect.New(outType).Elem()
22 case outSize <= 8:
23 r1 := syscall.a1
24 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
25 r1 = syscall.f1
26 if numFields == 2 {
27 r1 = syscall.f2<<32 | syscall.f1
28 }
29 }
30 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{r1})).Elem()
31 case outSize <= 16:
32 r1, r2 := syscall.a1, syscall.a2
33 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
34 switch numFields {
35 case 4:
36 r1 = syscall.f2<<32 | syscall.f1
37 r2 = syscall.f4<<32 | syscall.f3
38 case 3:
39 r1 = syscall.f2<<32 | syscall.f1
40 r2 = syscall.f3
41 case 2:
42 r1 = syscall.f1
43 r2 = syscall.f2
44 default:
45 panic("unreachable")
46 }
47 }
48 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem()
49 default:
50 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats && numFields <= 4 {
51 switch numFields {
52 case 4:
53 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b, c, d uintptr }{syscall.f1, syscall.f2, syscall.f3, syscall.f4})).Elem()
54 case 3:
55 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b, c uintptr }{syscall.f1, syscall.f2, syscall.f3})).Elem()
56 default:
57 panic("unreachable")
58 }
59 }
60 // create struct from the Go pointer created in arm64_r8
61 // weird pointer dereference to circumvent go vet
62 return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.arm64_r8))).Elem()
63 }
64 }
65
66 // https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst
67 const (
68 _NO_CLASS = 0b00
69 _FLOAT = 0b01
70 _INT = 0b11
71 )
72
73 func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
74 if v.Type().Size() == 0 {
75 return keepAlive
76 }
77
78 if hva, hfa, size := isHVA(v.Type()), isHFA(v.Type()), v.Type().Size(); hva || hfa || size <= 16 {
79 // if this doesn't fit entirely in registers then
80 // each element goes onto the stack
81 if hfa && *numFloats+v.NumField() > numOfFloatRegisters() {
82 *numFloats = numOfFloatRegisters()
83 } else if hva && *numInts+v.NumField() > numOfIntegerRegisters() {
84 *numInts = numOfIntegerRegisters()
85 }
86
87 placeRegisters(v, addFloat, addInt)
88 } else {
89 keepAlive = placeStack(v, keepAlive, addInt)
90 }
91 return keepAlive // the struct was allocated so don't panic
92 }
93
94 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
95 if runtime.GOOS == "darwin" {
96 placeRegistersDarwin(v, addFloat, addInt)
97 return
98 }
99 placeRegistersArm64(v, addFloat, addInt)
100 }
101
102 func placeRegistersArm64(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
103 var val uint64
104 var shift byte
105 var flushed bool
106 class := _NO_CLASS
107 var place func(v reflect.Value)
108 place = func(v reflect.Value) {
109 var numFields int
110 if v.Kind() == reflect.Struct {
111 numFields = v.Type().NumField()
112 } else {
113 numFields = v.Type().Len()
114 }
115 for k := 0; k < numFields; k++ {
116 flushed = false
117 var f reflect.Value
118 if v.Kind() == reflect.Struct {
119 f = v.Field(k)
120 } else {
121 f = v.Index(k)
122 }
123 align := byte(f.Type().Align()*8 - 1)
124 shift = (shift + align) &^ align
125 if shift >= 64 {
126 shift = 0
127 flushed = true
128 if class == _FLOAT {
129 addFloat(uintptr(val))
130 } else {
131 addInt(uintptr(val))
132 }
133 val = 0
134 class = _NO_CLASS
135 }
136 switch f.Type().Kind() {
137 case reflect.Struct:
138 place(f)
139 case reflect.Bool:
140 if f.Bool() {
141 val |= 1 << shift
142 }
143 shift += 8
144 class |= _INT
145 case reflect.Uint8:
146 val |= f.Uint() << shift
147 shift += 8
148 class |= _INT
149 case reflect.Uint16:
150 val |= f.Uint() << shift
151 shift += 16
152 class |= _INT
153 case reflect.Uint32:
154 val |= f.Uint() << shift
155 shift += 32
156 class |= _INT
157 case reflect.Uint64, reflect.Uint, reflect.Uintptr:
158 addInt(uintptr(f.Uint()))
159 shift = 0
160 flushed = true
161 class = _NO_CLASS
162 case reflect.Int8:
163 val |= uint64(f.Int()&0xFF) << shift
164 shift += 8
165 class |= _INT
166 case reflect.Int16:
167 val |= uint64(f.Int()&0xFFFF) << shift
168 shift += 16
169 class |= _INT
170 case reflect.Int32:
171 val |= uint64(f.Int()&0xFFFF_FFFF) << shift
172 shift += 32
173 class |= _INT
174 case reflect.Int64, reflect.Int:
175 addInt(uintptr(f.Int()))
176 shift = 0
177 flushed = true
178 class = _NO_CLASS
179 case reflect.Float32:
180 if class == _FLOAT {
181 addFloat(uintptr(val))
182 val = 0
183 shift = 0
184 }
185 val |= uint64(math.Float32bits(float32(f.Float()))) << shift
186 shift += 32
187 class |= _FLOAT
188 case reflect.Float64:
189 addFloat(uintptr(math.Float64bits(float64(f.Float()))))
190 shift = 0
191 flushed = true
192 class = _NO_CLASS
193 case reflect.Ptr, reflect.UnsafePointer:
194 addInt(f.Pointer())
195 shift = 0
196 flushed = true
197 class = _NO_CLASS
198 case reflect.Array:
199 place(f)
200 default:
201 panic("purego: unsupported kind " + f.Kind().String())
202 }
203 }
204 }
205 place(v)
206 if !flushed {
207 if class == _FLOAT {
208 addFloat(uintptr(val))
209 } else {
210 addInt(uintptr(val))
211 }
212 }
213 }
214
215 func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
216 // Struct is too big to be placed in registers.
217 // Copy to heap and place the pointer in register
218 ptrStruct := reflect.New(v.Type())
219 ptrStruct.Elem().Set(v)
220 ptr := ptrStruct.Elem().Addr().UnsafePointer()
221 keepAlive = append(keepAlive, ptr)
222 addInt(uintptr(ptr))
223 return keepAlive
224 }
225
226 // isHFA reports a Homogeneous Floating-point Aggregate (HFA) which is a Fundamental Data Type that is a
227 // Floating-Point type and at most four uniquely addressable members (5.9.5.1 in [Arm64 Calling Convention]).
228 // This type of struct will be placed more compactly than the individual fields.
229 //
230 // [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst
231 func isHFA(t reflect.Type) bool {
232 // round up struct size to nearest 8 see section B.4
233 structSize := roundUpTo8(t.Size())
234 if structSize == 0 || t.NumField() > 4 {
235 return false
236 }
237 first := t.Field(0)
238 switch first.Type.Kind() {
239 case reflect.Float32, reflect.Float64:
240 firstKind := first.Type.Kind()
241 for i := 0; i < t.NumField(); i++ {
242 if t.Field(i).Type.Kind() != firstKind {
243 return false
244 }
245 }
246 return true
247 case reflect.Array:
248 switch first.Type.Elem().Kind() {
249 case reflect.Float32, reflect.Float64:
250 return true
251 default:
252 return false
253 }
254 case reflect.Struct:
255 for i := 0; i < first.Type.NumField(); i++ {
256 if !isHFA(first.Type) {
257 return false
258 }
259 }
260 return true
261 default:
262 return false
263 }
264 }
265
266 // isHVA reports a Homogeneous Aggregate with a Fundamental Data Type that is a Short-Vector type
267 // and at most four uniquely addressable members (5.9.5.2 in [Arm64 Calling Convention]).
268 // A short vector is a machine type that is composed of repeated instances of one fundamental integral or
269 // floating-point type. It may be 8 or 16 bytes in total size (5.4 in [Arm64 Calling Convention]).
270 // This type of struct will be placed more compactly than the individual fields.
271 //
272 // [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst
273 func isHVA(t reflect.Type) bool {
274 // round up struct size to nearest 8 see section B.4
275 structSize := roundUpTo8(t.Size())
276 if structSize == 0 || (structSize != 8 && structSize != 16) {
277 return false
278 }
279 first := t.Field(0)
280 switch first.Type.Kind() {
281 case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Int8, reflect.Int16, reflect.Int32:
282 firstKind := first.Type.Kind()
283 for i := 0; i < t.NumField(); i++ {
284 if t.Field(i).Type.Kind() != firstKind {
285 return false
286 }
287 }
288 return true
289 case reflect.Array:
290 switch first.Type.Elem().Kind() {
291 case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Int8, reflect.Int16, reflect.Int32:
292 return true
293 default:
294 return false
295 }
296 default:
297 return false
298 }
299 }
300
301 // copyStruct8ByteChunks copies struct memory in 8-byte chunks to the provided callback.
302 // This is used for Darwin ARM64's byte-level packing of non-HFA/HVA structs.
303 func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintptr)) {
304 if runtime.GOOS != "darwin" {
305 panic("purego: should only be called on darwin")
306 }
307 for offset := uintptr(0); offset < size; offset += 8 {
308 var chunk uintptr
309 remaining := size - offset
310 if remaining >= 8 {
311 chunk = *(*uintptr)(unsafe.Add(ptr, offset))
312 } else {
313 // Read byte-by-byte to avoid reading beyond allocation
314 for i := uintptr(0); i < remaining; i++ {
315 b := *(*byte)(unsafe.Add(ptr, offset+i))
316 chunk |= uintptr(b) << (i * 8)
317 }
318 }
319 addChunk(chunk)
320 }
321 }
322
323 // placeRegisters implements Darwin ARM64 calling convention for struct arguments.
324 //
325 // For HFA/HVA structs, each element must go in a separate register (or stack slot for elements
326 // that don't fit in registers). We use placeRegistersArm64 for this.
327 //
328 // For non-HFA/HVA structs, Darwin uses byte-level packing. We copy the struct memory in
329 // 8-byte chunks, which works correctly for both register and stack placement.
330 func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
331 if runtime.GOOS != "darwin" {
332 panic("purego: placeRegistersDarwin should only be called on darwin")
333 }
334 // Check if this is an HFA/HVA
335 hfa := isHFA(v.Type())
336 hva := isHVA(v.Type())
337
338 // For HFA/HVA structs, use the standard ARM64 logic which places each element separately
339 if hfa || hva {
340 placeRegistersArm64(v, addFloat, addInt)
341 return
342 }
343
344 // For non-HFA/HVA structs, use byte-level copying
345 // If the value is not addressable, create an addressable copy
346 if !v.CanAddr() {
347 addressable := reflect.New(v.Type()).Elem()
348 addressable.Set(v)
349 v = addressable
350 }
351 ptr := unsafe.Pointer(v.Addr().Pointer())
352 size := v.Type().Size()
353 copyStruct8ByteChunks(ptr, size, addInt)
354 }
355
356 // shouldBundleStackArgs determines if we need to start C-style packing for
357 // Darwin ARM64 stack arguments. This happens when registers are exhausted.
358 func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
359 if runtime.GOOS != "darwin" {
360 return false
361 }
362
363 kind := v.Kind()
364 isFloat := kind == reflect.Float32 || kind == reflect.Float64
365 isInt := !isFloat && kind != reflect.Struct
366 primitiveOnStack :=
367 (isInt && numInts >= numOfIntegerRegisters()) ||
368 (isFloat && numFloats >= numOfFloatRegisters())
369 if primitiveOnStack {
370 return true
371 }
372 if kind != reflect.Struct {
373 return false
374 }
375 hfa := isHFA(v.Type())
376 hva := isHVA(v.Type())
377 size := v.Type().Size()
378 eligible := hfa || hva || size <= 16
379 if !eligible {
380 return false
381 }
382
383 if hfa {
384 need := v.NumField()
385 return numFloats+need > numOfFloatRegisters()
386 }
387
388 if hva {
389 need := v.NumField()
390 return numInts+need > numOfIntegerRegisters()
391 }
392
393 slotsNeeded := int((size + align8ByteMask) / align8ByteSize)
394 return numInts+slotsNeeded > numOfIntegerRegisters()
395 }
396
397 // structFitsInRegisters determines if a struct can still fit in remaining
398 // registers, used during stack argument bundling to decide if a struct
399 // should go through normal register allocation or be bundled with stack args.
400 func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
401 if runtime.GOOS != "darwin" {
402 panic("purego: structFitsInRegisters should only be called on darwin")
403 }
404 hfa := isHFA(val.Type())
405 hva := isHVA(val.Type())
406 size := val.Type().Size()
407
408 if hfa {
409 // HFA: check if elements fit in float registers
410 if tempNumFloats+val.NumField() <= numOfFloatRegisters() {
411 return true, tempNumInts, tempNumFloats + val.NumField()
412 }
413 } else if hva {
414 // HVA: check if elements fit in int registers
415 if tempNumInts+val.NumField() <= numOfIntegerRegisters() {
416 return true, tempNumInts + val.NumField(), tempNumFloats
417 }
418 } else if size <= 16 {
419 // Non-HFA/HVA small structs use int registers for byte-packing
420 slotsNeeded := int((size + align8ByteMask) / align8ByteSize)
421 if tempNumInts+slotsNeeded <= numOfIntegerRegisters() {
422 return true, tempNumInts + slotsNeeded, tempNumFloats
423 }
424 }
425
426 return false, tempNumInts, tempNumFloats
427 }
428
429 // collectStackArgs separates remaining arguments into those that fit in registers vs those that go on stack.
430 // It returns the stack arguments and processes register arguments through addValue.
431 func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
432 keepAlive []any, addInt, addFloat, addStack func(uintptr),
433 pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
434 if runtime.GOOS != "darwin" {
435 panic("purego: collectStackArgs should only be called on darwin")
436 }
437
438 var stackArgs []reflect.Value
439 tempNumInts := numInts
440 tempNumFloats := numFloats
441
442 for j, val := range args[startIdx:] {
443 // Determine if this argument goes to register or stack
444 var fitsInRegister bool
445 var newNumInts, newNumFloats int
446
447 if val.Kind() == reflect.Struct {
448 // Check if struct still fits in remaining registers
449 fitsInRegister, newNumInts, newNumFloats = structFitsInRegisters(val, tempNumInts, tempNumFloats)
450 } else {
451 // Primitive argument
452 isFloat := val.Kind() == reflect.Float32 || val.Kind() == reflect.Float64
453 if isFloat {
454 fitsInRegister = tempNumFloats < numOfFloatRegisters()
455 newNumFloats = tempNumFloats + 1
456 newNumInts = tempNumInts
457 } else {
458 fitsInRegister = tempNumInts < numOfIntegerRegisters()
459 newNumInts = tempNumInts + 1
460 newNumFloats = tempNumFloats
461 }
462 }
463
464 if fitsInRegister {
465 // Process through normal register allocation
466 tempNumInts = newNumInts
467 tempNumFloats = newNumFloats
468 keepAlive = addValue(val, keepAlive, addInt, addFloat, addStack, pNumInts, pNumFloats, pNumStack)
469 } else {
470 // Convert strings to C strings before bundling
471 if val.Kind() == reflect.String {
472 ptr := strings.CString(val.String())
473 keepAlive = append(keepAlive, ptr)
474 val = reflect.ValueOf(ptr)
475 args[startIdx+j] = val
476 }
477 stackArgs = append(stackArgs, val)
478 }
479 }
480
481 return stackArgs, keepAlive
482 }
483
484 const (
485 paddingFieldPrefix = "Pad"
486 )
487
488 // bundleStackArgs bundles remaining arguments for Darwin ARM64 C-style stack packing.
489 // It creates a packed struct with proper alignment and copies it to the stack in 8-byte chunks.
490 func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
491 if runtime.GOOS != "darwin" {
492 panic("purego: bundleStackArgs should only be called on darwin")
493 }
494 if len(stackArgs) == 0 {
495 return
496 }
497
498 // Build struct fields with proper C alignment and padding
499 var fields []reflect.StructField
500 currentOffset := uintptr(0)
501 fieldIndex := 0
502
503 for j, val := range stackArgs {
504 valSize := val.Type().Size()
505 valAlign := val.Type().Align()
506
507 // ARM64 requires 8-byte alignment for 8-byte or larger structs
508 if val.Kind() == reflect.Struct && valSize >= 8 {
509 valAlign = 8
510 }
511
512 // Add padding field if needed for alignment
513 if currentOffset%uintptr(valAlign) != 0 {
514 paddingNeeded := uintptr(valAlign) - (currentOffset % uintptr(valAlign))
515 fields = append(fields, reflect.StructField{
516 Name: paddingFieldPrefix + strconv.Itoa(fieldIndex),
517 Type: reflect.ArrayOf(int(paddingNeeded), reflect.TypeOf(byte(0))),
518 })
519 currentOffset += paddingNeeded
520 fieldIndex++
521 }
522
523 fields = append(fields, reflect.StructField{
524 Name: "X" + strconv.Itoa(j),
525 Type: val.Type(),
526 })
527 currentOffset += valSize
528 fieldIndex++
529 }
530
531 // Create and populate the packed struct
532 structType := reflect.StructOf(fields)
533 structInstance := reflect.New(structType).Elem()
534
535 // Set values (skip padding fields)
536 argIndex := 0
537 for j := 0; j < structInstance.NumField(); j++ {
538 fieldName := structType.Field(j).Name
539 if stdstrings.HasPrefix(fieldName, paddingFieldPrefix) {
540 continue
541 }
542 structInstance.Field(j).Set(stackArgs[argIndex])
543 argIndex++
544 }
545
546 ptr := unsafe.Pointer(structInstance.Addr().Pointer())
547 size := structType.Size()
548 copyStruct8ByteChunks(ptr, size, addStack)
549 }
550