detached 576826c

purego: support structs on Android and iOS (#467)

Enable passing and returning C structs by value on Android and iOS for
amd64 and arm64, in both RegisterFunc calls and NewCallback callbacks.

The struct-marshalling code already compiled into these builds and was
only gated off by runtime.GOOS string checks. iOS shares the Apple ABI
with macOS and Android shares the standard ABI with Linux, so route
GOOS=ios through the Darwin path via a new isDarwin helper and treat
GOOS=android like Linux. Open the ensureStructSupported gate, add ios
and android to the getCallbackStruct callback switches, and extend the
Darwin ARM64 stack-argument reads in the callback path to iOS.

Add an Android emulator CI job (amd64 and arm64) that runs the test
suite. Because the emulator has no C toolchain, the test libraries are
cross-compiled on the host with the NDK and loaded via the new
PUREGO_TEST_PREBUILT_LIBDIR instead of being compiled at run time.

Closes #466

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
commit
576826c1e01be83a93430e4b11a3e24c0cbfb3de
author
Hajime Hoshi · · 2026-06-23 02:57 UTC
committer
GitHub · · 2026-06-23 02:57 UTC
parent
7e97482
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
7 files · +129 -31
M .github/workflows/test.yml +74 -0
@@ -316,3 +316,77 @@ jobs:
316316 rm -fr /usr/local/go && tar -C /usr/local -xf go${{matrix.go}}.netbsd-amd64.tar.gz
317317 chmod +x $GITHUB_WORKSPACE/.github/scripts/bsd_tests.sh
318318 run: $GITHUB_WORKSPACE/.github/scripts/bsd_tests.sh
319+
320+ # Android is tested on amd64 only. The emulator requires the system image
321+ # architecture to match the host, and GitHub's arm64 hosted runners do not
322+ # provide a usable KVM device, so there is no hosted hardware-accelerated path
323+ # to an Android arm64 emulator. The arm64 struct ABI is already covered by the
324+ # linux/arm64 `test` job, and Android's Bionic/cgo integration by this job.
325+ android:
326+ strategy:
327+ fail-fast: false
328+ matrix:
329+ go: ['1.25.x', '1.26.x']
330+ name: Test with Go ${{ matrix.go }} on Android amd64
331+ runs-on: ubuntu-latest
332+ defaults:
333+ run:
334+ shell: bash
335+ steps:
336+ - uses: actions/checkout@v5
337+
338+ - name: Setup Go
339+ uses: actions/setup-go@v6
340+ with:
341+ go-version: ${{ matrix.go }}
342+
343+ # The tests compile their C libraries at run time via `go env CC`, but the
344+ # emulator has no C toolchain. Every library the tests load must therefore be
345+ # cross-compiled here and loaded as a prebuilt library at run time instead.
346+ - name: Cross-compile the test binary and C libraries
347+ run: |
348+ set -eux
349+ NDK="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
350+ CC="$NDK/x86_64-linux-android21-clang"
351+ mkdir -p prebuilt
352+ # Build every library the tests load. Keep this in sync with the
353+ # buildSharedLib callers in the tests; a missing entry surfaces as a
354+ # "prebuilt lib ... no such file" failure on the emulator.
355+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/structtest.so testdata/structtest/struct_test.c
356+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/structreturntest.so testdata/structtest/structreturn_test.c
357+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/libcbtest.so testdata/libcbtest/callback_test.c
358+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/abitest.so testdata/abitest/abi_test.c
359+ env GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC="$CC" go test -c -o purego.test .
360+
361+ - name: Enable KVM
362+ run: |
363+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
364+ sudo udevadm control --reload-rules
365+ sudo udevadm trigger --name-match=kvm
366+
367+ - name: Run the test suite on the Android emulator
368+ uses: reactivecircus/android-emulator-runner@v2
369+ with:
370+ api-level: 34
371+ arch: x86_64
372+ target: google_apis
373+ emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
374+ script: |
375+ set -eux
376+ adb push purego.test /data/local/tmp/purego.test
377+ adb push prebuilt /data/local/tmp/prebuilt
378+ adb shell "chmod 755 /data/local/tmp/purego.test"
379+ # TestSetuidEtc needs root and glibc (not Bionic) semantics; TestNestedDlopenCall
380+ # needs the C++ runtime present on the device. Both are covered by the Linux
381+ # runners, so skip them here rather than special-casing the emulator.
382+ adb shell "cd /data/local/tmp && TMPDIR=/data/local/tmp PUREGO_TEST_PREBUILT_LIBDIR=/data/local/tmp/prebuilt ./purego.test -test.v -test.skip 'TestSetuidEtc|TestNestedDlopenCall'" | tee result.txt
383+ grep -q '^PASS' result.txt
384+ ! grep -q '^FAIL' result.txt
385+
386+ # TODO: Add an iOS job that runs the test suite on the iOS Simulator.
387+ # iOS has no emulator runner equivalent: on a macOS runner the test binary must
388+ # be wrapped into a .app and launched via `xcrun simctl` (or x/mobile's
389+ # go_ios_exec), with the C libraries cross-compiled against the iphonesimulator
390+ # SDK and loaded through PUREGO_TEST_PREBUILT_LIBDIR (as the android job does).
391+ # Deprioritized because the iOS arm64 ABI matches macOS arm64, which the `test`
392+ # job already runs.
@@ -316,3 +316,77 @@ jobs:
316 rm -fr /usr/local/go && tar -C /usr/local -xf go${{matrix.go}}.netbsd-amd64.tar.gz316 rm -fr /usr/local/go && tar -C /usr/local -xf go${{matrix.go}}.netbsd-amd64.tar.gz
317 chmod +x $GITHUB_WORKSPACE/.github/scripts/bsd_tests.sh317 chmod +x $GITHUB_WORKSPACE/.github/scripts/bsd_tests.sh
318 run: $GITHUB_WORKSPACE/.github/scripts/bsd_tests.sh318 run: $GITHUB_WORKSPACE/.github/scripts/bsd_tests.sh
319+
320+ # Android is tested on amd64 only. The emulator requires the system image
321+ # architecture to match the host, and GitHub's arm64 hosted runners do not
322+ # provide a usable KVM device, so there is no hosted hardware-accelerated path
323+ # to an Android arm64 emulator. The arm64 struct ABI is already covered by the
324+ # linux/arm64 `test` job, and Android's Bionic/cgo integration by this job.
325+ android:
326+ strategy:
327+ fail-fast: false
328+ matrix:
329+ go: ['1.25.x', '1.26.x']
330+ name: Test with Go ${{ matrix.go }} on Android amd64
331+ runs-on: ubuntu-latest
332+ defaults:
333+ run:
334+ shell: bash
335+ steps:
336+ - uses: actions/checkout@v5
337+
338+ - name: Setup Go
339+ uses: actions/setup-go@v6
340+ with:
341+ go-version: ${{ matrix.go }}
342+
343+ # The tests compile their C libraries at run time via `go env CC`, but the
344+ # emulator has no C toolchain. Every library the tests load must therefore be
345+ # cross-compiled here and loaded as a prebuilt library at run time instead.
346+ - name: Cross-compile the test binary and C libraries
347+ run: |
348+ set -eux
349+ NDK="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
350+ CC="$NDK/x86_64-linux-android21-clang"
351+ mkdir -p prebuilt
352+ # Build every library the tests load. Keep this in sync with the
353+ # buildSharedLib callers in the tests; a missing entry surfaces as a
354+ # "prebuilt lib ... no such file" failure on the emulator.
355+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/structtest.so testdata/structtest/struct_test.c
356+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/structreturntest.so testdata/structtest/structreturn_test.c
357+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/libcbtest.so testdata/libcbtest/callback_test.c
358+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/abitest.so testdata/abitest/abi_test.c
359+ env GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC="$CC" go test -c -o purego.test .
360+
361+ - name: Enable KVM
362+ run: |
363+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
364+ sudo udevadm control --reload-rules
365+ sudo udevadm trigger --name-match=kvm
366+
367+ - name: Run the test suite on the Android emulator
368+ uses: reactivecircus/android-emulator-runner@v2
369+ with:
370+ api-level: 34
371+ arch: x86_64
372+ target: google_apis
373+ emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
374+ script: |
375+ set -eux
376+ adb push purego.test /data/local/tmp/purego.test
377+ adb push prebuilt /data/local/tmp/prebuilt
378+ adb shell "chmod 755 /data/local/tmp/purego.test"
379+ # TestSetuidEtc needs root and glibc (not Bionic) semantics; TestNestedDlopenCall
380+ # needs the C++ runtime present on the device. Both are covered by the Linux
381+ # runners, so skip them here rather than special-casing the emulator.
382+ adb shell "cd /data/local/tmp && TMPDIR=/data/local/tmp PUREGO_TEST_PREBUILT_LIBDIR=/data/local/tmp/prebuilt ./purego.test -test.v -test.skip 'TestSetuidEtc|TestNestedDlopenCall'" | tee result.txt
383+ grep -q '^PASS' result.txt
384+ ! grep -q '^FAIL' result.txt
385+
386+ # TODO: Add an iOS job that runs the test suite on the iOS Simulator.
387+ # iOS has no emulator runner equivalent: on a macOS runner the test binary must
388+ # be wrapped into a .app and launched via `xcrun simctl` (or x/mobile's
389+ # go_ios_exec), with the C libraries cross-compiled against the iphonesimulator
390+ # SDK and loaded through PUREGO_TEST_PREBUILT_LIBDIR (as the android job does).
391+ # Deprioritized because the iOS arm64 ABI matches macOS arm64, which the `test`
392+ # job already runs.
M README.md +10 -10
@@ -30,27 +30,27 @@ except for float arguments and return values.
3030
3131 Tier 1 platforms are the primary targets officially supported by PureGo. When a new version of PureGo is released, any critical bugs found on Tier 1 platforms are treated as release blockers. The release will be postponed until such issues are resolved.
3232
33-- **Android**: amd64<sup>1,2</sup>, arm64<sup>1,2</sup>
34-- **iOS**: amd64<sup>1,2</sup>, arm64<sup>1,2</sup>
33+- **Android**: amd64<sup>1</sup>, arm64<sup>1</sup>
34+- **iOS**: amd64<sup>1</sup>, arm64<sup>1</sup>
3535 - **Linux**: amd64, arm64
3636 - **macOS**: amd64, arm64
37-- **Windows**: amd64<sup>3</sup>, arm64<sup>3</sup>
37+- **Windows**: amd64<sup>2</sup>, arm64<sup>2</sup>
3838
3939 ### Tier 2
4040
4141 Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs on Tier 2 platforms do not block new PureGo releases. However, fixes contributed by external contributors are very welcome and encouraged.
4242
43-- **Android**: 386<sup>1,2</sup>, arm<sup>1,2</sup>
44-- **FreeBSD**: amd64<sup>2,4</sup>, arm64<sup>2,4</sup>
45-- **Linux**: 386<sup>2</sup>, arm<sup>2</sup>, loong64<sup>2</sup>, ppc64le<sup>2</sup>, riscv64<sup>2</sup>, s390x<sup>1,2</sup>
46-- **NetBSD**: amd64<sup>2,4</sup>, arm64<sup>2,4</sup>
47-- **Windows**: 386<sup>2,5</sup>, arm<sup>2,5,6</sup>
43+- **Android**: 386<sup>1,3</sup>, arm<sup>1,3</sup>
44+- **FreeBSD**: amd64<sup>3,4</sup>, arm64<sup>3,4</sup>
45+- **Linux**: 386<sup>3</sup>, arm<sup>3</sup>, loong64<sup>3</sup>, ppc64le<sup>3</sup>, riscv64<sup>3</sup>, s390x<sup>1,3</sup>
46+- **NetBSD**: amd64<sup>3,4</sup>, arm64<sup>3,4</sup>
47+- **Windows**: 386<sup>3,5</sup>, arm<sup>3,5,6</sup>
4848
4949 #### Support Notes
5050
5151 1. These architectures require CGO_ENABLED=1 to compile
52-2. These architectures do not support passing structs by value as arguments or return values
53-3. These architectures support passing structs by value as arguments and return values when calling C functions, but not in callbacks created with `NewCallback`
52+2. These architectures support passing structs by value as arguments and return values when calling C functions, but not in callbacks created with `NewCallback`
53+3. These architectures do not support passing structs by value as arguments or return values
5454 4. These architectures require the special flag `-gcflags="github.com/ebitengine/purego/internal/fakecgo=-std"` to compile with CGO_ENABLED=0
5555 5. These architectures only support `SyscallN` and `NewCallback`
5656 6. These architectures are no longer supported as of Go 1.26
@@ -30,27 +30,27 @@ except for float arguments and return values.
30 30
31 Tier 1 platforms are the primary targets officially supported by PureGo. When a new version of PureGo is released, any critical bugs found on Tier 1 platforms are treated as release blockers. The release will be postponed until such issues are resolved.31 Tier 1 platforms are the primary targets officially supported by PureGo. When a new version of PureGo is released, any critical bugs found on Tier 1 platforms are treated as release blockers. The release will be postponed until such issues are resolved.
32 32
33-- **Android**: amd64<sup>1,2</sup>, arm64<sup>1,2</sup>33+- **Android**: amd64<sup>1</sup>, arm64<sup>1</sup>
34-- **iOS**: amd64<sup>1,2</sup>, arm64<sup>1,2</sup>34+- **iOS**: amd64<sup>1</sup>, arm64<sup>1</sup>
35 - **Linux**: amd64, arm6435 - **Linux**: amd64, arm64
36 - **macOS**: amd64, arm6436 - **macOS**: amd64, arm64
37-- **Windows**: amd64<sup>3</sup>, arm64<sup>3</sup>37+- **Windows**: amd64<sup>2</sup>, arm64<sup>2</sup>
38 38
39 ### Tier 239 ### Tier 2
40 40
41 Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs on Tier 2 platforms do not block new PureGo releases. However, fixes contributed by external contributors are very welcome and encouraged.41 Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs on Tier 2 platforms do not block new PureGo releases. However, fixes contributed by external contributors are very welcome and encouraged.
42 42
43-- **Android**: 386<sup>1,2</sup>, arm<sup>1,2</sup>43+- **Android**: 386<sup>1,3</sup>, arm<sup>1,3</sup>
44-- **FreeBSD**: amd64<sup>2,4</sup>, arm64<sup>2,4</sup>44+- **FreeBSD**: amd64<sup>3,4</sup>, arm64<sup>3,4</sup>
45-- **Linux**: 386<sup>2</sup>, arm<sup>2</sup>, loong64<sup>2</sup>, ppc64le<sup>2</sup>, riscv64<sup>2</sup>, s390x<sup>1,2</sup>45+- **Linux**: 386<sup>3</sup>, arm<sup>3</sup>, loong64<sup>3</sup>, ppc64le<sup>3</sup>, riscv64<sup>3</sup>, s390x<sup>1,3</sup>
46-- **NetBSD**: amd64<sup>2,4</sup>, arm64<sup>2,4</sup>46+- **NetBSD**: amd64<sup>3,4</sup>, arm64<sup>3,4</sup>
47-- **Windows**: 386<sup>2,5</sup>, arm<sup>2,5,6</sup>47+- **Windows**: 386<sup>3,5</sup>, arm<sup>3,5,6</sup>
48 48
49 #### Support Notes49 #### Support Notes
50 50
51 1. These architectures require CGO_ENABLED=1 to compile51 1. These architectures require CGO_ENABLED=1 to compile
52-2. These architectures do not support passing structs by value as arguments or return values52+2. These architectures support passing structs by value as arguments and return values when calling C functions, but not in callbacks created with `NewCallback`
53-3. These architectures support passing structs by value as arguments and return values when calling C functions, but not in callbacks created with `NewCallback`53+3. These architectures do not support passing structs by value as arguments or return values
54 4. These architectures require the special flag `-gcflags="github.com/ebitengine/purego/internal/fakecgo=-std"` to compile with CGO_ENABLED=054 4. These architectures require the special flag `-gcflags="github.com/ebitengine/purego/internal/fakecgo=-std"` to compile with CGO_ENABLED=0
55 5. These architectures only support `SyscallN` and `NewCallback`55 5. These architectures only support `SyscallN` and `NewCallback`
56 6. These architectures are no longer supported as of Go 1.2656 6. These architectures are no longer supported as of Go 1.26
M func.go +13 -7
@@ -64,7 +64,7 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
6464 // int64 <=> int64_t
6565 // float32 <=> float
6666 // float64 <=> double
67-// struct <=> struct (darwin amd64/arm64, linux amd64/arm64, windows amd64/arm64)
67+// struct <=> struct (android, darwin, ios, linux, and windows on amd64/arm64)
6868 // func <=> C function
6969 // unsafe.Pointer, *T <=> void*
7070 // []T => void*
@@ -99,8 +99,8 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
9999 // it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure
100100 // that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.
101101 //
102-// On Darwin ARM64, purego handles proper alignment of struct arguments when passing them on the stack,
103-// following the C ABI's byte-level packing rules.
102+// On Apple ARM64 platforms (macOS and iOS), purego handles proper alignment of struct arguments
103+// when passing them on the stack, following the C ABI's byte-level packing rules.
104104 //
105105 // On Windows, struct arguments and returns are supported on amd64 and arm64 when calling C functions.
106106 // Passing or returning structs in callbacks created with [NewCallback] is not supported on Windows.
@@ -213,7 +213,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
213213 if ints+floats+stack > argsLimit {
214214 panic("purego: too many stack arguments")
215215 }
216- } else if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
216+ } else if isDarwin && runtime.GOARCH == "arm64" {
217217 // On Darwin ARM64, use byte-based validation since arguments pack efficiently.
218218 // See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
219219 stackBytes := estimateStackBytes(ty)
@@ -313,7 +313,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
313313 continue
314314 }
315315 // Check if we need to start Darwin ARM64 C-style stack packing
316- if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
316+ if runtime.GOARCH == "arm64" && isDarwin && shouldBundleStackArgs(v, numInts, numFloats) {
317317 // Collect and separate remaining args into register vs stack
318318 stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
319319 keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
@@ -505,11 +505,17 @@ func ensureStructSupported() {
505505 if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
506506 panic("purego: struct arguments/returns are only supported on amd64 and arm64")
507507 }
508- if runtime.GOOS != "darwin" && runtime.GOOS != "linux" && runtime.GOOS != "windows" {
509- panic("purego: struct arguments/returns are only supported on darwin, linux, and windows")
508+ switch runtime.GOOS {
509+ case "android", "darwin", "ios", "linux", "windows":
510+ default:
511+ panic("purego: struct arguments/returns are only supported on android, darwin, ios, linux, and windows")
510512 }
511513 }
512514
515+// isDarwin is true on platforms that use Apple's calling convention.
516+// iOS (GOOS=ios) shares it with macOS (GOOS=darwin).
517+const isDarwin = runtime.GOOS == "darwin" || runtime.GOOS == "ios"
518+
513519 // amd64StructReturnInMemory reports whether a struct return value of the given
514520 // size is returned through a caller-allocated hidden pointer (true) rather than
515521 // in registers (false). It must only be consulted on amd64.
@@ -64,7 +64,7 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
64 // int64 <=> int64_t64 // int64 <=> int64_t
65 // float32 <=> float65 // float32 <=> float
66 // float64 <=> double66 // float64 <=> double
67-// struct <=> struct (darwin amd64/arm64, linux amd64/arm64, windows amd64/arm64)67+// struct <=> struct (android, darwin, ios, linux, and windows on amd64/arm64)
68 // func <=> C function68 // func <=> C function
69 // unsafe.Pointer, *T <=> void*69 // unsafe.Pointer, *T <=> void*
70 // []T => void*70 // []T => void*
@@ -99,8 +99,8 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
99 // it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure99 // it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure
100 // that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.100 // that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.
101 //101 //
102-// On Darwin ARM64, purego handles proper alignment of struct arguments when passing them on the stack,102+// On Apple ARM64 platforms (macOS and iOS), purego handles proper alignment of struct arguments
103-// following the C ABI's byte-level packing rules.103+// when passing them on the stack, following the C ABI's byte-level packing rules.
104 //104 //
105 // On Windows, struct arguments and returns are supported on amd64 and arm64 when calling C functions.105 // On Windows, struct arguments and returns are supported on amd64 and arm64 when calling C functions.
106 // Passing or returning structs in callbacks created with [NewCallback] is not supported on Windows.106 // Passing or returning structs in callbacks created with [NewCallback] is not supported on Windows.
@@ -213,7 +213,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
213 if ints+floats+stack > argsLimit {213 if ints+floats+stack > argsLimit {
214 panic("purego: too many stack arguments")214 panic("purego: too many stack arguments")
215 }215 }
216- } else if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {216+ } else if isDarwin && runtime.GOARCH == "arm64" {
217 // On Darwin ARM64, use byte-based validation since arguments pack efficiently.217 // On Darwin ARM64, use byte-based validation since arguments pack efficiently.
218 // See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms218 // See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
219 stackBytes := estimateStackBytes(ty)219 stackBytes := estimateStackBytes(ty)
@@ -313,7 +313,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
313 continue313 continue
314 }314 }
315 // Check if we need to start Darwin ARM64 C-style stack packing315 // Check if we need to start Darwin ARM64 C-style stack packing
316- if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {316+ if runtime.GOARCH == "arm64" && isDarwin && shouldBundleStackArgs(v, numInts, numFloats) {
317 // Collect and separate remaining args into register vs stack317 // Collect and separate remaining args into register vs stack
318 stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,318 stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
319 keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)319 keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
@@ -505,11 +505,17 @@ func ensureStructSupported() {
505 if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {505 if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
506 panic("purego: struct arguments/returns are only supported on amd64 and arm64")506 panic("purego: struct arguments/returns are only supported on amd64 and arm64")
507 }507 }
508- if runtime.GOOS != "darwin" && runtime.GOOS != "linux" && runtime.GOOS != "windows" {508+ switch runtime.GOOS {
509- panic("purego: struct arguments/returns are only supported on darwin, linux, and windows")509+ case "android", "darwin", "ios", "linux", "windows":
510+ default:
511+ panic("purego: struct arguments/returns are only supported on android, darwin, ios, linux, and windows")
510 }512 }
511 }513 }
512 514
515+// isDarwin is true on platforms that use Apple's calling convention.
516+// iOS (GOOS=ios) shares it with macOS (GOOS=darwin).
517+const isDarwin = runtime.GOOS == "darwin" || runtime.GOOS == "ios"
518+
513 // amd64StructReturnInMemory reports whether a struct return value of the given519 // amd64StructReturnInMemory reports whether a struct return value of the given
514 // size is returned through a caller-allocated hidden pointer (true) rather than520 // size is returned through a caller-allocated hidden pointer (true) rather than
515 // in registers (false). It must only be consulted on amd64.521 // in registers (false). It must only be consulted on amd64.
M func_test.go +19 -1
@@ -7,6 +7,7 @@ import (
77 "bytes"
88 "errors"
99 "fmt"
10+ "os"
1011 "os/exec"
1112 "path/filepath"
1213 "runtime"
@@ -21,7 +22,9 @@ import (
2122
2223 func getSystemLibrary() (string, error) {
2324 switch runtime.GOOS {
24- case "darwin":
25+ case "android":
26+ return "libc.so", nil
27+ case "darwin", "ios":
2528 return "/usr/lib/libSystem.B.dylib", nil
2629 case "freebsd":
2730 return "libc.so.7", nil
@@ -601,6 +604,21 @@ func TestABI_TooManyArguments(t *testing.T) {
601604 }
602605
603606 func buildSharedLib(compilerEnv, libFile string, sources ...string) error {
607+ // When PUREGO_TEST_PREBUILT_LIBDIR is set, the shared library has been
608+ // cross-compiled ahead of time and placed in that directory under the
609+ // base name of libFile. This allows running the tests on a target that
610+ // has no C toolchain, such as an Android emulator.
611+ if dir := os.Getenv("PUREGO_TEST_PREBUILT_LIBDIR"); dir != "" {
612+ data, err := os.ReadFile(filepath.Join(dir, filepath.Base(libFile)))
613+ if err != nil {
614+ return fmt.Errorf("prebuilt lib: %w", err)
615+ }
616+ if err := os.WriteFile(libFile, data, 0o755); err != nil {
617+ return fmt.Errorf("prebuilt lib: %w", err)
618+ }
619+ return nil
620+ }
621+
604622 out, err := exec.Command("go", "env", compilerEnv).Output()
605623 if err != nil {
606624 return fmt.Errorf("go env %s error: %w", compilerEnv, err)
@@ -7,6 +7,7 @@ import (
7 "bytes"7 "bytes"
8 "errors"8 "errors"
9 "fmt"9 "fmt"
10+ "os"
10 "os/exec"11 "os/exec"
11 "path/filepath"12 "path/filepath"
12 "runtime"13 "runtime"
@@ -21,7 +22,9 @@ import (
21 22
22 func getSystemLibrary() (string, error) {23 func getSystemLibrary() (string, error) {
23 switch runtime.GOOS {24 switch runtime.GOOS {
24- case "darwin":25+ case "android":
26+ return "libc.so", nil
27+ case "darwin", "ios":
25 return "/usr/lib/libSystem.B.dylib", nil28 return "/usr/lib/libSystem.B.dylib", nil
26 case "freebsd":29 case "freebsd":
27 return "libc.so.7", nil30 return "libc.so.7", nil
@@ -601,6 +604,21 @@ func TestABI_TooManyArguments(t *testing.T) {
601 }604 }
602 605
603 func buildSharedLib(compilerEnv, libFile string, sources ...string) error {606 func buildSharedLib(compilerEnv, libFile string, sources ...string) error {
607+ // When PUREGO_TEST_PREBUILT_LIBDIR is set, the shared library has been
608+ // cross-compiled ahead of time and placed in that directory under the
609+ // base name of libFile. This allows running the tests on a target that
610+ // has no C toolchain, such as an Android emulator.
611+ if dir := os.Getenv("PUREGO_TEST_PREBUILT_LIBDIR"); dir != "" {
612+ data, err := os.ReadFile(filepath.Join(dir, filepath.Base(libFile)))
613+ if err != nil {
614+ return fmt.Errorf("prebuilt lib: %w", err)
615+ }
616+ if err := os.WriteFile(libFile, data, 0o755); err != nil {
617+ return fmt.Errorf("prebuilt lib: %w", err)
618+ }
619+ return nil
620+ }
621+
604 out, err := exec.Command("go", "env", compilerEnv).Output()622 out, err := exec.Command("go", "env", compilerEnv).Output()
605 if err != nil {623 if err != nil {
606 return fmt.Errorf("go env %s error: %w", compilerEnv, err)624 return fmt.Errorf("go env %s error: %w", compilerEnv, err)
M struct_amd64.go +1 -1
@@ -339,7 +339,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
339339 // - If not enough registers for all eightbytes: entire struct goes on the stack
340340 func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
341341 switch runtime.GOOS {
342- case "darwin", "freebsd", "linux", "netbsd":
342+ case "android", "darwin", "freebsd", "ios", "linux", "netbsd":
343343 default:
344344 panic("purego: getCallbackStruct is not supported on " + runtime.GOOS)
345345 }
@@ -339,7 +339,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
339 // - If not enough registers for all eightbytes: entire struct goes on the stack339 // - If not enough registers for all eightbytes: entire struct goes on the stack
340 func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {340 func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
341 switch runtime.GOOS {341 switch runtime.GOOS {
342- case "darwin", "freebsd", "linux", "netbsd":342+ case "android", "darwin", "freebsd", "ios", "linux", "netbsd":
343 default:343 default:
344 panic("purego: getCallbackStruct is not supported on " + runtime.GOOS)344 panic("purego: getCallbackStruct is not supported on " + runtime.GOOS)
345 }345 }
M struct_arm64.go +10 -10
@@ -92,7 +92,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
9292 }
9393
9494 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
95- if runtime.GOOS == "darwin" {
95+ if isDarwin {
9696 placeRegistersDarwin(v, addFloat, addInt)
9797 return
9898 }
@@ -301,7 +301,7 @@ func isHVA(t reflect.Type) bool {
301301 // copyStruct8ByteChunks copies struct memory in 8-byte chunks to the provided callback.
302302 // This is used for Darwin ARM64's byte-level packing of non-HFA/HVA structs.
303303 func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintptr)) {
304- if runtime.GOOS != "darwin" {
304+ if !isDarwin {
305305 panic("purego: should only be called on darwin")
306306 }
307307 for offset := uintptr(0); offset < size; offset += 8 {
@@ -328,7 +328,7 @@ func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintp
328328 // For non-HFA/HVA structs, Darwin uses byte-level packing. We copy the struct memory in
329329 // 8-byte chunks, which works correctly for both register and stack placement.
330330 func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
331- if runtime.GOOS != "darwin" {
331+ if !isDarwin {
332332 panic("purego: placeRegistersDarwin should only be called on darwin")
333333 }
334334 // Check if this is an HFA/HVA
@@ -356,7 +356,7 @@ func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(u
356356 // shouldBundleStackArgs determines if we need to start C-style packing for
357357 // Darwin ARM64 stack arguments. This happens when registers are exhausted.
358358 func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
359- if runtime.GOOS != "darwin" {
359+ if !isDarwin {
360360 return false
361361 }
362362
@@ -398,7 +398,7 @@ func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
398398 // registers, used during stack argument bundling to decide if a struct
399399 // should go through normal register allocation or be bundled with stack args.
400400 func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
401- if runtime.GOOS != "darwin" {
401+ if !isDarwin {
402402 panic("purego: structFitsInRegisters should only be called on darwin")
403403 }
404404 hfa := isHFA(val.Type())
@@ -431,7 +431,7 @@ func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (b
431431 func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
432432 keepAlive []any, addInt, addFloat, addStack func(uintptr),
433433 pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
434- if runtime.GOOS != "darwin" {
434+ if !isDarwin {
435435 panic("purego: collectStackArgs should only be called on darwin")
436436 }
437437
@@ -488,7 +488,7 @@ const (
488488 // bundleStackArgs bundles remaining arguments for Darwin ARM64 C-style stack packing.
489489 // It creates a packed struct with proper alignment and copies it to the stack in 8-byte chunks.
490490 func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
491- if runtime.GOOS != "darwin" {
491+ if !isDarwin {
492492 panic("purego: bundleStackArgs should only be called on darwin")
493493 }
494494 if len(stackArgs) == 0 {
@@ -562,7 +562,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
562562 // - Darwin ARM64: byte-level packing on the stack
563563 func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
564564 switch runtime.GOOS {
565- case "darwin", "freebsd", "linux", "netbsd":
565+ case "android", "darwin", "freebsd", "ios", "linux", "netbsd":
566566 default:
567567 panic("purego: getCallbackStruct is not supported on " + runtime.GOOS)
568568 }
@@ -609,7 +609,7 @@ func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int,
609609 }
610610
611611 // Pointer on stack (rare: all integer registers exhausted).
612- if runtime.GOOS == "darwin" {
612+ if isDarwin {
613613 ptrVal := callbackArgFromStack(frame, *stackSlot, stackByteOffset, reflect.TypeOf(uintptr(0)))
614614 ptr := uintptr(ptrVal.Uint())
615615 return reflect.NewAt(inType, *(*unsafe.Pointer)(unsafe.Pointer(&ptr))).Elem()
@@ -690,7 +690,7 @@ func readHFAFromRegisters(inType reflect.Type, f *[callbackMaxFrame]uintptr, flo
690690 // callback frame. On Darwin ARM64, arguments are byte-packed on the stack.
691691 // On Linux ARM64, arguments are 8-byte aligned.
692692 func readStructFromStackArm64(inType reflect.Type, f *[callbackMaxFrame]uintptr, frame unsafe.Pointer, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
693- if runtime.GOOS == "darwin" {
693+ if isDarwin {
694694 return callbackArgFromStack(frame, *stackSlot, stackByteOffset, inType)
695695 }
696696 // Linux ARM64: 8-byte aligned slots.
@@ -92,7 +92,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
92 }92 }
93 93
94 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {94 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
95- if runtime.GOOS == "darwin" {95+ if isDarwin {
96 placeRegistersDarwin(v, addFloat, addInt)96 placeRegistersDarwin(v, addFloat, addInt)
97 return97 return
98 }98 }
@@ -301,7 +301,7 @@ func isHVA(t reflect.Type) bool {
301 // copyStruct8ByteChunks copies struct memory in 8-byte chunks to the provided callback.301 // copyStruct8ByteChunks copies struct memory in 8-byte chunks to the provided callback.
302 // This is used for Darwin ARM64's byte-level packing of non-HFA/HVA structs.302 // This is used for Darwin ARM64's byte-level packing of non-HFA/HVA structs.
303 func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintptr)) {303 func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintptr)) {
304- if runtime.GOOS != "darwin" {304+ if !isDarwin {
305 panic("purego: should only be called on darwin")305 panic("purego: should only be called on darwin")
306 }306 }
307 for offset := uintptr(0); offset < size; offset += 8 {307 for offset := uintptr(0); offset < size; offset += 8 {
@@ -328,7 +328,7 @@ func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintp
328 // For non-HFA/HVA structs, Darwin uses byte-level packing. We copy the struct memory in328 // For non-HFA/HVA structs, Darwin uses byte-level packing. We copy the struct memory in
329 // 8-byte chunks, which works correctly for both register and stack placement.329 // 8-byte chunks, which works correctly for both register and stack placement.
330 func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {330 func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
331- if runtime.GOOS != "darwin" {331+ if !isDarwin {
332 panic("purego: placeRegistersDarwin should only be called on darwin")332 panic("purego: placeRegistersDarwin should only be called on darwin")
333 }333 }
334 // Check if this is an HFA/HVA334 // Check if this is an HFA/HVA
@@ -356,7 +356,7 @@ func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(u
356 // shouldBundleStackArgs determines if we need to start C-style packing for356 // shouldBundleStackArgs determines if we need to start C-style packing for
357 // Darwin ARM64 stack arguments. This happens when registers are exhausted.357 // Darwin ARM64 stack arguments. This happens when registers are exhausted.
358 func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {358 func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
359- if runtime.GOOS != "darwin" {359+ if !isDarwin {
360 return false360 return false
361 }361 }
362 362
@@ -398,7 +398,7 @@ func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
398 // registers, used during stack argument bundling to decide if a struct398 // registers, used during stack argument bundling to decide if a struct
399 // should go through normal register allocation or be bundled with stack args.399 // should go through normal register allocation or be bundled with stack args.
400 func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {400 func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
401- if runtime.GOOS != "darwin" {401+ if !isDarwin {
402 panic("purego: structFitsInRegisters should only be called on darwin")402 panic("purego: structFitsInRegisters should only be called on darwin")
403 }403 }
404 hfa := isHFA(val.Type())404 hfa := isHFA(val.Type())
@@ -431,7 +431,7 @@ func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (b
431 func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,431 func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
432 keepAlive []any, addInt, addFloat, addStack func(uintptr),432 keepAlive []any, addInt, addFloat, addStack func(uintptr),
433 pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {433 pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
434- if runtime.GOOS != "darwin" {434+ if !isDarwin {
435 panic("purego: collectStackArgs should only be called on darwin")435 panic("purego: collectStackArgs should only be called on darwin")
436 }436 }
437 437
@@ -488,7 +488,7 @@ const (
488 // bundleStackArgs bundles remaining arguments for Darwin ARM64 C-style stack packing.488 // bundleStackArgs bundles remaining arguments for Darwin ARM64 C-style stack packing.
489 // It creates a packed struct with proper alignment and copies it to the stack in 8-byte chunks.489 // It creates a packed struct with proper alignment and copies it to the stack in 8-byte chunks.
490 func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {490 func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
491- if runtime.GOOS != "darwin" {491+ if !isDarwin {
492 panic("purego: bundleStackArgs should only be called on darwin")492 panic("purego: bundleStackArgs should only be called on darwin")
493 }493 }
494 if len(stackArgs) == 0 {494 if len(stackArgs) == 0 {
@@ -562,7 +562,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
562 // - Darwin ARM64: byte-level packing on the stack562 // - Darwin ARM64: byte-level packing on the stack
563 func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {563 func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
564 switch runtime.GOOS {564 switch runtime.GOOS {
565- case "darwin", "freebsd", "linux", "netbsd":565+ case "android", "darwin", "freebsd", "ios", "linux", "netbsd":
566 default:566 default:
567 panic("purego: getCallbackStruct is not supported on " + runtime.GOOS)567 panic("purego: getCallbackStruct is not supported on " + runtime.GOOS)
568 }568 }
@@ -609,7 +609,7 @@ func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int,
609 }609 }
610 610
611 // Pointer on stack (rare: all integer registers exhausted).611 // Pointer on stack (rare: all integer registers exhausted).
612- if runtime.GOOS == "darwin" {612+ if isDarwin {
613 ptrVal := callbackArgFromStack(frame, *stackSlot, stackByteOffset, reflect.TypeOf(uintptr(0)))613 ptrVal := callbackArgFromStack(frame, *stackSlot, stackByteOffset, reflect.TypeOf(uintptr(0)))
614 ptr := uintptr(ptrVal.Uint())614 ptr := uintptr(ptrVal.Uint())
615 return reflect.NewAt(inType, *(*unsafe.Pointer)(unsafe.Pointer(&ptr))).Elem()615 return reflect.NewAt(inType, *(*unsafe.Pointer)(unsafe.Pointer(&ptr))).Elem()
@@ -690,7 +690,7 @@ func readHFAFromRegisters(inType reflect.Type, f *[callbackMaxFrame]uintptr, flo
690 // callback frame. On Darwin ARM64, arguments are byte-packed on the stack.690 // callback frame. On Darwin ARM64, arguments are byte-packed on the stack.
691 // On Linux ARM64, arguments are 8-byte aligned.691 // On Linux ARM64, arguments are 8-byte aligned.
692 func readStructFromStackArm64(inType reflect.Type, f *[callbackMaxFrame]uintptr, frame unsafe.Pointer, stackSlot *int, stackByteOffset *uintptr) reflect.Value {692 func readStructFromStackArm64(inType reflect.Type, f *[callbackMaxFrame]uintptr, frame unsafe.Pointer, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
693- if runtime.GOOS == "darwin" {693+ if isDarwin {
694 return callbackArgFromStack(frame, *stackSlot, stackByteOffset, inType)694 return callbackArgFromStack(frame, *stackSlot, stackByteOffset, inType)
695 }695 }
696 // Linux ARM64: 8-byte aligned slots.696 // Linux ARM64: 8-byte aligned slots.
M syscall_unix.go +2 -2
@@ -164,7 +164,7 @@ func callbackWrap(a *callbackArgs) {
164164 case reflect.Float32, reflect.Float64:
165165 slots = int((fnType.In(i).Size() + ptrSize - 1) / ptrSize)
166166 if floatsN+slots > numOfFloatRegisters() {
167- if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
167+ if isDarwin && runtime.GOARCH == "arm64" {
168168 // Darwin ARM64: read from packed stack with proper alignment
169169 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
170170 } else if stackFrame != nil {
@@ -203,7 +203,7 @@ func callbackWrap(a *callbackArgs) {
203203 default:
204204 slots = int((inType.Size() + ptrSize - 1) / ptrSize)
205205 if intsN+slots > numOfIntegerRegisters() {
206- if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
206+ if isDarwin && runtime.GOARCH == "arm64" {
207207 // Darwin ARM64: read from packed stack with proper alignment
208208 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
209209 } else if stackFrame != nil {
@@ -164,7 +164,7 @@ func callbackWrap(a *callbackArgs) {
164 case reflect.Float32, reflect.Float64:164 case reflect.Float32, reflect.Float64:
165 slots = int((fnType.In(i).Size() + ptrSize - 1) / ptrSize)165 slots = int((fnType.In(i).Size() + ptrSize - 1) / ptrSize)
166 if floatsN+slots > numOfFloatRegisters() {166 if floatsN+slots > numOfFloatRegisters() {
167- if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {167+ if isDarwin && runtime.GOARCH == "arm64" {
168 // Darwin ARM64: read from packed stack with proper alignment168 // Darwin ARM64: read from packed stack with proper alignment
169 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)169 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
170 } else if stackFrame != nil {170 } else if stackFrame != nil {
@@ -203,7 +203,7 @@ func callbackWrap(a *callbackArgs) {
203 default:203 default:
204 slots = int((inType.Size() + ptrSize - 1) / ptrSize)204 slots = int((inType.Size() + ptrSize - 1) / ptrSize)
205 if intsN+slots > numOfIntegerRegisters() {205 if intsN+slots > numOfIntegerRegisters() {
206- if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {206+ if isDarwin && runtime.GOARCH == "arm64" {
207 // Darwin ARM64: read from packed stack with proper alignment207 // Darwin ARM64: read from packed stack with proper alignment
208 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)208 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
209 } else if stackFrame != nil {209 } else if stackFrame != nil {

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