From 57c0f81cda5b780c04be6d9167e899c11ab77468 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sat, 20 Jun 2026 23:39:20 +0900 Subject: [PATCH] all: drop support for Go 1.18 through 1.24 Set the go.mod language version to 1.25.0 and trim the CI matrix to the two most recent Go releases. Exercising every release back to 1.18 made test runs long, and that cost grows as more targets are added (e.g. a future windows/arm64). Narrowing the supported range also lowers the maintenance burden of carrying version-gated workarounds. Remove the compatibility shims that only existed for older toolchains: - internal/xreflect, which polyfilled reflect.TypeAssert; callers now use the standard library function directly. - the objc clone helper, replaced by slices.Clone. Modernize the remaining code to the new baseline: integer range loops and reflect.TypeFor, and an iter.Seq-based field iterator in the tests. Updates #468 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/test.yml | 9 ++---- callback_test.go | 2 +- examples/objc/main_darwin.go | 2 +- func.go | 5 ++- func_test.go | 2 +- go.mod | 2 +- internal/xreflect/reflect_go124.go | 15 --------- internal/xreflect/reflect_go125.go | 12 ------- objc/objc_block_darwin.go | 7 ++--- objc/objc_runtime_darwin.go | 37 ++++++++-------------- objc/objc_runtime_darwin_test.go | 4 +-- struct_arm64.go | 6 ++-- struct_test.go | 50 ++++++++++-------------------- syscall_bench_test.go | 14 ++++----- syscall_unix.go | 6 ++-- 15 files changed, 56 insertions(+), 117 deletions(-) delete mode 100644 internal/xreflect/reflect_go124.go delete mode 100644 internal/xreflect/reflect_go125.go 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 }