From 6846ad2a748b20fb1b26ee4ab2c30b52e32f121b Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 5 Jul 2026 23:54:37 +0900 Subject: [PATCH 1/7] purego: support structs on loong64, ppc64le, riscv64, s390x Struct arguments and return values can now be passed to and received from C functions via RegisterFunc and RegisterLibFunc on linux/loong64, linux/ppc64le, linux/riscv64, and linux/s390x. The per-architecture addStruct and getStruct implementations already existed but were unreachable behind a guard that only permitted amd64 and arm64. The guard is now direction-aware. ensureStructSupported gates the forward call path (RegisterFunc), while the new ensureCallbackStructSupported keeps the callback path (NewCallback) restricted to amd64 and arm64, where getCallbackStruct and setStruct are implemented. Struct callbacks therefore remain unsupported on the newly enabled architectures. The struct tests are un-gated for these architectures, with the callback-based variants skipped where callbacks are unsupported. 32-bit arm and 386 stay unsupported: arm's EABI struct handling is still a placeholder and the tests assume a 64-bit int. Co-authored-by: Claude Opus 4.8 --- README.md | 2 +- func.go | 22 ++++++++++++++++++++-- struct_test.go | 12 +++++++++++- syscall_unix.go | 2 +- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3239ab50..35552c30 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o - **Android**: 3861,3, arm1,3 - **FreeBSD**: amd643,4, arm643,4 -- **Linux**: 3863, arm3, loong643, ppc64le3, riscv643, s390x3, 5 +- **Linux**: 3863, arm3, loong642, ppc64le2, riscv642, s390x2, 5 - **NetBSD**: amd643,4, arm643,4 - **Windows**: 3863,6, arm3,6,7 diff --git a/func.go b/func.go index 8d3a6f7e..44f392a5 100644 --- a/func.go +++ b/func.go @@ -500,9 +500,13 @@ func checkStructFieldsSupported(ty reflect.Type) { } } +// ensureStructSupported panics if passing or returning structs through a call to +// a C function is unsupported on the current platform. func ensureStructSupported() { - if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { - panic("purego: struct arguments/returns are only supported on amd64 and arm64") + switch runtime.GOARCH { + case "amd64", "arm64", "loong64", "ppc64le", "riscv64", "s390x": + default: + panic("purego: struct arguments/returns are only supported on amd64, arm64, loong64, ppc64le, riscv64, and s390x") } switch runtime.GOOS { case "android", "darwin", "ios", "linux", "windows": @@ -511,6 +515,20 @@ func ensureStructSupported() { } } +// ensureCallbackStructSupported panics if passing or returning structs through a +// callback is unsupported on the current platform. Callbacks support structs on +// fewer architectures than a direct call to a C function. +func ensureCallbackStructSupported() { + if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { + panic("purego: struct arguments/returns in callbacks are only supported on amd64 and arm64") + } + switch runtime.GOOS { + case "android", "darwin", "ios", "linux", "windows": + default: + panic("purego: struct arguments/returns in callbacks are only supported on android, darwin, ios, linux, and windows") + } +} + // isDarwin is true on platforms that use Apple's calling convention. // iOS (GOOS=ios) shares it with macOS (GOOS=darwin). const isDarwin = runtime.GOOS == "darwin" || runtime.GOOS == "ios" diff --git a/struct_test.go b/struct_test.go index 14ed0584..a0da5403 100644 --- a/struct_test.go +++ b/struct_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2024 The Ebitengine Authors -//go:build (darwin || linux || windows) && (amd64 || arm64) +//go:build (darwin || linux || windows) && (amd64 || arm64 || loong64 || ppc64le || riscv64 || s390x) package purego_test @@ -83,6 +83,11 @@ func TestRegisterFunc_structArgs(t *testing.T) { // not support struct arguments or returns. continue } + if imp.usesCallbacks && runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { + // Struct arguments and returns in callbacks are only supported on + // amd64 and arm64. + continue + } t.Run(imp.name, func(t *testing.T) { register := imp.register { @@ -908,6 +913,11 @@ func TestRegisterFunc_structReturns(t *testing.T) { // not support struct arguments or returns. continue } + if imp.usesCallbacks && runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { + // Struct arguments and returns in callbacks are only supported on + // amd64 and arm64. + continue + } t.Run(imp.name, func(t *testing.T) { register := imp.register { diff --git a/syscall_unix.go b/syscall_unix.go index 7a7f9e7e..6e281ab4 100644 --- a/syscall_unix.go +++ b/syscall_unix.go @@ -64,7 +64,7 @@ func compileCallback(fn any) uintptr { if i == 0 && in.AssignableTo(reflect.TypeFor[CDecl]()) { continue } - ensureStructSupported() + ensureCallbackStructSupported() checkStructFieldsSupported(in) continue case reflect.Interface, reflect.Func, reflect.Slice, From f32bed0faef2b5b0ac21aa291eb256fb34ec3fc9 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 6 Jul 2026 00:32:03 +0900 Subject: [PATCH 2/7] purego: fix loong64 struct float-aggregate ABI Passing and returning structs on loong64 mishandled floating-point aggregates. getStruct did not mask the NaN-boxed upper bits of a single-precision value read back from an FP register, so a struct of two float32 came back with a corrupted second field. addStruct placed every floating-point field in its own FP register, so a struct with three or more float members was passed in FP registers instead of the integer registers the LoongArch hard-float ABI requires. Replace the isAllSameFloat heuristic with a classifier that flattens nested structs and arrays and applies the LoongArch rule: an aggregate uses FP registers only when it has one or two floating-point members and nothing else, or exactly one floating-point member with one integer member. Every other aggregate uses the integer calling convention. This also fixes aggregates of two differently sized floating-point members such as a float32 and a float64, which the old heuristic misclassified. Also validate struct return types when creating a callback so an unsupported signature fails at NewCallback time instead of panicking later when the callback is invoked. Co-authored-by: Claude Opus 4.8 --- struct_loong64.go | 305 ++++++++++++++++++++++++---------------------- syscall_unix.go | 6 +- 2 files changed, 163 insertions(+), 148 deletions(-) diff --git a/struct_loong64.go b/struct_loong64.go index 7a8a7c7c..211cc380 100644 --- a/struct_loong64.go +++ b/struct_loong64.go @@ -4,177 +4,188 @@ package purego import ( - "math" "reflect" "unsafe" ) -func getStruct(outType reflect.Type, syscall syscallArgs) (v reflect.Value) { - outSize := outType.Size() +// loongLeaf is a scalar member of an aggregate after flattening nested structs +// and arrays. offset is the byte offset of the member within the aggregate. +type loongLeaf struct { + isFloat bool + kind reflect.Kind + offset uintptr + size uintptr +} + +// loongFlatten appends the scalar leaves of t to leaves, offsetting each member +// by base. Nested structs and arrays are expanded into their members. +func loongFlatten(t reflect.Type, base uintptr, leaves *[]loongLeaf) { + switch t.Kind() { + case reflect.Struct: + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + loongFlatten(f.Type, base+f.Offset, leaves) + } + case reflect.Array: + elem := t.Elem() + for i := 0; i < t.Len(); i++ { + loongFlatten(elem, base+uintptr(i)*elem.Size(), leaves) + } + default: + k := t.Kind() + *leaves = append(*leaves, loongLeaf{ + isFloat: k == reflect.Float32 || k == reflect.Float64, + kind: k, + offset: base, + size: t.Size(), + }) + } +} + +// loongClassify flattens t and reports whether it is passed and returned through +// the floating-point calling convention. Under the LoongArch hard-float ABI an +// aggregate uses FP registers only when, after flattening, it has one or two +// floating-point members and nothing else, or exactly one floating-point member +// together with one integer member. +func loongClassify(t reflect.Type) (leaves []loongLeaf, useFP bool) { + loongFlatten(t, 0, &leaves) + var floats, ints int + for _, l := range leaves { + if l.isFloat { + floats++ + } else { + ints++ + } + } switch { - case outSize == 0: + case ints == 0 && (floats == 1 || floats == 2): + return leaves, true + case ints == 1 && floats == 1: + return leaves, true + default: + return leaves, false + } +} + +func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value { + outSize := outType.Size() + if outSize == 0 { return reflect.New(outType).Elem() - case outSize <= 8: - r1 := syscall.a1 - if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats { - r1 = syscall.f1 - if numFields == 2 { - r1 = syscall.f2<<32 | syscall.f1 + } + if outSize > 16 { + // Returned indirectly through a pointer in the first integer register. + return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem() + } + + var buf [16]byte + base := unsafe.Pointer(&buf[0]) + if leaves, useFP := loongClassify(outType); useFP { + floatRegs := [2]uintptr{syscall.f1, syscall.f2} + intRegs := [2]uintptr{syscall.a1, syscall.a2} + var fi, ii int + for _, l := range leaves { + dst := unsafe.Add(base, l.offset) + if l.isFloat { + r := floatRegs[fi] + fi++ + if l.kind == reflect.Float32 { + // A single-precision value is NaN-boxed in the register. + *(*uint32)(dst) = uint32(r) + } else { + *(*uint64)(dst) = uint64(r) + } + } else { + loongStoreInt(dst, l.size, intRegs[ii]) + ii++ } } - return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{r1})).Elem() - case outSize <= 16: - r1, r2 := syscall.a1, syscall.a2 - if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats { - switch numFields { - case 4: - r1 = syscall.f2<<32 | syscall.f1 - r2 = syscall.f4<<32 | syscall.f3 - case 3: - r1 = syscall.f2<<32 | syscall.f1 - r2 = syscall.f3 - case 2: - r1 = syscall.f1 - r2 = syscall.f2 - default: - panic("not reached") - } + } else { + *(*uintptr)(base) = syscall.a1 + if outSize > 8 { + *(*uintptr)(unsafe.Add(base, 8)) = syscall.a2 } - return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem() - default: - // create struct from the Go pointer created above - // weird pointer dereference to circumvent go vet - return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem() } + return reflect.NewAt(outType, base).Elem() } -const ( - _NO_CLASS = 0b00 - _FLOAT = 0b01 - _INT = 0b11 -) +func loongStoreInt(dst unsafe.Pointer, size uintptr, r uintptr) { + switch size { + case 1: + *(*uint8)(dst) = uint8(r) + case 2: + *(*uint16)(dst) = uint16(r) + case 4: + *(*uint32)(dst) = uint32(r) + default: + *(*uint64)(dst) = uint64(r) + } +} func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { - if v.Type().Size() == 0 { + size := v.Type().Size() + if size == 0 { return keepAlive } + if size > 16 { + return placeStack(v, keepAlive, addInt) + } - if size := v.Type().Size(); size <= 16 { - placeRegisters(v, addFloat, addInt) + var ptr unsafe.Pointer + if v.CanAddr() { + ptr = v.Addr().UnsafePointer() } else { - keepAlive = placeStack(v, keepAlive, addInt) + tmp := reflect.New(v.Type()) + tmp.Elem().Set(v) + ptr = tmp.UnsafePointer() + keepAlive = append(keepAlive, tmp.Interface()) } - return keepAlive // the struct was allocated so don't panic -} -func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { - var val uint64 - var shift byte - var flushed bool - class := _NO_CLASS - var place func(v reflect.Value) - place = func(v reflect.Value) { - var numFields int - if v.Kind() == reflect.Struct { - numFields = v.Type().NumField() - } else { - numFields = v.Type().Len() - } - for k := 0; k < numFields; k++ { - flushed = false - var f reflect.Value - if v.Kind() == reflect.Struct { - f = v.Field(k) - } else { - f = v.Index(k) - } - align := byte(f.Type().Align()*8 - 1) - shift = (shift + align) &^ align - if shift >= 64 { - shift = 0 - flushed = true - if class == _FLOAT { - addFloat(uintptr(val)) - } else { - addInt(uintptr(val)) - } - } - switch f.Type().Kind() { - case reflect.Struct: - place(f) - case reflect.Bool: - if f.Bool() { - val |= 1 << shift - } - shift += 8 - class |= _INT - case reflect.Uint8: - val |= f.Uint() << shift - shift += 8 - class |= _INT - case reflect.Uint16: - val |= f.Uint() << shift - shift += 16 - class |= _INT - case reflect.Uint32: - val |= f.Uint() << shift - shift += 32 - class |= _INT - case reflect.Uint64, reflect.Uint, reflect.Uintptr: - addInt(uintptr(f.Uint())) - shift = 0 - flushed = true - class = _NO_CLASS - case reflect.Int8: - val |= uint64(f.Int()&0xFF) << shift - shift += 8 - class |= _INT - case reflect.Int16: - val |= uint64(f.Int()&0xFFFF) << shift - shift += 16 - class |= _INT - case reflect.Int32: - val |= uint64(f.Int()&0xFFFF_FFFF) << shift - shift += 32 - class |= _INT - case reflect.Int64, reflect.Int: - addInt(uintptr(f.Int())) - shift = 0 - flushed = true - class = _NO_CLASS - case reflect.Float32: - if class == _FLOAT { - addFloat(uintptr(val)) - val = 0 - shift = 0 - } - val |= uint64(math.Float32bits(float32(f.Float()))) << shift - shift += 32 - class |= _FLOAT - case reflect.Float64: - addFloat(uintptr(math.Float64bits(float64(f.Float())))) - shift = 0 - flushed = true - class = _NO_CLASS - case reflect.Ptr, reflect.UnsafePointer: - addInt(f.Pointer()) - shift = 0 - flushed = true - class = _NO_CLASS - case reflect.Array: - place(f) + if leaves, useFP := loongClassify(v.Type()); useFP { + for _, l := range leaves { + src := unsafe.Add(ptr, l.offset) + switch { + case l.isFloat && l.kind == reflect.Float32: + // NaN-box the single-precision value in the 64-bit FP register. + addFloat(uintptr(*(*uint32)(src)) | 0xFFFFFFFF_00000000) + case l.isFloat: + addFloat(uintptr(*(*uint64)(src))) default: - panic("purego: unsupported kind " + f.Kind().String()) + addInt(loongLoadInt(src, l)) } } + return keepAlive } - place(v) - if !flushed { - if class == _FLOAT { - addFloat(uintptr(val)) - } else { - addInt(uintptr(val)) - } + + // Integer calling convention: pass the raw aggregate in one or two GARs. + var words [16]byte + copy(words[:], unsafe.Slice((*byte)(ptr), size)) + addInt(*(*uintptr)(unsafe.Pointer(&words[0]))) + if size > 8 { + addInt(*(*uintptr)(unsafe.Pointer(&words[8]))) + } + return keepAlive +} + +// loongLoadInt reads an integer leaf into a register value, sign-extending +// signed members and zero-extending the rest. +func loongLoadInt(src unsafe.Pointer, l loongLeaf) uintptr { + switch l.kind { + case reflect.Int8: + return uintptr(int64(*(*int8)(src))) + case reflect.Int16: + return uintptr(int64(*(*int16)(src))) + case reflect.Int32: + return uintptr(int64(*(*int32)(src))) + case reflect.Int, reflect.Int64: + return uintptr(*(*int64)(src)) + case reflect.Bool, reflect.Uint8: + return uintptr(*(*uint8)(src)) + case reflect.Uint16: + return uintptr(*(*uint16)(src)) + case reflect.Uint32: + return uintptr(*(*uint32)(src)) + default: + return uintptr(*(*uint64)(src)) } } diff --git a/syscall_unix.go b/syscall_unix.go index 6e281ab4..19b96ca5 100644 --- a/syscall_unix.go +++ b/syscall_unix.go @@ -77,9 +77,13 @@ output: switch { case ty.NumOut() == 1: switch ty.Out(0).Kind() { + case reflect.Struct: + ensureCallbackStructSupported() + checkStructFieldsSupported(ty.Out(0)) + break output case reflect.Pointer, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, - reflect.Bool, reflect.UnsafePointer, reflect.Struct: + reflect.Bool, reflect.UnsafePointer: break output } panic("purego: unsupported return type: " + ty.String()) From 845d46315482bdc0b408601656cf078310fff49e Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 6 Jul 2026 00:41:46 +0900 Subject: [PATCH 3/7] purego: ignore blank padding fields in loong64 struct ABI The LoongArch struct classifier flattened blank (_) padding fields into members, so a struct padded to match the C layout was misclassified. A bool and a float32 separated by byte padding looked like one float and several integers, and a float32 and float64 separated by a blank float32 looked like three floats. Both fell back to the integer calling convention instead of the floating-point registers the ABI requires. Skip blank fields when flattening: they are explicit padding, not members that the C calling convention counts. Co-authored-by: Claude Opus 4.8 --- struct_loong64.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/struct_loong64.go b/struct_loong64.go index 211cc380..364ecdad 100644 --- a/struct_loong64.go +++ b/struct_loong64.go @@ -24,6 +24,11 @@ func loongFlatten(t reflect.Type, base uintptr, leaves *[]loongLeaf) { case reflect.Struct: for i := 0; i < t.NumField(); i++ { f := t.Field(i) + if f.Name == "_" { + // Blank fields are explicit padding to match the C layout, not + // members that the calling convention counts. + continue + } loongFlatten(f.Type, base+f.Offset, leaves) } case reflect.Array: From 79850cfc730f99f2705e5592123e306ba7590d23 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 6 Jul 2026 00:53:53 +0900 Subject: [PATCH 4/7] purego: limit this change to loong64 Only loong64 is enabled and tested here. ppc64le, riscv64, and s390x still use the isAllSameFloat heuristic that mishandles floating-point aggregates (ppc64le currently crashes under test), so each will be enabled separately once its calling convention is implemented correctly. Drop them from the struct-support guard, the test build tag, and the README so the guard once again permits only amd64, arm64, and loong64. Updates #474 Co-authored-by: Claude Opus 4.8 --- README.md | 2 +- func.go | 4 ++-- struct_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 35552c30..cbbf5c98 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o - **Android**: 3861,3, arm1,3 - **FreeBSD**: amd643,4, arm643,4 -- **Linux**: 3863, arm3, loong642, ppc64le2, riscv642, s390x2, 5 +- **Linux**: 3863, arm3, loong642, ppc64le3, riscv643, s390x3, 5 - **NetBSD**: amd643,4, arm643,4 - **Windows**: 3863,6, arm3,6,7 diff --git a/func.go b/func.go index 44f392a5..3ba44bbc 100644 --- a/func.go +++ b/func.go @@ -504,9 +504,9 @@ func checkStructFieldsSupported(ty reflect.Type) { // a C function is unsupported on the current platform. func ensureStructSupported() { switch runtime.GOARCH { - case "amd64", "arm64", "loong64", "ppc64le", "riscv64", "s390x": + case "amd64", "arm64", "loong64": default: - panic("purego: struct arguments/returns are only supported on amd64, arm64, loong64, ppc64le, riscv64, and s390x") + panic("purego: struct arguments/returns are only supported on amd64, arm64, and loong64") } switch runtime.GOOS { case "android", "darwin", "ios", "linux", "windows": diff --git a/struct_test.go b/struct_test.go index a0da5403..c2687c95 100644 --- a/struct_test.go +++ b/struct_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2024 The Ebitengine Authors -//go:build (darwin || linux || windows) && (amd64 || arm64 || loong64 || ppc64le || riscv64 || s390x) +//go:build (darwin || linux || windows) && (amd64 || arm64 || loong64) package purego_test From fa889500e1cb14874fd82b0ca238395a18cadb00 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 6 Jul 2026 03:02:22 +0900 Subject: [PATCH 5/7] purego: rename loong* struct helpers to loong64* The loong-prefixed identifiers in struct_loong64.go read like a misspelling of "long". Prefix them with the full GOARCH name: loongLeaf, loongFlatten, loongClassify, loongStoreInt, and loongLoadInt become loong64Leaf, loong64Flatten, loong64Classify, loong64StoreInt, and loong64LoadInt. Co-authored-by: Claude Opus 4.8 --- struct_loong64.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/struct_loong64.go b/struct_loong64.go index 364ecdad..5660a557 100644 --- a/struct_loong64.go +++ b/struct_loong64.go @@ -8,18 +8,18 @@ import ( "unsafe" ) -// loongLeaf is a scalar member of an aggregate after flattening nested structs +// loong64Leaf is a scalar member of an aggregate after flattening nested structs // and arrays. offset is the byte offset of the member within the aggregate. -type loongLeaf struct { +type loong64Leaf struct { isFloat bool kind reflect.Kind offset uintptr size uintptr } -// loongFlatten appends the scalar leaves of t to leaves, offsetting each member +// loong64Flatten appends the scalar leaves of t to leaves, offsetting each member // by base. Nested structs and arrays are expanded into their members. -func loongFlatten(t reflect.Type, base uintptr, leaves *[]loongLeaf) { +func loong64Flatten(t reflect.Type, base uintptr, leaves *[]loong64Leaf) { switch t.Kind() { case reflect.Struct: for i := 0; i < t.NumField(); i++ { @@ -29,16 +29,16 @@ func loongFlatten(t reflect.Type, base uintptr, leaves *[]loongLeaf) { // members that the calling convention counts. continue } - loongFlatten(f.Type, base+f.Offset, leaves) + loong64Flatten(f.Type, base+f.Offset, leaves) } case reflect.Array: elem := t.Elem() for i := 0; i < t.Len(); i++ { - loongFlatten(elem, base+uintptr(i)*elem.Size(), leaves) + loong64Flatten(elem, base+uintptr(i)*elem.Size(), leaves) } default: k := t.Kind() - *leaves = append(*leaves, loongLeaf{ + *leaves = append(*leaves, loong64Leaf{ isFloat: k == reflect.Float32 || k == reflect.Float64, kind: k, offset: base, @@ -47,13 +47,13 @@ func loongFlatten(t reflect.Type, base uintptr, leaves *[]loongLeaf) { } } -// loongClassify flattens t and reports whether it is passed and returned through +// loong64Classify flattens t and reports whether it is passed and returned through // the floating-point calling convention. Under the LoongArch hard-float ABI an // aggregate uses FP registers only when, after flattening, it has one or two // floating-point members and nothing else, or exactly one floating-point member // together with one integer member. -func loongClassify(t reflect.Type) (leaves []loongLeaf, useFP bool) { - loongFlatten(t, 0, &leaves) +func loong64Classify(t reflect.Type) (leaves []loong64Leaf, useFP bool) { + loong64Flatten(t, 0, &leaves) var floats, ints int for _, l := range leaves { if l.isFloat { @@ -84,7 +84,7 @@ func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value { var buf [16]byte base := unsafe.Pointer(&buf[0]) - if leaves, useFP := loongClassify(outType); useFP { + if leaves, useFP := loong64Classify(outType); useFP { floatRegs := [2]uintptr{syscall.f1, syscall.f2} intRegs := [2]uintptr{syscall.a1, syscall.a2} var fi, ii int @@ -100,7 +100,7 @@ func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value { *(*uint64)(dst) = uint64(r) } } else { - loongStoreInt(dst, l.size, intRegs[ii]) + loong64StoreInt(dst, l.size, intRegs[ii]) ii++ } } @@ -113,7 +113,7 @@ func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value { return reflect.NewAt(outType, base).Elem() } -func loongStoreInt(dst unsafe.Pointer, size uintptr, r uintptr) { +func loong64StoreInt(dst unsafe.Pointer, size uintptr, r uintptr) { switch size { case 1: *(*uint8)(dst) = uint8(r) @@ -145,7 +145,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl keepAlive = append(keepAlive, tmp.Interface()) } - if leaves, useFP := loongClassify(v.Type()); useFP { + if leaves, useFP := loong64Classify(v.Type()); useFP { for _, l := range leaves { src := unsafe.Add(ptr, l.offset) switch { @@ -155,7 +155,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl case l.isFloat: addFloat(uintptr(*(*uint64)(src))) default: - addInt(loongLoadInt(src, l)) + addInt(loong64LoadInt(src, l)) } } return keepAlive @@ -171,9 +171,9 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl return keepAlive } -// loongLoadInt reads an integer leaf into a register value, sign-extending +// loong64LoadInt reads an integer leaf into a register value, sign-extending // signed members and zero-extending the rest. -func loongLoadInt(src unsafe.Pointer, l loongLeaf) uintptr { +func loong64LoadInt(src unsafe.Pointer, l loong64Leaf) uintptr { switch l.kind { case reflect.Int8: return uintptr(int64(*(*int8)(src))) From bea8d3d276cda5ae2c5756f366444104cd1a5e2c Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 6 Jul 2026 12:07:11 +0900 Subject: [PATCH 6/7] purego: address loong64 struct review feedback - Use `for range` instead of a C-style loop in loong64Flatten. - Copy the return-pointer register into a local before taking its address so the reflect.Value cannot alias the pooled syscallArgs. - Invert the float check in getStruct to flatten the integer path. - Drop the redundant amd64/arm64 comments in the callback test skip. Co-authored-by: Claude Opus 4.8 --- struct_loong64.go | 29 ++++++++++++++++------------- struct_test.go | 4 ---- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/struct_loong64.go b/struct_loong64.go index 5660a557..999b3589 100644 --- a/struct_loong64.go +++ b/struct_loong64.go @@ -22,7 +22,7 @@ type loong64Leaf struct { func loong64Flatten(t reflect.Type, base uintptr, leaves *[]loong64Leaf) { switch t.Kind() { case reflect.Struct: - for i := 0; i < t.NumField(); i++ { + for i := range t.NumField() { f := t.Field(i) if f.Name == "_" { // Blank fields are explicit padding to match the C layout, not @@ -33,7 +33,7 @@ func loong64Flatten(t reflect.Type, base uintptr, leaves *[]loong64Leaf) { } case reflect.Array: elem := t.Elem() - for i := 0; i < t.Len(); i++ { + for i := range t.Len() { loong64Flatten(elem, base+uintptr(i)*elem.Size(), leaves) } default: @@ -79,7 +79,10 @@ func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value { } if outSize > 16 { // Returned indirectly through a pointer in the first integer register. - return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem() + // Read it from a local copy so the address does not alias the pooled + // syscallArgs. + a1 := syscall.a1 + return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&a1))).Elem() } var buf [16]byte @@ -90,18 +93,18 @@ func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value { var fi, ii int for _, l := range leaves { dst := unsafe.Add(base, l.offset) - if l.isFloat { - r := floatRegs[fi] - fi++ - if l.kind == reflect.Float32 { - // A single-precision value is NaN-boxed in the register. - *(*uint32)(dst) = uint32(r) - } else { - *(*uint64)(dst) = uint64(r) - } - } else { + if !l.isFloat { loong64StoreInt(dst, l.size, intRegs[ii]) ii++ + continue + } + r := floatRegs[fi] + fi++ + if l.kind == reflect.Float32 { + // A single-precision value is NaN-boxed in the register. + *(*uint32)(dst) = uint32(r) + } else { + *(*uint64)(dst) = uint64(r) } } } else { diff --git a/struct_test.go b/struct_test.go index c2687c95..98c91845 100644 --- a/struct_test.go +++ b/struct_test.go @@ -84,8 +84,6 @@ func TestRegisterFunc_structArgs(t *testing.T) { continue } if imp.usesCallbacks && runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { - // Struct arguments and returns in callbacks are only supported on - // amd64 and arm64. continue } t.Run(imp.name, func(t *testing.T) { @@ -914,8 +912,6 @@ func TestRegisterFunc_structReturns(t *testing.T) { continue } if imp.usesCallbacks && runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { - // Struct arguments and returns in callbacks are only supported on - // amd64 and arm64. continue } t.Run(imp.name, func(t *testing.T) { From c980e7d909cde68762070d58a5c56bb5aad1b477 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 6 Jul 2026 12:28:04 +0900 Subject: [PATCH 7/7] purego: drop the unnecessary local copy in loong64 getStruct getStruct receives syscallArgs by value, so taking the address of syscall.a1 does not alias the pooled syscallArgs and the local copy added earlier is unnecessary. Read the return pointer directly. Co-authored-by: Claude Opus 4.8 --- struct_loong64.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/struct_loong64.go b/struct_loong64.go index 999b3589..ad425441 100644 --- a/struct_loong64.go +++ b/struct_loong64.go @@ -79,10 +79,7 @@ func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value { } if outSize > 16 { // Returned indirectly through a pointer in the first integer register. - // Read it from a local copy so the address does not alias the pooled - // syscallArgs. - a1 := syscall.a1 - return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&a1))).Elem() + return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem() } var buf [16]byte