diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 05128098..0ba03923 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] - go: ['1.18.x', '1.19.x', '1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x'] + go: ['1.25.x', '1.26.x'] name: Test with Go ${{ matrix.go }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} defaults: @@ -174,21 +174,18 @@ jobs: go env -u CXX - name: go test race (no Cgo) - if: ${{ runner.os == 'macOS' && !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') }} + if: runner.os == 'macOS' run: | # -race usually requires Cgo, but macOS is an exception. See https://go.dev/doc/articles/race_detector#Requirements env CGO_ENABLED=0 go test -race -shuffle=on -v -count=10 ./... - name: go test race (Cgo) - if: ${{ !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') }} run: | env CGO_ENABLED=1 go test -race -shuffle=on -v -count=10 ./... minor-arches: strategy: matrix: - # Test only the latest two Go versions to save CI time. - # See https://go.dev/doc/devel/release#policy go: ['1.25.x', '1.26.x'] name: Test with Go ${{ matrix.go }} on Linux minor architectures runs-on: ubuntu-latest @@ -285,8 +282,6 @@ jobs: strategy: matrix: os: ['FreeBSD', 'NetBSD'] - # Test only the latest two Go versions to save CI time. - # See https://go.dev/doc/devel/release#policy go: ['1.25.8', '1.26.1'] name: Test with Go ${{ matrix.go }} on ${{ matrix.os }} runs-on: ubuntu-22.04 diff --git a/callback_test.go b/callback_test.go index e373c02a..b298081b 100644 --- a/callback_test.go +++ b/callback_test.go @@ -43,7 +43,7 @@ func TestCallGoFromSharedLib(t *testing.T) { const want = 10101 cb := purego.NewCallback(goFunc) - for i := 0; i < 10; i++ { + for i := range 10 { got := callCallback(cb, "a test string") if got != want { t.Fatalf("%d: callCallback() got %v want %v", i, got, want) diff --git a/examples/objc/main_darwin.go b/examples/objc/main_darwin.go index df9bcdbc..1d7bee06 100644 --- a/examples/objc/main_darwin.go +++ b/examples/objc/main_darwin.go @@ -36,7 +36,7 @@ func main() { []objc.FieldDef{ { Name: "bar", - Type: reflect.TypeOf(int(0)), + Type: reflect.TypeFor[int](), Attribute: objc.ReadWrite, }, }, diff --git a/func.go b/func.go index dc6863cd..8d3a6f7e 100644 --- a/func.go +++ b/func.go @@ -14,7 +14,6 @@ import ( "unsafe" "github.com/ebitengine/purego/internal/strings" - "github.com/ebitengine/purego/internal/xreflect" ) const ( @@ -155,7 +154,7 @@ func RegisterFunc(fptr any, cfn uintptr) { // created in NewCallback. for j := 0; j < arg.NumIn(); j++ { in := arg.In(j) - if !in.AssignableTo(reflect.TypeOf(CDecl{})) { + if !in.AssignableTo(reflect.TypeFor[CDecl]()) { continue } if j != 0 { @@ -303,7 +302,7 @@ func RegisterFunc(fptr any, cfn uintptr) { } } for i, v := range args { - if variadic, ok := xreflect.TypeAssert[[]any](args[i]); ok { + if variadic, ok := reflect.TypeAssert[[]any](args[i]); ok { if i != len(args)-1 { panic("purego: can only expand last parameter") } diff --git a/func_test.go b/func_test.go index cf53b88e..85e80e82 100644 --- a/func_test.go +++ b/func_test.go @@ -60,7 +60,7 @@ func TestRegisterFunc_ConcurrentPointerReturn(t *testing.T) { wg.Add(1) go func(id int) { defer wg.Done() - for j := 0; j < 400_000; j++ { + for range 400_000 { ptr := alloc(5) if ptr == nil { continue diff --git a/go.mod b/go.mod index ea4133e8..24a93665 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/ebitengine/purego -go 1.18 +go 1.25.0 diff --git a/internal/xreflect/reflect_go124.go b/internal/xreflect/reflect_go124.go deleted file mode 100644 index 5eb0580e..00000000 --- a/internal/xreflect/reflect_go124.go +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2025 The Ebitengine Authors - -//go:build !go1.25 - -package xreflect - -import "reflect" - -// TODO: remove this and use Go 1.25's reflect.TypeAssert when minimum go.mod version is 1.25 - -func TypeAssert[T any](v reflect.Value) (T, bool) { - v2, ok := v.Interface().(T) - return v2, ok -} diff --git a/internal/xreflect/reflect_go125.go b/internal/xreflect/reflect_go125.go deleted file mode 100644 index 62ee13d6..00000000 --- a/internal/xreflect/reflect_go125.go +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2025 The Ebitengine Authors - -//go:build go1.25 - -package xreflect - -import "reflect" - -func TypeAssert[T any](v reflect.Value) (T, bool) { - return reflect.TypeAssert[T](v) -} diff --git a/objc/objc_block_darwin.go b/objc/objc_block_darwin.go index f0b7c1d5..7872b045 100644 --- a/objc/objc_block_darwin.go +++ b/objc/objc_block_darwin.go @@ -10,7 +10,6 @@ import ( "unsafe" "github.com/ebitengine/purego" - "github.com/ebitengine/purego/internal/xreflect" ) const ( @@ -125,7 +124,7 @@ func (*blockCache) encode(typ reflect.Type) *uint8 { encoding = returnType } - if typ.NumIn() == 0 || typ.In(0) != reflect.TypeOf(Block(0)) { + if typ.NumIn() == 0 || typ.In(0) != reflect.TypeFor[Block]() { panic(fmt.Sprintf("objc: A Block implementation must take a Block as its first argument; got %v", typ.String())) } @@ -168,7 +167,7 @@ func (b *blockCache) getLayout(typ reflect.Type) blockLayout { reflect.MakeFunc( typ, func(args []reflect.Value) (results []reflect.Value) { - block, ok := xreflect.TypeAssert[Block](args[0]) + block, ok := reflect.TypeAssert[Block](args[0]) if !ok { panic(fmt.Sprintf("objc: block argument is not a block but %s", args[0].Type().String())) } @@ -263,7 +262,7 @@ func InvokeBlock[T any](block Block, args ...any) (result T, err error) { callResult := fn.Call(reflectedArgs) var ok bool - result, ok = xreflect.TypeAssert[T](callResult[0]) + result, ok = reflect.TypeAssert[T](callResult[0]) if !ok { return result, fmt.Errorf("objc: the returned value type %s was not %T", callResult[0].Type().String(), result) } diff --git a/objc/objc_runtime_darwin.go b/objc/objc_runtime_darwin.go index ee520dee..31e70ec9 100644 --- a/objc/objc_runtime_darwin.go +++ b/objc/objc_runtime_darwin.go @@ -12,13 +12,13 @@ import ( "reflect" "regexp" "runtime" + "slices" stdstrings "strings" "unicode" "unsafe" "github.com/ebitengine/purego" "github.com/ebitengine/purego/internal/strings" - "github.com/ebitengine/purego/internal/xreflect" ) // TODO: support try/catch? @@ -322,7 +322,7 @@ func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars [ case ReadWrite: ty := reflect.FuncOf( []reflect.Type{ - reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type, + reflect.TypeFor[ID](), reflect.TypeFor[SEL](), ivar.Type, }, nil, false, ) @@ -343,7 +343,7 @@ func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars [ // })(unsafe.Pointer(args[0].Interface().(ID)))).v = 123 // // However, since the type of the variable is unknown reflection is used to actually assign the value - id, ok := xreflect.TypeAssert[ID](args[0]) + id, ok := reflect.TypeAssert[ID](args[0]) if !ok { panic(fmt.Sprintf("objc: id argument is not a ID but %s", args[0].Type().String())) } @@ -358,7 +358,7 @@ func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars [ case ReadOnly: ty := reflect.FuncOf( []reflect.Type{ - reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), + reflect.TypeFor[ID](), reflect.TypeFor[SEL](), }, []reflect.Type{ivar.Type}, false, ) @@ -371,7 +371,7 @@ func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars [ if len(args) != 2 { panic(fmt.Sprintf("objc: incorrect number of args. expected 2 got %d", len(args))) } - id, ok := xreflect.TypeAssert[ID](args[0]) + id, ok := reflect.TypeAssert[ID](args[0]) if !ok { panic(fmt.Sprintf("objc: id argument is not a ID but %s", args[0].Type().String())) } @@ -419,11 +419,11 @@ const ( // Source: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100 func encodeType(typ reflect.Type, insidePtr bool) (string, error) { switch typ { - case reflect.TypeOf(Class(0)): + case reflect.TypeFor[Class](): return encClass, nil - case reflect.TypeOf(ID(0)), reflect.TypeOf(Block(0)): + case reflect.TypeFor[ID](), reflect.TypeFor[Block](): return encId, nil - case reflect.TypeOf(SEL(0)): + case reflect.TypeFor[SEL](): return encSelector, nil } @@ -626,7 +626,7 @@ func (p *Protocol) Register() { func (p *Protocol) CopyMethodDescriptionList(isRequiredMethod, isInstanceMethod bool) []MethodDescription { count := uint32(0) desc := protocol_copyMethodDescriptionList(p, isRequiredMethod, isInstanceMethod, &count) - methods := clone(unsafe.Slice(desc, count)) + methods := slices.Clone(unsafe.Slice(desc, count)) free(unsafe.Pointer(desc)) return methods } @@ -635,7 +635,7 @@ func (p *Protocol) CopyMethodDescriptionList(isRequiredMethod, isInstanceMethod func (p *Protocol) CopyProtocolList() []*Protocol { count := uint32(0) desc := protocol_copyProtocolList(p, &count) - protocols := clone(unsafe.Slice(desc, count)) + protocols := slices.Clone(unsafe.Slice(desc, count)) free(unsafe.Pointer(desc)) return protocols } @@ -644,7 +644,7 @@ func (p *Protocol) CopyProtocolList() []*Protocol { func (p *Protocol) CopyPropertyList(isRequiredProperty, isInstanceProperty bool) []Property { count := uint32(0) desc := protocol_copyPropertyList2(p, &count, isRequiredProperty, isInstanceProperty) - protocols := clone(unsafe.Slice(desc, count)) + protocols := slices.Clone(unsafe.Slice(desc, count)) free(unsafe.Pointer(desc)) return protocols } @@ -691,21 +691,10 @@ func NewIMP(fn any) IMP { switch { case ty.NumIn() < 2: fallthrough - case ty.In(0) != reflect.TypeOf(ID(0)): + case ty.In(0) != reflect.TypeFor[ID](): fallthrough - case ty.In(1) != reflect.TypeOf(SEL(0)): + case ty.In(1) != reflect.TypeFor[SEL](): panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String()) } return IMP(purego.NewCallback(fn)) } - -// TODO: remove and use slices.Clone when minimum version for purego is 1.21 -func clone[S ~[]E, E any](s S) S { - // Preserve nilness in case it matters. - if s == nil { - return nil - } - // Avoid s[:0:0] as it leads to unwanted liveness when cloning a - // zero-length slice of a large array; see https://go.dev/issue/68488. - return append(S{}, s...) -} diff --git a/objc/objc_runtime_darwin_test.go b/objc/objc_runtime_darwin_test.go index 1e73bfb2..168b9513 100644 --- a/objc/objc_runtime_darwin_test.go +++ b/objc/objc_runtime_darwin_test.go @@ -58,12 +58,12 @@ func ExampleRegisterClass() { []objc.FieldDef{ { Name: "bar", - Type: reflect.TypeOf(int(0)), + Type: reflect.TypeFor[int](), Attribute: objc.ReadWrite, }, { Name: "foo", - Type: reflect.TypeOf(false), + Type: reflect.TypeFor[bool](), Attribute: objc.ReadWrite, }, }, diff --git a/struct_arm64.go b/struct_arm64.go index bd54984a..47e6f452 100644 --- a/struct_arm64.go +++ b/struct_arm64.go @@ -311,7 +311,7 @@ func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintp chunk = *(*uintptr)(unsafe.Add(ptr, offset)) } else { // Read byte-by-byte to avoid reading beyond allocation - for i := uintptr(0); i < remaining; i++ { + for i := range remaining { b := *(*byte)(unsafe.Add(ptr, offset+i)) chunk |= uintptr(b) << (i * 8) } @@ -514,7 +514,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { paddingNeeded := uintptr(valAlign) - (currentOffset % uintptr(valAlign)) fields = append(fields, reflect.StructField{ Name: paddingFieldPrefix + strconv.Itoa(fieldIndex), - Type: reflect.ArrayOf(int(paddingNeeded), reflect.TypeOf(byte(0))), + Type: reflect.ArrayOf(int(paddingNeeded), reflect.TypeFor[byte]()), }) currentOffset += paddingNeeded fieldIndex++ @@ -610,7 +610,7 @@ func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, // Pointer on stack (rare: all integer registers exhausted). if isDarwin { - ptrVal := callbackArgFromStack(frame, *stackSlot, stackByteOffset, reflect.TypeOf(uintptr(0))) + ptrVal := callbackArgFromStack(frame, *stackSlot, stackByteOffset, reflect.TypeFor[uintptr]()) ptr := uintptr(ptrVal.Uint()) return reflect.NewAt(inType, *(*unsafe.Pointer)(unsafe.Pointer(&ptr))).Elem() } diff --git a/struct_test.go b/struct_test.go index 1cd30d39..a9cd6148 100644 --- a/struct_test.go +++ b/struct_test.go @@ -6,11 +6,13 @@ package purego_test import ( + "iter" "math" "os" "path/filepath" "reflect" "runtime" + "slices" "testing" "unsafe" @@ -76,7 +78,6 @@ func TestRegisterFunc_structArgs(t *testing.T) { }, } for _, imp := range implementations { - imp := imp if imp.usesCallbacks && runtime.GOOS == "windows" { // Callbacks on Windows use the stdlib syscall.NewCallback, which does // not support struct arguments or returns. @@ -834,38 +835,23 @@ func TestRegisterFunc_structArgs(t *testing.T) { } } -// TODO: this could use the iter.Seq interface when purego supports Go 1.23 -func nextFieldFn(v reflect.Value) func() (reflect.Value, bool) { - var fieldIndex int - var tracker func() (reflect.Value, bool) - return func() (reflect.Value, bool) { - if v.NumField() == 0 { - return reflect.Value{}, false - } - if tracker != nil { - if field, ok := tracker(); ok { - return field, ok - } - tracker = nil - } - for fieldIndex < v.NumField() { - if v.Type().Field(fieldIndex).Name == "_" { - fieldIndex++ +func fields(v reflect.Value) iter.Seq[reflect.Value] { + return func(yield func(reflect.Value) bool) { + for i := range v.NumField() { + if v.Type().Field(i).Name == "_" { continue } - field := v.Field(fieldIndex) - fieldIndex++ + field := v.Field(i) if field.Kind() == reflect.Struct { - tracker = nextFieldFn(field) - if inner, ok := tracker(); ok { - return inner, ok + for inner := range fields(field) { + if !yield(inner) { + return + } } - tracker = nil - } else { - return field, true + } else if !yield(field) { + return } } - return reflect.Value{}, false } } @@ -903,13 +889,12 @@ func TestRegisterFunc_structReturns(t *testing.T) { fn := reflect.MakeFunc(reflect.TypeOf(fptr).Elem(), func(args []reflect.Value) []reflect.Value { retType := reflect.TypeOf(fptr).Elem().Out(0) ret := reflect.New(retType).Elem() - next := nextFieldFn(ret) - for _, a := range args { - field, ok := next() - if !ok { + leaves := slices.Collect(fields(ret)) + for i, a := range args { + if i >= len(leaves) { panic("purego: no more fields") } - field.Set(a) + leaves[i].Set(a) } return []reflect.Value{ret} }) @@ -918,7 +903,6 @@ func TestRegisterFunc_structReturns(t *testing.T) { }, } for _, imp := range implementations { - imp := imp if imp.usesCallbacks && runtime.GOOS == "windows" { // Callbacks on Windows use the stdlib syscall.NewCallback, which does // not support struct arguments or returns. diff --git a/syscall_bench_test.go b/syscall_bench_test.go index ce923b93..86e8a0c7 100644 --- a/syscall_bench_test.go +++ b/syscall_bench_test.go @@ -205,40 +205,40 @@ func callRegisterFunc(registerFn any, n int, args []int64, iterations int) int64 switch n { case 1: f := registerFn.(*func(int64) int64) - for i := 0; i < iterations; i++ { + for range iterations { result = (*f)(args[0]) } case 2: f := registerFn.(*func(int64, int64) int64) - for i := 0; i < iterations; i++ { + for range iterations { result = (*f)(args[0], args[1]) } case 3: f := registerFn.(*func(int64, int64, int64) int64) - for i := 0; i < iterations; i++ { + for range iterations { result = (*f)(args[0], args[1], args[2]) } case 5: f := registerFn.(*func(int64, int64, int64, int64, int64) int64) - for i := 0; i < iterations; i++ { + for range iterations { result = (*f)(args[0], args[1], args[2], args[3], args[4]) } case 10: f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64) - for i := 0; i < iterations; i++ { + for range iterations { result = (*f)(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]) } case 14: f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64) - for i := 0; i < iterations; i++ { + for range iterations { result = (*f)(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13]) } case 15: f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64) - for i := 0; i < iterations; i++ { + for range iterations { result = (*f)(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14]) diff --git a/syscall_unix.go b/syscall_unix.go index 3ec4cb92..2c904371 100644 --- a/syscall_unix.go +++ b/syscall_unix.go @@ -29,7 +29,7 @@ func NewCallback(fn any) uintptr { ty := reflect.TypeOf(fn) for i := 0; i < ty.NumIn(); i++ { in := ty.In(i) - if !in.AssignableTo(reflect.TypeOf(CDecl{})) { + if !in.AssignableTo(reflect.TypeFor[CDecl]()) { continue } if i != 0 { @@ -62,7 +62,7 @@ func compileCallback(fn any) uintptr { in := ty.In(i) switch in.Kind() { case reflect.Struct: - if i == 0 && in.AssignableTo(reflect.TypeOf(CDecl{})) { + if i == 0 && in.AssignableTo(reflect.TypeFor[CDecl]()) { continue } ensureStructSupported() @@ -190,7 +190,7 @@ func callbackWrap(a *callbackArgs) { } floatsN += slots case reflect.Struct: - if i == 0 && inType.AssignableTo(reflect.TypeOf(CDecl{})) { + if i == 0 && inType.AssignableTo(reflect.TypeFor[CDecl]()) { args[i] = reflect.Zero(inType) continue }