detached 93970fe

purego: add support for linux/arm32 (#391)

This PR adds the necessary code logic to support linux/arm32 with both CGO
enabled/disabled.

Closes #336
Updats #369
commit
93970fe7f85ee188499e64091de23e8929094062
author
George Adams · · 2026-01-29 14:02 UTC
committer
GitHub · · 2026-01-29 14:02 UTC
parent
1d6e403
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
18 files · +4665 -53
M .github/workflows/test.yml +16 -0
@@ -119,6 +119,22 @@ jobs:
119119 sudo apt-get install gcc-multilib
120120 sudo apt-get install g++-multilib
121121 env CGO_ENABLED=1 GOARCH=386 go test -shuffle=on -v -count=10 ./...
122+
123+ - name: go test (Linux arm)
124+ if: ${{ runner.os == 'Linux' && runner.arch == 'ARM64' }}
125+ run: |
126+ sudo apt-get update
127+ sudo apt-get install -y qemu-user gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross
128+ # Create symlink for the dynamic linker that QEMU expects
129+ sudo ln -sf /usr/arm-linux-gnueabihf/lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3
130+ go env -w CC=arm-linux-gnueabihf-gcc
131+ go env -w CXX=arm-linux-gnueabihf-g++
132+ env GOOS=linux GOARCH=arm CGO_ENABLED=0 go test -c -o=purego-test-nocgo .
133+ env QEMU_LD_PREFIX=/usr/arm-linux-gnueabihf qemu-arm ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10
134+ env GOOS=linux GOARCH=arm CGO_ENABLED=1 go test -c -o=purego-test-cgo .
135+ env QEMU_LD_PREFIX=/usr/arm-linux-gnueabihf qemu-arm ./purego-test-cgo -test.shuffle=on -test.v -test.count=10
136+ go env -u CC
137+ go env -u CXX
122138
123139 - name: go test (Linux riscv64)
124140 if: ${{ runner.os == 'Linux' && runner.arch != 'ARM64' && !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') && !startsWith(matrix.go, '1.20.') && !startsWith(matrix.go, '1.21.') }}
@@ -119,6 +119,22 @@ jobs:
119 sudo apt-get install gcc-multilib119 sudo apt-get install gcc-multilib
120 sudo apt-get install g++-multilib120 sudo apt-get install g++-multilib
121 env CGO_ENABLED=1 GOARCH=386 go test -shuffle=on -v -count=10 ./...121 env CGO_ENABLED=1 GOARCH=386 go test -shuffle=on -v -count=10 ./...
122+
123+ - name: go test (Linux arm)
124+ if: ${{ runner.os == 'Linux' && runner.arch == 'ARM64' }}
125+ run: |
126+ sudo apt-get update
127+ sudo apt-get install -y qemu-user gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross
128+ # Create symlink for the dynamic linker that QEMU expects
129+ sudo ln -sf /usr/arm-linux-gnueabihf/lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3
130+ go env -w CC=arm-linux-gnueabihf-gcc
131+ go env -w CXX=arm-linux-gnueabihf-g++
132+ env GOOS=linux GOARCH=arm CGO_ENABLED=0 go test -c -o=purego-test-nocgo .
133+ env QEMU_LD_PREFIX=/usr/arm-linux-gnueabihf qemu-arm ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10
134+ env GOOS=linux GOARCH=arm CGO_ENABLED=1 go test -c -o=purego-test-cgo .
135+ env QEMU_LD_PREFIX=/usr/arm-linux-gnueabihf qemu-arm ./purego-test-cgo -test.shuffle=on -test.v -test.count=10
136+ go env -u CC
137+ go env -u CXX
122 138
123 - name: go test (Linux riscv64)139 - name: go test (Linux riscv64)
124 if: ${{ runner.os == 'Linux' && runner.arch != 'ARM64' && !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') && !startsWith(matrix.go, '1.20.') && !startsWith(matrix.go, '1.21.') }}140 if: ${{ runner.os == 'Linux' && runner.arch != 'ARM64' && !startsWith(matrix.go, '1.18.') && !startsWith(matrix.go, '1.19.') && !startsWith(matrix.go, '1.20.') && !startsWith(matrix.go, '1.21.') }}
M README.md +1 -1
@@ -42,7 +42,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o
4242
4343 - **Android**: 386<sup>1</sup>, arm<sup>1</sup>
4444 - **FreeBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>
45-- **Linux**: 386<sup>1</sup>, arm<sup>1</sup>, loong64, riscv64
45+- **Linux**: 386<sup>1</sup>, arm, loong64, riscv64
4646 - **Windows**: 386<sup>3</sup>, arm<sup>3,4</sup>
4747
4848 #### Support Notes
@@ -42,7 +42,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o
42 42
43 - **Android**: 386<sup>1</sup>, arm<sup>1</sup>43 - **Android**: 386<sup>1</sup>, arm<sup>1</sup>
44 - **FreeBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>44 - **FreeBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>
45-- **Linux**: 386<sup>1</sup>, arm<sup>1</sup>, loong64, riscv6445+- **Linux**: 386<sup>1</sup>, arm, loong64, riscv64
46 - **Windows**: 386<sup>3</sup>, arm<sup>3,4</sup>46 - **Windows**: 386<sup>3</sup>, arm<sup>3,4</sup>
47 47
48 #### Support Notes48 #### Support Notes
M callback_test.go +1 -1
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2023 The Ebitengine Authors
33
4-//go:build darwin || (linux && (amd64 || arm64 || loong64 || riscv64))
4+//go:build darwin || (linux && (amd64 || arm || arm64 || loong64 || riscv64))
55
66 package purego_test
77
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2023 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2023 The Ebitengine Authors
3 3
4-//go:build darwin || (linux && (amd64 || arm64 || loong64 || riscv64))4+//go:build darwin || (linux && (amd64 || arm || arm64 || loong64 || riscv64))
5 5
6 package purego_test6 package purego_test
7 7
M func.go +21 -22
@@ -120,6 +120,7 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
120120 //
121121 // [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C
122122 func RegisterFunc(fptr any, cfn uintptr) {
123+ const is32bit = unsafe.Sizeof(uintptr(0)) == 4
123124 fn := reflect.ValueOf(fptr).Elem()
124125 ty := fn.Type()
125126 if ty.Kind() != reflect.Func {
@@ -132,7 +133,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
132133 panic("purego: cfn is nil")
133134 }
134135 if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&
135- runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
136+ runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
136137 panic("purego: float returns are not supported")
137138 }
138139 {
@@ -166,8 +167,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
166167 stack++
167168 }
168169 case reflect.Float32, reflect.Float64:
169- const is32bit = unsafe.Sizeof(uintptr(0)) == 4
170- if is32bit {
170+ if is32bit && runtime.GOARCH != "arm" {
171171 panic("purego: floats only supported on 64bit platforms")
172172 }
173173 if floats < numOfFloatRegisters {
@@ -247,7 +247,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
247247 }
248248 }
249249 addFloat = func(x uintptr) {
250- if numFloats < len(floats) {
250+ if numFloats < numOfFloatRegisters {
251251 floats[numFloats] = x
252252 numFloats++
253253 } else {
@@ -318,25 +318,11 @@ func RegisterFunc(fptr any, cfn uintptr) {
318318 defer thePool.Put(syscall)
319319
320320 if runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64" {
321- *syscall = syscall15Args{
322- cfn,
323- sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5],
324- sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
325- sysargs[12], sysargs[13], sysargs[14],
326- floats[0], floats[1], floats[2], floats[3], floats[4], floats[5], floats[6], floats[7],
327- 0,
328- }
321+ syscall.Set(cfn, sysargs[:], floats[:], 0)
329322 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
330323 } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
331324 // Use the normal arm64 calling convention even on Windows
332- *syscall = syscall15Args{
333- cfn,
334- sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5],
335- sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
336- sysargs[12], sysargs[13], sysargs[14],
337- floats[0], floats[1], floats[2], floats[3], floats[4], floats[5], floats[6], floats[7],
338- arm64_r8,
339- }
325+ syscall.Set(cfn, sysargs[:], floats[:], arm64_r8)
340326 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
341327 } else {
342328 *syscall = syscall15Args{}
@@ -376,7 +362,11 @@ func RegisterFunc(fptr any, cfn uintptr) {
376362 case reflect.Float64:
377363 // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
378364 // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
379- v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
365+ if is32bit {
366+ v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
367+ } else {
368+ v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
369+ }
380370 case reflect.Struct:
381371 v = getStruct(outType, *syscall)
382372 default:
@@ -394,6 +384,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
394384 }
395385
396386 func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any {
387+ const is32bit = unsafe.Sizeof(uintptr(0)) == 4
397388 switch v.Kind() {
398389 case reflect.String:
399390 ptr := strings.CString(v.String())
@@ -417,7 +408,13 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
417408 case reflect.Float32:
418409 addFloat(uintptr(math.Float32bits(float32(v.Float()))))
419410 case reflect.Float64:
420- addFloat(uintptr(math.Float64bits(v.Float())))
411+ if is32bit {
412+ bits := math.Float64bits(v.Float())
413+ addFloat(uintptr(bits))
414+ addFloat(uintptr(bits >> 32))
415+ } else {
416+ addFloat(uintptr(math.Float64bits(v.Float())))
417+ }
421418 case reflect.Struct:
422419 keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)
423420 default:
@@ -488,6 +485,8 @@ func numOfIntegerRegisters() int {
488485 return 8
489486 case "amd64":
490487 return 6
488+ case "arm":
489+ return 4
491490 default:
492491 // since this platform isn't supported and can therefore only access
493492 // integer registers it is fine to return the maxArgs
@@ -120,6 +120,7 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
120 //120 //
121 // [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C121 // [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C
122 func RegisterFunc(fptr any, cfn uintptr) {122 func RegisterFunc(fptr any, cfn uintptr) {
123+ const is32bit = unsafe.Sizeof(uintptr(0)) == 4
123 fn := reflect.ValueOf(fptr).Elem()124 fn := reflect.ValueOf(fptr).Elem()
124 ty := fn.Type()125 ty := fn.Type()
125 if ty.Kind() != reflect.Func {126 if ty.Kind() != reflect.Func {
@@ -132,7 +133,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
132 panic("purego: cfn is nil")133 panic("purego: cfn is nil")
133 }134 }
134 if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&135 if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&
135- runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {136+ runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
136 panic("purego: float returns are not supported")137 panic("purego: float returns are not supported")
137 }138 }
138 {139 {
@@ -166,8 +167,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
166 stack++167 stack++
167 }168 }
168 case reflect.Float32, reflect.Float64:169 case reflect.Float32, reflect.Float64:
169- const is32bit = unsafe.Sizeof(uintptr(0)) == 4170+ if is32bit && runtime.GOARCH != "arm" {
170- if is32bit {
171 panic("purego: floats only supported on 64bit platforms")171 panic("purego: floats only supported on 64bit platforms")
172 }172 }
173 if floats < numOfFloatRegisters {173 if floats < numOfFloatRegisters {
@@ -247,7 +247,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
247 }247 }
248 }248 }
249 addFloat = func(x uintptr) {249 addFloat = func(x uintptr) {
250- if numFloats < len(floats) {250+ if numFloats < numOfFloatRegisters {
251 floats[numFloats] = x251 floats[numFloats] = x
252 numFloats++252 numFloats++
253 } else {253 } else {
@@ -318,25 +318,11 @@ func RegisterFunc(fptr any, cfn uintptr) {
318 defer thePool.Put(syscall)318 defer thePool.Put(syscall)
319 319
320 if runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64" {320 if runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64" {
321- *syscall = syscall15Args{321+ syscall.Set(cfn, sysargs[:], floats[:], 0)
322- cfn,
323- sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5],
324- sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
325- sysargs[12], sysargs[13], sysargs[14],
326- floats[0], floats[1], floats[2], floats[3], floats[4], floats[5], floats[6], floats[7],
327- 0,
328- }
329 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))322 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
330 } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {323 } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
331 // Use the normal arm64 calling convention even on Windows324 // Use the normal arm64 calling convention even on Windows
332- *syscall = syscall15Args{325+ syscall.Set(cfn, sysargs[:], floats[:], arm64_r8)
333- cfn,
334- sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5],
335- sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
336- sysargs[12], sysargs[13], sysargs[14],
337- floats[0], floats[1], floats[2], floats[3], floats[4], floats[5], floats[6], floats[7],
338- arm64_r8,
339- }
340 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))326 runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
341 } else {327 } else {
342 *syscall = syscall15Args{}328 *syscall = syscall15Args{}
@@ -376,7 +362,11 @@ func RegisterFunc(fptr any, cfn uintptr) {
376 case reflect.Float64:362 case reflect.Float64:
377 // NOTE: syscall.r2 is only the floating return value on 64bit platforms.363 // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
378 // On 32bit platforms syscall.r2 is the upper part of a 64bit return.364 // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
379- v.SetFloat(math.Float64frombits(uint64(syscall.f1)))365+ if is32bit {
366+ v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
367+ } else {
368+ v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
369+ }
380 case reflect.Struct:370 case reflect.Struct:
381 v = getStruct(outType, *syscall)371 v = getStruct(outType, *syscall)
382 default:372 default:
@@ -394,6 +384,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
394 }384 }
395 385
396 func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any {386 func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any {
387+ const is32bit = unsafe.Sizeof(uintptr(0)) == 4
397 switch v.Kind() {388 switch v.Kind() {
398 case reflect.String:389 case reflect.String:
399 ptr := strings.CString(v.String())390 ptr := strings.CString(v.String())
@@ -417,7 +408,13 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
417 case reflect.Float32:408 case reflect.Float32:
418 addFloat(uintptr(math.Float32bits(float32(v.Float()))))409 addFloat(uintptr(math.Float32bits(float32(v.Float()))))
419 case reflect.Float64:410 case reflect.Float64:
420- addFloat(uintptr(math.Float64bits(v.Float())))411+ if is32bit {
412+ bits := math.Float64bits(v.Float())
413+ addFloat(uintptr(bits))
414+ addFloat(uintptr(bits >> 32))
415+ } else {
416+ addFloat(uintptr(math.Float64bits(v.Float())))
417+ }
421 case reflect.Struct:418 case reflect.Struct:
422 keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)419 keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)
423 default:420 default:
@@ -488,6 +485,8 @@ func numOfIntegerRegisters() int {
488 return 8485 return 8
489 case "amd64":486 case "amd64":
490 return 6487 return 6
488+ case "arm":
489+ return 4
491 default:490 default:
492 // since this platform isn't supported and can therefore only access491 // since this platform isn't supported and can therefore only access
493 // integer registers it is fine to return the maxArgs492 // integer registers it is fine to return the maxArgs
M func_test.go +7 -7
@@ -50,7 +50,7 @@ func TestRegisterFunc(t *testing.T) {
5050 }
5151
5252 func Test_qsort(t *testing.T) {
53- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
53+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
5454 t.Skip("Platform doesn't support Floats")
5555 return
5656 }
@@ -79,7 +79,7 @@ func Test_qsort(t *testing.T) {
7979 }
8080
8181 func TestRegisterFunc_Floats(t *testing.T) {
82- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
82+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
8383 t.Skip("Platform doesn't support Floats")
8484 return
8585 }
@@ -118,7 +118,7 @@ func TestRegisterFunc_Floats(t *testing.T) {
118118 }
119119
120120 func TestRegisterLibFunc_Bool(t *testing.T) {
121- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
121+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
122122 t.Skip("Platform doesn't support callbacks")
123123 return
124124 }
@@ -412,8 +412,8 @@ func TestABI_TooManyArguments(t *testing.T) {
412412 }
413413 })
414414
415- // Test that 25 int64 arguments (17 slots needed) exceeds the limit
416- t.Run("25_int64_exceeds_limit", func(t *testing.T) {
415+ // Test that 35 int64 arguments (27 slots needed) exceeds the limit
416+ t.Run("35_int64_exceeds_limit", func(t *testing.T) {
417417 defer func() {
418418 if r := recover(); r != nil {
419419 t.Logf("Got expected panic: %v", r)
@@ -422,8 +422,8 @@ func TestABI_TooManyArguments(t *testing.T) {
422422 }
423423 }()
424424
425- var fn func(*byte, uintptr, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64)
426- purego.RegisterLibFunc(&fn, lib, "stack_25_int64_exceeds")
425+ var fn func(*byte, uintptr, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64)
426+ purego.RegisterLibFunc(&fn, lib, "stack_35_int64_exceeds")
427427 })
428428 }
429429
@@ -50,7 +50,7 @@ func TestRegisterFunc(t *testing.T) {
50 }50 }
51 51
52 func Test_qsort(t *testing.T) {52 func Test_qsort(t *testing.T) {
53- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {53+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
54 t.Skip("Platform doesn't support Floats")54 t.Skip("Platform doesn't support Floats")
55 return55 return
56 }56 }
@@ -79,7 +79,7 @@ func Test_qsort(t *testing.T) {
79 }79 }
80 80
81 func TestRegisterFunc_Floats(t *testing.T) {81 func TestRegisterFunc_Floats(t *testing.T) {
82- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {82+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
83 t.Skip("Platform doesn't support Floats")83 t.Skip("Platform doesn't support Floats")
84 return84 return
85 }85 }
@@ -118,7 +118,7 @@ func TestRegisterFunc_Floats(t *testing.T) {
118 }118 }
119 119
120 func TestRegisterLibFunc_Bool(t *testing.T) {120 func TestRegisterLibFunc_Bool(t *testing.T) {
121- if runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {121+ if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "riscv64" {
122 t.Skip("Platform doesn't support callbacks")122 t.Skip("Platform doesn't support callbacks")
123 return123 return
124 }124 }
@@ -412,8 +412,8 @@ func TestABI_TooManyArguments(t *testing.T) {
412 }412 }
413 })413 })
414 414
415- // Test that 25 int64 arguments (17 slots needed) exceeds the limit415+ // Test that 35 int64 arguments (27 slots needed) exceeds the limit
416- t.Run("25_int64_exceeds_limit", func(t *testing.T) {416+ t.Run("35_int64_exceeds_limit", func(t *testing.T) {
417 defer func() {417 defer func() {
418 if r := recover(); r != nil {418 if r := recover(); r != nil {
419 t.Logf("Got expected panic: %v", r)419 t.Logf("Got expected panic: %v", r)
@@ -422,8 +422,8 @@ func TestABI_TooManyArguments(t *testing.T) {
422 }422 }
423 }()423 }()
424 424
425- var fn func(*byte, uintptr, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64)425+ var fn func(*byte, uintptr, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64)
426- purego.RegisterLibFunc(&fn, lib, "stack_25_int64_exceeds")426+ purego.RegisterLibFunc(&fn, lib, "stack_35_int64_exceeds")
427 })427 })
428 }428 }
429 429
M internal/cgo/syscall_cgo_unix.go +1 -1
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-//go:build freebsd || (linux && !(arm64 || amd64 || loong64 || riscv64)) || netbsd
4+//go:build freebsd || (linux && !(amd64 || arm || arm64 || loong64 || riscv64)) || netbsd
55
66 package cgo
77
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3 3
4-//go:build freebsd || (linux && !(arm64 || amd64 || loong64 || riscv64)) || netbsd4+//go:build freebsd || (linux && !(amd64 || arm || arm64 || loong64 || riscv64)) || netbsd
5 5
6 package cgo6 package cgo
7 7
A internal/fakecgo/asm_arm.s +52 -0
@@ -0,0 +1,52 @@
1+// Copyright 2012 The Go Authors. All rights reserved.
2+// Use of this source code is governed by a BSD-style
3+// license that can be found in the LICENSE file.
4+
5+#include "textflag.h"
6+
7+// Called by C code generated by cmd/cgo.
8+// func crosscall2(fn, a unsafe.Pointer, n int32, ctxt uintptr)
9+// Saves C callee-saved registers and calls cgocallback with three arguments.
10+// fn is the PC of a func(a unsafe.Pointer) function.
11+TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
12+ SUB $(8*9), R13 // Reserve space for the floating point registers.
13+
14+ // The C arguments arrive in R0, R1, R2, and R3. We want to
15+ // pass R0, R1, and R3 to Go, so we push those on the stack.
16+ // Also, save C callee-save registers R4-R12.
17+ MOVM.WP [R0, R1, R3, R4, R5, R6, R7, R8, R9, g, R11, R12], (R13)
18+
19+ // Finally, save the link register R14. This also puts the
20+ // arguments we pushed for cgocallback where they need to be,
21+ // starting at 4(R13).
22+ MOVW.W R14, -4(R13)
23+
24+ // Save VFP callee-saved registers D8-D15 (same as S16-S31).
25+ // Note: We always save these since we target hard-float ABI.
26+ MOVD F8, (13*4+8*1)(R13)
27+ MOVD F9, (13*4+8*2)(R13)
28+ MOVD F10, (13*4+8*3)(R13)
29+ MOVD F11, (13*4+8*4)(R13)
30+ MOVD F12, (13*4+8*5)(R13)
31+ MOVD F13, (13*4+8*6)(R13)
32+ MOVD F14, (13*4+8*7)(R13)
33+ MOVD F15, (13*4+8*8)(R13)
34+
35+ BL runtime·load_g(SB)
36+
37+ // We set up the arguments to cgocallback when saving registers above.
38+ BL runtime·cgocallback(SB)
39+
40+ MOVD (13*4+8*1)(R13), F8
41+ MOVD (13*4+8*2)(R13), F9
42+ MOVD (13*4+8*3)(R13), F10
43+ MOVD (13*4+8*4)(R13), F11
44+ MOVD (13*4+8*5)(R13), F12
45+ MOVD (13*4+8*6)(R13), F13
46+ MOVD (13*4+8*7)(R13), F14
47+ MOVD (13*4+8*8)(R13), F15
48+
49+ MOVW.P 4(R13), R14
50+ MOVM.IAW (R13), [R0, R1, R3, R4, R5, R6, R7, R8, R9, g, R11, R12]
51+ ADD $(8*9), R13
52+ MOVW R14, R15
@@ -0,0 +1,52 @@
1+// Copyright 2012 The Go Authors. All rights reserved.
2+// Use of this source code is governed by a BSD-style
3+// license that can be found in the LICENSE file.
4+
5+#include "textflag.h"
6+
7+// Called by C code generated by cmd/cgo.
8+// func crosscall2(fn, a unsafe.Pointer, n int32, ctxt uintptr)
9+// Saves C callee-saved registers and calls cgocallback with three arguments.
10+// fn is the PC of a func(a unsafe.Pointer) function.
11+TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
12+ SUB $(8*9), R13 // Reserve space for the floating point registers.
13+
14+ // The C arguments arrive in R0, R1, R2, and R3. We want to
15+ // pass R0, R1, and R3 to Go, so we push those on the stack.
16+ // Also, save C callee-save registers R4-R12.
17+ MOVM.WP [R0, R1, R3, R4, R5, R6, R7, R8, R9, g, R11, R12], (R13)
18+
19+ // Finally, save the link register R14. This also puts the
20+ // arguments we pushed for cgocallback where they need to be,
21+ // starting at 4(R13).
22+ MOVW.W R14, -4(R13)
23+
24+ // Save VFP callee-saved registers D8-D15 (same as S16-S31).
25+ // Note: We always save these since we target hard-float ABI.
26+ MOVD F8, (13*4+8*1)(R13)
27+ MOVD F9, (13*4+8*2)(R13)
28+ MOVD F10, (13*4+8*3)(R13)
29+ MOVD F11, (13*4+8*4)(R13)
30+ MOVD F12, (13*4+8*5)(R13)
31+ MOVD F13, (13*4+8*6)(R13)
32+ MOVD F14, (13*4+8*7)(R13)
33+ MOVD F15, (13*4+8*8)(R13)
34+
35+ BL runtime·load_g(SB)
36+
37+ // We set up the arguments to cgocallback when saving registers above.
38+ BL runtime·cgocallback(SB)
39+
40+ MOVD (13*4+8*1)(R13), F8
41+ MOVD (13*4+8*2)(R13), F9
42+ MOVD (13*4+8*3)(R13), F10
43+ MOVD (13*4+8*4)(R13), F11
44+ MOVD (13*4+8*5)(R13), F12
45+ MOVD (13*4+8*6)(R13), F13
46+ MOVD (13*4+8*7)(R13), F14
47+ MOVD (13*4+8*8)(R13), F15
48+
49+ MOVW.P 4(R13), R14
50+ MOVM.IAW (R13), [R0, R1, R3, R4, R5, R6, R7, R8, R9, g, R11, R12]
51+ ADD $(8*9), R13
52+ MOVW R14, R15
M internal/fakecgo/go_util.go +3 -2
@@ -28,8 +28,9 @@ func x_cgo_thread_start(arg *ThreadStart) {
2828 abort()
2929 }
3030 // *ts = *arg would cause a writebarrier so copy using slices
31- s1 := unsafe.Slice((*uintptr)(unsafe.Pointer(ts)), unsafe.Sizeof(*ts)/8)
32- s2 := unsafe.Slice((*uintptr)(unsafe.Pointer(arg)), unsafe.Sizeof(*arg)/8)
31+ const ptrSize = unsafe.Sizeof(uintptr(0))
32+ s1 := unsafe.Slice((*uintptr)(unsafe.Pointer(ts)), unsafe.Sizeof(*ts)/ptrSize)
33+ s2 := unsafe.Slice((*uintptr)(unsafe.Pointer(arg)), unsafe.Sizeof(*arg)/ptrSize)
3334 for i := range s2 {
3435 s1[i] = s2[i]
3536 }
@@ -28,8 +28,9 @@ func x_cgo_thread_start(arg *ThreadStart) {
28 abort()28 abort()
29 }29 }
30 // *ts = *arg would cause a writebarrier so copy using slices30 // *ts = *arg would cause a writebarrier so copy using slices
31- s1 := unsafe.Slice((*uintptr)(unsafe.Pointer(ts)), unsafe.Sizeof(*ts)/8)31+ const ptrSize = unsafe.Sizeof(uintptr(0))
32- s2 := unsafe.Slice((*uintptr)(unsafe.Pointer(arg)), unsafe.Sizeof(*arg)/8)32+ s1 := unsafe.Slice((*uintptr)(unsafe.Pointer(ts)), unsafe.Sizeof(*ts)/ptrSize)
33+ s2 := unsafe.Slice((*uintptr)(unsafe.Pointer(arg)), unsafe.Sizeof(*arg)/ptrSize)
33 for i := range s2 {34 for i := range s2 {
34 s1[i] = s2[i]35 s1[i] = s2[i]
35 }36 }
A internal/fakecgo/trampolines_arm.s +81 -0
@@ -0,0 +1,81 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build !cgo && (freebsd || linux)
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+
9+// These trampolines map the gcc ABI to Go ABI0 and then call into the Go equivalent functions.
10+// On ARM32, Go ABI0 uses stack-based calling convention.
11+// Arguments are placed on the stack starting at 4(SP) after the prologue.
12+
13+TEXT x_cgo_init_trampoline(SB), NOSPLIT, $8-0
14+ MOVW R0, 4(R13)
15+ MOVW R1, 8(R13)
16+ MOVW ·x_cgo_init_call(SB), R12
17+ MOVW (R12), R12
18+ CALL (R12)
19+ RET
20+
21+TEXT x_cgo_thread_start_trampoline(SB), NOSPLIT, $8-0
22+ MOVW R0, 4(R13)
23+ MOVW ·x_cgo_thread_start_call(SB), R12
24+ MOVW (R12), R12
25+ CALL (R12)
26+ RET
27+
28+TEXT x_cgo_setenv_trampoline(SB), NOSPLIT, $8-0
29+ MOVW R0, 4(R13)
30+ MOVW ·x_cgo_setenv_call(SB), R12
31+ MOVW (R12), R12
32+ CALL (R12)
33+ RET
34+
35+TEXT x_cgo_unsetenv_trampoline(SB), NOSPLIT, $8-0
36+ MOVW R0, 4(R13)
37+ MOVW ·x_cgo_unsetenv_call(SB), R12
38+ MOVW (R12), R12
39+ CALL (R12)
40+ RET
41+
42+TEXT x_cgo_notify_runtime_init_done_trampoline(SB), NOSPLIT, $0-0
43+ CALL ·x_cgo_notify_runtime_init_done(SB)
44+ RET
45+
46+TEXT x_cgo_bindm_trampoline(SB), NOSPLIT, $0
47+ CALL ·x_cgo_bindm(SB)
48+ RET
49+
50+// func setg_trampoline(setg uintptr, g uintptr)
51+TEXT ·setg_trampoline(SB), NOSPLIT, $0-8
52+ MOVW G+4(FP), R0
53+ MOVW setg+0(FP), R12
54+ BL (R12)
55+ RET
56+
57+TEXT threadentry_trampoline(SB), NOSPLIT, $8-0
58+ // See crosscall2.
59+ MOVW R0, 4(R13)
60+ MOVW ·threadentry_call(SB), R12
61+ MOVW (R12), R12
62+ CALL (R12)
63+ RET
64+
65+TEXT ·call5(SB), NOSPLIT, $8-28
66+ MOVW fn+0(FP), R12
67+ MOVW a1+4(FP), R0
68+ MOVW a2+8(FP), R1
69+ MOVW a3+12(FP), R2
70+ MOVW a4+16(FP), R3
71+ MOVW a5+20(FP), R4
72+
73+ // Store 5th arg below SP (in local frame area)
74+ MOVW R4, arg5-8(SP)
75+
76+ // Align SP to 8 bytes for call (required by ARM AAPCS)
77+ SUB $8, R13
78+ CALL (R12)
79+ ADD $8, R13
80+ MOVW R0, r1+24(FP)
81+ RET
@@ -0,0 +1,81 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build !cgo && (freebsd || linux)
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+
9+// These trampolines map the gcc ABI to Go ABI0 and then call into the Go equivalent functions.
10+// On ARM32, Go ABI0 uses stack-based calling convention.
11+// Arguments are placed on the stack starting at 4(SP) after the prologue.
12+
13+TEXT x_cgo_init_trampoline(SB), NOSPLIT, $8-0
14+ MOVW R0, 4(R13)
15+ MOVW R1, 8(R13)
16+ MOVW ·x_cgo_init_call(SB), R12
17+ MOVW (R12), R12
18+ CALL (R12)
19+ RET
20+
21+TEXT x_cgo_thread_start_trampoline(SB), NOSPLIT, $8-0
22+ MOVW R0, 4(R13)
23+ MOVW ·x_cgo_thread_start_call(SB), R12
24+ MOVW (R12), R12
25+ CALL (R12)
26+ RET
27+
28+TEXT x_cgo_setenv_trampoline(SB), NOSPLIT, $8-0
29+ MOVW R0, 4(R13)
30+ MOVW ·x_cgo_setenv_call(SB), R12
31+ MOVW (R12), R12
32+ CALL (R12)
33+ RET
34+
35+TEXT x_cgo_unsetenv_trampoline(SB), NOSPLIT, $8-0
36+ MOVW R0, 4(R13)
37+ MOVW ·x_cgo_unsetenv_call(SB), R12
38+ MOVW (R12), R12
39+ CALL (R12)
40+ RET
41+
42+TEXT x_cgo_notify_runtime_init_done_trampoline(SB), NOSPLIT, $0-0
43+ CALL ·x_cgo_notify_runtime_init_done(SB)
44+ RET
45+
46+TEXT x_cgo_bindm_trampoline(SB), NOSPLIT, $0
47+ CALL ·x_cgo_bindm(SB)
48+ RET
49+
50+// func setg_trampoline(setg uintptr, g uintptr)
51+TEXT ·setg_trampoline(SB), NOSPLIT, $0-8
52+ MOVW G+4(FP), R0
53+ MOVW setg+0(FP), R12
54+ BL (R12)
55+ RET
56+
57+TEXT threadentry_trampoline(SB), NOSPLIT, $8-0
58+ // See crosscall2.
59+ MOVW R0, 4(R13)
60+ MOVW ·threadentry_call(SB), R12
61+ MOVW (R12), R12
62+ CALL (R12)
63+ RET
64+
65+TEXT ·call5(SB), NOSPLIT, $8-28
66+ MOVW fn+0(FP), R12
67+ MOVW a1+4(FP), R0
68+ MOVW a2+8(FP), R1
69+ MOVW a3+12(FP), R2
70+ MOVW a4+16(FP), R3
71+ MOVW a5+20(FP), R4
72+
73+ // Store 5th arg below SP (in local frame area)
74+ MOVW R4, arg5-8(SP)
75+
76+ // Align SP to 8 bytes for call (required by ARM AAPCS)
77+ SUB $8, R13
78+ CALL (R12)
79+ ADD $8, R13
80+ MOVW R0, r1+24(FP)
81+ RET
M struct_arm.go +48 -4
@@ -3,18 +3,62 @@
33
44 package purego
55
6-import "reflect"
6+import (
7+ "reflect"
8+ "unsafe"
9+)
710
811 func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
9- panic("purego: struct arguments are not supported")
12+ size := v.Type().Size()
13+ if size == 0 {
14+ return keepAlive
15+ }
16+
17+ // TODO: ARM EABI: small structs are passed in registers or on stack
18+ // For simplicity, pass by pointer for now
19+ ptr := v.Addr().UnsafePointer()
20+ keepAlive = append(keepAlive, ptr)
21+ if *numInts < 4 {
22+ addInt(uintptr(ptr))
23+ *numInts++
24+ } else {
25+ addStack(uintptr(ptr))
26+ *numStack++
27+ }
28+ return keepAlive
1029 }
1130
1231 func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
13- panic("purego: struct returns are not supported")
32+ outSize := outType.Size()
33+ if outSize == 0 {
34+ return reflect.New(outType).Elem()
35+ }
36+ if outSize <= 4 {
37+ // Fits in one register
38+ return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem()
39+ }
40+ if outSize <= 8 {
41+ // Fits in two registers
42+ return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{syscall.a1, syscall.a2})).Elem()
43+ }
44+ // Larger structs returned via pointer in a1
45+ return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem()
1446 }
1547
1648 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
17- panic("purego: placeRegisters not implemented on arm")
49+ // TODO: For ARM32, just pass the struct data directly
50+ // This is a simplified implementation
51+ size := v.Type().Size()
52+ if size == 0 {
53+ return
54+ }
55+ ptr := unsafe.Pointer(v.UnsafeAddr())
56+ if size <= 4 {
57+ addInt(*(*uintptr)(ptr))
58+ } else if size <= 8 {
59+ addInt(*(*uintptr)(ptr))
60+ addInt(*(*uintptr)(unsafe.Add(ptr, 4)))
61+ }
1862 }
1963
2064 // shouldBundleStackArgs always returns false on arm
@@ -3,18 +3,62 @@
3 3
4 package purego4 package purego
5 5
6-import "reflect"6+import (
7+ "reflect"
8+ "unsafe"
9+)
7 10
8 func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {11 func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
9- panic("purego: struct arguments are not supported")12+ size := v.Type().Size()
13+ if size == 0 {
14+ return keepAlive
15+ }
16+
17+ // TODO: ARM EABI: small structs are passed in registers or on stack
18+ // For simplicity, pass by pointer for now
19+ ptr := v.Addr().UnsafePointer()
20+ keepAlive = append(keepAlive, ptr)
21+ if *numInts < 4 {
22+ addInt(uintptr(ptr))
23+ *numInts++
24+ } else {
25+ addStack(uintptr(ptr))
26+ *numStack++
27+ }
28+ return keepAlive
10 }29 }
11 30
12 func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {31 func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
13- panic("purego: struct returns are not supported")32+ outSize := outType.Size()
33+ if outSize == 0 {
34+ return reflect.New(outType).Elem()
35+ }
36+ if outSize <= 4 {
37+ // Fits in one register
38+ return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem()
39+ }
40+ if outSize <= 8 {
41+ // Fits in two registers
42+ return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{syscall.a1, syscall.a2})).Elem()
43+ }
44+ // Larger structs returned via pointer in a1
45+ return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem()
14 }46 }
15 47
16 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {48 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
17- panic("purego: placeRegisters not implemented on arm")49+ // TODO: For ARM32, just pass the struct data directly
50+ // This is a simplified implementation
51+ size := v.Type().Size()
52+ if size == 0 {
53+ return
54+ }
55+ ptr := unsafe.Pointer(v.UnsafeAddr())
56+ if size <= 4 {
57+ addInt(*(*uintptr)(ptr))
58+ } else if size <= 8 {
59+ addInt(*(*uintptr)(ptr))
60+ addInt(*(*uintptr)(unsafe.Add(ptr, 4)))
61+ }
18 }62 }
19 63
20 // shouldBundleStackArgs always returns false on arm64 // shouldBundleStackArgs always returns false on arm
A sys_arm.s +142 -0
@@ -0,0 +1,142 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+#define STACK_SIZE 128
11+#define PTR_ADDRESS (STACK_SIZE - 4)
12+
13+// syscall15X calls a function in libc on behalf of the syscall package.
14+// syscall15X takes a pointer to a struct like:
15+// struct {
16+// fn uintptr
17+// a1 uintptr
18+// ...
19+// a32 uintptr
20+// f1 uintptr
21+// ...
22+// f16 uintptr
23+// arm64_r8 uintptr
24+// }
25+// syscall15X must be called on the g0 stack with the
26+// C calling convention (use libcCall).
27+GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $4
28+DATA ·syscall15XABI0(SB)/4, $syscall15X(SB)
29+TEXT syscall15X(SB), NOSPLIT|NOFRAME, $0-0
30+ // Called via C calling convention: R0 = pointer to syscall15Args
31+ // NOT via Go calling convention
32+ // Save link register and callee-saved registers first
33+ MOVW.W R14, -4(R13) // save LR (decrement and store)
34+ MOVM.DB.W [R4, R5, R6, R7, R8, R9, R11], (R13) // save callee-saved regs
35+
36+ MOVW R0, R8
37+ SUB $STACK_SIZE, R13
38+ MOVW R8, PTR_ADDRESS(R13)
39+
40+ // Load function pointer first (before anything can corrupt R8)
41+ MOVW syscall15Args_fn(R8), R5
42+ MOVW R5, (PTR_ADDRESS-4)(R13) // save fn at offset 56
43+
44+ // Load floating point arguments
45+ // Each float64 spans 2 uintptr slots (8 bytes) on ARM32, so we skip by 2
46+ MOVD syscall15Args_f1(R8), F0 // f1+f2 -> D0
47+ MOVD syscall15Args_f3(R8), F1 // f3+f4 -> D1
48+ MOVD syscall15Args_f5(R8), F2 // f5+f6 -> D2
49+ MOVD syscall15Args_f7(R8), F3 // f7+f8 -> D3
50+ MOVD syscall15Args_f9(R8), F4 // f9+f10 -> D4
51+ MOVD syscall15Args_f11(R8), F5 // f11+f12 -> D5
52+ MOVD syscall15Args_f13(R8), F6 // f13+f14 -> D6
53+ MOVD syscall15Args_f15(R8), F7 // f15+f16 -> D7
54+
55+ // Load integer arguments into registers (R0-R3 for ARM EABI)
56+ MOVW syscall15Args_a1(R8), R0 // a1
57+ MOVW syscall15Args_a2(R8), R1 // a2
58+ MOVW syscall15Args_a3(R8), R2 // a3
59+ MOVW syscall15Args_a4(R8), R3 // a4
60+
61+ // push a5-a32 onto stack
62+ MOVW syscall15Args_a5(R8), R4
63+ MOVW R4, 0(R13)
64+ MOVW syscall15Args_a6(R8), R4
65+ MOVW R4, 4(R13)
66+ MOVW syscall15Args_a7(R8), R4
67+ MOVW R4, 8(R13)
68+ MOVW syscall15Args_a8(R8), R4
69+ MOVW R4, 12(R13)
70+ MOVW syscall15Args_a9(R8), R4
71+ MOVW R4, 16(R13)
72+ MOVW syscall15Args_a10(R8), R4
73+ MOVW R4, 20(R13)
74+ MOVW syscall15Args_a11(R8), R4
75+ MOVW R4, 24(R13)
76+ MOVW syscall15Args_a12(R8), R4
77+ MOVW R4, 28(R13)
78+ MOVW syscall15Args_a13(R8), R4
79+ MOVW R4, 32(R13)
80+ MOVW syscall15Args_a14(R8), R4
81+ MOVW R4, 36(R13)
82+ MOVW syscall15Args_a15(R8), R4
83+ MOVW R4, 40(R13)
84+ MOVW syscall15Args_a16(R8), R4
85+ MOVW R4, 44(R13)
86+ MOVW syscall15Args_a17(R8), R4
87+ MOVW R4, 48(R13)
88+ MOVW syscall15Args_a18(R8), R4
89+ MOVW R4, 52(R13)
90+ MOVW syscall15Args_a19(R8), R4
91+ MOVW R4, 56(R13)
92+ MOVW syscall15Args_a20(R8), R4
93+ MOVW R4, 60(R13)
94+ MOVW syscall15Args_a21(R8), R4
95+ MOVW R4, 64(R13)
96+ MOVW syscall15Args_a22(R8), R4
97+ MOVW R4, 68(R13)
98+ MOVW syscall15Args_a23(R8), R4
99+ MOVW R4, 72(R13)
100+ MOVW syscall15Args_a24(R8), R4
101+ MOVW R4, 76(R13)
102+ MOVW syscall15Args_a25(R8), R4
103+ MOVW R4, 80(R13)
104+ MOVW syscall15Args_a26(R8), R4
105+ MOVW R4, 84(R13)
106+ MOVW syscall15Args_a27(R8), R4
107+ MOVW R4, 88(R13)
108+ MOVW syscall15Args_a28(R8), R4
109+ MOVW R4, 92(R13)
110+ MOVW syscall15Args_a29(R8), R4
111+ MOVW R4, 96(R13)
112+ MOVW syscall15Args_a30(R8), R4
113+ MOVW R4, 100(R13)
114+ MOVW syscall15Args_a31(R8), R4
115+ MOVW R4, 104(R13)
116+ MOVW syscall15Args_a32(R8), R4
117+ MOVW R4, 108(R13)
118+
119+ // Load saved function pointer and call
120+ MOVW (PTR_ADDRESS-4)(R13), R4
121+
122+ // Use BLX for Thumb interworking - Go assembler doesn't support BLX Rn
123+ // BLX R4 = 0xE12FFF34 (ARM encoding, always condition)
124+ WORD $0xE12FFF34 // blx r4
125+
126+ // pop structure pointer
127+ MOVW PTR_ADDRESS(R13), R8
128+ ADD $STACK_SIZE, R13
129+
130+ // save R0, R1
131+ MOVW R0, syscall15Args_a1(R8)
132+ MOVW R1, syscall15Args_a2(R8)
133+
134+ // save f0-f3 (each float64 spans 2 uintptr slots on ARM32)
135+ MOVD F0, syscall15Args_f1(R8)
136+ MOVD F1, syscall15Args_f3(R8)
137+ MOVD F2, syscall15Args_f5(R8)
138+ MOVD F3, syscall15Args_f7(R8)
139+
140+ // Restore callee-saved registers and return
141+ MOVM.IA.W (R13), [R4, R5, R6, R7, R8, R9, R11]
142+ MOVW.P 4(R13), R15 // pop LR into PC (return)
@@ -0,0 +1,142 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+#define STACK_SIZE 128
11+#define PTR_ADDRESS (STACK_SIZE - 4)
12+
13+// syscall15X calls a function in libc on behalf of the syscall package.
14+// syscall15X takes a pointer to a struct like:
15+// struct {
16+// fn uintptr
17+// a1 uintptr
18+// ...
19+// a32 uintptr
20+// f1 uintptr
21+// ...
22+// f16 uintptr
23+// arm64_r8 uintptr
24+// }
25+// syscall15X must be called on the g0 stack with the
26+// C calling convention (use libcCall).
27+GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $4
28+DATA ·syscall15XABI0(SB)/4, $syscall15X(SB)
29+TEXT syscall15X(SB), NOSPLIT|NOFRAME, $0-0
30+ // Called via C calling convention: R0 = pointer to syscall15Args
31+ // NOT via Go calling convention
32+ // Save link register and callee-saved registers first
33+ MOVW.W R14, -4(R13) // save LR (decrement and store)
34+ MOVM.DB.W [R4, R5, R6, R7, R8, R9, R11], (R13) // save callee-saved regs
35+
36+ MOVW R0, R8
37+ SUB $STACK_SIZE, R13
38+ MOVW R8, PTR_ADDRESS(R13)
39+
40+ // Load function pointer first (before anything can corrupt R8)
41+ MOVW syscall15Args_fn(R8), R5
42+ MOVW R5, (PTR_ADDRESS-4)(R13) // save fn at offset 56
43+
44+ // Load floating point arguments
45+ // Each float64 spans 2 uintptr slots (8 bytes) on ARM32, so we skip by 2
46+ MOVD syscall15Args_f1(R8), F0 // f1+f2 -> D0
47+ MOVD syscall15Args_f3(R8), F1 // f3+f4 -> D1
48+ MOVD syscall15Args_f5(R8), F2 // f5+f6 -> D2
49+ MOVD syscall15Args_f7(R8), F3 // f7+f8 -> D3
50+ MOVD syscall15Args_f9(R8), F4 // f9+f10 -> D4
51+ MOVD syscall15Args_f11(R8), F5 // f11+f12 -> D5
52+ MOVD syscall15Args_f13(R8), F6 // f13+f14 -> D6
53+ MOVD syscall15Args_f15(R8), F7 // f15+f16 -> D7
54+
55+ // Load integer arguments into registers (R0-R3 for ARM EABI)
56+ MOVW syscall15Args_a1(R8), R0 // a1
57+ MOVW syscall15Args_a2(R8), R1 // a2
58+ MOVW syscall15Args_a3(R8), R2 // a3
59+ MOVW syscall15Args_a4(R8), R3 // a4
60+
61+ // push a5-a32 onto stack
62+ MOVW syscall15Args_a5(R8), R4
63+ MOVW R4, 0(R13)
64+ MOVW syscall15Args_a6(R8), R4
65+ MOVW R4, 4(R13)
66+ MOVW syscall15Args_a7(R8), R4
67+ MOVW R4, 8(R13)
68+ MOVW syscall15Args_a8(R8), R4
69+ MOVW R4, 12(R13)
70+ MOVW syscall15Args_a9(R8), R4
71+ MOVW R4, 16(R13)
72+ MOVW syscall15Args_a10(R8), R4
73+ MOVW R4, 20(R13)
74+ MOVW syscall15Args_a11(R8), R4
75+ MOVW R4, 24(R13)
76+ MOVW syscall15Args_a12(R8), R4
77+ MOVW R4, 28(R13)
78+ MOVW syscall15Args_a13(R8), R4
79+ MOVW R4, 32(R13)
80+ MOVW syscall15Args_a14(R8), R4
81+ MOVW R4, 36(R13)
82+ MOVW syscall15Args_a15(R8), R4
83+ MOVW R4, 40(R13)
84+ MOVW syscall15Args_a16(R8), R4
85+ MOVW R4, 44(R13)
86+ MOVW syscall15Args_a17(R8), R4
87+ MOVW R4, 48(R13)
88+ MOVW syscall15Args_a18(R8), R4
89+ MOVW R4, 52(R13)
90+ MOVW syscall15Args_a19(R8), R4
91+ MOVW R4, 56(R13)
92+ MOVW syscall15Args_a20(R8), R4
93+ MOVW R4, 60(R13)
94+ MOVW syscall15Args_a21(R8), R4
95+ MOVW R4, 64(R13)
96+ MOVW syscall15Args_a22(R8), R4
97+ MOVW R4, 68(R13)
98+ MOVW syscall15Args_a23(R8), R4
99+ MOVW R4, 72(R13)
100+ MOVW syscall15Args_a24(R8), R4
101+ MOVW R4, 76(R13)
102+ MOVW syscall15Args_a25(R8), R4
103+ MOVW R4, 80(R13)
104+ MOVW syscall15Args_a26(R8), R4
105+ MOVW R4, 84(R13)
106+ MOVW syscall15Args_a27(R8), R4
107+ MOVW R4, 88(R13)
108+ MOVW syscall15Args_a28(R8), R4
109+ MOVW R4, 92(R13)
110+ MOVW syscall15Args_a29(R8), R4
111+ MOVW R4, 96(R13)
112+ MOVW syscall15Args_a30(R8), R4
113+ MOVW R4, 100(R13)
114+ MOVW syscall15Args_a31(R8), R4
115+ MOVW R4, 104(R13)
116+ MOVW syscall15Args_a32(R8), R4
117+ MOVW R4, 108(R13)
118+
119+ // Load saved function pointer and call
120+ MOVW (PTR_ADDRESS-4)(R13), R4
121+
122+ // Use BLX for Thumb interworking - Go assembler doesn't support BLX Rn
123+ // BLX R4 = 0xE12FFF34 (ARM encoding, always condition)
124+ WORD $0xE12FFF34 // blx r4
125+
126+ // pop structure pointer
127+ MOVW PTR_ADDRESS(R13), R8
128+ ADD $STACK_SIZE, R13
129+
130+ // save R0, R1
131+ MOVW R0, syscall15Args_a1(R8)
132+ MOVW R1, syscall15Args_a2(R8)
133+
134+ // save f0-f3 (each float64 spans 2 uintptr slots on ARM32)
135+ MOVD F0, syscall15Args_f1(R8)
136+ MOVD F1, syscall15Args_f3(R8)
137+ MOVD F2, syscall15Args_f5(R8)
138+ MOVD F3, syscall15Args_f7(R8)
139+
140+ // Restore callee-saved registers and return
141+ MOVM.IA.W (R13), [R4, R5, R6, R7, R8, R9, R11]
142+ MOVW.P 4(R13), R15 // pop LR into PC (return)
A sys_unix_arm.s +89 -0
@@ -0,0 +1,89 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
11+ NO_LOCAL_POINTERS
12+
13+ // Allocate stack frame: 48 + 16 + 64 + 16 = 144 bytes
14+ SUB $144, R13
15+
16+ // Save callee-saved registers at SP+0
17+ MOVW R4, 0(R13)
18+ MOVW R5, 4(R13)
19+ MOVW R6, 8(R13)
20+ MOVW R7, 12(R13)
21+ MOVW R8, 16(R13)
22+ MOVW R9, 20(R13)
23+ MOVW g, 24(R13)
24+ MOVW R11, 28(R13)
25+ MOVW R14, 32(R13)
26+
27+ // Save callback index (passed in R12) at SP+36
28+ MOVW R12, 36(R13)
29+
30+ // Save integer arguments R0-R3 at SP+128 (frame[16..19])
31+ MOVW R0, 128(R13)
32+ MOVW R1, 132(R13)
33+ MOVW R2, 136(R13)
34+ MOVW R3, 140(R13)
35+
36+ // Save floating point registers F0-F7 at SP+64 (frame[0..15])
37+ // Note: We always save these since we target hard-float ABI.
38+ MOVD F0, 64(R13)
39+ MOVD F1, 72(R13)
40+ MOVD F2, 80(R13)
41+ MOVD F3, 88(R13)
42+ MOVD F4, 96(R13)
43+ MOVD F5, 104(R13)
44+ MOVD F6, 112(R13)
45+ MOVD F7, 120(R13)
46+
47+ // Set up callbackArgs at SP+48
48+ MOVW 36(R13), R4
49+ MOVW R4, 48(R13)
50+ ADD $64, R13, R4
51+ MOVW R4, 52(R13)
52+ MOVW $0, R4
53+ MOVW R4, 56(R13)
54+
55+ // Call crosscall2(fn, frame, 0, ctxt)
56+ MOVW ·callbackWrap_call(SB), R0
57+ MOVW (R0), R0
58+ ADD $48, R13, R1
59+ MOVW $0, R2
60+ MOVW $0, R3
61+
62+ BL crosscall2(SB)
63+
64+ // Get result
65+ MOVW 56(R13), R0
66+
67+ // Restore float registers
68+ MOVD 64(R13), F0
69+ MOVD 72(R13), F1
70+ MOVD 80(R13), F2
71+ MOVD 88(R13), F3
72+ MOVD 96(R13), F4
73+ MOVD 104(R13), F5
74+ MOVD 112(R13), F6
75+ MOVD 120(R13), F7
76+
77+ // Restore callee-saved registers
78+ MOVW 0(R13), R4
79+ MOVW 4(R13), R5
80+ MOVW 8(R13), R6
81+ MOVW 12(R13), R7
82+ MOVW 16(R13), R8
83+ MOVW 20(R13), R9
84+ MOVW 24(R13), g
85+ MOVW 28(R13), R11
86+ MOVW 32(R13), R14
87+
88+ ADD $144, R13
89+ RET
@@ -0,0 +1,89 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+#include "funcdata.h"
9+
10+TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
11+ NO_LOCAL_POINTERS
12+
13+ // Allocate stack frame: 48 + 16 + 64 + 16 = 144 bytes
14+ SUB $144, R13
15+
16+ // Save callee-saved registers at SP+0
17+ MOVW R4, 0(R13)
18+ MOVW R5, 4(R13)
19+ MOVW R6, 8(R13)
20+ MOVW R7, 12(R13)
21+ MOVW R8, 16(R13)
22+ MOVW R9, 20(R13)
23+ MOVW g, 24(R13)
24+ MOVW R11, 28(R13)
25+ MOVW R14, 32(R13)
26+
27+ // Save callback index (passed in R12) at SP+36
28+ MOVW R12, 36(R13)
29+
30+ // Save integer arguments R0-R3 at SP+128 (frame[16..19])
31+ MOVW R0, 128(R13)
32+ MOVW R1, 132(R13)
33+ MOVW R2, 136(R13)
34+ MOVW R3, 140(R13)
35+
36+ // Save floating point registers F0-F7 at SP+64 (frame[0..15])
37+ // Note: We always save these since we target hard-float ABI.
38+ MOVD F0, 64(R13)
39+ MOVD F1, 72(R13)
40+ MOVD F2, 80(R13)
41+ MOVD F3, 88(R13)
42+ MOVD F4, 96(R13)
43+ MOVD F5, 104(R13)
44+ MOVD F6, 112(R13)
45+ MOVD F7, 120(R13)
46+
47+ // Set up callbackArgs at SP+48
48+ MOVW 36(R13), R4
49+ MOVW R4, 48(R13)
50+ ADD $64, R13, R4
51+ MOVW R4, 52(R13)
52+ MOVW $0, R4
53+ MOVW R4, 56(R13)
54+
55+ // Call crosscall2(fn, frame, 0, ctxt)
56+ MOVW ·callbackWrap_call(SB), R0
57+ MOVW (R0), R0
58+ ADD $48, R13, R1
59+ MOVW $0, R2
60+ MOVW $0, R3
61+
62+ BL crosscall2(SB)
63+
64+ // Get result
65+ MOVW 56(R13), R0
66+
67+ // Restore float registers
68+ MOVD 64(R13), F0
69+ MOVD 72(R13), F1
70+ MOVD 80(R13), F2
71+ MOVD 88(R13), F3
72+ MOVD 96(R13), F4
73+ MOVD 104(R13), F5
74+ MOVD 112(R13), F6
75+ MOVD 120(R13), F7
76+
77+ // Restore callee-saved registers
78+ MOVW 0(R13), R4
79+ MOVW 4(R13), R5
80+ MOVW 8(R13), R6
81+ MOVW 12(R13), R7
82+ MOVW 16(R13), R8
83+ MOVW 20(R13), R9
84+ MOVW 24(R13), g
85+ MOVW 28(R13), R11
86+ MOVW 32(R13), R14
87+
88+ ADD $144, R13
89+ RET
M syscall.go +29 -1
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-//go:build darwin || freebsd || linux || netbsd || windows
4+//go:build !arm && (darwin || freebsd || linux || netbsd || windows)
55
66 package purego
77
@@ -23,6 +23,34 @@ type syscall15Args struct {
2323 arm64_r8 uintptr
2424 }
2525
26+func (s *syscall15Args) Set(fn uintptr, ints []uintptr, floats []uintptr, r8 uintptr) {
27+ s.fn = fn
28+ s.a1 = ints[0]
29+ s.a2 = ints[1]
30+ s.a3 = ints[2]
31+ s.a4 = ints[3]
32+ s.a5 = ints[4]
33+ s.a6 = ints[5]
34+ s.a7 = ints[6]
35+ s.a8 = ints[7]
36+ s.a9 = ints[8]
37+ s.a10 = ints[9]
38+ s.a11 = ints[10]
39+ s.a12 = ints[11]
40+ s.a13 = ints[12]
41+ s.a14 = ints[13]
42+ s.a15 = ints[14]
43+ s.f1 = floats[0]
44+ s.f2 = floats[1]
45+ s.f3 = floats[2]
46+ s.f4 = floats[3]
47+ s.f5 = floats[4]
48+ s.f6 = floats[5]
49+ s.f7 = floats[6]
50+ s.f8 = floats[7]
51+ s.arm64_r8 = r8
52+}
53+
2654 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
2755 // There is an internal maximum number of arguments that SyscallN can take. It panics
2856 // when the maximum is exceeded. It returns the result and the libc error code if there is one.
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3 3
4-//go:build darwin || freebsd || linux || netbsd || windows4+//go:build !arm && (darwin || freebsd || linux || netbsd || windows)
5 5
6 package purego6 package purego
7 7
@@ -23,6 +23,34 @@ type syscall15Args struct {
23 arm64_r8 uintptr23 arm64_r8 uintptr
24 }24 }
25 25
26+func (s *syscall15Args) Set(fn uintptr, ints []uintptr, floats []uintptr, r8 uintptr) {
27+ s.fn = fn
28+ s.a1 = ints[0]
29+ s.a2 = ints[1]
30+ s.a3 = ints[2]
31+ s.a4 = ints[3]
32+ s.a5 = ints[4]
33+ s.a6 = ints[5]
34+ s.a7 = ints[6]
35+ s.a8 = ints[7]
36+ s.a9 = ints[8]
37+ s.a10 = ints[9]
38+ s.a11 = ints[10]
39+ s.a12 = ints[11]
40+ s.a13 = ints[12]
41+ s.a14 = ints[13]
42+ s.a15 = ints[14]
43+ s.f1 = floats[0]
44+ s.f2 = floats[1]
45+ s.f3 = floats[2]
46+ s.f4 = floats[3]
47+ s.f5 = floats[4]
48+ s.f6 = floats[5]
49+ s.f7 = floats[6]
50+ s.f8 = floats[7]
51+ s.arm64_r8 = r8
52+}
53+
26 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.54 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
27 // There is an internal maximum number of arguments that SyscallN can take. It panics55 // There is an internal maximum number of arguments that SyscallN can take. It panics
28 // when the maximum is exceeded. It returns the result and the libc error code if there is one.56 // when the maximum is exceeded. It returns the result and the libc error code if there is one.
A syscall_32bit.go +110 -0
@@ -0,0 +1,110 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build arm && (freebsd || linux || netbsd || windows)
5+
6+package purego
7+
8+// CDecl marks a function as being called using the __cdecl calling convention as defined in
9+// the [MSDocs] when passed to NewCallback. It must be the first argument to the function.
10+// This is only useful on 386 Windows, but it is safe to use on other platforms.
11+//
12+// [MSDocs]: https://learn.microsoft.com/en-us/cpp/cpp/cdecl?view=msvc-170
13+type CDecl struct{}
14+
15+const (
16+ maxArgs = 32
17+ numOfFloatRegisters = 16
18+)
19+
20+type syscall15Args struct {
21+ fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
22+ a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr
23+ f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16 uintptr
24+ arm64_r8 uintptr
25+}
26+
27+func (s *syscall15Args) Set(fn uintptr, ints []uintptr, floats []uintptr, r8 uintptr) {
28+ s.fn = fn
29+ s.a1 = ints[0]
30+ s.a2 = ints[1]
31+ s.a3 = ints[2]
32+ s.a4 = ints[3]
33+ s.a5 = ints[4]
34+ s.a6 = ints[5]
35+ s.a7 = ints[6]
36+ s.a8 = ints[7]
37+ s.a9 = ints[8]
38+ s.a10 = ints[9]
39+ s.a11 = ints[10]
40+ s.a12 = ints[11]
41+ s.a13 = ints[12]
42+ s.a14 = ints[13]
43+ s.a15 = ints[14]
44+ s.a16 = ints[15]
45+ s.a17 = ints[16]
46+ s.a18 = ints[17]
47+ s.a19 = ints[18]
48+ s.a20 = ints[19]
49+ s.a21 = ints[20]
50+ s.a22 = ints[21]
51+ s.a23 = ints[22]
52+ s.a24 = ints[23]
53+ s.a25 = ints[24]
54+ s.a26 = ints[25]
55+ s.a27 = ints[26]
56+ s.a28 = ints[27]
57+ s.a29 = ints[28]
58+ s.a30 = ints[29]
59+ s.a31 = ints[30]
60+ s.a32 = ints[31]
61+ s.f1 = floats[0]
62+ s.f2 = floats[1]
63+ s.f3 = floats[2]
64+ s.f4 = floats[3]
65+ s.f5 = floats[4]
66+ s.f6 = floats[5]
67+ s.f7 = floats[6]
68+ s.f8 = floats[7]
69+ s.f9 = floats[8]
70+ s.f10 = floats[9]
71+ s.f11 = floats[10]
72+ s.f12 = floats[11]
73+ s.f13 = floats[12]
74+ s.f14 = floats[13]
75+ s.f15 = floats[14]
76+ s.f16 = floats[15]
77+ s.arm64_r8 = r8
78+}
79+
80+// SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
81+// There is an internal maximum number of arguments that SyscallN can take. It panics
82+// when the maximum is exceeded. It returns the result and the libc error code if there is one.
83+//
84+// In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
85+// especially point 4.
86+//
87+// NOTE: SyscallN does not properly call functions that have both integer and float parameters.
88+// See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607
89+// for an explanation of why that is.
90+//
91+// On amd64, if there are more than 8 floats the 9th and so on will be placed incorrectly on the
92+// stack.
93+//
94+// The pragma go:nosplit is not needed at this function declaration because it uses go:uintptrescapes
95+// which forces all the objects that the uintptrs point to onto the heap where a stack split won't affect
96+// their memory location.
97+//
98+//go:uintptrescapes
99+func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
100+ if fn == 0 {
101+ panic("purego: fn is nil")
102+ }
103+ if len(args) > maxArgs {
104+ panic("purego: too many arguments to SyscallN")
105+ }
106+ // add padding so there is no out-of-bounds slicing
107+ var tmp [maxArgs]uintptr
108+ copy(tmp[:], args)
109+ return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])
110+}
@@ -0,0 +1,110 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build arm && (freebsd || linux || netbsd || windows)
5+
6+package purego
7+
8+// CDecl marks a function as being called using the __cdecl calling convention as defined in
9+// the [MSDocs] when passed to NewCallback. It must be the first argument to the function.
10+// This is only useful on 386 Windows, but it is safe to use on other platforms.
11+//
12+// [MSDocs]: https://learn.microsoft.com/en-us/cpp/cpp/cdecl?view=msvc-170
13+type CDecl struct{}
14+
15+const (
16+ maxArgs = 32
17+ numOfFloatRegisters = 16
18+)
19+
20+type syscall15Args struct {
21+ fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
22+ a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr
23+ f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16 uintptr
24+ arm64_r8 uintptr
25+}
26+
27+func (s *syscall15Args) Set(fn uintptr, ints []uintptr, floats []uintptr, r8 uintptr) {
28+ s.fn = fn
29+ s.a1 = ints[0]
30+ s.a2 = ints[1]
31+ s.a3 = ints[2]
32+ s.a4 = ints[3]
33+ s.a5 = ints[4]
34+ s.a6 = ints[5]
35+ s.a7 = ints[6]
36+ s.a8 = ints[7]
37+ s.a9 = ints[8]
38+ s.a10 = ints[9]
39+ s.a11 = ints[10]
40+ s.a12 = ints[11]
41+ s.a13 = ints[12]
42+ s.a14 = ints[13]
43+ s.a15 = ints[14]
44+ s.a16 = ints[15]
45+ s.a17 = ints[16]
46+ s.a18 = ints[17]
47+ s.a19 = ints[18]
48+ s.a20 = ints[19]
49+ s.a21 = ints[20]
50+ s.a22 = ints[21]
51+ s.a23 = ints[22]
52+ s.a24 = ints[23]
53+ s.a25 = ints[24]
54+ s.a26 = ints[25]
55+ s.a27 = ints[26]
56+ s.a28 = ints[27]
57+ s.a29 = ints[28]
58+ s.a30 = ints[29]
59+ s.a31 = ints[30]
60+ s.a32 = ints[31]
61+ s.f1 = floats[0]
62+ s.f2 = floats[1]
63+ s.f3 = floats[2]
64+ s.f4 = floats[3]
65+ s.f5 = floats[4]
66+ s.f6 = floats[5]
67+ s.f7 = floats[6]
68+ s.f8 = floats[7]
69+ s.f9 = floats[8]
70+ s.f10 = floats[9]
71+ s.f11 = floats[10]
72+ s.f12 = floats[11]
73+ s.f13 = floats[12]
74+ s.f14 = floats[13]
75+ s.f15 = floats[14]
76+ s.f16 = floats[15]
77+ s.arm64_r8 = r8
78+}
79+
80+// SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
81+// There is an internal maximum number of arguments that SyscallN can take. It panics
82+// when the maximum is exceeded. It returns the result and the libc error code if there is one.
83+//
84+// In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
85+// especially point 4.
86+//
87+// NOTE: SyscallN does not properly call functions that have both integer and float parameters.
88+// See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607
89+// for an explanation of why that is.
90+//
91+// On amd64, if there are more than 8 floats the 9th and so on will be placed incorrectly on the
92+// stack.
93+//
94+// The pragma go:nosplit is not needed at this function declaration because it uses go:uintptrescapes
95+// which forces all the objects that the uintptrs point to onto the heap where a stack split won't affect
96+// their memory location.
97+//
98+//go:uintptrescapes
99+func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
100+ if fn == 0 {
101+ panic("purego: fn is nil")
102+ }
103+ if len(args) > maxArgs {
104+ panic("purego: too many arguments to SyscallN")
105+ }
106+ // add padding so there is no out-of-bounds slicing
107+ var tmp [maxArgs]uintptr
108+ copy(tmp[:], args)
109+ return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14])
110+}
M syscall_cgo_linux.go +2 -2
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-//go:build cgo && !(amd64 || arm64 || loong64 || riscv64)
4+//go:build cgo && !(amd64 || arm || arm64 || loong64 || riscv64)
55
66 package purego
77
@@ -17,5 +17,5 @@ func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a
1717 }
1818
1919 func NewCallback(_ any) uintptr {
20- panic("purego: NewCallback on Linux is only supported on amd64/arm64/loong64/riscv64")
20+ panic("purego: NewCallback on Linux is only supported on amd64/arm64/arm/loong64/riscv64")
2121 }
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3 3
4-//go:build cgo && !(amd64 || arm64 || loong64 || riscv64)4+//go:build cgo && !(amd64 || arm || arm64 || loong64 || riscv64)
5 5
6 package purego6 package purego
7 7
@@ -17,5 +17,5 @@ func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a
17 }17 }
18 18
19 func NewCallback(_ any) uintptr {19 func NewCallback(_ any) uintptr {
20- panic("purego: NewCallback on Linux is only supported on amd64/arm64/loong64/riscv64")20+ panic("purego: NewCallback on Linux is only supported on amd64/arm64/arm/loong64/riscv64")
21 }21 }
M syscall_sysv.go +19 -12
@@ -1,7 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-//go:build darwin || freebsd || (linux && (amd64 || arm64 || loong64 || riscv64)) || netbsd
4+//go:build darwin || freebsd || (linux && (amd64 || arm || arm64 || loong64 || riscv64)) || netbsd
55
66 package purego
77
@@ -19,9 +19,10 @@ func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a
1919 defer thePool.Put(args)
2020
2121 *args = syscall15Args{
22- fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15,
23- a1, a2, a3, a4, a5, a6, a7, a8,
24- 0,
22+ fn: fn,
23+ a1: a1, a2: a2, a3: a3, a4: a4, a5: a5, a6: a6, a7: a7, a8: a8,
24+ a9: a9, a10: a10, a11: a11, a12: a12, a13: a13, a14: a14, a15: a15,
25+ f1: a1, f2: a2, f3: a3, f4: a4, f5: a5, f6: a6, f7: a7, f8: a8,
2526 }
2627
2728 runtime_cgocall(syscall15XABI0, unsafe.Pointer(args))
@@ -145,8 +146,10 @@ func callbackWrap(a *callbackArgs) {
145146 fnType := fn.Type()
146147 args := make([]reflect.Value, fnType.NumIn())
147148 frame := (*[callbackMaxFrame]uintptr)(a.args)
148- var floatsN int // floatsN represents the number of float arguments processed
149- var intsN int // intsN represents the number of integer arguments processed
149+ // floatsN and intsN track the number of register slots used, not argument count.
150+ // This distinction matters on ARM32 where float64 uses 2 slots (32-bit registers).
151+ var floatsN int
152+ var intsN int
150153 // stackSlot points to the index into frame of the current stack element.
151154 // The stack begins after the float and integer registers.
152155 stackSlot := numOfIntegerRegisters() + numOfFloatRegisters
@@ -154,39 +157,43 @@ func callbackWrap(a *callbackArgs) {
154157 // tight packing. On Darwin ARM64, C passes small types packed on the stack.
155158 stackByteOffset := uintptr(0)
156159 for i := range args {
160+ // slots is the number of pointer-sized slots the argument takes
161+ var slots int
157162 inType := fnType.In(i)
158163 switch inType.Kind() {
159164 case reflect.Float32, reflect.Float64:
160- if floatsN >= numOfFloatRegisters {
165+ slots = int((fnType.In(i).Size() + ptrSize - 1) / ptrSize)
166+ if floatsN+slots > numOfFloatRegisters {
161167 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
162168 // Darwin ARM64: read from packed stack with proper alignment
163169 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
164170 } else {
165171 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
166- stackSlot++
172+ stackSlot += slots
167173 }
168174 } else {
169175 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()
170176 }
171- floatsN++
177+ floatsN += slots
172178 case reflect.Struct:
173179 // This is the CDecl field
174180 args[i] = reflect.Zero(inType)
175181 default:
176- if intsN >= numOfIntegerRegisters() {
182+ slots = int((inType.Size() + ptrSize - 1) / ptrSize)
183+ if intsN+slots > numOfIntegerRegisters() {
177184 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
178185 // Darwin ARM64: read from packed stack with proper alignment
179186 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
180187 } else {
181188 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
182- stackSlot++
189+ stackSlot += slots
183190 }
184191 } else {
185192 // the integers begin after the floats in frame
186193 pos := intsN + numOfFloatRegisters
187194 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()
188195 }
189- intsN++
196+ intsN += slots
190197 }
191198 }
192199 ret := fn.Call(args)
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: Apache-2.01 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors2 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3 3
4-//go:build darwin || freebsd || (linux && (amd64 || arm64 || loong64 || riscv64)) || netbsd4+//go:build darwin || freebsd || (linux && (amd64 || arm || arm64 || loong64 || riscv64)) || netbsd
5 5
6 package purego6 package purego
7 7
@@ -19,9 +19,10 @@ func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a
19 defer thePool.Put(args)19 defer thePool.Put(args)
20 20
21 *args = syscall15Args{21 *args = syscall15Args{
22- fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15,22+ fn: fn,
23- a1, a2, a3, a4, a5, a6, a7, a8,23+ a1: a1, a2: a2, a3: a3, a4: a4, a5: a5, a6: a6, a7: a7, a8: a8,
24- 0,24+ a9: a9, a10: a10, a11: a11, a12: a12, a13: a13, a14: a14, a15: a15,
25+ f1: a1, f2: a2, f3: a3, f4: a4, f5: a5, f6: a6, f7: a7, f8: a8,
25 }26 }
26 27
27 runtime_cgocall(syscall15XABI0, unsafe.Pointer(args))28 runtime_cgocall(syscall15XABI0, unsafe.Pointer(args))
@@ -145,8 +146,10 @@ func callbackWrap(a *callbackArgs) {
145 fnType := fn.Type()146 fnType := fn.Type()
146 args := make([]reflect.Value, fnType.NumIn())147 args := make([]reflect.Value, fnType.NumIn())
147 frame := (*[callbackMaxFrame]uintptr)(a.args)148 frame := (*[callbackMaxFrame]uintptr)(a.args)
148- var floatsN int // floatsN represents the number of float arguments processed149+ // floatsN and intsN track the number of register slots used, not argument count.
149- var intsN int // intsN represents the number of integer arguments processed150+ // This distinction matters on ARM32 where float64 uses 2 slots (32-bit registers).
151+ var floatsN int
152+ var intsN int
150 // stackSlot points to the index into frame of the current stack element.153 // stackSlot points to the index into frame of the current stack element.
151 // The stack begins after the float and integer registers.154 // The stack begins after the float and integer registers.
152 stackSlot := numOfIntegerRegisters() + numOfFloatRegisters155 stackSlot := numOfIntegerRegisters() + numOfFloatRegisters
@@ -154,39 +157,43 @@ func callbackWrap(a *callbackArgs) {
154 // tight packing. On Darwin ARM64, C passes small types packed on the stack.157 // tight packing. On Darwin ARM64, C passes small types packed on the stack.
155 stackByteOffset := uintptr(0)158 stackByteOffset := uintptr(0)
156 for i := range args {159 for i := range args {
160+ // slots is the number of pointer-sized slots the argument takes
161+ var slots int
157 inType := fnType.In(i)162 inType := fnType.In(i)
158 switch inType.Kind() {163 switch inType.Kind() {
159 case reflect.Float32, reflect.Float64:164 case reflect.Float32, reflect.Float64:
160- if floatsN >= numOfFloatRegisters {165+ slots = int((fnType.In(i).Size() + ptrSize - 1) / ptrSize)
166+ if floatsN+slots > numOfFloatRegisters {
161 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {167 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
162 // Darwin ARM64: read from packed stack with proper alignment168 // Darwin ARM64: read from packed stack with proper alignment
163 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)169 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
164 } else {170 } else {
165 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()171 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
166- stackSlot++172+ stackSlot += slots
167 }173 }
168 } else {174 } else {
169 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()175 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()
170 }176 }
171- floatsN++177+ floatsN += slots
172 case reflect.Struct:178 case reflect.Struct:
173 // This is the CDecl field179 // This is the CDecl field
174 args[i] = reflect.Zero(inType)180 args[i] = reflect.Zero(inType)
175 default:181 default:
176- if intsN >= numOfIntegerRegisters() {182+ slots = int((inType.Size() + ptrSize - 1) / ptrSize)
183+ if intsN+slots > numOfIntegerRegisters() {
177 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {184 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
178 // Darwin ARM64: read from packed stack with proper alignment185 // Darwin ARM64: read from packed stack with proper alignment
179 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)186 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
180 } else {187 } else {
181 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()188 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
182- stackSlot++189+ stackSlot += slots
183 }190 }
184 } else {191 } else {
185 // the integers begin after the floats in frame192 // the integers begin after the floats in frame
186 pos := intsN + numOfFloatRegisters193 pos := intsN + numOfFloatRegisters
187 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()194 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()
188 }195 }
189- intsN++196+ intsN += slots
190 }197 }
191 }198 }
192 ret := fn.Call(args)199 ret := fn.Call(args)
M wincallback.go +29 -0
@@ -44,6 +44,34 @@ TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0
4444 }
4545 }
4646
47+func genasmArm() {
48+ var buf bytes.Buffer
49+
50+ buf.WriteString(`// Code generated by wincallback.go using 'go generate'. DO NOT EDIT.
51+
52+//go:build linux
53+
54+// External code calls into callbackasm at an offset corresponding
55+// to the callback index. Callbackasm is a table of MOVW and B instructions.
56+// The MOVW instruction loads R12 with the callback index, and the
57+// B instruction branches to callbackasm1.
58+// callbackasm1 takes the callback index from R12 and
59+// indexes into an array that stores information about each callback.
60+// It then calls the Go implementation for that callback.
61+#include "textflag.h"
62+
63+TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0
64+`)
65+ for i := 0; i < maxCallback; i++ {
66+ fmt.Fprintf(&buf, "\tMOVW\t$%d, R12\n", i)
67+ buf.WriteString("\tB\tcallbackasm1(SB)\n")
68+ }
69+ if err := os.WriteFile("zcallback_arm.s", buf.Bytes(), 0644); err != nil {
70+ fmt.Fprintf(os.Stderr, "wincallback: %s\n", err)
71+ os.Exit(2)
72+ }
73+}
74+
4775 func genasmArm64() {
4876 var buf bytes.Buffer
4977
@@ -137,6 +165,7 @@ TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0
137165
138166 func main() {
139167 genasmAmd64()
168+ genasmArm()
140169 genasmArm64()
141170 genasmLoong64()
142171 genasmRiscv64()
@@ -44,6 +44,34 @@ TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0
44 }44 }
45 }45 }
46 46
47+func genasmArm() {
48+ var buf bytes.Buffer
49+
50+ buf.WriteString(`// Code generated by wincallback.go using 'go generate'. DO NOT EDIT.
51+
52+//go:build linux
53+
54+// External code calls into callbackasm at an offset corresponding
55+// to the callback index. Callbackasm is a table of MOVW and B instructions.
56+// The MOVW instruction loads R12 with the callback index, and the
57+// B instruction branches to callbackasm1.
58+// callbackasm1 takes the callback index from R12 and
59+// indexes into an array that stores information about each callback.
60+// It then calls the Go implementation for that callback.
61+#include "textflag.h"
62+
63+TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0
64+`)
65+ for i := 0; i < maxCallback; i++ {
66+ fmt.Fprintf(&buf, "\tMOVW\t$%d, R12\n", i)
67+ buf.WriteString("\tB\tcallbackasm1(SB)\n")
68+ }
69+ if err := os.WriteFile("zcallback_arm.s", buf.Bytes(), 0644); err != nil {
70+ fmt.Fprintf(os.Stderr, "wincallback: %s\n", err)
71+ os.Exit(2)
72+ }
73+}
74+
47 func genasmArm64() {75 func genasmArm64() {
48 var buf bytes.Buffer76 var buf bytes.Buffer
49 77
@@ -137,6 +165,7 @@ TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0
137 165
138 func main() {166 func main() {
139 genasmAmd64()167 genasmAmd64()
168+ genasmArm()
140 genasmArm64()169 genasmArm64()
141 genasmLoong64()170 genasmLoong64()
142 genasmRiscv64()171 genasmRiscv64()
A zcallback_arm.s +4014 -0 large diff (4014 lines changed): collapsed

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close