diff --git a/README.md b/README.md index 3239ab50..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, loong643, ppc64le3, riscv643, s390x3, 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 8d3a6f7e..3ba44bbc 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": + default: + panic("purego: struct arguments/returns are only supported on amd64, arm64, and loong64") } 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_loong64.go b/struct_loong64.go index 7a8a7c7c..ad425441 100644 --- a/struct_loong64.go +++ b/struct_loong64.go @@ -4,177 +4,193 @@ package purego import ( - "math" "reflect" "unsafe" ) -func getStruct(outType reflect.Type, syscall syscallArgs) (v reflect.Value) { - outSize := outType.Size() - switch { - case 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 +// 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 loong64Leaf struct { + isFloat bool + kind reflect.Kind + offset uintptr + size uintptr +} + +// loong64Flatten appends the scalar leaves of t to leaves, offsetting each member +// by base. Nested structs and arrays are expanded into their members. +func loong64Flatten(t reflect.Type, base uintptr, leaves *[]loong64Leaf) { + switch t.Kind() { + case reflect.Struct: + for i := range t.NumField() { + 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 } + loong64Flatten(f.Type, base+f.Offset, leaves) } - 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") - } + case reflect.Array: + elem := t.Elem() + for i := range t.Len() { + loong64Flatten(elem, base+uintptr(i)*elem.Size(), leaves) } - 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 + k := t.Kind() + *leaves = append(*leaves, loong64Leaf{ + isFloat: k == reflect.Float32 || k == reflect.Float64, + kind: k, + offset: base, + size: t.Size(), + }) + } +} + +// 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 loong64Classify(t reflect.Type) (leaves []loong64Leaf, useFP bool) { + loong64Flatten(t, 0, &leaves) + var floats, ints int + for _, l := range leaves { + if l.isFloat { + floats++ + } else { + ints++ + } + } + switch { + 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() + } + 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 := loong64Classify(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 { + 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 { + *(*uintptr)(base) = syscall.a1 + if outSize > 8 { + *(*uintptr)(unsafe.Add(base, 8)) = syscall.a2 + } + } + return reflect.NewAt(outType, base).Elem() } -const ( - _NO_CLASS = 0b00 - _FLOAT = 0b01 - _INT = 0b11 -) +func loong64StoreInt(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 := loong64Classify(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(loong64LoadInt(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 +} + +// loong64LoadInt reads an integer leaf into a register value, sign-extending +// signed members and zero-extending the rest. +func loong64LoadInt(src unsafe.Pointer, l loong64Leaf) 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/struct_test.go b/struct_test.go index 14ed0584..98c91845 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) package purego_test @@ -83,6 +83,9 @@ func TestRegisterFunc_structArgs(t *testing.T) { // not support struct arguments or returns. continue } + if imp.usesCallbacks && runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { + continue + } t.Run(imp.name, func(t *testing.T) { register := imp.register { @@ -908,6 +911,9 @@ func TestRegisterFunc_structReturns(t *testing.T) { // not support struct arguments or returns. continue } + if imp.usesCallbacks && runtime.GOARCH != "amd64" && runtime.GOARCH != "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..19b96ca5 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, @@ -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())