1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3
4// TODO: remove s390x cgo dependency once golang/go#77449 is resolved
5//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (cgo && s390x))) || netbsd
6
7package purego
8
9import (
10 "encoding/json"
11 "errors"
12 "fmt"
13 "os"
14 "reflect"
15 "runtime"
16 "runtime/debug"
17 "strings"
18 "sync"
19 "time"
20 "unsafe"
21
22 internalstrings "github.com/bnema/purego/internal/strings"
23)
24
25var syscall15XABI0 uintptr
26
27func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) {
28 args := &syscall15Args{
29 fn: fn,
30 a1: a1, a2: a2, a3: a3, a4: a4, a5: a5, a6: a6, a7: a7, a8: a8,
31 a9: a9, a10: a10, a11: a11, a12: a12, a13: a13, a14: a14, a15: a15,
32 f1: a1, f2: a2, f3: a3, f4: a4, f5: a5, f6: a6, f7: a7, f8: a8,
33 }
34
35 runtime_cgocall(syscall15XABI0, unsafe.Pointer(args))
36 return args.a1, args.a2, args.a3
37}
38
39// NewCallback converts a Go function to a function pointer conforming to the C calling convention.
40// This is useful when interoperating with C code requiring callbacks. The argument is expected to be a
41// function with zero or one uintptr-sized result. The function must not have arguments with size larger than the size
42// of uintptr. Only a limited number of callbacks may be created in a single Go process, and any memory allocated
43// for these callbacks is never released. At least 2000 callbacks can always be created. Although this function
44// provides similar functionality to windows.NewCallback it is distinct.
45func NewCallback(fn any) uintptr {
46 ty := reflect.TypeOf(fn)
47 for i := 0; i < ty.NumIn(); i++ {
48 in := ty.In(i)
49 if !in.AssignableTo(reflect.TypeOf(CDecl{})) {
50 continue
51 }
52 if i != 0 {
53 panic("purego: CDecl must be the first argument")
54 }
55 }
56 return compileCallback(fn)
57}
58
59// NewCallbackFnPtr converts a Go function pointer to a function pointer conforming to the C calling convention.
60// Calling this function multiple times with the same function pointer returns the original callback address.
61func NewCallbackFnPtr(fnPtr any) uintptr {
62 val := reflect.ValueOf(fnPtr)
63 if val.IsNil() {
64 panic("purego: function must not be nil")
65 }
66 if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
67 panic("purego: the type must be a function pointer but was not")
68 }
69 if addr, idx, ok := getCallbackByFnPtr(val); ok {
70 cbs.lock.Lock()
71 recordCallbackLedger("dedupe", idx, addr, val.Pointer(), val.Elem(), false, "")
72 cbs.lock.Unlock()
73 return addr
74 }
75 addr := compileCallback(val.Elem().Interface())
76 cbs.lock.Lock()
77 cbs.knownFnPtr[val.Pointer()] = addr
78 if idx, ok := cbs.knownIdx[addr]; ok {
79 cbs.fnPtrKeys[idx] = val.Pointer()
80 recordCallbackLedger("fnptr-bind", idx, addr, val.Pointer(), val.Elem(), false, "")
81 }
82 cbs.lock.Unlock()
83 return addr
84}
85
86// UnrefCallback unreferences a callback created by NewCallback or NewCallbackFnPtr and frees its slot.
87func UnrefCallback(cb uintptr) error {
88 cbs.lock.Lock()
89 defer cbs.lock.Unlock()
90 idx, ok := cbs.knownIdx[cb]
91 if !ok {
92 recordCallbackLedger("release-miss", -1, cb, 0, reflect.Value{}, false, "callback not found")
93 return errors.New("callback not found")
94 }
95 val := cbs.funcs[idx]
96 key := cbs.fnPtrKeys[idx]
97 if key != 0 {
98 delete(cbs.knownFnPtr, key)
99 cbs.fnPtrKeys[idx] = 0
100 }
101 delete(cbs.knownIdx, cb)
102 cbs.holes[idx] = struct{}{}
103 cbs.funcs[idx] = reflect.Value{}
104 cbs.argPools[idx] = nil
105 recordCallbackLedger("release", idx, cb, key, val, false, "")
106 return nil
107}
108
109// UnrefCallbackFnPtr unreferences a callback previously created via NewCallbackFnPtr.
110func UnrefCallbackFnPtr(fnPtr any) error {
111 val := reflect.ValueOf(fnPtr)
112 if val.IsNil() {
113 panic("purego: function must not be nil")
114 }
115 if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Func {
116 panic("purego: the type must be a function pointer but was not")
117 }
118 cbs.lock.Lock()
119 defer cbs.lock.Unlock()
120 addr, ok := cbs.knownFnPtr[val.Pointer()]
121 if !ok {
122 recordCallbackLedger("release-fnptr-miss", -1, 0, val.Pointer(), val.Elem(), false, "callback not found")
123 return errors.New("callback not found")
124 }
125 idx, ok := cbs.knownIdx[addr]
126 if !ok {
127 delete(cbs.knownFnPtr, val.Pointer())
128 recordCallbackLedger("release-fnptr-miss", -1, addr, val.Pointer(), val.Elem(), false, "callback not found")
129 return errors.New("callback not found")
130 }
131 callbackVal := cbs.funcs[idx]
132 delete(cbs.knownFnPtr, val.Pointer())
133 delete(cbs.knownIdx, addr)
134 cbs.fnPtrKeys[idx] = 0
135 cbs.holes[idx] = struct{}{}
136 cbs.funcs[idx] = reflect.Value{}
137 cbs.argPools[idx] = nil
138 recordCallbackLedger("release-fnptr", idx, addr, val.Pointer(), callbackVal, false, "")
139 return nil
140}
141
142// maxCb is the maximum number of callbacks
143// only increase this if you have added more to the callbackasm function
144const maxCB = 2000
145
146var cbs = struct {
147 lock sync.RWMutex
148 numFn int // the highest allocated callback index + 1
149 holes map[int]struct{} // reusable callback slots
150 funcs [maxCB]reflect.Value // the saved callbacks
151 argPools [maxCB]*sync.Pool // pre-allocated argument buffers per callback
152 knownIdx map[uintptr]int // callback address -> slot index
153 knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address
154 fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address
155 ledgerSeq uint64 // monotonic callback ledger event sequence
156}{
157 holes: make(map[int]struct{}),
158 knownIdx: make(map[uintptr]int, maxCB),
159 knownFnPtr: make(map[uintptr]uintptr, maxCB),
160}
161
162func getCallbackByFnPtr(val reflect.Value) (uintptr, int, bool) {
163 cbs.lock.RLock()
164 defer cbs.lock.RUnlock()
165 addr, ok := cbs.knownFnPtr[val.Pointer()]
166 if !ok {
167 return 0, -1, false
168 }
169 idx, ok := cbs.knownIdx[addr]
170 if !ok {
171 return addr, -1, true
172 }
173 return addr, idx, true
174}
175
176type callbackLedgerEvent struct {
177 Marker string `json:"marker"`
178 PID int `json:"pid"`
179 Seq uint64 `json:"seq"`
180 Time string `json:"time"`
181 Event string `json:"event"`
182 Index int `json:"index"`
183 Addr string `json:"addr,omitempty"`
184 FnPtrKey string `json:"fn_ptr_key,omitempty"`
185 Type string `json:"type,omitempty"`
186 Family string `json:"family"`
187 StackKey string `json:"stack_key"`
188 NumFn int `json:"num_fn"`
189 Occupied int `json:"occupied"`
190 Reusable int `json:"reusable"`
191 KnownFnPtr int `json:"known_fn_ptr"`
192 Remaining int `json:"remaining"`
193 Max int `json:"max"`
194 ReusedSlot bool `json:"reused_slot,omitempty"`
195 Stack string `json:"stack,omitempty"`
196 Error string `json:"error,omitempty"`
197}
198
199func callbackEnvEnabled(name string) bool {
200 switch strings.ToLower(os.Getenv(name)) {
201 case "1", "true", "yes", "on":
202 return true
203 default:
204 return false
205 }
206}
207
208func callbackLedgerEnabled() bool {
209 return callbackEnvEnabled("PUREGO_CALLBACK_LEDGER")
210}
211
212func callbackLedgerStackEnabled() bool {
213 return callbackEnvEnabled("PUREGO_CALLBACK_LEDGER_STACK")
214}
215
216func callbackTraceEnabled() bool {
217 return callbackEnvEnabled("PUREGO_CALLBACK_TRACE")
218}
219
220func callbackLedgerPath() string {
221 if path := os.Getenv("PUREGO_CALLBACK_LEDGER_FILE"); path != "" {
222 return path
223 }
224 return fmt.Sprintf("/tmp/purego-callback-ledger-%d.jsonl", os.Getpid())
225}
226
227func typeName(val reflect.Value) string {
228 if !val.IsValid() {
229 return ""
230 }
231 return val.Type().String()
232}
233
234func hexUintptr(v uintptr) string {
235 if v == 0 {
236 return ""
237 }
238 return fmt.Sprintf("0x%x", v)
239}
240
241func classifyCallbackStack(stack string) (family, stackKey string) {
242 family = "unknown"
243 stackKey = "unknown"
244 lines := strings.Split(stack, "\n")
245 for i, line := range lines {
246 if line == "" || strings.HasPrefix(line, "goroutine ") || strings.Contains(line, "runtime/debug.Stack") || strings.Contains(line, "recordCallbackLedger") || strings.Contains(line, "traceCallbackAllocation") {
247 continue
248 }
249 if strings.Contains(line, "github.com/bnema/purego.") || strings.Contains(line, "github.com/ebitengine/purego.") {
250 continue
251 }
252 if strings.HasPrefix(strings.TrimSpace(line), "/") || strings.HasPrefix(strings.TrimSpace(line), "../") {
253 continue
254 }
255 stackKey = strings.TrimSpace(line)
256 for j := i + 1; j < len(lines); j++ {
257 fileLine := strings.TrimSpace(lines[j])
258 if fileLine == "" {
259 continue
260 }
261 family = classifyCallbackFamily(fileLine + " " + stackKey)
262 return family, stackKey
263 }
264 family = classifyCallbackFamily(stackKey)
265 return family, stackKey
266 }
267 return family, stackKey
268}
269
270func classifyCallbackFamily(s string) string {
271 switch {
272 case strings.Contains(s, "purego-cef2gtk"):
273 return "purego-cef2gtk"
274 case strings.Contains(s, "purego-cef"):
275 return "purego-cef"
276 case strings.Contains(s, "puregotk"):
277 return "puregotk"
278 case strings.Contains(s, "dumber"):
279 return "dumber"
280 case strings.Contains(s, "github.com/bnema/purego") || strings.Contains(s, "/purego/"):
281 return "purego"
282 default:
283 return "unknown"
284 }
285}
286
287func recordCallbackLedger(event string, idx int, addr uintptr, fnPtrKey uintptr, val reflect.Value, reused bool, errText string) {
288 if !callbackLedgerEnabled() {
289 return
290 }
291 stack := ""
292 family := "unknown"
293 stackKey := "unknown"
294 if callbackLedgerStackEnabled() {
295 stack = string(debug.Stack())
296 family, stackKey = classifyCallbackStack(stack)
297 }
298 cbs.ledgerSeq++
299 occupied := cbs.numFn - len(cbs.holes)
300 entry := callbackLedgerEvent{
301 Marker: "PUREGO-CALLBACK-LEDGER",
302 PID: os.Getpid(),
303 Seq: cbs.ledgerSeq,
304 Time: time.Now().Format(time.RFC3339Nano),
305 Event: event,
306 Index: idx,
307 Addr: hexUintptr(addr),
308 FnPtrKey: hexUintptr(fnPtrKey),
309 Type: typeName(val),
310 Family: family,
311 StackKey: stackKey,
312 NumFn: cbs.numFn,
313 Occupied: occupied,
314 Reusable: len(cbs.holes),
315 KnownFnPtr: len(cbs.knownFnPtr),
316 Remaining: maxCB - occupied,
317 Max: maxCB,
318 ReusedSlot: reused,
319 Error: errText,
320 }
321 if callbackLedgerStackEnabled() {
322 entry.Stack = stack
323 }
324 payload, err := json.Marshal(entry)
325 if err != nil {
326 return
327 }
328 payload = append(payload, '\n')
329 if f, err := os.OpenFile(callbackLedgerPath(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); err == nil {
330 _, _ = f.Write(payload)
331 _ = f.Close()
332 }
333}
334
335func traceCallbackAllocation(event string, val reflect.Value, remaining int) {
336 if !callbackTraceEnabled() {
337 return
338 }
339 message := fmt.Sprintf("PUREGO-CALLBACK-TRACE event=%s remaining=%d live=%d type=%s\n%s\n", event, remaining, maxCB-remaining, val.Type(), debug.Stack())
340 fmt.Fprint(os.Stderr, message)
341 traceFile := os.Getenv("PUREGO_CALLBACK_TRACE_FILE")
342 if traceFile == "" {
343 traceFile = fmt.Sprintf("/tmp/purego-callback-trace-%d.log", os.Getpid())
344 }
345 // traceCallbackAllocation intentionally accepts the opt-in
346 // PUREGO_CALLBACK_TRACE_FILE path. Users must set PUREGO_CALLBACK_TRACE=1 to
347 // enable tracing, and callers who override the path control their debugging
348 // environment. This documents the deliberate gosec G304 tradeoff for audits.
349 if f, err := os.OpenFile(traceFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); err == nil {
350 _, _ = f.WriteString(message)
351 _ = f.Close()
352 }
353}
354
355func compileCallback(fn any) uintptr {
356 val := reflect.ValueOf(fn)
357 if val.Kind() != reflect.Func {
358 panic("purego: the type must be a function but was not")
359 }
360 if val.IsNil() {
361 panic("purego: function must not be nil")
362 }
363 ty := val.Type()
364 for i := 0; i < ty.NumIn(); i++ {
365 in := ty.In(i)
366 switch in.Kind() {
367 case reflect.Struct:
368 if i == 0 && in.AssignableTo(reflect.TypeOf(CDecl{})) {
369 continue
370 }
371 ensureStructSupported()
372 checkStructFieldsSupported(in)
373 continue
374 case reflect.Interface, reflect.Func, reflect.Slice,
375 reflect.Chan, reflect.Complex64, reflect.Complex128,
376 reflect.Map, reflect.Invalid:
377 panic("purego: unsupported argument type: " + in.Kind().String())
378 }
379 }
380output:
381 switch {
382 case ty.NumOut() == 1:
383 switch ty.Out(0).Kind() {
384 case reflect.Pointer, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
385 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
386 reflect.Bool, reflect.UnsafePointer, reflect.Struct:
387 break output
388 }
389 panic("purego: unsupported return type: " + ty.String())
390 case ty.NumOut() > 1:
391 panic("purego: callbacks can only have one return")
392 }
393 cbs.lock.Lock()
394 defer cbs.lock.Unlock()
395 index := -1
396 reused := false
397 for i := range cbs.holes {
398 index = i
399 delete(cbs.holes, i)
400 reused = true
401 break
402 }
403 if index < 0 {
404 remaining := maxCB - cbs.numFn
405 if remaining <= 0 {
406 recordCallbackLedger("exhausted", -1, 0, 0, val, false, "maximum callbacks reached")
407 traceCallbackAllocation("exhausted", val, 0)
408 panic("purego: the maximum number of callbacks has been reached")
409 }
410 if remaining <= 100 || remaining%250 == 0 {
411 traceCallbackAllocation("allocated", val, remaining)
412 }
413 index = cbs.numFn
414 cbs.numFn++
415 }
416 cbs.funcs[index] = val
417 numIn := ty.NumIn()
418 cbs.argPools[index] = &sync.Pool{
419 New: func() any {
420 return make([]reflect.Value, numIn)
421 },
422 }
423 addr := callbackasmAddr(index)
424 cbs.knownIdx[addr] = index
425 if reused {
426 recordCallbackLedger("reuse", index, addr, 0, val, true, "")
427 } else {
428 recordCallbackLedger("alloc", index, addr, 0, val, false, "")
429 }
430 return addr
431}
432
433const ptrSize = unsafe.Sizeof((*int)(nil))
434
435const callbackMaxFrame = 64 * ptrSize
436
437// callbackasm is implemented in zcallback_GOOS_GOARCH.s
438//
439//go:linkname __callbackasm callbackasm
440var __callbackasm byte
441var callbackasmABI0 = uintptr(unsafe.Pointer(&__callbackasm))
442
443// callbackWrap_call allows the calling of the ABIInternal wrapper
444// which is required for runtime.cgocallback without the
445// <ABIInternal> tag which is only allowed in the runtime.
446// This closure is used inside sys_darwin_GOARCH.s
447var callbackWrap_call = callbackWrap
448
449// callbackWrap is called by assembly code which determines which Go function to call.
450// This function takes the arguments and passes them to the Go function and returns the result.
451func callbackWrap(a *callbackArgs) {
452 cbs.lock.RLock()
453 fn := cbs.funcs[a.index]
454 pool := cbs.argPools[a.index]
455 cbs.lock.RUnlock()
456 fnType := fn.Type()
457 args := pool.Get().([]reflect.Value)
458 defer func() {
459 for i := range args {
460 args[i] = reflect.Value{}
461 }
462 pool.Put(args)
463 }()
464 frame := (*[callbackMaxFrame]uintptr)(a.args)
465 // stackFrame points to stack-passed arguments. On most architectures this is
466 // contiguous with frame (after register args), but on ppc64le it's separate.
467 var stackFrame *[callbackMaxFrame]uintptr
468 if sf := a.stackFrame(); sf != nil {
469 // Only ppc64le uses separate stackArgs pointer due to NOSPLIT constraints
470 stackFrame = (*[callbackMaxFrame]uintptr)(sf)
471 }
472 // floatsN and intsN track the number of register slots used, not argument count.
473 // This distinction matters on ARM32 where float64 uses 2 slots (32-bit registers).
474 var floatsN int
475 var intsN int
476 // On amd64/loong64/ppc64le/riscv64/s390x, when returning a struct larger than
477 // maxRegAllocStructSize, the caller passes a hidden pointer in the first integer
478 // register. Skip it to avoid misreading it as the first function argument.
479 if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") &&
480 fnType.NumOut() == 1 && fnType.Out(0).Kind() == reflect.Struct &&
481 fnType.Out(0).Size() > maxRegAllocStructSize {
482 intsN = 1
483 }
484 // stackSlot points to the index into frame (or stackFrame) of the current stack element.
485 // When stackFrame is nil, stack begins after float and integer registers in frame.
486 // When stackFrame is not nil (ppc64le), stackSlot indexes into stackFrame starting at 0.
487 stackSlot := numOfIntegerRegisters() + numOfFloatRegisters()
488 if stackFrame != nil {
489 // ppc64le: stackArgs is a separate pointer, indices start at 0
490 stackSlot = 0
491 }
492 // stackByteOffset tracks the byte offset within the stack area for Darwin ARM64
493 // tight packing. On Darwin ARM64, C passes small types packed on the stack.
494 stackByteOffset := uintptr(0)
495 for i := range args {
496 // slots is the number of pointer-sized slots the argument takes
497 var slots int
498 inType := fnType.In(i)
499 switch inType.Kind() {
500 case reflect.Float32, reflect.Float64:
501 slots = int((fnType.In(i).Size() + ptrSize - 1) / ptrSize)
502 if floatsN+slots > numOfFloatRegisters() {
503 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
504 // Darwin ARM64: read from packed stack with proper alignment
505 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
506 } else if stackFrame != nil {
507 // ppc64le/s390x: stack args are in separate stackFrame
508 if runtime.GOARCH == "s390x" {
509 // s390x big-endian: sub-8-byte values are right-justified
510 args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&stackFrame[stackSlot]), inType)
511 } else {
512 args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()
513 }
514 stackSlot += slots
515 } else {
516 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
517 stackSlot += slots
518 }
519 } else {
520 if runtime.GOARCH == "s390x" {
521 // s390x big-endian: float32 is right-justified in 8-byte FPR slot
522 args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&frame[floatsN]), inType)
523 } else {
524 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()
525 }
526 }
527 floatsN += slots
528 case reflect.String:
529 slots = 1
530 var ptr uintptr
531 if intsN+slots > numOfIntegerRegisters() {
532 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
533 ptr = uintptr(callbackArgFromStack(a.args, stackSlot, &stackByteOffset, reflect.TypeOf(uintptr(0))).Uint())
534 } else if stackFrame != nil {
535 if runtime.GOARCH == "s390x" {
536 ptr = uintptr(callbackArgFromSlotBigEndian(unsafe.Pointer(&stackFrame[stackSlot]), reflect.TypeOf(uintptr(0))).Uint())
537 } else {
538 ptr = *(*uintptr)(unsafe.Pointer(&stackFrame[stackSlot]))
539 }
540 stackSlot += slots
541 } else {
542 ptr = frame[stackSlot]
543 stackSlot += slots
544 }
545 } else {
546 pos := intsN + numOfFloatRegisters()
547 ptr = frame[pos]
548 }
549 intsN += slots
550 args[i] = reflect.ValueOf(internalstrings.GoString(ptr))
551 continue
552 case reflect.Struct:
553 if i == 0 && inType.AssignableTo(reflect.TypeOf(CDecl{})) {
554 args[i] = reflect.Zero(inType)
555 continue
556 }
557 if inType.Size() == 0 {
558 args[i] = reflect.New(inType).Elem()
559 continue
560 }
561 args[i] = getCallbackStruct(inType, a.args, &floatsN, &intsN, &stackSlot, &stackByteOffset)
562 continue
563 default:
564 slots = int((inType.Size() + ptrSize - 1) / ptrSize)
565 if intsN+slots > numOfIntegerRegisters() {
566 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
567 // Darwin ARM64: read from packed stack with proper alignment
568 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
569 } else if stackFrame != nil {
570 // ppc64le/s390x: stack args are in separate stackFrame
571 if runtime.GOARCH == "s390x" {
572 // s390x big-endian: sub-8-byte values are right-justified
573 args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&stackFrame[stackSlot]), inType)
574 } else {
575 args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()
576 }
577 stackSlot += slots
578 } else {
579 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
580 stackSlot += slots
581 }
582 } else {
583 // the integers begin after the floats in frame
584 pos := intsN + numOfFloatRegisters()
585 if runtime.GOARCH == "s390x" {
586 // s390x big-endian: sub-8-byte values are right-justified in GPR slot
587 args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&frame[pos]), inType)
588 } else {
589 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()
590 }
591 }
592 intsN += slots
593 }
594 }
595 ret := fn.Call(args)
596 if len(ret) > 0 {
597 switch k := ret[0].Kind(); k {
598 case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uintptr:
599 a.result[0] = uintptr(ret[0].Uint())
600 case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
601 a.result[0] = uintptr(ret[0].Int())
602 case reflect.Bool:
603 if ret[0].Bool() {
604 a.result[0] = 1
605 } else {
606 a.result[0] = 0
607 }
608 case reflect.Pointer:
609 a.result[0] = ret[0].Pointer()
610 case reflect.UnsafePointer:
611 a.result[0] = ret[0].Pointer()
612 case reflect.Struct:
613 setStruct(a, ret[0])
614 default:
615 panic("purego: unsupported kind: " + k.String())
616 }
617 }
618}
619
620// callbackArgFromStack reads an argument from the tightly-packed stack area on Darwin ARM64.
621// The C ABI on Darwin ARM64 packs small types on the stack without padding to 8 bytes.
622// This function handles proper alignment and advances stackByteOffset accordingly.
623func callbackArgFromStack(argsBase unsafe.Pointer, stackSlot int, stackByteOffset *uintptr, inType reflect.Type) reflect.Value {
624 // Calculate base address of stack area (after float and int registers)
625 stackBase := unsafe.Add(argsBase, stackSlot*int(ptrSize))
626
627 // Get type's natural alignment
628 align := uintptr(inType.Align())
629 size := inType.Size()
630
631 // Align the offset
632 if *stackByteOffset%align != 0 {
633 *stackByteOffset = (*stackByteOffset + align - 1) &^ (align - 1)
634 }
635
636 // Read value at aligned offset
637 ptr := unsafe.Add(stackBase, *stackByteOffset)
638 *stackByteOffset += size
639
640 return reflect.NewAt(inType, ptr).Elem()
641}
642
643// callbackArgFromSlotBigEndian reads an argument from an 8-byte slot on big-endian architectures.
644// On s390x:
645// - Integer types are right-justified in GPRs: sub-8-byte values are at offset (8 - size)
646// - Float32 in FPRs is left-justified: stored in upper 32 bits, so at offset 0
647// - Float64 occupies the full 8-byte slot
648func callbackArgFromSlotBigEndian(slotPtr unsafe.Pointer, inType reflect.Type) reflect.Value {
649 size := inType.Size()
650 if size >= 8 {
651 // 8-byte values occupy the entire slot
652 return reflect.NewAt(inType, slotPtr).Elem()
653 }
654 // Float32 is left-justified in FPRs (upper 32 bits), so offset is 0
655 if inType.Kind() == reflect.Float32 {
656 return reflect.NewAt(inType, slotPtr).Elem()
657 }
658 // Integer types are right-justified: offset = 8 - size
659 offset := 8 - size
660 ptr := unsafe.Add(slotPtr, offset)
661 return reflect.NewAt(inType, ptr).Elem()
662}
663
664// callbackasmAddr returns address of runtime.callbackasm
665// function adjusted by i.
666// On x86 and amd64, runtime.callbackasm is a series of CALL instructions,
667// and we want callback to arrive at
668// correspondent call instruction instead of start of
669// runtime.callbackasm.
670// On ARM, runtime.callbackasm is a series of mov and branch instructions.
671// R12 is loaded with the callback index. Each entry is two instructions,
672// hence 8 bytes.
673func callbackasmAddr(i int) uintptr {
674 var entrySize int
675 switch runtime.GOARCH {
676 default:
677 panic("purego: unsupported architecture")
678 case "amd64":
679 // On amd64, each callback entry is just a CALL instruction (5 bytes)
680 entrySize = 5
681 case "386":
682 // On 386, each callback entry is MOVL $imm, CX (5 bytes) + JMP (5 bytes)
683 entrySize = 10
684 case "arm", "arm64", "loong64", "ppc64le", "riscv64":
685 // On ARM, ARM64, Loong64, PPC64LE and RISCV64, each entry is a MOV instruction
686 // followed by a branch instruction
687 entrySize = 8
688 case "s390x":
689 // On S390X, each entry is LGHI (4 bytes) + JG (6 bytes)
690 entrySize = 10
691 }
692 return callbackasmABI0 + uintptr(i*entrySize)
693}