detached 06b205c

Merge pull request #3 from bnema/realign-upstream

chore: realign fork with upstream ebitengine/purego
commit
06b205cdb626057ac7a9b636b7afc6338f7c3b04
author
bnema · · 2026-09-27 09:00 UTC
committer
GitHub · · 2026-09-27 09:00 UTC
merge of
cf1b83e + 61051dd · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
104 files · +4501 -2327
M .github/ISSUE_TEMPLATE/config.yml +1 -1
@@ -2,4 +2,4 @@ blank_issues_enabled: false
22 contact_links:
33 - name: Questions
44 about: Please use one of the forums for questions or general discussions
5- url: https://github.com/bnema/purego/discussions
5+ url: https://github.com/ebitengine/purego/discussions
M .github/workflows/test.yml +168 -37
@@ -3,15 +3,15 @@ name: Test
33 on: [push, pull_request]
44
55 env:
6- QEMU_VERSION: "11.0.1+ds-1"
6+ QEMU_MAJOR_VERSION: "10"
77
88 jobs:
99 test:
1010 strategy:
1111 fail-fast: false
1212 matrix:
13- os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
14- go: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x']
13+ os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest, windows-11-arm]
14+ go: ['1.25.x', '1.26.x', '1.27.x']
1515 name: Test with Go ${{ matrix.go }} on ${{ matrix.os }}
1616 runs-on: ${{ matrix.os }}
1717 defaults:
@@ -32,8 +32,8 @@ jobs:
3232 with:
3333 go-version: ${{ matrix.go }}
3434
35- - name: Set up the prerequisites
36- if: runner.os == 'Windows'
35+ - name: Set up Msys2
36+ if: runner.os == 'Windows' && runner.arch != 'ARM64'
3737 uses: msys2/setup-msys2@v2
3838
3939 - name: go vet
@@ -76,13 +76,21 @@ jobs:
7676
7777 - name: go build (Linux minor architectures)
7878 # Test them only on the latest Go to reduce CI time.
79- # s390x cannot be tested here as it requires Cgo.
80- if: startsWith(matrix.go, '1.26.')
79+ if: startsWith(matrix.go, '1.27.')
8180 run: |
8281 # Check cross-compiling Linux binaries for minor architectures.
8382 env GOOS=linux GOARCH=loong64 go build -v ./...
8483 env GOOS=linux GOARCH=ppc64le go build -v ./...
8584 env GOOS=linux GOARCH=riscv64 go build -v ./...
85+ env GOOS=linux GOARCH=s390x go build -v ./...
86+
87+ - name: go build (Linux mips, Cgo)
88+ # mips builds only via the Cgo fallback (CGO_ENABLED=1). See #460.
89+ if: runner.os == 'Linux'
90+ run: |
91+ sudo apt-get update
92+ sudo apt-get install -y gcc-mips-linux-gnu
93+ env GOOS=linux GOARCH=mips CGO_ENABLED=1 CC=mips-linux-gnu-gcc go build -v ./...
8694
8795 - name: go build (plugin)
8896 if: runner.os == 'Linux' || runner.os == 'macOS'
@@ -120,32 +128,68 @@ jobs:
120128 if: runner.os == 'Windows'
121129 run: |
122130 env CGO_ENABLED=0 GOARCH=386 go test -shuffle=on -v -count=10 ./...
131+
132+ - name: go test (Windows 386, Cgo)
133+ if: runner.os == 'Windows' && runner.arch != 'ARM64'
134+ run: |
123135 env CGO_ENABLED=1 GOARCH=386 go test -shuffle=on -v -count=10 ./...
124136
137+ - name: go test (Linux 386)
138+ if: ${{ runner.os == 'Linux' && runner.arch != 'ARM64' }}
139+ run: |
140+ sudo apt-get update
141+ sudo apt-get install gcc-multilib
142+ sudo apt-get install g++-multilib
143+ env CGO_ENABLED=0 GOARCH=386 go test -shuffle=on -v -count=10 ./...
144+ env CGO_ENABLED=1 GOARCH=386 go test -shuffle=on -v -count=10 ./...
145+
125146 - name: go test (Linux arm)
126147 if: ${{ runner.os == 'Linux' && runner.arch == 'ARM64' }}
127148 run: |
128- sudo apt-get update
129- sudo apt-get install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross
130-
131- # Install QEMU 10.x from Debian trixie. Ubuntu 24.04 ships QEMU 8.2
132- # which has a bug where inter-thread signal delivery (tgkill) hangs,
133- # causing TestSetuidEtc to deadlock.
134- wget -q https://ftp.debian.org/debian/pool/main/q/qemu/qemu-user_${QEMU_VERSION}_arm64.deb -O /tmp/qemu-user.deb
135- wget -q https://ftp.debian.org/debian/pool/main/q/qemu/qemu-user-binfmt_${QEMU_VERSION}_arm64.deb -O /tmp/qemu-user-binfmt.deb
136- sudo dpkg --force-depends -i /tmp/qemu-user.deb /tmp/qemu-user-binfmt.deb
137- sudo apt-get -f install -y
138-
139- # Create symlink for the dynamic linker that QEMU expects
140- sudo ln -sf /usr/arm-linux-gnueabihf/lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3
141- go env -w CC=arm-linux-gnueabihf-gcc
142- go env -w CXX=arm-linux-gnueabihf-g++
143- env GOOS=linux GOARCH=arm CGO_ENABLED=0 go test -c -o=purego-test-nocgo .
144- env QEMU_LD_PREFIX=/usr/arm-linux-gnueabihf qemu-arm ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10
145- env GOOS=linux GOARCH=arm CGO_ENABLED=1 go test -c -o=purego-test-cgo .
146- env QEMU_LD_PREFIX=/usr/arm-linux-gnueabihf qemu-arm ./purego-test-cgo -test.shuffle=on -test.v -test.count=10
147- go env -u CC
148- go env -u CXX
149+ sudo apt-get update
150+ sudo apt-get install -y \
151+ gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf libc6-armhf-cross \
152+ gcc-arm-linux-gnueabi g++-arm-linux-gnueabi libc6-armel-cross
153+
154+ # Install the latest QEMU 10.x package from Debian. Ubuntu 24.04 ships QEMU 8.2
155+ # which has a bug where inter-thread signal delivery (tgkill) hangs,
156+ # causing TestSetuidEtc to deadlock.
157+ arch=$(dpkg --print-architecture)
158+ qemu_user_deb=$(
159+ curl -fsSL https://ftp.debian.org/debian/pool/main/q/qemu/ |
160+ grep -Eo "qemu-user_${QEMU_MAJOR_VERSION}\.[0-9.]+\+ds-[0-9]+([+~][A-Za-z0-9.+:~-]+)*_${arch}\.deb" |
161+ sort -V |
162+ tail -n 1
163+ )
164+ if [ -z "${qemu_user_deb}" ]; then
165+ echo "missing qemu-user ${QEMU_MAJOR_VERSION}.x package for ${arch}" >&2
166+ exit 1
167+ fi
168+ wget -q "https://ftp.debian.org/debian/pool/main/q/qemu/${qemu_user_deb}" -O /tmp/qemu-user.deb
169+ sudo dpkg --force-depends -i /tmp/qemu-user.deb
170+ sudo apt-get -f install -y
171+
172+ # Create symlink for the dynamic linker that QEMU expects
173+ sudo ln -sf /usr/arm-linux-gnueabihf/lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3
174+ go env -w CC=arm-linux-gnueabihf-gcc
175+ go env -w CXX=arm-linux-gnueabihf-g++
176+ env GOOS=linux GOARCH=arm CGO_ENABLED=0 go test -c -o=purego-test-nocgo .
177+ env QEMU_LD_PREFIX=/usr/arm-linux-gnueabihf qemu-arm ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10
178+ env GOOS=linux GOARCH=arm CGO_ENABLED=1 go test -c -o=purego-test-cgo .
179+ env QEMU_LD_PREFIX=/usr/arm-linux-gnueabihf qemu-arm ./purego-test-cgo -test.shuffle=on -test.v -test.count=10
180+ go env -u CC
181+ go env -u CXX
182+
183+ # Repeat the same for ARMv5 target (soft-float)
184+ sudo ln -sf /usr/arm-linux-gnueabi/lib/ld-linux.so.3 /lib/ld-linux.so.3
185+ go env -w CC=arm-linux-gnueabi-gcc
186+ go env -w CXX=arm-linux-gnueabi-g++
187+ env GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=0 go test -c -o=purego-test-nocgo .
188+ env QEMU_LD_PREFIX=/usr/arm-linux-gnueabi qemu-arm ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10
189+ env GOOS=linux GOARCH=arm GOARM=5 CGO_ENABLED=1 go test -c -o=purego-test-cgo .
190+ env QEMU_LD_PREFIX=/usr/arm-linux-gnueabi qemu-arm ./purego-test-cgo -test.shuffle=on -test.v -test.count=10
191+ go env -u CC
192+ go env -u CXX
149193
150194 - name: go test race (no Cgo)
151195 if: runner.os == 'macOS'
@@ -154,6 +198,7 @@ jobs:
154198 env CGO_ENABLED=0 go test -race -shuffle=on -v -count=10 ./...
155199
156200 - name: go test race (Cgo)
201+ if: ${{ !(runner.os == 'Windows' && runner.arch == 'ARM64') }}
157202 run: |
158203 env CGO_ENABLED=1 go test -race -shuffle=on -v -count=10 ./...
159204
@@ -161,9 +206,7 @@ jobs:
161206 strategy:
162207 fail-fast: false
163208 matrix:
164- # Test only the latest two Go versions to save CI time.
165- # See https://go.dev/doc/devel/release#policy
166- go: ['1.25.x', '1.26.x']
209+ go: ['1.25.x', '1.26.x', '1.27.x']
167210 name: Test with Go ${{ matrix.go }} on Linux minor architectures
168211 runs-on: ubuntu-latest
169212 defaults:
@@ -187,12 +230,22 @@ jobs:
187230 gcc-riscv64-linux-gnu g++-riscv64-linux-gnu \
188231 gcc-s390x-linux-gnu g++-s390x-linux-gnu
189232
190- # Install QEMU 10.x from Debian trixie. Ubuntu 24.04 ships QEMU 8.2
233+ # Install the latest QEMU 10.x package from Debian. Ubuntu 24.04 ships QEMU 8.2
191234 # which has a bug where inter-thread signal delivery (tgkill) hangs,
192235 # causing TestSetuidEtc to deadlock.
193- wget -q https://ftp.debian.org/debian/pool/main/q/qemu/qemu-user_${QEMU_VERSION}_amd64.deb -O /tmp/qemu-user.deb
194- wget -q https://ftp.debian.org/debian/pool/main/q/qemu/qemu-user-binfmt_${QEMU_VERSION}_amd64.deb -O /tmp/qemu-user-binfmt.deb
195- sudo dpkg --force-depends -i /tmp/qemu-user.deb /tmp/qemu-user-binfmt.deb
236+ arch=$(dpkg --print-architecture)
237+ qemu_user_deb=$(
238+ curl -fsSL https://ftp.debian.org/debian/pool/main/q/qemu/ |
239+ grep -Eo "qemu-user_${QEMU_MAJOR_VERSION}\.[0-9.]+\+ds-[0-9]+([+~][A-Za-z0-9.+:~-]+)*_${arch}\.deb" |
240+ sort -V |
241+ tail -n 1
242+ )
243+ if [ -z "${qemu_user_deb}" ]; then
244+ echo "missing qemu-user ${QEMU_MAJOR_VERSION}.x package for ${arch}" >&2
245+ exit 1
246+ fi
247+ wget -q "https://ftp.debian.org/debian/pool/main/q/qemu/${qemu_user_deb}" -O /tmp/qemu-user.deb
248+ sudo dpkg --force-depends -i /tmp/qemu-user.deb
196249 sudo apt-get -f install -y
197250
198251 # Verify that the expected QEMU binaries are installed and usable.
@@ -240,6 +293,11 @@ jobs:
240293 run: |
241294 go env -w CC=s390x-linux-gnu-gcc
242295 go env -w CXX=s390x-linux-gnu-g++
296+ # CGO_ENABLED=0 is only supported on Go 1.27+ for s390x.
297+ if [[ "${{ matrix.go }}" != 1.25.* && "${{ matrix.go }}" != 1.26.* ]]; then
298+ env GOOS=linux GOARCH=s390x CGO_ENABLED=0 go test -c -o=purego-test-nocgo .
299+ env QEMU_LD_PREFIX=/usr/s390x-linux-gnu qemu-s390x ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10
300+ fi
243301 env GOOS=linux GOARCH=s390x CGO_ENABLED=1 go test -c -o=purego-test-cgo .
244302 env QEMU_LD_PREFIX=/usr/s390x-linux-gnu qemu-s390x ./purego-test-cgo -test.shuffle=on -test.v -test.count=10
245303 go env -u CC
@@ -250,8 +308,6 @@ jobs:
250308 fail-fast: false
251309 matrix:
252310 os: ['FreeBSD', 'NetBSD']
253- # Test only the latest two Go versions to save CI time.
254- # See https://go.dev/doc/devel/release#policy
255311 go: ['1.25.8', '1.26.1']
256312 name: Test with Go ${{ matrix.go }} on ${{ matrix.os }}
257313 runs-on: ubuntu-22.04
@@ -281,3 +337,78 @@ jobs:
281337 rm -fr /usr/local/go && tar -C /usr/local -xf go${{matrix.go}}.netbsd-amd64.tar.gz
282338 chmod +x $GITHUB_WORKSPACE/.github/scripts/bsd_tests.sh
283339 run: $GITHUB_WORKSPACE/.github/scripts/bsd_tests.sh
340+
341+ # Android is tested on amd64 only. The emulator requires the system image
342+ # architecture to match the host, and GitHub's arm64 hosted runners do not
343+ # provide a usable KVM device, so there is no hosted hardware-accelerated path
344+ # to an Android arm64 emulator. The arm64 struct ABI is already covered by the
345+ # linux/arm64 `test` job, and Android's Bionic/cgo integration by this job.
346+ android:
347+ strategy:
348+ fail-fast: false
349+ matrix:
350+ go: ['1.25.x', '1.26.x', '1.27.x']
351+ name: Test with Go ${{ matrix.go }} on Android amd64
352+ runs-on: ubuntu-latest
353+ defaults:
354+ run:
355+ shell: bash
356+ steps:
357+ - uses: actions/checkout@v5
358+
359+ - name: Setup Go
360+ uses: actions/setup-go@v6
361+ with:
362+ go-version: ${{ matrix.go }}
363+
364+ # The tests compile their C libraries at run time via `go env CC`, but the
365+ # emulator has no C toolchain. Every library the tests load must therefore be
366+ # cross-compiled here and loaded as a prebuilt library at run time instead.
367+ - name: Cross-compile the test binary and C libraries
368+ run: |
369+ set -eux
370+ NDK="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
371+ CC="$NDK/x86_64-linux-android21-clang"
372+ mkdir -p prebuilt
373+ # Build every library the tests load. Keep this in sync with the
374+ # buildSharedLib callers in the tests; a missing entry surfaces as a
375+ # "prebuilt lib ... no such file" failure on the emulator.
376+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/structtest.so testdata/structtest/struct_test.c
377+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/structreturntest.so testdata/structtest/structreturn_test.c
378+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/libcbtest.so testdata/libcbtest/callback_test.c
379+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/abitest.so testdata/abitest/abi_test.c
380+ "$CC" -shared -Wall -Werror -fPIC -o prebuilt/slicetest.so testdata/slicetest/slice_test.c
381+ env GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC="$CC" go test -c -o purego.test .
382+
383+ - name: Enable KVM
384+ run: |
385+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
386+ sudo udevadm control --reload-rules
387+ sudo udevadm trigger --name-match=kvm
388+
389+ - name: Run the test suite on the Android emulator
390+ uses: reactivecircus/android-emulator-runner@v2
391+ with:
392+ api-level: 34
393+ arch: x86_64
394+ target: google_apis
395+ emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
396+ script: |
397+ set -eux
398+ adb push purego.test /data/local/tmp/purego.test
399+ adb push prebuilt /data/local/tmp/prebuilt
400+ adb shell "chmod 755 /data/local/tmp/purego.test"
401+ # TestSetuidEtc needs root and glibc (not Bionic) semantics; TestNestedDlopenCall
402+ # needs the C++ runtime present on the device. Both are covered by the Linux
403+ # runners, so skip them here rather than special-casing the emulator.
404+ 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
405+ grep -q '^PASS' result.txt
406+ ! grep -q '^FAIL' result.txt
407+
408+ # TODO: Add an iOS job that runs the test suite on the iOS Simulator.
409+ # iOS has no emulator runner equivalent: on a macOS runner the test binary must
410+ # be wrapped into a .app and launched via `xcrun simctl` (or x/mobile's
411+ # go_ios_exec), with the C libraries cross-compiled against the iphonesimulator
412+ # SDK and loaded through PUREGO_TEST_PREBUILT_LIBDIR (as the android job does).
413+ # Deprioritized because the iOS arm64 ABI matches macOS arm64, which the `test`
414+ # job already runs.
M README.md +15 -27
@@ -3,8 +3,6 @@
33
44 A library for calling C functions from Go without Cgo.
55
6-> Fork notice: this repository is a maintained fork of `github.com/ebitengine/purego` with additional ABI, callback, and generator-facing fixes needed by the PureGoTK stack.
7-
86 > This is beta software so expect bugs and potentially API breaking changes
97 > but each release will be tagged to avoid breaking people's code.
108 > Bug reports are encouraged.
@@ -36,24 +34,27 @@ Tier 1 platforms are the primary targets officially supported by PureGo. When a
3634 - **iOS**: amd64<sup>1</sup>, arm64<sup>1</sup>
3735 - **Linux**: amd64, arm64
3836 - **macOS**: amd64, arm64
39-- **Windows**: amd64, arm64
37+- **Windows**: amd64<sup>2</sup>, arm64<sup>2</sup>
4038
4139 ### Tier 2
4240
4341 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.
4442
45-- **Android**: 386<sup>1</sup>, arm<sup>1</sup>
46-- **FreeBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>
47-- **Linux**: 386, arm, loong64, ppc64le, riscv64, s390x<sup>1</sup>
48-- **NetBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>
49-- **Windows**: 386<sup>3</sup>, arm<sup>3,4</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>2</sup>, ppc64le<sup>2</sup>, riscv64<sup>3</sup>, s390x<sup>3, 5</sup>
46+- **NetBSD**: amd64<sup>3,4</sup>, arm64<sup>3,4</sup>
47+- **Windows**: 386<sup>3,6</sup>, arm<sup>3,6,7</sup>
5048
5149 #### Support Notes
5250
5351 1. These architectures require CGO_ENABLED=1 to compile
54-2. These architectures require the special flag `-gcflags="github.com/bnema/purego/internal/fakecgo=-std"` to compile with CGO_ENABLED=0
55-3. These architectures only support `SyscallN` and `NewCallback`
56-4. These architectures are no longer supported as of Go 1.26
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
54+4. These architectures require the special flag `-gcflags="github.com/bnema/purego/internal/fakecgo=-std"` to compile with CGO_ENABLED=0
55+5. These architectures require CGO_ENABLED=1 to compile in versions before Go 1.27, but will be supported without Cgo in Go 1.27 and later
56+6. These architectures only support `SyscallN` and `NewCallback`
57+7. These architectures are no longer supported as of Go 1.26
5758
5859 ## Example
5960
@@ -101,21 +102,9 @@ exhaustion on Unix-like platforms, set `PUREGO_CALLBACK_LEDGER=1` to append JSON
101102 callback lifecycle events to `/tmp/purego-callback-ledger-<pid>.jsonl`, or set
102103 `PUREGO_CALLBACK_LEDGER_FILE=/path/to/file.jsonl` to choose the output file. Stack
103104 capture is also disabled by default; set `PUREGO_CALLBACK_LEDGER_STACK=1` when call
104-site attribution is needed. Stack capture adds per-callback overhead and should be
105-reserved for active debugging sessions. For ad-hoc stderr/file traces near callback
106-exhaustion, set `PUREGO_CALLBACK_TRACE=1`; `PUREGO_CALLBACK_TRACE_FILE` overrides
107-the default `/tmp/purego-callback-trace-<pid>.log` path.
108-
109-Diagnostic files are created with `0o600` permissions. They are append-only and
110-unbounded, so long-running processes can exhaust disk space if diagnostics stay
111-enabled. A ledger line is a JSON object such as:
112-
113-```json
114-{"time":"2026-06-09T14:03:00Z","pid":1234,"seq":42,"event":"alloc","index":7,"addr":"0x1234","stack":"..."}
115-```
116-
117-Use `time` as the timestamp, `index` as the callback ID, and the `pid`, `seq`,
118-`event`, `addr`, and optional `stack` fields when parsing the ledger output.
105+site attribution is needed. For ad-hoc stderr/file traces near callback exhaustion,
106+set `PUREGO_CALLBACK_TRACE=1`; `PUREGO_CALLBACK_TRACE_FILE` overrides the default
107+`/tmp/purego-callback-trace-<pid>.log` path.
119108
120109 ## Questions
121110
@@ -130,7 +119,6 @@ This is a list of the copied files:
130119
131120 * `abi_*.h` from package `runtime/cgo`
132121 * `wincallback.go` from package `runtime`
133-* `zcallback_darwin_*.s` from package `runtime`
134122 * `internal/fakecgo/abi_*.h` from package `runtime/cgo`
135123 * `internal/fakecgo/asm_GOARCH.s` from package `runtime/cgo`
136124 * `internal/fakecgo/callbacks.go` from package `runtime/cgo`
M callback_test.go +102 -9
@@ -22,7 +22,7 @@ func TestCallGoFromSharedLib(t *testing.T) {
2222 libFileName := filepath.Join(t.TempDir(), "libcbtest.so")
2323 t.Logf("Build %v", libFileName)
2424
25- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {
25+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {
2626 t.Fatal(err)
2727 }
2828 defer os.Remove(libFileName)
@@ -43,7 +43,7 @@ func TestCallGoFromSharedLib(t *testing.T) {
4343
4444 const want = 10101
4545 cb := purego.NewCallback(goFunc)
46- for i := 0; i < 10; i++ {
46+ for i := range 10 {
4747 got := callCallback(cb, "a test string")
4848 if got != want {
4949 t.Fatalf("%d: callCallback() got %v want %v", i, got, want)
@@ -57,7 +57,7 @@ func TestCallbackStringConversion(t *testing.T) {
5757 libFileName := filepath.Join(t.TempDir(), "libcbtest.so")
5858 t.Logf("Build %v", libFileName)
5959
60- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {
60+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {
6161 t.Fatal(err)
6262 }
6363 defer os.Remove(libFileName)
@@ -191,6 +191,53 @@ func TestNewCallbackFloat32AndFloat64(t *testing.T) {
191191 }
192192 }
193193
194+func TestNewCallbackInt64Result(t *testing.T) {
195+ // A 64-bit result needs two words: EDX:EAX on 386 and R1:R0 on arm.
196+ // The trampoline must forward both halves of the value.
197+ const mask = int64(0x0123456789abcdef)
198+ imp := purego.NewCallback(func(v int64) int64 { return v ^ mask })
199+ var fn func(v int64) int64
200+ purego.RegisterFunc(&fn, imp)
201+ for _, v := range []int64{
202+ 0,
203+ -1,
204+ 1 << 32,
205+ -0x7edcba9876543210,
206+ } {
207+ if got, want := fn(v), v^mask; got != want {
208+ t.Errorf("callback(%#x) = %#x, want %#x", uint64(v), uint64(got), uint64(want))
209+ }
210+ }
211+}
212+
213+func TestNewCallbackUint64Result(t *testing.T) {
214+ // The upper half of a uint64 result must not be dropped by the trampoline.
215+ imp := purego.NewCallback(func(lo, hi uint32) uint64 { return uint64(hi)<<32 | uint64(lo) })
216+ var fn func(lo, hi uint32) uint64
217+ purego.RegisterFunc(&fn, imp)
218+ const want = uint64(0x0123456789abcdef)
219+ if got := fn(0x89abcdef, 0x01234567); got != want {
220+ t.Errorf("callback() = %#x, want %#x", got, want)
221+ }
222+}
223+
224+func TestNewCallbackInt64ResultWithStackArgs(t *testing.T) {
225+ // On 386 all the arguments are passed on the stack, so this also checks
226+ // that the copied arguments do not overlap the result of the callback.
227+ // The number of arguments is kept small enough for ppc64le, which only
228+ // has maxArgs(15) slots in total, 8 of them in integer registers.
229+ const wantSum = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12
230+ imp := purego.NewCallback(func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 int) int64 {
231+ return int64(a1+a2+a3+a4+a5+a6+a7+a8+a9+a10+a11+a12)<<32 | 0x0badf00d
232+ })
233+ var fn func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 int) int64
234+ purego.RegisterFunc(&fn, imp)
235+ const want = int64(wantSum)<<32 | 0x0badf00d
236+ if got := fn(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); got != want {
237+ t.Errorf("callback() = %#x, want %#x", uint64(got), uint64(want))
238+ }
239+}
240+
194241 func ExampleNewCallback() {
195242 cb := purego.NewCallback(func(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 int) int {
196243 fmt.Println(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15)
@@ -223,7 +270,7 @@ func TestCallbackInt32Packing(t *testing.T) {
223270 }
224271
225272 libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
226- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
273+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
227274 t.Fatal(err)
228275 }
229276 defer os.Remove(libFileName)
@@ -255,7 +302,7 @@ func TestCallbackMixedStackPacking(t *testing.T) {
255302 }
256303
257304 libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
258- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
305+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
259306 t.Fatal(err)
260307 }
261308 defer os.Remove(libFileName)
@@ -288,7 +335,7 @@ func TestCallbackSmallTypesPacking(t *testing.T) {
288335 }
289336
290337 libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
291- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
338+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
292339 t.Fatal(err)
293340 }
294341 defer os.Remove(libFileName)
@@ -356,7 +403,7 @@ func TestCallback10Int32Packing(t *testing.T) {
356403 }
357404
358405 libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
359- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
406+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
360407 t.Fatal(err)
361408 }
362409 defer os.Remove(libFileName)
@@ -387,7 +434,7 @@ func TestCallbackFloat64StackPacking(t *testing.T) {
387434 }
388435
389436 libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
390- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
437+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
391438 t.Fatal(err)
392439 }
393440 defer os.Remove(libFileName)
@@ -421,7 +468,7 @@ func TestCallbackFloat32StackPacking(t *testing.T) {
421468 }
422469
423470 libFileName := filepath.Join(t.TempDir(), "libcbtest_packing.so")
424- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
471+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_packing_test.c")); err != nil {
425472 t.Fatal(err)
426473 }
427474 defer os.Remove(libFileName)
@@ -448,3 +495,49 @@ func TestCallbackFloat32StackPacking(t *testing.T) {
448495 t.Errorf("callCallback12Float32() = %d, want %d", got, want)
449496 }
450497 }
498+
499+func TestRegisterFuncExpandFinalAnyArgument(t *testing.T) {
500+ // The last argument is expanded into the C arguments held in it,
501+ // whether it is declared as ...any or as []any (#506).
502+ cb := purego.NewCallback(func(a, b, c uintptr) uintptr {
503+ return a*100 + b*10 + c
504+ })
505+ const want = uintptr(123)
506+ t.Run("Variadic", func(t *testing.T) {
507+ var fn func(a uintptr, args ...any) uintptr
508+ purego.RegisterFunc(&fn, cb)
509+ if got := fn(1, uintptr(2), uintptr(3)); got != want {
510+ t.Errorf("func(uintptr, ...any) = %d, want %d", got, want)
511+ }
512+ })
513+ t.Run("Slice", func(t *testing.T) {
514+ var fn func(a uintptr, args []any) uintptr
515+ purego.RegisterFunc(&fn, cb)
516+ if got := fn(1, []any{uintptr(2), uintptr(3)}); got != want {
517+ t.Errorf("func(uintptr, []any) = %d, want %d", got, want)
518+ }
519+ })
520+}
521+
522+func TestRegisterFuncKeepNonFinalAnyArgument(t *testing.T) {
523+ // A []any that isn't the last argument is a regular argument:
524+ // it is passed as one value and must not panic (#506).
525+ var first any
526+ var second uintptr
527+ cb := purego.NewCallback(func(args *any, b uintptr) uintptr {
528+ first = *args
529+ second = b
530+ return 0
531+ })
532+ var fn func(args []any, b uintptr) uintptr
533+ purego.RegisterFunc(&fn, cb)
534+ s := []any{uintptr(2), uintptr(3)}
535+ fn(s, 7)
536+ runtime.KeepAlive(s)
537+ if want := any(uintptr(2)); first != want {
538+ t.Errorf("the first C argument = %v, want %v", first, want)
539+ }
540+ if second != 7 {
541+ t.Errorf("the second C argument = %d, want 7", second)
542+ }
543+}
A cdecl.go +11 -0
@@ -0,0 +1,11 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+// CDecl marks a function as being called using the __cdecl calling convention as defined in
7+// the [MSDocs] when passed to NewCallback. It must be the first argument to the function.
8+// This is only useful on 386 Windows, but it is safe to use on other platforms.
9+//
10+// [MSDocs]: https://learn.microsoft.com/en-us/cpp/cpp/cdecl?view=msvc-170
11+type CDecl struct{}
A cstrings/nsstrings_darwin.go +44 -0
@@ -0,0 +1,44 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package cstrings
5+
6+import (
7+ "fmt"
8+
9+ "github.com/bnema/purego"
10+ "github.com/bnema/purego/internal/strings"
11+ "github.com/bnema/purego/objc"
12+)
13+
14+var (
15+ sel_isKindOf objc.SEL
16+ sel_UTF8String objc.SEL
17+
18+ class_NSString objc.Class
19+)
20+
21+func init() {
22+ // Must pull in Foundation to get the NSString class.
23+ _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW)
24+ if err != nil {
25+ panic(fmt.Errorf("cstrings: %w", err))
26+ }
27+ sel_isKindOf = objc.RegisterName("isKindOfClass:")
28+ sel_UTF8String = objc.RegisterName("UTF8String")
29+ class_NSString = objc.GetClass("NSString")
30+}
31+
32+// NSStringToString returns a copy of the NSString contents as a Go string.
33+// If the ID is 0 then an empty string is returned.
34+// If the ID is not an NSString class then the function panics.
35+// This function is only available on darwin.
36+func NSStringToString(str objc.ID) string {
37+ if str == 0 {
38+ return ""
39+ }
40+ if str.Send(sel_isKindOf, class_NSString) == 0 {
41+ panic("cstrings: provided ID is not an NSString")
42+ }
43+ return strings.GoString(uintptr(str.Send(sel_UTF8String)))
44+}
A cstrings/nsstrings_darwin_test.go +54 -0
@@ -0,0 +1,54 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package cstrings_test
5+
6+import (
7+ "testing"
8+
9+ "github.com/bnema/purego/cstrings"
10+ "github.com/bnema/purego/objc"
11+)
12+
13+func TestNSStringToString(t *testing.T) {
14+ t.Run("nil ID returns empty string", func(t *testing.T) {
15+ result := cstrings.NSStringToString(0)
16+ if result != "" {
17+ t.Errorf("expected empty string for nil ID, got %q", result)
18+ }
19+ })
20+
21+ t.Run("valid NSString returns correct string", func(t *testing.T) {
22+ sel := objc.RegisterName("stringWithUTF8String:")
23+ nsString := objc.ID(objc.GetClass("NSString")).Send(sel, "Hello, World!\x00")
24+
25+ result := cstrings.NSStringToString(nsString)
26+ if result != "Hello, World!" {
27+ t.Errorf("expected %q, got %q", "Hello, World!", result)
28+ }
29+ })
30+
31+ t.Run("empty NSString returns empty string", func(t *testing.T) {
32+ sel := objc.RegisterName("stringWithUTF8String:")
33+ nsString := objc.ID(objc.GetClass("NSString")).Send(sel, "\x00")
34+
35+ result := cstrings.NSStringToString(nsString)
36+ if result != "" {
37+ t.Errorf("expected empty string, got %q", result)
38+ }
39+ })
40+
41+ t.Run("non-NSString panics", func(t *testing.T) {
42+ defer func() {
43+ if r := recover(); r == nil {
44+ t.Error("expected panic for non-NSString ID")
45+ }
46+ }()
47+
48+ classNSNumber := objc.GetClass("NSNumber")
49+ sel := objc.RegisterName("numberWithInt:")
50+ nsNumber := objc.ID(classNSNumber).Send(sel, 42)
51+
52+ cstrings.NSStringToString(nsNumber)
53+ })
54+}
M dlfcn_test.go +31 -1
@@ -8,6 +8,7 @@ package purego_test
88 import (
99 "os"
1010 "path/filepath"
11+ "syscall"
1112 "testing"
1213 "unsafe"
1314
@@ -24,7 +25,7 @@ func TestNestedDlopenCall(t *testing.T) {
2425 libFileName := filepath.Join(t.TempDir(), "libdlnested.so")
2526 t.Logf("Build %v", libFileName)
2627
27- if err := buildSharedLib("CXX", libFileName, filepath.Join("testdata", "libdlnested", "nested_test.cpp")); err != nil {
28+ if err := buildSharedLib(t, "CXX", libFileName, filepath.Join("testdata", "libdlnested", "nested_test.cpp")); err != nil {
2829 t.Fatal(err)
2930 }
3031 defer os.Remove(libFileName)
@@ -48,3 +49,32 @@ func TestSyscallN(t *testing.T) {
4849 t.Fatalf("SyscallN didn't return the same result as purego.Dlsym: %d", err2)
4950 }
5051 }
52+
53+func TestSyscallNErrnoIsNotAnInputArgument(t *testing.T) {
54+ if errnoIsCaptured() {
55+ t.Skip("this platform saves errno; see TestErrno")
56+ }
57+
58+ openSym, err := purego.Dlsym(purego.RTLD_DEFAULT, "open")
59+ if err != nil {
60+ t.Fatal(err)
61+ }
62+
63+ path, err := syscall.BytePtrFromString("_file_that_does_not_exist_")
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+
68+ // The third argument is non-zero so that echoing it back as errno is visible.
69+ const third = uintptr(0o600)
70+ r1, _, errno := purego.SyscallN(openSym,
71+ uintptr(unsafe.Pointer(path)),
72+ uintptr(os.O_RDWR),
73+ third)
74+ if int32(r1) != -1 {
75+ t.Fatalf("open returned %d, wanted -1", r1)
76+ }
77+ if errno != 0 {
78+ t.Errorf("SyscallN returned %d as errno where errno is not captured, wanted 0", errno)
79+ }
80+}
M examples/objc/main_darwin.go +2 -2
@@ -18,7 +18,7 @@ var (
1818 )
1919
2020 func BarInit(id objc.ID, cmd objc.SEL) objc.ID {
21- return id.SendSuper(cmd)
21+ return id.SendSuper2(objc.GetClass("BarObject"), cmd)
2222 }
2323
2424 func main() {
@@ -36,7 +36,7 @@ func main() {
3636 []objc.FieldDef{
3737 {
3838 Name: "bar",
39- Type: reflect.TypeOf(int(0)),
39+ Type: reflect.TypeFor[int](),
4040 Attribute: objc.ReadWrite,
4141 },
4242 },
M examples/window/main_darwin.go +5 -1
@@ -5,6 +5,7 @@ package main
55
66 import (
77 "runtime"
8+ "structs"
89
910 "github.com/bnema/purego"
1011 "github.com/bnema/purego/objc"
@@ -17,14 +18,17 @@ const (
1718 )
1819
1920 type NSPoint struct {
21+ _ structs.HostLayout
2022 X, Y float64
2123 }
2224
2325 type NSSize struct {
26+ _ structs.HostLayout
2427 Width, Height float64
2528 }
2629
2730 type NSRect struct {
31+ _ structs.HostLayout
2832 Origin NSPoint
2933 Size NSSize
3034 }
@@ -56,5 +60,5 @@ func main() {
5660 }
5761
5862 func NSMakeRect(x, y, width, height float64) NSRect {
59- return NSRect{Origin: NSPoint{x, y}, Size: NSSize{width, height}}
63+ return NSRect{Origin: NSPoint{X: x, Y: y}, Size: NSSize{Width: width, Height: height}}
6064 }
M examples/window/main_windows.go +5 -0
@@ -5,6 +5,7 @@ package main
55
66 import (
77 "runtime"
8+ "structs"
89 "syscall"
910 "unsafe"
1011
@@ -30,6 +31,7 @@ type (
3031 )
3132
3233 type WNDCLASSEX struct {
34+ _ structs.HostLayout
3335 Size uint32
3436 Style uint32
3537 WndProc uintptr
@@ -45,14 +47,17 @@ type WNDCLASSEX struct {
4547 }
4648
4749 type RECT struct {
50+ _ structs.HostLayout
4851 Left, Top, Right, Bottom int32
4952 }
5053
5154 type POINT struct {
55+ _ structs.HostLayout
5256 X, Y int32
5357 }
5458
5559 type MSG struct {
60+ _ structs.HostLayout
5661 Hwnd HWND
5762 Message uint32
5863 WParam uintptr
A export_test.go +16 -0
@@ -0,0 +1,16 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || linux || netbsd || windows
5+
6+package purego
7+
8+import "reflect"
9+
10+// MaxArgs re-exports maxArgs for external tests.
11+const MaxArgs = maxArgs
12+
13+// StructReturnInMemory re-exports structReturnInMemory for external tests.
14+func StructReturnInMemory(outType reflect.Type) bool {
15+ return structReturnInMemory(outType)
16+}
M func.go +399 -251
@@ -10,10 +10,11 @@ import (
1010 "math"
1111 "reflect"
1212 "runtime"
13+ "structs"
14+ "sync"
1315 "unsafe"
1416
1517 "github.com/bnema/purego/internal/strings"
16- "github.com/bnema/purego/internal/xreflect"
1718 )
1819
1920 const (
@@ -21,6 +22,14 @@ const (
2122 align8ByteSize = 8 // 8-byte alignment boundary
2223 )
2324
25+func isARMSoftFloat() bool {
26+ return runtime.GOARCH == "arm" && *(*uint8)(unsafe.Pointer(&runtime_goarmsoftfp)) != 0
27+}
28+
29+var thePool = sync.Pool{New: func() any {
30+ return new(syscallArgs)
31+}}
32+
2433 // RegisterLibFunc is a wrapper around RegisterFunc that uses the C function returned from Dlsym(handle, name).
2534 // It panics if it can't find the name symbol.
2635 func RegisterLibFunc(fptr any, handle uintptr, name string) {
@@ -59,15 +68,17 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
5968 // int64 <=> int64_t
6069 // float32 <=> float
6170 // float64 <=> double
62-// struct <=> struct (darwin amd64/arm64, linux amd64/arm64)
71+// struct <=> struct (android, darwin, ios, linux, and windows on amd64/arm64)
6372 // func <=> C function
6473 // unsafe.Pointer, *T <=> void*
6574 // []T => void*
6675 //
67-// There is a special case when the last argument of fptr is a variadic interface (or []interface}
76+// There is a special case when the last argument of fptr is a variadic interface (or []any):
6877 // it will be expanded into a call to the C function as if it had the arguments in that slice.
6978 // This means that using arg ...any is like a cast to the function with the arguments inside arg.
7079 // This is not the same as C variadic.
80+// Only the last argument is expanded this way. A []any in any other position is an ordinary
81+// argument and is passed as a single value like any other slice.
7182 //
7283 // # Memory
7384 //
@@ -94,8 +105,11 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
94105 // it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure
95106 // that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.
96107 //
97-// On Darwin ARM64, purego handles proper alignment of struct arguments when passing them on the stack,
98-// following the C ABI's byte-level packing rules.
108+// On Apple ARM64 platforms (macOS and iOS), purego handles proper alignment of struct arguments
109+// when passing them on the stack, following the C ABI's byte-level packing rules.
110+//
111+// On Windows, struct arguments and returns are supported on amd64 and arm64 when calling C functions.
112+// Passing or returning structs in callbacks created with [NewCallback] is not supported on Windows.
99113 //
100114 // # Example
101115 //
@@ -116,7 +130,11 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
116130 // [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C
117131 func RegisterFunc(fptr any, cfn uintptr) {
118132 const is32bit = unsafe.Sizeof(uintptr(0)) == 4
119- fn := reflect.ValueOf(fptr).Elem()
133+ rv := reflect.ValueOf(fptr)
134+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
135+ panic("purego: fptr must be a non-nil function pointer")
136+ }
137+ fn := rv.Elem()
120138 ty := fn.Type()
121139 if ty.Kind() != reflect.Func {
122140 panic("purego: fptr must be a function pointer")
@@ -136,40 +154,71 @@ func RegisterFunc(fptr any, cfn uintptr) {
136154 // to avoid crashing with too many arguments
137155 var ints int
138156 var floats int
157+ floatArgRegs := numOfFloatRegisters()
158+ ptrSize := unsafe.Sizeof(uintptr(0))
139159 var stack int
140- for i := 0; i < ty.NumIn(); i++ {
160+ for i := range ty.NumIn() {
141161 arg := ty.In(i)
142162 switch arg.Kind() {
143163 case reflect.Func:
144164 // This only does preliminary testing to ensure the CDecl argument
145165 // is the first argument. Full testing is done when the callback is actually
146166 // created in NewCallback.
147- for j := 0; j < arg.NumIn(); j++ {
167+ for j := range arg.NumIn() {
148168 in := arg.In(j)
149- if !in.AssignableTo(reflect.TypeOf(CDecl{})) {
169+ if !in.AssignableTo(reflect.TypeFor[CDecl]()) {
150170 continue
151171 }
152172 if j != 0 {
153173 panic("purego: CDecl must be the first argument")
154174 }
155175 }
156- case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
157- reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Pointer, reflect.UnsafePointer,
176+ case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
177+ reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Pointer, reflect.UnsafePointer,
158178 reflect.Slice, reflect.Array, reflect.Bool:
159179 if ints < numOfIntegerRegisters() {
160180 ints++
161181 } else {
162182 stack++
163183 }
184+ case reflect.Uint64, reflect.Int64:
185+ usesSlots := max(1, int(arg.Size()/ptrSize))
186+ if isARMPaddingNeeded(arg, ints, stack) {
187+ usesSlots++
188+ }
189+
190+ if ints < numOfIntegerRegisters() {
191+ ints += usesSlots
192+ } else {
193+ stack += usesSlots
194+ }
164195 case reflect.Float32, reflect.Float64:
165- if floats < numOfFloatRegisters() {
196+ usesSlots := max(1, int(arg.Size()/ptrSize))
197+ if isARMSoftFloat() {
198+ // float64 for arm with softfloat uses same rules as int64
199+ if isARMPaddingNeeded(arg, ints, stack) {
200+ usesSlots++
201+ }
202+ if ints < numOfIntegerRegisters() {
203+ ints += usesSlots
204+ } else {
205+ stack += usesSlots
206+ }
207+ continue
208+ }
209+
210+ if isARMFloatPaddingNeeded(arg, floats, stack) {
211+ usesSlots++
212+ }
213+ if floats < floatArgRegs {
166214 floats++
167215 } else {
168- stack++
216+ stack += usesSlots
169217 }
170218 case reflect.Struct:
171219 ensureStructSupported()
172- if arg.Size() == 0 {
220+ if arg.Size() == 0 && runtime.GOOS != "windows" {
221+ // On Windows an empty struct still consumes one argument slot.
173222 continue
174223 }
175224 addInt := func(u uintptr) {
@@ -190,17 +239,28 @@ func RegisterFunc(fptr any, cfn uintptr) {
190239 ensureStructSupported()
191240 outType := ty.Out(0)
192241 checkStructFieldsSupported(outType)
193- if runtime.GOARCH == "amd64" && outType.Size() > maxRegAllocStructSize {
194- // on amd64 if struct is bigger than 16 bytes allocate the return struct
195- // and pass it in as a hidden first argument.
196- ints++
242+ if structReturnInMemory(outType) {
243+ // A struct returned in memory is allocated by the caller and its
244+ // pointer is passed as a hidden first integer argument. When the
245+ // integer registers are already full, prepending it spills a
246+ // regular argument onto the stack.
247+ if ints < numOfIntegerRegisters() {
248+ ints++
249+ } else {
250+ stack++
251+ }
197252 }
198253 }
199254
200- sizeOfStack := maxArgs - numOfIntegerRegisters()
201- // On Darwin ARM64, use byte-based validation since arguments pack efficiently.
202- // See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
203- if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
255+ argsLimit := maxArgs
256+ sizeOfStack := argsLimit - numOfIntegerRegisters()
257+ if runtime.GOOS == "windows" {
258+ if ints+floats+stack > argsLimit {
259+ panic("purego: too many stack arguments")
260+ }
261+ } else if isDarwin && runtime.GOARCH == "arm64" {
262+ // On Darwin ARM64, use byte-based validation since arguments pack efficiently.
263+ // See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
204264 stackBytes := estimateStackBytes(ty)
205265 maxStackBytes := sizeOfStack * 8
206266 if stackBytes > maxStackBytes {
@@ -213,35 +273,13 @@ func RegisterFunc(fptr any, cfn uintptr) {
213273 }
214274 }
215275
216- // Task 3: Analyze types at RegisterFunc time to determine if keepAlive is needed.
217- // keepAlive is only used for: string args (CString alloc), struct args (addStruct),
218- // struct returns > maxRegAllocStructSize (hidden pointer), array args (addressable copy),
219- // and variadic []any args (unknown types at registration time).
220- needsKeepAlive := false
221- for i := 0; i < ty.NumIn(); i++ {
222- inKind := ty.In(i).Kind()
223- switch inKind {
224- case reflect.String, reflect.Struct, reflect.Array, reflect.Slice:
225- // Slice covers []string (CString allocs) and variadic ...any ([]any)
226- needsKeepAlive = true
227- case reflect.Interface:
228- needsKeepAlive = true
229- }
230- }
231- if ty.NumOut() == 1 {
232- switch ty.Out(0).Kind() {
233- case reflect.Struct:
234- // struct returns may need hidden pointer allocation stored in keepAlive
235- needsKeepAlive = true
236- }
237- }
238-
239276 v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
240277 var sysargs [maxArgs]uintptr
241278 // Use maxArgs instead of numOfFloatRegisters() to keep this code path allocation-free,
242279 // since numOfFloatRegisters() is a function call, not a constant.
243280 // maxArgs is always greater than or equal to numOfFloatRegisters() so this is safe.
244281 var floats [maxArgs]uintptr
282+ floatArgRegs := numOfFloatRegisters()
245283 var numInts int
246284 var numFloats int
247285 var numStack int
@@ -261,7 +299,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
261299 }
262300 }
263301 addFloat = func(x uintptr) {
264- if numFloats < numOfFloatRegisters() {
302+ if numFloats < floatArgRegs {
265303 floats[numFloats] = x
266304 numFloats++
267305 } else {
@@ -275,6 +313,9 @@ func RegisterFunc(fptr any, cfn uintptr) {
275313 // This is in contrast to how macOS and Linux pass arguments which
276314 // tries to use as many registers as possible in the calling convention.
277315 addStack = func(x uintptr) {
316+ if numStack >= maxArgs {
317+ panic("purego: too many stack arguments")
318+ }
278319 sysargs[numStack] = x
279320 numStack++
280321 }
@@ -282,189 +323,169 @@ func RegisterFunc(fptr any, cfn uintptr) {
282323 addFloat = addStack
283324 }
284325
285- if needsKeepAlive {
286- // Slow path: allocate keepAlive slice and defer cleanup.
287- // Used when args include strings, structs, arrays, or variadic []any.
288- var keepAlive []any
289- defer func() {
290- runtime.KeepAlive(keepAlive)
291- runtime.KeepAlive(args)
292- }()
326+ var keepAlive []any
327+ defer func() {
328+ runtime.KeepAlive(keepAlive)
329+ runtime.KeepAlive(args)
330+ }()
293331
294- var arm64_r8 uintptr
295- if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
296- outType := ty.Out(0)
297- if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize {
332+ var arm64_r8 uintptr
333+ if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
334+ outType := ty.Out(0)
335+ if structReturnInMemory(outType) {
336+ // The caller allocates the return value and passes its pointer
337+ // as a hidden first integer argument.
338+ val := reflect.New(outType)
339+ keepAlive = append(keepAlive, val)
340+ addInt(val.Pointer())
341+ } else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize {
342+ isAllFloats, numFields := isAllSameFloat(outType)
343+ if !isAllFloats || numFields > 4 {
298344 val := reflect.New(outType)
299345 keepAlive = append(keepAlive, val)
300- addInt(val.Pointer())
301- } else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize {
302- isAllFloats, numFields := isAllSameFloat(outType)
303- if !isAllFloats || numFields > 4 {
304- val := reflect.New(outType)
305- keepAlive = append(keepAlive, val)
306- arm64_r8 = val.Pointer()
307- }
346+ arm64_r8 = val.Pointer()
308347 }
309348 }
310- for i, v := range args {
311- if variadic, ok := xreflect.TypeAssert[[]any](v); ok {
312- if i != len(args)-1 {
313- panic("purego: can only expand last parameter")
314- }
349+ }
350+ for i, v := range args {
351+ // Only the last argument is expanded into several C arguments.
352+ // Both a declared ...any parameter and a final []any parameter
353+ // arrive here as a []any holding those arguments. A []any in any
354+ // other position is an ordinary argument that is passed as one value.
355+ if i == len(args)-1 {
356+ if variadic, ok := reflect.TypeAssert[[]any](args[i]); ok {
315357 for _, x := range variadic {
316358 keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
317359 }
318360 continue
319361 }
320- // Check if we need to start Darwin ARM64 C-style stack packing
321- if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
322- // Collect and separate remaining args into register vs stack
323- stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
324- keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
325- keepAlive = newKeepAlive
326-
327- // Bundle stack arguments with C-style packing
328- bundleStackArgs(stackArgs, addStack)
329- break
330- }
331- keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
332- }
333- return callAndReturn(ty, is32bit, cfn, &sysargs, &floats, arm64_r8, args)
334- }
335-
336- // Fast path: no keepAlive needed (pointer/integer/bool/float args only).
337- // Skip keepAlive slice allocation and defer overhead.
338- defer runtime.KeepAlive(args)
339-
340- for i, v := range args {
341- if variadic, ok := xreflect.TypeAssert[[]any](v); ok {
342- if i != len(args)-1 {
343- panic("purego: can only expand last parameter")
344- }
345- for _, x := range variadic {
346- addValue(reflect.ValueOf(x), nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
347- }
348- continue
349362 }
350363 // Check if we need to start Darwin ARM64 C-style stack packing
351- if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
352- stackArgs, _ := collectStackArgs(args, i, numInts, numFloats,
353- nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
364+ if runtime.GOARCH == "arm64" && isDarwin && shouldBundleStackArgs(v, numInts, numFloats) {
365+ // Collect and separate remaining args into register vs stack
366+ stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
367+ keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
368+ keepAlive = newKeepAlive
369+
370+ // Bundle stack arguments with C-style packing
354371 bundleStackArgs(stackArgs, addStack)
355372 break
356373 }
357- addValue(v, nil, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
374+ keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
358375 }
359376
360- return callAndReturn(ty, is32bit, cfn, &sysargs, &floats, 0, args)
361- })
362- fn.Set(v)
363-}
364-
365-// callAndReturn performs the actual C function call and constructs the return value.
366-// It is shared between the fast path (no keepAlive) and slow path (with keepAlive).
367-func callAndReturn(ty reflect.Type, is32bit bool, cfn uintptr, sysargs *[maxArgs]uintptr, floats *[maxArgs]uintptr, arm64_r8 uintptr, args []reflect.Value) []reflect.Value {
368- syscall := &syscall15Args{}
369-
370- if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
371- syscall.Set(cfn, sysargs[:], floats[:], 0)
372- runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
373- } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
374- // Use the normal arm64 calling convention even on Windows
375- syscall.Set(cfn, sysargs[:], floats[:], arm64_r8)
376- runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
377- } else {
378- // This is a fallback for Windows amd64, 386, and arm. Note this may not support floats
379- syscall.a1, syscall.a2, _ = syscall_syscall15X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4],
380- sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11],
381- sysargs[12], sysargs[13], sysargs[14])
382- syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support
383- }
384- if ty.NumOut() == 0 {
385- return nil
386- }
387- outType := ty.Out(0)
388- v := reflect.New(outType).Elem()
389- switch outType.Kind() {
390- case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
391- v.SetUint(uint64(syscall.a1))
392- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
393- v.SetInt(int64(syscall.a1))
394- case reflect.Bool:
395- v.SetBool(byte(syscall.a1) != 0)
396- case reflect.UnsafePointer:
397- // We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer
398- v = reflect.New(outType).Elem()
399- v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1)))
400- case reflect.Pointer:
401- v = reflect.NewAt(outType, unsafe.Pointer(&syscall.a1)).Elem()
402- case reflect.Func:
403- // wrap this C function in a nicely typed Go function
404- v = reflect.New(outType)
405- RegisterFunc(v.Interface(), syscall.a1)
406- case reflect.String:
407- v = reflect.ValueOf(strings.GoString(syscall.a1))
408- case reflect.Float32:
409- // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
410- // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
411- // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
412- // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
413- // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
414- v = reflect.New(outType).Elem()
415- switch runtime.GOARCH {
416- case "386":
417- v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
418- case "ppc64le":
419- v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
420- case "s390x":
421- // S390X is big-endian: float32 in upper 32 bits of 64-bit register
422- v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
423- default:
424- v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
425- }
426- case reflect.Float64:
427- // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
428- // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
429- v = reflect.New(outType).Elem()
430- if is32bit {
431- v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
377+ var syscall *syscallArgs
378+ if runtime.GOOS == "windows" && runtime.GOARCH != "arm64" {
379+ // Windows amd64, 386, and arm use syscall.SyscallN.
380+ syscall = thePool.Get().(*syscallArgs)
381+ syscall.a1, syscall.a2, _ = syscall_syscallN(cfn, sysargs[:numStack]...)
382+ syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support
432383 } else {
433- v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
384+ syscall = syscall_SyscallN(cfn, sysargs[:], floats[:], arm64_r8)
434385 }
435- case reflect.Struct:
436- v = getStruct(outType, *syscall)
437- case reflect.Slice:
438- if outType.Elem().Kind() == reflect.String {
439- // C returns char** (NULL-terminated array of strings)
440- if syscall.a1 == 0 {
441- v = reflect.Zero(outType)
386+ defer thePool.Put(syscall)
387+ if ty.NumOut() == 0 {
388+ return nil
389+ }
390+ outType := ty.Out(0)
391+ v := reflect.New(outType).Elem()
392+ switch outType.Kind() {
393+ case reflect.Uint64:
394+ if is32bit {
395+ // high-word is recorded at a2 for 32-bit platforms and 64-bit returns
396+ v.SetUint(uint64(syscall.a1) | (uint64(syscall.a2) << 32))
397+ } else {
398+ v.SetUint(uint64(syscall.a1))
399+ }
400+ case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
401+ v.SetUint(uint64(syscall.a1))
402+ case reflect.Int64:
403+ if is32bit {
404+ v.SetInt(int64(syscall.a1) | (int64(syscall.a2) << 32))
405+ } else {
406+ v.SetInt(int64(syscall.a1))
407+ }
408+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
409+ v.SetInt(int64(syscall.a1))
410+ case reflect.Bool:
411+ v.SetBool(byte(syscall.a1) != 0)
412+ case reflect.UnsafePointer:
413+ // We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer
414+ v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1)))
415+ case reflect.Pointer:
416+ // Copy syscall.a1 into a local variable to prevent v
417+ // from holding a pointer to the pooled syscallArgs field.
418+ a1 := syscall.a1
419+ v = reflect.NewAt(outType, unsafe.Pointer(&a1)).Elem()
420+ case reflect.Func:
421+ // wrap this C function in a nicely typed Go function
422+ if syscall.a1 != 0 {
423+ RegisterFunc(v.Addr().Interface(), syscall.a1)
424+ }
425+ case reflect.String:
426+ v.SetString(strings.GoString(syscall.a1))
427+ case reflect.Float32:
428+ // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
429+ // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
430+ // On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
431+ // On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
432+ // On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
433+ // On 32bit ARM with softfloat float32 returned as integer
434+ switch runtime.GOARCH {
435+ case "386":
436+ v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
437+ case "ppc64le":
438+ v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
439+ case "s390x":
440+ // S390X is big-endian: float32 in upper 32 bits of 64-bit register
441+ v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
442+ case "arm":
443+ if isARMSoftFloat() {
444+ v.SetFloat(float64(math.Float32frombits(uint32(syscall.a1))))
445+ } else {
446+ v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
447+ }
448+ default:
449+ v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
450+ }
451+ case reflect.Float64:
452+ // NOTE: syscall.r2 is only the floating return value on 64bit platforms.
453+ // On 32bit platforms syscall.r2 is the upper part of a 64bit return.
454+ if isARMSoftFloat() {
455+ // a1,a2 are populated in this case
456+ v.SetFloat(math.Float64frombits(uint64(syscall.a1) | (uint64(syscall.a2) << 32)))
457+ } else if is32bit {
458+ v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
442459 } else {
460+ v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
461+ }
462+ case reflect.Struct:
463+ v = getStruct(outType, *syscall)
464+ case reflect.Slice:
465+ if outType.Elem().Kind() != reflect.String {
466+ panic("purego: unsupported return slice element kind: " + outType.Elem().Kind().String())
467+ }
468+ // C returns a NULL-terminated char** array.
469+ if syscall.a1 != 0 {
443470 var result []string
444471 p := *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))
445- for {
446- strPtr := *(*uintptr)(p)
447- if strPtr == 0 {
448- break
449- }
450- result = append(result, strings.GoString(strPtr))
451- p = unsafe.Add(p, unsafe.Sizeof(uintptr(0)))
472+ for ; *(*uintptr)(p) != 0; p = unsafe.Add(p, unsafe.Sizeof(uintptr(0))) {
473+ result = append(result, strings.GoString(*(*uintptr)(p)))
452474 }
453- v = reflect.ValueOf(result)
475+ v = reflect.ValueOf(result).Convert(outType)
454476 }
477+ default:
478+ panic("purego: unsupported return kind: " + outType.Kind().String())
479+ }
480+ if len(args) > 0 {
481+ // reuse args slice instead of allocating one when possible
482+ args[0] = v
483+ return args[:1]
455484 } else {
456- panic("purego: unsupported return slice element kind: " + outType.Elem().Kind().String())
485+ return []reflect.Value{v}
457486 }
458- default:
459- panic("purego: unsupported return kind: " + outType.Kind().String())
460- }
461- if len(args) > 0 {
462- // reuse args slice instead of allocating one when possible
463- args[0] = v
464- return args[:1]
465- } else {
466- return []reflect.Value{v}
467- }
487+ })
488+ fn.Set(v)
468489 }
469490
470491 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 {
@@ -474,33 +495,52 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
474495 ptr := strings.CString(v.String())
475496 keepAlive = append(keepAlive, ptr)
476497 addInt(uintptr(unsafe.Pointer(ptr)))
477- case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
498+ case reflect.Uint64:
499+ if isARMPaddingNeeded(v.Type(), *numInts, *numStack) {
500+ addInt(0)
501+ }
478502 addInt(uintptr(v.Uint()))
479- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
503+ if is32bit {
504+ addInt(uintptr(v.Uint() >> 32)) // on 32bit we must add high word too
505+ }
506+ case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
507+ addInt(uintptr(v.Uint()))
508+ case reflect.Int64:
509+ if isARMPaddingNeeded(v.Type(), *numInts, *numStack) {
510+ addInt(0)
511+ }
512+ addInt(uintptr(v.Int()))
513+ if is32bit {
514+ addInt(uintptr(v.Int() >> 32))
515+ }
516+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
480517 addInt(uintptr(v.Int()))
481- case reflect.Pointer, reflect.UnsafePointer:
482- // There is no need to keepAlive this pointer separately because it is kept alive in the args variable
483- addInt(v.Pointer())
484518 case reflect.Slice:
485- if v.Type().Elem().Kind() == reflect.String {
486- ss := v.Interface().([]string)
487- if ss == nil {
488- addInt(0)
489- } else {
490- res := make([]*byte, len(ss)+1)
491- for i, s := range ss {
492- ptr := strings.CString(s)
493- keepAlive = append(keepAlive, ptr)
494- res[i] = ptr
495- }
496- // NULL-terminated for C
497- res[len(ss)] = nil
498- keepAlive = append(keepAlive, res)
499- addInt(uintptr(unsafe.Pointer(&res[0])))
500- }
501- } else {
519+ if v.Type().Elem().Kind() != reflect.String {
520+ // There is no need to keepAlive this pointer separately because it is kept alive in the args variable
502521 addInt(v.Pointer())
522+ break
523+ }
524+ if v.IsNil() {
525+ addInt(0)
526+ break
503527 }
528+ // Pass []string as a NULL-terminated char** array.
529+ res := make([]*byte, v.Len()+1)
530+ for i := range v.Len() {
531+ res[i] = strings.CString(v.Index(i).String())
532+ }
533+ keepAlive = append(keepAlive, res)
534+ addInt(uintptr(unsafe.Pointer(&res[0])))
535+ case reflect.Array:
536+ // C array parameters decay to pointers, so pass a pointer to a copy.
537+ tmp := reflect.New(v.Type())
538+ tmp.Elem().Set(v)
539+ keepAlive = append(keepAlive, tmp)
540+ addInt(tmp.Pointer())
541+ case reflect.Pointer, reflect.UnsafePointer:
542+ // There is no need to keepAlive this pointer separately because it is kept alive in the args variable
543+ addInt(v.Pointer())
504544 case reflect.Func:
505545 addInt(NewCallback(v.Interface()))
506546 case reflect.Bool:
@@ -510,30 +550,41 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
510550 addInt(0)
511551 }
512552 case reflect.Float32:
513- // On S390X big-endian, float32 goes in upper 32 bits of 64-bit FP register
514- if runtime.GOARCH == "s390x" {
553+ switch runtime.GOARCH {
554+ case "ppc64le":
555+ // A single-precision argument occupies a floating-point register in double format on Power.
556+ addFloat(uintptr(math.Float64bits(v.Float())))
557+ case "s390x":
558+ // S390X big-endian: float32 goes in the upper 32 bits of the 64-bit FP register.
515559 addFloat(uintptr(math.Float32bits(float32(v.Float()))) << 32)
516- } else {
560+ case "arm":
561+ if isARMSoftFloat() {
562+ // 32-bit ARM with softfloat: float32 goes as integer
563+ addInt(uintptr(math.Float32bits(float32(v.Float()))))
564+ } else {
565+ addFloat(uintptr(math.Float32bits(float32(v.Float()))))
566+ }
567+ default:
517568 addFloat(uintptr(math.Float32bits(float32(v.Float()))))
518569 }
519570 case reflect.Float64:
520- if is32bit {
521- bits := math.Float64bits(v.Float())
571+ bits := math.Float64bits(v.Float())
572+ if isARMFloatPaddingNeeded(v.Type(), *numFloats, *numStack) {
573+ // if floats are spilled onto stack on ARM than we must follow AAPCS C.7
574+ addFloat(0)
575+ }
576+ if isARMSoftFloat() {
577+ // add as uint64
578+ if isARMPaddingNeeded(v.Type(), *numInts, *numStack) {
579+ addInt(0)
580+ }
581+ addInt(uintptr(bits))
582+ addInt(uintptr(bits >> 32))
583+ } else if is32bit {
522584 addFloat(uintptr(bits))
523585 addFloat(uintptr(bits >> 32))
524586 } else {
525- addFloat(uintptr(math.Float64bits(v.Float())))
526- }
527- case reflect.Array:
528- // Task 7: Support fixed-size array arguments by passing a pointer to the array data.
529- if v.CanAddr() {
530- addInt(v.Addr().Pointer())
531- } else {
532- // Array is not addressable (e.g. passed by value); make an addressable copy.
533- tmp := reflect.New(v.Type()).Elem()
534- tmp.Set(v)
535- keepAlive = append(keepAlive, tmp)
536- addInt(tmp.Addr().Pointer())
587+ addFloat(uintptr(bits))
537588 }
538589 case reflect.Struct:
539590 keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive)
@@ -550,17 +601,61 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
550601 // If you change this make sure to update it in objc_runtime_darwin.go
551602 const maxRegAllocStructSize = 16
552603
604+var hostLayoutType = reflect.TypeFor[structs.HostLayout]()
605+
606+// isABIField reports whether f takes part in the C ABI of the struct that
607+// contains it. Only the structs.HostLayout marker does not.
608+func isABIField(f reflect.StructField) bool {
609+ return !f.Type.ConvertibleTo(hostLayoutType)
610+}
611+
612+// numABIFields returns how many of ty's fields take part in the C ABI.
613+func numABIFields(ty reflect.Type) int {
614+ var n int
615+ for i := range ty.NumField() {
616+ if isABIField(ty.Field(i)) {
617+ n++
618+ }
619+ }
620+ return n
621+}
622+
623+// abiField returns the i'th field of ty that takes part in the C ABI. It panics
624+// if ty has fewer than i+1 such fields.
625+func abiField(ty reflect.Type, i int) reflect.StructField {
626+ for j := range ty.NumField() {
627+ f := ty.Field(j)
628+ if !isABIField(f) {
629+ continue
630+ }
631+ if i == 0 {
632+ return f
633+ }
634+ i--
635+ }
636+ panic("purego: struct field index out of range")
637+}
638+
553639 func isAllSameFloat(ty reflect.Type) (allFloats bool, numFields int) {
554640 allFloats = true
555- root := ty.Field(0).Type
641+ if numABIFields(ty) == 0 {
642+ return false, 0
643+ }
644+ root := abiField(ty, 0).Type
556645 for root.Kind() == reflect.Struct {
557- root = root.Field(0).Type
646+ if numABIFields(root) == 0 {
647+ return false, 0
648+ }
649+ root = abiField(root, 0).Type
558650 }
559651 first := root.Kind()
560652 if first != reflect.Float32 && first != reflect.Float64 {
561653 allFloats = false
562654 }
563- for i := 0; i < ty.NumField(); i++ {
655+ for i := range ty.NumField() {
656+ if !isABIField(ty.Field(i)) {
657+ continue
658+ }
564659 f := ty.Field(i).Type
565660 if f.Kind() == reflect.Struct {
566661 var structNumFields int
@@ -577,7 +672,10 @@ func isAllSameFloat(ty reflect.Type) (allFloats bool, numFields int) {
577672 }
578673
579674 func checkStructFieldsSupported(ty reflect.Type) {
580- for i := 0; i < ty.NumField(); i++ {
675+ for i := range ty.NumField() {
676+ if !isABIField(ty.Field(i)) {
677+ continue
678+ }
581679 f := ty.Field(i).Type
582680 if f.Kind() == reflect.Array {
583681 f = f.Elem()
@@ -596,15 +694,39 @@ func checkStructFieldsSupported(ty reflect.Type) {
596694 }
597695 }
598696
697+// ensureStructSupported panics if passing or returning structs through a call to
698+// a C function is unsupported on the current platform.
599699 func ensureStructSupported() {
700+ switch runtime.GOARCH {
701+ case "amd64", "arm64", "loong64", "ppc64le":
702+ default:
703+ panic("purego: struct arguments/returns are only supported on amd64, arm64, loong64, and ppc64le")
704+ }
705+ switch runtime.GOOS {
706+ case "android", "darwin", "ios", "linux", "windows":
707+ default:
708+ panic("purego: struct arguments/returns are only supported on android, darwin, ios, linux, and windows")
709+ }
710+}
711+
712+// ensureCallbackStructSupported panics if passing or returning structs through a
713+// callback is unsupported on the current platform. Callbacks support structs on
714+// fewer architectures than a direct call to a C function.
715+func ensureCallbackStructSupported() {
600716 if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
601- panic("purego: struct arguments/returns are only supported on amd64 and arm64")
717+ panic("purego: struct arguments/returns in callbacks are only supported on amd64 and arm64")
602718 }
603- if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
604- panic("purego: struct arguments/returns are only supported on darwin and linux")
719+ switch runtime.GOOS {
720+ case "android", "darwin", "ios", "linux", "windows":
721+ default:
722+ panic("purego: struct arguments/returns in callbacks are only supported on android, darwin, ios, linux, and windows")
605723 }
606724 }
607725
726+// isDarwin is true on platforms that use Apple's calling convention.
727+// iOS (GOOS=ios) shares it with macOS (GOOS=darwin).
728+const isDarwin = runtime.GOOS == "darwin" || runtime.GOOS == "ios"
729+
608730 func roundUpTo8(val uintptr) uintptr {
609731 return (val + align8ByteMask) &^ align8ByteMask
610732 }
@@ -616,6 +738,7 @@ func numOfFloatRegisters() int {
616738 case "s390x":
617739 return 4
618740 case "arm":
741+ // 8 doubles (16 words) are always reserved by asm trampolines, even if softfloat is used
619742 return 16
620743 case "386":
621744 // i386 SysV ABI passes all arguments on the stack, including floats
@@ -654,7 +777,7 @@ func estimateStackBytes(ty reflect.Type) int {
654777 var numInts, numFloats int
655778 var stackBytes int
656779
657- for i := 0; i < ty.NumIn(); i++ {
780+ for i := range ty.NumIn() {
658781 arg := ty.In(i)
659782 size := int(arg.Size())
660783
@@ -665,7 +788,6 @@ func estimateStackBytes(ty reflect.Type) int {
665788 } else if !usesInt && numFloats < numOfFloatRegisters() {
666789 numFloats++
667790 } else {
668- // Goes to stack - accumulate total bytes
669791 stackBytes += size
670792 }
671793 }
@@ -675,3 +797,29 @@ func estimateStackBytes(ty reflect.Type) int {
675797 }
676798 return stackBytes
677799 }
800+
801+func isARMPaddingNeeded(ty reflect.Type, numInts, numStack int) bool {
802+ // ARM EABI (AAPCS): 8-byte-aligned types (int64/uint64) start on an
803+ // even core register (C.3); if they then spill, the stack slot is
804+ // 8-byte aligned too (C.7).
805+ // https://github.com/ARM-software/abi-aa/blob/main/aapcs32/aapcs32.rst#6111handling-values-larger-than-32-bits
806+ if runtime.GOARCH != "arm" || ty.Size() != 8 {
807+ return false
808+ }
809+ if numInts >= 0 && numInts < numOfIntegerRegisters() {
810+ return numInts%2 != 0
811+ }
812+ return numStack%2 != 0
813+}
814+
815+func isARMFloatPaddingNeeded(ty reflect.Type, numFloats, numStack int) bool {
816+ if runtime.GOARCH != "arm" || ty.Size() != 8 {
817+ return false
818+ }
819+ if numFloats >= 0 && numFloats < numOfFloatRegisters() {
820+ // float registers are 64bit so alignment never needed for args in registers
821+ return false
822+ }
823+ // Only check if AAPCS C.7 is applicable here
824+ return numStack%2 != 0
825+}
A func_slice_test.go +61 -0
@@ -0,0 +1,61 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || linux || netbsd
5+
6+package purego_test
7+
8+import (
9+ "path/filepath"
10+ "slices"
11+ "testing"
12+
13+ "github.com/bnema/purego"
14+ "github.com/bnema/purego/internal/load"
15+)
16+
17+func TestRegisterFuncStringSliceAndArray(t *testing.T) {
18+ libFileName := filepath.Join(t.TempDir(), "slicetest.so")
19+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "slicetest", "slice_test.c")); err != nil {
20+ t.Fatal(err)
21+ }
22+ lib, err := load.OpenLibrary(libFileName)
23+ if err != nil {
24+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
25+ }
26+ defer load.CloseLibrary(lib)
27+
28+ var join func([]string) string
29+ purego.RegisterLibFunc(&join, lib, "join_strings")
30+ var get func(int32) []string
31+ purego.RegisterLibFunc(&get, lib, "get_strings")
32+ var sum func([4]float32) float32
33+ purego.RegisterLibFunc(&sum, lib, "sum_floats4")
34+
35+ t.Run("[]string argument", func(t *testing.T) {
36+ if got := join([]string{"a", "bc", "def"}); got != "a,bc,def" {
37+ t.Errorf("join = %q, want %q", got, "a,bc,def")
38+ }
39+ if got := join([]string{}); got != "" {
40+ t.Errorf("join(empty) = %q, want empty", got)
41+ }
42+ if got := join(nil); got != "(null)" {
43+ t.Errorf("join(nil) = %q, want %q", got, "(null)")
44+ }
45+ })
46+
47+ t.Run("[]string return", func(t *testing.T) {
48+ if got, want := get(0), []string{"alpha", "beta", "gamma"}; !slices.Equal(got, want) {
49+ t.Errorf("get = %q, want %q", got, want)
50+ }
51+ if got := get(1); got != nil {
52+ t.Errorf("get(NULL) = %q, want nil", got)
53+ }
54+ })
55+
56+ t.Run("array argument", func(t *testing.T) {
57+ if got := sum([4]float32{1, 2, 3, 4.5}); got != 10.5 {
58+ t.Errorf("sum = %v, want 10.5", got)
59+ }
60+ })
61+}
M func_test.go +412 -45
@@ -7,10 +7,15 @@ import (
77 "bytes"
88 "errors"
99 "fmt"
10+ "math"
11+ "os"
1012 "os/exec"
1113 "path/filepath"
14+ "reflect"
1215 "runtime"
16+ "strconv"
1317 "strings"
18+ "structs"
1419 "sync"
1520 "testing"
1621 "unsafe"
@@ -21,7 +26,9 @@ import (
2126
2227 func getSystemLibrary() (string, error) {
2328 switch runtime.GOOS {
24- case "darwin":
29+ case "android":
30+ return "libc.so", nil
31+ case "darwin", "ios":
2532 return "/usr/lib/libSystem.B.dylib", nil
2633 case "freebsd":
2734 return "libc.so.7", nil
@@ -52,19 +59,21 @@ func TestRegisterFunc_ConcurrentPointerReturn(t *testing.T) {
5259 purego.RegisterLibFunc(&free, libc, "free")
5360
5461 var wg sync.WaitGroup
55- for i := 0; i < runtime.NumCPU(); i++ {
62+
63+ for i := range runtime.NumCPU() {
5664 wg.Add(1)
57- go func() {
65+ go func(id int) {
5866 defer wg.Done()
59- for j := 0; j < 400_000; j++ {
67+ for range 400_000 {
6068 ptr := alloc(5)
6169 if ptr == nil {
6270 continue
6371 }
6472 free(ptr)
6573 }
66- }()
74+ }(i)
6775 }
76+
6877 wg.Wait()
6978 }
7079
@@ -172,14 +181,65 @@ func TestRegisterLibFunc_Bool(t *testing.T) {
172181 }
173182 }
174183
175-func TestABI(t *testing.T) {
176- if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
177- t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
184+func TestNewCallback_NotAFunction(t *testing.T) {
185+ for _, tc := range []struct {
186+ name string
187+ fn any
188+ }{
189+ {"nil", nil},
190+ {"int", 42},
191+ {"string", "not a function"},
192+ {"pointer", new(int)},
193+ } {
194+ t.Run(tc.name, func(t *testing.T) {
195+ defer func() {
196+ r := recover()
197+ if r == nil {
198+ t.Fatal("NewCallback did not panic")
199+ }
200+ const want = "purego: the type must be a function but was not"
201+ if got := fmt.Sprint(r); got != want {
202+ t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, want)
203+ }
204+ }()
205+ purego.NewCallback(tc.fn)
206+ })
207+ }
208+}
209+
210+func TestRegisterFunc_InvalidFunctionPointer(t *testing.T) {
211+ for _, tc := range []struct {
212+ name string
213+ fptr any
214+ want string
215+ }{
216+ {"nil", nil, "purego: fptr must be a non-nil function pointer"},
217+ {"non_pointer", 42, "purego: fptr must be a non-nil function pointer"},
218+ {"function_value", func() {}, "purego: fptr must be a non-nil function pointer"},
219+ {"nil_function_pointer", (*func())(nil), "purego: fptr must be a non-nil function pointer"},
220+ {"pointer_to_non_function", new(int), "purego: fptr must be a function pointer"},
221+ {"pointer_to_function_pointer", new(*func()), "purego: fptr must be a function pointer"},
222+ } {
223+ t.Run(tc.name, func(t *testing.T) {
224+ defer func() {
225+ r := recover()
226+ if r == nil {
227+ t.Fatal("RegisterFunc did not panic")
228+ }
229+ if got := fmt.Sprint(r); got != tc.want {
230+ t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, tc.want)
231+ }
232+ }()
233+ purego.RegisterFunc(tc.fptr, 1)
234+ })
178235 }
236+}
237+
238+func TestABI(t *testing.T) {
179239 libFileName := filepath.Join(t.TempDir(), "abitest.so")
180240 t.Logf("Build %v", libFileName)
181241
182- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
242+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
183243 t.Fatal(err)
184244 }
185245
@@ -234,14 +294,29 @@ func TestABI(t *testing.T) {
234294 t.Fatalf("%s: got %q, want %q", cName, res, want)
235295 }
236296 }
297+ {
298+ const cName = "return_func_ptr"
299+ var fn func() func(a, b int32) int32
300+ purego.RegisterLibFunc(&fn, lib, cName)
301+ add := fn()
302+ const expect = 5
303+ if res := add(2, 3); res != expect {
304+ t.Fatalf("%s: got %d, want %d", cName, res, expect)
305+ }
306+ }
307+ {
308+ const cName = "return_null_func_ptr"
309+ var fn func() func(a, b int32) int32
310+ purego.RegisterLibFunc(&fn, lib, cName)
311+ if fn() != nil {
312+ t.Fatalf("%s: got a non-nil func, want nil", cName)
313+ }
314+ }
237315 }
238316
239317 func TestABI_ArgumentPassing(t *testing.T) {
240- if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
241- t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
242- }
243318 libFileName := filepath.Join(t.TempDir(), "abitest.so")
244- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
319+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
245320 t.Fatal(err)
246321 }
247322 lib, err := load.OpenLibrary(libFileName)
@@ -373,11 +448,20 @@ func TestABI_ArgumentPassing(t *testing.T) {
373448 },
374449 {
375450 name: "8int_hfa4_stack",
376- fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })),
377- cFn: "stack_8int_hfa4_stack",
451+ fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
452+ _ structs.HostLayout
453+ x, y, z, w float32
454+ })),
455+ cFn: "stack_8int_hfa4_stack",
378456 call: func(f any) string {
379457 buf := make([]byte, 256)
380- (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y, z, w float32 }{10.0, 20.0, 30.0, 40.0})
458+ (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
459+ _ structs.HostLayout
460+ x, y, z, w float32
461+ })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct {
462+ _ structs.HostLayout
463+ x, y, z, w float32
464+ }{x: 10.0, y: 20.0, z: 30.0, w: 40.0})
381465 return string(buf[:bytes.IndexByte(buf, 0)])
382466 },
383467 want: "1:2:3:4:5:6:7:8:10.0:20.0:30.0:40.0",
@@ -385,6 +469,7 @@ func TestABI_ArgumentPassing(t *testing.T) {
385469 {
386470 name: "8int_mixed_struct",
387471 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
472+ _ structs.HostLayout
388473 a int32
389474 b float32
390475 })),
@@ -392,24 +477,108 @@ func TestABI_ArgumentPassing(t *testing.T) {
392477 call: func(f any) string {
393478 buf := make([]byte, 256)
394479 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
480+ _ structs.HostLayout
395481 a int32
396482 b float32
397483 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct {
484+ _ structs.HostLayout
398485 a int32
399486 b float32
400- }{9, 10.0})
487+ }{a: 9, b: 10.0})
401488 return string(buf[:bytes.IndexByte(buf, 0)])
402489 },
403490 want: "1:2:3:4:5:6:7:8:9:10.0",
404491 },
492+ {
493+ // check if unaligned 64bit argument and 64bit returned value is properly passed via registers
494+ // arm-specific but must work everywhere
495+ name: "arm_int64_unaligned_in_registers",
496+ fn: new(func(uintptr, int64) int64),
497+ cFn: "arm_int64_unaligned_in_registers",
498+ call: func(f any) string {
499+ fn := *(f).(*func(x uintptr, y int64) int64)
500+ return strconv.FormatInt(fn(456, math.MaxInt32+1500), 10)
501+ },
502+ want: strconv.FormatInt(456*123+math.MaxInt32+1500, 10),
503+ },
504+ {
505+ // check if unaligned 64bit argument and 64bit returned value is properly passed via stack
506+ // arm-specific but must work everywhere
507+ name: "arm_int64_unaligned_on_stack",
508+ fn: new(func(uintptr, uintptr, uintptr, uintptr, uintptr, int64) int64),
509+ cFn: "arm_int64_unaligned_on_stack",
510+ call: func(f any) string {
511+ fn := *(f).(*func(a1, a2, a3, a4, a5 uintptr, a6 int64) int64)
512+ return strconv.FormatInt(fn(12, 34, 56, 78, 90, math.MaxInt32+1500), 10)
513+ },
514+ want: strconv.FormatInt(12*1+34*2+56*3+78*4+90*5+math.MaxInt32+1500, 10),
515+ },
516+ {
517+ // check if unaligned 64bit argument and 64bit returned value is properly passed via stack when it's occupied by floats
518+ // arm-specific but must work everywhere
519+ name: "arm_int64_unaligned_on_stack_after_floats",
520+ fn: new(func(
521+ uintptr, uintptr, uintptr, uintptr,
522+ float32, float32, float32, float32,
523+ float32, float32, float32, float32,
524+ float32, float32, float32, float32,
525+ float32, float32, float32, float32,
526+ float32, int64,
527+ ) int64),
528+ cFn: "arm_int64_unaligned_on_stack_after_floats",
529+ call: func(f any) string {
530+ fn := *(f).(*func(
531+ a1, a2, a3, a4 uintptr,
532+ f1, f2, f3, f4 float32,
533+ f5, f6, f7, f8 float32,
534+ f9, f10, f11, f12 float32,
535+ f13, f14, f15, f16 float32,
536+ f17 float32, a5 int64,
537+ ) int64)
538+ return strconv.FormatInt(fn(
539+ 12, 34, 56, 78,
540+ 0, 0, 0, 0,
541+ 0, 0, 0, 0,
542+ 0, 0, 0, 0,
543+ 0, 0, 0, 0,
544+ 0, math.MaxInt32+1500,
545+ ), 10)
546+ },
547+ want: strconv.FormatInt(12*1+34*2+56*3+78*4+math.MaxInt32+1500, 10),
548+ },
549+ {
550+ // check if unaligned float 64bit argument and float 64bit returned value is properly passed via registers
551+ // arm-softfloat-specific but must work everywhere
552+ name: "arm_float64_unaligned_in_registers",
553+ fn: new(func(uintptr, float64) float64),
554+ cFn: "arm_float64_unaligned_in_registers",
555+ call: func(f any) string {
556+ fn := *(f).(*func(x uintptr, y float64) float64)
557+ return strconv.FormatFloat(fn(456, math.MaxFloat32+1500), 'b', 10, 64)
558+ },
559+ want: strconv.FormatFloat(456*123.5+math.MaxFloat32+1500, 'b', 10, 64),
560+ },
561+ {
562+ // check if unaligned float 64bit argument and float 64bit returned value is properly passed via stack
563+ // arm-softfloat-specific but must work everywhere
564+ name: "arm_float64_unaligned_on_stack",
565+ fn: new(func(uintptr, uintptr, uintptr, uintptr, uintptr, float64) float64),
566+ cFn: "arm_float64_unaligned_on_stack",
567+ call: func(f any) string {
568+ fn := *(f).(*func(a1, a2, a3, a4, a5 uintptr, a6 float64) float64)
569+ return strconv.FormatFloat(fn(12, 34, 56, 78, 90, math.MaxFloat32+1500), 'b', 10, 64)
570+ },
571+ want: strconv.FormatFloat(12*1+34*2+56*3+78*4+90*5+math.MaxFloat32+1500, 'b', 10, 64),
572+ },
405573 }
406574
407575 for _, tt := range tests {
408576 t.Run(tt.name, func(t *testing.T) {
409- if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {
410- t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
577+ if tt.name == "20_int32" && runtime.GOARCH == "ppc64le" {
578+ t.Skip("ppc64le retains the 15-argument limit")
411579 }
412- if tt.name == "10_float32" && (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") {
580+ if (tt.name == "10_float32" || tt.name == "arm_int64_unaligned_on_stack_after_floats") &&
581+ (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") {
413582 t.Skip("float32 stack arguments not yet supported on this platform")
414583 }
415584 // Struct tests require Darwin ARM64 or AMD64
@@ -427,43 +596,241 @@ func TestABI_ArgumentPassing(t *testing.T) {
427596 }
428597 })
429598 }
430-}
431599
432-func TestABI_TooManyArguments(t *testing.T) {
433- if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
434- t.Skip("This test is specific to Darwin ARM64")
435- }
600+ t.Run("20_uintptr", func(t *testing.T) {
601+ if runtime.GOARCH == "ppc64le" {
602+ t.Skip("ppc64le retains the 15-argument limit")
603+ }
604+ var fn func(uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr) uintptr
605+ purego.RegisterLibFunc(&fn, lib, "stack_20_uintptr")
606+ got := fn(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
607+ const want = uintptr(210)
608+ if got != want {
609+ t.Fatalf("stack_20_uintptr: got %d, want %d", got, want)
610+ }
611+ })
436612
437- libFileName := filepath.Join(t.TempDir(), "abitest.so")
438- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
439- t.Fatal(err)
440- }
441- lib, err := load.OpenLibrary(libFileName)
442- if err != nil {
443- t.Fatalf("Failed to open library %q: %v", libFileName, err)
444- }
445- t.Cleanup(func() {
446- if err := load.CloseLibrary(lib); err != nil {
447- t.Errorf("Failed to close library: %v", err)
613+ t.Run("32_uintptr", func(t *testing.T) {
614+ if runtime.GOARCH == "ppc64le" {
615+ t.Skip("ppc64le retains the 15-argument limit")
616+ }
617+ var fn func(
618+ uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
619+ uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
620+ uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
621+ uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
622+ ) uintptr
623+ purego.RegisterLibFunc(&fn, lib, "stack_32_uintptr")
624+ got := fn(
625+ 1, 2, 3, 4, 5, 6, 7, 8,
626+ 9, 10, 11, 12, 13, 14, 15, 16,
627+ 17, 18, 19, 20, 21, 22, 23, 24,
628+ 25, 26, 27, 28, 29, 30, 31, 32,
629+ )
630+ const want = uintptr(528)
631+ if got != want {
632+ t.Fatalf("stack_32_uintptr: got %d, want %d", got, want)
633+ }
634+ })
635+
636+ t.Run("syscalln_20_uintptr", func(t *testing.T) {
637+ if runtime.GOARCH == "ppc64le" {
638+ t.Skip("ppc64le retains the 15-argument limit")
639+ }
640+ fn, err := load.OpenSymbol(lib, "stack_20_uintptr")
641+ if err != nil {
642+ t.Fatalf("OpenSymbol(stack_20_uintptr) failed: %v", err)
643+ }
644+ got, _, _ := purego.SyscallN(fn,
645+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
646+ 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
647+ )
648+ const want = uintptr(210)
649+ if got != want {
650+ t.Fatalf("stack_20_uintptr SyscallN: got %d, want %d", got, want)
448651 }
449652 })
450653
451- // Test that 35 int64 arguments (27 slots needed) exceeds the limit
452- t.Run("35_int64_exceeds_limit", func(t *testing.T) {
654+ t.Run("syscalln_32_uintptr", func(t *testing.T) {
655+ if runtime.GOARCH == "ppc64le" {
656+ t.Skip("ppc64le retains the 15-argument limit")
657+ }
658+ fn, err := load.OpenSymbol(lib, "stack_32_uintptr")
659+ if err != nil {
660+ t.Fatalf("OpenSymbol(stack_32_uintptr) failed: %v", err)
661+ }
662+ got, _, _ := purego.SyscallN(fn,
663+ 1, 2, 3, 4, 5, 6, 7, 8,
664+ 9, 10, 11, 12, 13, 14, 15, 16,
665+ 17, 18, 19, 20, 21, 22, 23, 24,
666+ 25, 26, 27, 28, 29, 30, 31, 32,
667+ )
668+ const want = uintptr(528)
669+ if got != want {
670+ t.Fatalf("stack_32_uintptr SyscallN: got %d, want %d", got, want)
671+ }
672+ })
673+
674+ t.Run("32_mixed_int_float", func(t *testing.T) {
675+ if unsafe.Sizeof(uintptr(0)) == 4 {
676+ t.Skip("requires 64-bit uintptr slots")
677+ }
678+ if runtime.GOARCH == "ppc64le" {
679+ t.Skip("mixed int/float stack arguments are not yet supported on ppc64le")
680+ }
681+
682+ var fn func(
683+ uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
684+ uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
685+ float64, float64, float64, float64, float64, float64, float64, float64,
686+ float64, float64, float64, float64, float64, float64, float64, float64,
687+ ) float64
688+ purego.RegisterLibFunc(&fn, lib, "stack_32_mixed_int_float")
689+ got := fn(
690+ 1, 2, 3, 4, 5, 6, 7, 8,
691+ 9, 10, 11, 12, 13, 14, 15, 16,
692+ 1, 2, 3, 4, 5, 6, 7, 8,
693+ 9, 10, 11, 12, 13, 14, 15, 16,
694+ )
695+ const want = 5168.0
696+ if got != want {
697+ t.Fatalf("stack_32_mixed_int_float: got %f, want %f", got, want)
698+ }
699+ })
700+}
701+
702+func TestABI_TooManyArguments(t *testing.T) {
703+ mustPanic := func(t *testing.T, want string, f func()) {
704+ t.Helper()
453705 defer func() {
454- if r := recover(); r != nil {
455- t.Logf("Got expected panic: %v", r)
456- } else {
457- t.Errorf("Expected panic but didn't get one")
706+ r := recover()
707+ if r == nil {
708+ t.Fatalf("expected panic %q, got none", want)
709+ }
710+ got := fmt.Sprint(r)
711+ if got != want {
712+ t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, want)
458713 }
459714 }()
715+ f()
716+ }
460717
461- 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)
462- purego.RegisterLibFunc(&fn, lib, "stack_35_int64_exceeds")
718+ // 33 int64 parameters exceeds maxArgs=32.
719+ t.Run("registerfunc_33_int64_exceeds_limit", func(t *testing.T) {
720+ mustPanic(t, "purego: too many stack arguments", func() {
721+ var fn func(
722+ int64, int64, int64, int64, int64, int64, int64, int64,
723+ int64, int64, int64, int64, int64, int64, int64, int64,
724+ int64, int64, int64, int64, int64, int64, int64, int64,
725+ int64, int64, int64, int64, int64, int64, int64, int64,
726+ int64,
727+ )
728+ purego.RegisterFunc(&fn, 1)
729+ })
730+ })
731+
732+ t.Run("registerfunc_16_int64_exceeds_ppc64le_limit", func(t *testing.T) {
733+ if runtime.GOARCH != "ppc64le" {
734+ t.Skip("ppc64le retains the 15-argument limit")
735+ }
736+ mustPanic(t, "purego: too many stack arguments", func() {
737+ var fn func(
738+ int64, int64, int64, int64, int64, int64, int64, int64,
739+ int64, int64, int64, int64, int64, int64, int64, int64,
740+ )
741+ purego.RegisterFunc(&fn, 1)
742+ })
743+ })
744+
745+ t.Run("syscalln_33_uintptr_exceeds_limit", func(t *testing.T) {
746+ mustPanic(t, "purego: too many arguments to SyscallN", func() {
747+ purego.SyscallN(1,
748+ 1, 2, 3, 4, 5, 6, 7, 8,
749+ 9, 10, 11, 12, 13, 14, 15, 16,
750+ 17, 18, 19, 20, 21, 22, 23, 24,
751+ 25, 26, 27, 28, 29, 30, 31, 32,
752+ 33,
753+ )
754+ })
755+ })
756+
757+ t.Run("syscalln_16_uintptr_exceeds_ppc64le_limit", func(t *testing.T) {
758+ if runtime.GOARCH != "ppc64le" {
759+ t.Skip("ppc64le retains the 15-argument limit")
760+ }
761+ mustPanic(t, "purego: too many arguments to SyscallN", func() {
762+ purego.SyscallN(1,
763+ 1, 2, 3, 4, 5, 6, 7, 8,
764+ 9, 10, 11, 12, 13, 14, 15, 16,
765+ )
766+ })
463767 })
464768 }
465769
466-func buildSharedLib(compilerEnv, libFile string, sources ...string) error {
770+func TestABI_StructReturnHiddenPointer(t *testing.T) {
771+ // A struct returned in memory is passed a hidden pointer as the first
772+ // integer argument, so maxArgs integer arguments plus it need one more slot
773+ // than sysargs holds and RegisterFunc must reject the registration.
774+ type bigStruct struct{ A, B, C uint64 } // larger than two eightbytes
775+
776+ if !purego.StructReturnInMemory(reflect.TypeFor[bigStruct]()) {
777+ t.Skipf("GOARCH=%s does not return large structs via a hidden integer argument", runtime.GOARCH)
778+ }
779+
780+ in := make([]reflect.Type, purego.MaxArgs)
781+ for i := range in {
782+ in[i] = reflect.TypeFor[uintptr]()
783+ }
784+ fnType := reflect.FuncOf(in, []reflect.Type{reflect.TypeFor[bigStruct]()}, false)
785+ fptr := reflect.New(fnType)
786+
787+ defer func() {
788+ switch r := recover(); {
789+ case r == nil:
790+ t.Fatal("RegisterFunc accepted the call; the hidden struct-return pointer was not counted against the stack limit")
791+ case strings.Contains(fmt.Sprint(r), "too many stack arguments"):
792+ // Expected: the guard fired.
793+ case strings.Contains(fmt.Sprint(r), "only supported on"):
794+ t.Skipf("struct returns are unsupported on this platform: %v", r)
795+ default:
796+ t.Fatalf("unexpected panic: %v", r)
797+ }
798+ }()
799+
800+ // A non-zero cfn passes the nil check; the panic fires during the preflight
801+ // argument count, before the function is ever called.
802+ purego.RegisterFunc(fptr.Interface(), uintptr(1))
803+}
804+
805+func buildSharedLib(tb testing.TB, compilerEnv, libFile string, sources ...string) error {
806+ tb.Helper()
807+ // When PUREGO_TEST_PREBUILT_LIBDIR is set, the shared library has been
808+ // cross-compiled ahead of time and placed in that directory under the
809+ // base name of libFile. This allows running the tests on a target that
810+ // has no C toolchain, such as an Android emulator.
811+ if dir := os.Getenv("PUREGO_TEST_PREBUILT_LIBDIR"); dir != "" {
812+ data, err := os.ReadFile(filepath.Join(dir, filepath.Base(libFile)))
813+ if err != nil {
814+ return fmt.Errorf("prebuilt lib: %w", err)
815+ }
816+ if err := os.WriteFile(libFile, data, 0o755); err != nil {
817+ return fmt.Errorf("prebuilt lib: %w", err)
818+ }
819+ return nil
820+ }
821+
822+ // Compiling the library needs a C toolchain targeting GOARCH. CI has none
823+ // for Windows on 386 or arm64, so skip those (the prebuilt path above
824+ // avoids the toolchain).
825+ if runtime.GOOS == "windows" {
826+ switch runtime.GOARCH {
827+ case "386":
828+ tb.Skip("need a 386 C toolchain to run this test") // TODO: find a 386 C toolchain for test
829+ case "arm64":
830+ tb.Skip("need an arm64 C toolchain to run this test")
831+ }
832+ }
833+
467834 out, err := exec.Command("go", "env", compilerEnv).Output()
468835 if err != nil {
469836 return fmt.Errorf("go env %s error: %w", compilerEnv, err)
M go.mod +1 -1
@@ -1,3 +1,3 @@
11 module github.com/bnema/purego
22
3-go 1.20
3+go 1.25.0
M go_runtime.go +6 -0
@@ -11,3 +11,9 @@ import (
1111
1212 //go:linkname runtime_cgocall runtime.cgocall
1313 func runtime_cgocall(fn uintptr, arg unsafe.Pointer) int32 // from runtime/sys_libc.go
14+
15+// from runtime/runtime2.go, exported via go:linkname for usage with cgo assembly
16+// pulled in as struct{} for proper linking, see https://github.com/golang/go/issues/72032
17+//
18+//go:linkname runtime_goarmsoftfp runtime.goarmsoftfp
19+var runtime_goarmsoftfp struct{}
D hostlayout_go123.go +0 -12
@@ -1,12 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3-
4-//go:build go1.23
5-
6-package purego
7-
8-import "structs"
9-
10-// hostLayout is structs.HostLayout on Go 1.23+, ensuring the compiler
11-// uses host (C-compatible) memory layout for structs that embed it.
12-type hostLayout = structs.HostLayout
D hostlayout_nogo123.go +0 -11
@@ -1,11 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3-
4-//go:build !go1.23
5-
6-package purego
7-
8-// hostLayout is a zero-size placeholder for Go versions before 1.23
9-// that lack the structs package. On Go 1.23+ this is replaced by
10-// structs.HostLayout which tells the compiler to use host memory layout.
11-type hostLayout struct{}
M internal/cgo/syscall_cgo_unix.go +16 -22
@@ -16,40 +16,34 @@ package cgo
1616 #include <errno.h>
1717 #include <assert.h>
1818
19-typedef struct syscall15Args {
19+typedef struct syscallArgs {
2020 uintptr_t fn;
2121 uintptr_t a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15;
22+ uintptr_t a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32;
2223 uintptr_t f1, f2, f3, f4, f5, f6, f7, f8;
23- uintptr_t err;
24-} syscall15Args;
24+ uintptr_t arm64_r8;
25+} syscallArgs;
2526
26-void syscall15(struct syscall15Args *args) {
27+void syscall15(struct syscallArgs *args) {
2728 assert((args->f1|args->f2|args->f3|args->f4|args->f5|args->f6|args->f7|args->f8) == 0);
2829 uintptr_t (*func_name)(uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, uintptr_t a5, uintptr_t a6,
2930 uintptr_t a7, uintptr_t a8, uintptr_t a9, uintptr_t a10, uintptr_t a11, uintptr_t a12,
30- uintptr_t a13, uintptr_t a14, uintptr_t a15);
31+ uintptr_t a13, uintptr_t a14, uintptr_t a15, uintptr_t a16, uintptr_t a17, uintptr_t a18,
32+ uintptr_t a19, uintptr_t a20, uintptr_t a21, uintptr_t a22, uintptr_t a23, uintptr_t a24,
33+ uintptr_t a25, uintptr_t a26, uintptr_t a27, uintptr_t a28, uintptr_t a29, uintptr_t a30,
34+ uintptr_t a31, uintptr_t a32);
3135 *(void**)(&func_name) = (void*)(args->fn);
32- uintptr_t r1 = func_name(args->a1,args->a2,args->a3,args->a4,args->a5,args->a6,args->a7,args->a8,args->a9,
33- args->a10,args->a11,args->a12,args->a13,args->a14,args->a15);
36+ uintptr_t r1 = func_name(args->a1,args->a2,args->a3,args->a4,args->a5,args->a6,args->a7,args->a8,args->a9,
37+ args->a10,args->a11,args->a12,args->a13,args->a14,args->a15,args->a16,args->a17,args->a18,
38+ args->a19,args->a20,args->a21,args->a22,args->a23,args->a24,args->a25,args->a26,args->a27,
39+ args->a28,args->a29,args->a30,args->a31,args->a32);
3440 args->a1 = r1;
35- args->err = errno;
41+ args->a3 = errno;
3642 }
3743
3844 */
3945 import "C"
4046 import "unsafe"
4147
42-// assign purego.syscall15XABI0 to the C version of this function.
43-var Syscall15XABI0 = unsafe.Pointer(C.syscall15)
44-
45-//go:nosplit
46-func Syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) {
47- args := C.syscall15Args{
48- C.uintptr_t(fn), C.uintptr_t(a1), C.uintptr_t(a2), C.uintptr_t(a3),
49- C.uintptr_t(a4), C.uintptr_t(a5), C.uintptr_t(a6),
50- C.uintptr_t(a7), C.uintptr_t(a8), C.uintptr_t(a9), C.uintptr_t(a10), C.uintptr_t(a11), C.uintptr_t(a12),
51- C.uintptr_t(a13), C.uintptr_t(a14), C.uintptr_t(a15), 0, 0, 0, 0, 0, 0, 0, 0, 0,
52- }
53- C.syscall15(&args)
54- return uintptr(args.a1), 0, uintptr(args.err)
55-}
48+// assign purego.syscallXABI0 to the C version of this function.
49+var SyscallXABI0 = unsafe.Pointer(C.syscall15)
M internal/fakecgo/asm_arm.s +9 -3
@@ -21,8 +21,11 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
2121 // starting at 4(R13).
2222 MOVW.W R14, -4(R13)
2323
24+ // Skip floating point registers if runtime.goarmsoftfp!=0.
25+ MOVB runtime·goarmsoftfp(SB), R11
26+ CMP $0, R11
27+ BNE skipfpsave
2428 // Save VFP callee-saved registers D8-D15 (same as S16-S31).
25- // Note: We always save these since we target hard-float ABI.
2629 MOVD F8, (13*4+8*1)(R13)
2730 MOVD F9, (13*4+8*2)(R13)
2831 MOVD F10, (13*4+8*3)(R13)
@@ -32,11 +35,13 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
3235 MOVD F14, (13*4+8*7)(R13)
3336 MOVD F15, (13*4+8*8)(R13)
3437
35- BL runtime·load_g(SB)
36-
38+skipfpsave:
3739 // We set up the arguments to cgocallback when saving registers above.
3840 BL runtime·cgocallback(SB)
3941
42+ MOVB runtime·goarmsoftfp(SB), R11
43+ CMP $0, R11
44+ BNE skipfprest
4045 MOVD (13*4+8*1)(R13), F8
4146 MOVD (13*4+8*2)(R13), F9
4247 MOVD (13*4+8*3)(R13), F10
@@ -46,6 +51,7 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
4651 MOVD (13*4+8*7)(R13), F14
4752 MOVD (13*4+8*8)(R13), F15
4853
54+skipfprest:
4955 MOVW.P 4(R13), R14
5056 MOVM.IAW (R13), [R0, R1, R3, R4, R5, R6, R7, R8, R9, g, R11, R12]
5157 ADD $(8*9), R13
M internal/fakecgo/asm_arm64.s +0 -1
@@ -25,7 +25,6 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
2525 STP (R29, R30), (8*22)(RSP)
2626
2727 // Initialize Go ABI environment
28- BL runtime·load_g(SB)
2928 BL runtime·cgocallback(SB)
3029
3130 RESTORE_R19_TO_R28(8*4)
M internal/fakecgo/asm_loong64.s +0 -2
@@ -27,8 +27,6 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
2727 MOVV R1, (22*8)(R3)
2828
2929 // Initialize Go ABI environment
30- JAL runtime·load_g(SB)
31-
3230 JAL runtime·cgocallback(SB)
3331
3432 RESTORE_R22_TO_R31((4*8))
M internal/fakecgo/asm_ppc64le.s +0 -3
@@ -50,9 +50,6 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
5050 // Initialize R0 to 0 as expected by Go
5151 MOVD $0, R0
5252
53- // Load the current g.
54- BL runtime·load_g(SB)
55-
5653 // Set up arguments for cgocallback
5754 MOVD R3, FIXED_FRAME+0(R1) // fn unsafe.Pointer
5855 MOVD R4, FIXED_FRAME+8(R1) // a unsafe.Pointer
M internal/fakecgo/asm_riscv64.s +0 -1
@@ -25,7 +25,6 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
2525 SAVE_FPR((8*17))
2626
2727 // Initialize Go ABI environment
28- CALL runtime·load_g(SB)
2928 CALL runtime·cgocallback(SB)
3029
3130 RESTORE_GPR((8*4))
M internal/fakecgo/asm_s390x.s +0 -3
@@ -27,9 +27,6 @@ TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
2727 FMOVD F14, 80(R15)
2828 FMOVD F15, 88(R15)
2929
30- // Initialize Go ABI environment.
31- BL runtime·load_g(SB)
32-
3330 MOVD R2, 8(R15) // fn unsafe.Pointer
3431 MOVD R3, 16(R15) // a unsafe.Pointer
3532
M internal/fakecgo/gen.go +1 -1
@@ -268,7 +268,7 @@ func run() error {
268268 pthreadSO = "/usr/lib/libSystem.B.dylib"
269269 case "freebsd":
270270 libcSO = "libc.so.7"
271- pthreadSO = "libpthread.so"
271+ pthreadSO = "libthr.so.3"
272272 case "linux":
273273 libcSO = "libc.so.6"
274274 pthreadSO = "libpthread.so.0"
M internal/fakecgo/go_darwin.go +1 -3
@@ -55,9 +55,7 @@ func threadentry(v unsafe.Pointer) unsafe.Pointer {
5555 //#endif
5656 setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g)))
5757
58- // faking funcs in go is a bit a... involved - but the following works :)
59- fn := uintptr(unsafe.Pointer(&ts.fn))
60- (*(*func())(unsafe.Pointer(&fn)))()
58+ callThreadEntryFn(ts.fn)
6159
6260 return nil
6361 }
M internal/fakecgo/go_freebsd.go +2 -4
@@ -2,7 +2,7 @@
22 // Use of this source code is governed by a BSD-style
33 // license that can be found in the LICENSE file.
44
5-//go:build !cgo
5+//go:build !cgo && (amd64 || arm64)
66
77 package fakecgo
88
@@ -49,9 +49,7 @@ func threadentry(v unsafe.Pointer) unsafe.Pointer {
4949
5050 setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g)))
5151
52- // faking funcs in go is a bit a... involved - but the following works :)
53- fn := uintptr(unsafe.Pointer(&ts.fn))
54- (*(*func())(unsafe.Pointer(&fn)))()
52+ callThreadEntryFn(ts.fn)
5553
5654 return nil
5755 }
M internal/fakecgo/go_libinit.go +1 -1
@@ -52,7 +52,7 @@ func _cgo_try_pthread_create(thread *pthread_t, attr *pthread_attr_t, pfn unsafe
5252 tries := ts.Nsec
5353 var err int
5454
55- for tries = 0; tries < 20; tries++ {
55+ for tries = range 20 {
5656 // inlined this call because it ran out of stack when inlining was disabled
5757 err = int(call5(pthread_createABI0, uintptr(unsafe.Pointer(thread)), uintptr(unsafe.Pointer(attr)), uintptr(pfn), uintptr(unsafe.Pointer(arg)), 0))
5858 if err == 0 {
M internal/fakecgo/go_linux.go +1 -3
@@ -49,9 +49,7 @@ func threadentry(v unsafe.Pointer) unsafe.Pointer {
4949
5050 setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g)))
5151
52- // faking funcs in go is a bit a... involved - but the following works :)
53- fn := uintptr(unsafe.Pointer(&ts.fn))
54- (*(*func())(unsafe.Pointer(&fn)))()
52+ callThreadEntryFn(ts.fn)
5553
5654 return nil
5755 }
M internal/fakecgo/go_netbsd.go +1 -3
@@ -60,9 +60,7 @@ func threadentry(v unsafe.Pointer) unsafe.Pointer {
6060
6161 setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g)))
6262
63- // faking funcs in go is a bit a... involved - but the following works :)
64- fn := uintptr(unsafe.Pointer(&ts.fn))
65- (*(*func())(unsafe.Pointer(&fn)))()
63+ callThreadEntryFn(ts.fn)
6664
6765 return nil
6866 }
M internal/fakecgo/libcgo.go +4 -0
@@ -5,6 +5,8 @@
55
66 package fakecgo
77
8+import "structs"
9+
810 type (
911 size_t uintptr
1012 // Sources:
@@ -28,11 +30,13 @@ const (
2830 )
2931
3032 type G struct {
33+ _ structs.HostLayout
3134 stacklo uintptr
3235 stackhi uintptr
3336 }
3437
3538 type ThreadStart struct {
39+ _ structs.HostLayout
3640 g *G
3741 tls *uintptr
3842 fn uintptr
M internal/fakecgo/libcgo_darwin.go +4 -0
@@ -5,12 +5,16 @@
55
66 package fakecgo
77
8+import "structs"
9+
810 type (
911 pthread_mutex_t struct {
12+ _ structs.HostLayout
1013 sig int64
1114 opaque [56]byte
1215 }
1316 pthread_cond_t struct {
17+ _ structs.HostLayout
1418 sig int64
1519 opaque [40]byte
1620 }
M internal/fakecgo/libcgo_netbsd.go +3 -0
@@ -5,6 +5,8 @@
55
66 package fakecgo
77
8+import "structs"
9+
810 type (
911 pthread_cond_t uintptr
1012 pthread_mutex_t uintptr
@@ -17,6 +19,7 @@ var (
1719
1820 // Source: https://github.com/NetBSD/src/blob/613e27c65223fd2283b6ed679da1197e12f50e27/sys/sys/signal.h#L225
1921 type stack_t struct {
22+ _ structs.HostLayout
2023 ss_sp uintptr
2124 ss_size uintptr
2225 ss_flags int32
M internal/fakecgo/linux.go +7 -2
@@ -6,10 +6,15 @@
66
77 package fakecgo
88
9-import "unsafe"
9+import (
10+ "structs"
11+ "unsafe"
12+)
1013
11-// argset matches runtime/cgocall.go:argset.
14+// argset matches runtime/cgocall.go:argset, which in turn matches
15+// runtime/cgo/linux_syscall.c:argset_t.
1216 type argset struct {
17+ _ structs.HostLayout
1318 args *uintptr
1419 retval uintptr
1520 }
A internal/fakecgo/threadentry_amd64.go +15 -0
@@ -0,0 +1,15 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build !cgo && amd64 && (darwin || freebsd || linux || netbsd)
5+
6+package fakecgo
7+
8+// callThreadEntryFn calls fn (runtime.mstart) while saving and restoring the
9+// frame pointer and other callee-saved registers around the call. mstart
10+// returns with BP clobbered, so without this shim the caller's frame-pointer
11+// epilogue would fault. Implemented in trampolines_amd64.s.
12+//
13+//go:nosplit
14+//go:norace
15+func callThreadEntryFn(fn uintptr)
A internal/fakecgo/threadentry_noasm.go +24 -0
@@ -0,0 +1,24 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build !cgo && !amd64 && (darwin || freebsd || linux || netbsd)
5+
6+package fakecgo
7+
8+import "unsafe"
9+
10+// callThreadEntryFn calls fn (runtime.mstart). On architectures without the
11+// amd64 frame-pointer epilogue issue this is a plain indirect call. It must be
12+// nosplit and norace like the threadentry callers so it neither inserts a
13+// morestack preamble nor runs race instrumentation during the fragile
14+// thread-bootstrap window.
15+//
16+//go:nosplit
17+//go:norace
18+func callThreadEntryFn(fn uintptr) {
19+ // fn is the code pointer. Build a func value whose first word is fn by
20+ // pointing the closure at &fn, then call it (same trick fakecgo has always
21+ // used to call a raw PC from Go).
22+ fnPtr := uintptr(unsafe.Pointer(&fn))
23+ (*(*func())(unsafe.Pointer(&fnPtr)))()
24+}
A internal/fakecgo/threadentry_test.go +50 -0
@@ -0,0 +1,50 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+// 386/arm are excluded: the standalone fakecgo test binary resolves pthread
5+// from libpthread.so.0, which fails the dynamic symbol lookup on 32-bit linux.
6+//go:build !cgo && (linux || darwin || freebsd || netbsd) && !386 && !arm
7+
8+package fakecgo
9+
10+import (
11+ "runtime"
12+ "sync"
13+ "testing"
14+)
15+
16+// TestThreadEntryReturn exercises the fakecgo threadentry -> runtime.mstart
17+// return path, i.e. that an M created by fakecgo can exit cleanly.
18+//
19+// When iscgo is forced true by fakecgo, the runtime creates every new OS
20+// thread through _cgo_thread_start -> threadentry_trampoline -> threadentry,
21+// which calls runtime.mstart. For a fakecgo thread the M's stack is
22+// system-allocated, so when the M exits, mexit(osStack=true) returns from
23+// mstart back into threadentry. This return path has been the source of
24+// crashes (a frame-pointer LEAVE fault on amd64, a bad indirect call and race
25+// instrumentation on other platforms), so it is worth exercising everywhere.
26+//
27+// Locking an OS thread and then letting the goroutine exit (without calling
28+// UnlockOSThread) forces the locked fakecgo M to exit, driving mstart to return
29+// into threadentry. A busy loop that never lets the goroutine exit does NOT
30+// reproduce the crash, because the M never exits and mstart never returns.
31+func TestThreadEntryReturn(t *testing.T) {
32+ // The M exits on the first goroutine return, so a modest amount of churn is
33+ // enough to catch a broken teardown while staying quick under emulation.
34+ const rounds = 10
35+ const workers = 16
36+ for range rounds {
37+ var wg sync.WaitGroup
38+ wg.Add(workers)
39+ for range workers {
40+ go func() {
41+ defer wg.Done()
42+ runtime.LockOSThread()
43+ // Intentionally do NOT call UnlockOSThread: returning here exits
44+ // the goroutine while its OS thread is locked, forcing the M to
45+ // exit and mstart to return into fakecgo.threadentry.
46+ }()
47+ }
48+ wg.Wait()
49+ }
50+}
M internal/fakecgo/trampolines_amd64.s +17 -0
@@ -83,6 +83,23 @@ TEXT threadentry_trampoline(SB), NOSPLIT, $0
8383 POP_REGS_HOST_TO_ABI0()
8484 RET
8585
86+// func callThreadEntryFn(fn uintptr)
87+// Calls fn (runtime.mstart) with the frame pointer and other callee-saved
88+// registers saved on the stack across the call. mstart returns with BP
89+// clobbered (BP==0); saving/restoring BP here (as real cgo's crosscall does)
90+// keeps the caller's frame-pointer LEAVE epilogue valid on return.
91+TEXT ·callThreadEntryFn(SB), NOSPLIT, $0-8
92+ MOVQ fn+0(FP), R11
93+ PUSH_REGS_HOST_TO_ABI0()
94+
95+ // X15 is designated by Go as a fixed zero register.
96+ PXOR X15, X15
97+
98+ CALL R11
99+
100+ POP_REGS_HOST_TO_ABI0()
101+ RET
102+
86103 TEXT ·call5(SB), NOSPLIT, $0-56
87104 MOVQ fn+0(FP), R11
88105 MOVQ a1+8(FP), DI
M internal/fakecgo/trampolines_arm.s +9 -0
@@ -70,6 +70,10 @@ TEXT threadentry_trampoline(SB), NOSPLIT, $104-0
7070 MOVW g, 32(R13) // R10
7171 MOVW R11, 36(R13)
7272
73+ // Skip floating point registers if runtime.goarmsoftfp!=0.
74+ MOVB runtime·goarmsoftfp(SB), R11
75+ CMP $0, R11
76+ BNE skipfpsave
7377 MOVD F8, 40(R13)
7478 MOVD F9, 48(R13)
7579 MOVD F10, 56(R13)
@@ -79,10 +83,14 @@ TEXT threadentry_trampoline(SB), NOSPLIT, $104-0
7983 MOVD F14, 88(R13)
8084 MOVD F15, 96(R13)
8185
86+skipfpsave:
8287 MOVW ·threadentry_call(SB), R12
8388 MOVW (R12), R12
8489 CALL (R12)
8590
91+ MOVB runtime·goarmsoftfp(SB), R11
92+ CMP $0, R11
93+ BNE skipfprest
8694 MOVD 40(R13), F8
8795 MOVD 48(R13), F9
8896 MOVD 56(R13), F10
@@ -92,6 +100,7 @@ TEXT threadentry_trampoline(SB), NOSPLIT, $104-0
92100 MOVD 88(R13), F14
93101 MOVD 96(R13), F15
94102
103+skipfprest:
95104 MOVW 8(R13), R4
96105 MOVW 12(R13), R5
97106 MOVW 16(R13), R6
A internal/fakecgo/trampolines_linux_s390x.s +51 -0
@@ -0,0 +1,51 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build go1.27 && !cgo
5+
6+#include "textflag.h"
7+
8+TEXT _cgo_purego_setegid_trampoline(SB), NOSPLIT|NOFRAME, $0
9+ MOVD ·x_cgo_purego_setegid_call(SB), R1
10+ MOVD (R1), R1
11+ BR R1
12+
13+TEXT _cgo_purego_seteuid_trampoline(SB), NOSPLIT|NOFRAME, $0
14+ MOVD ·x_cgo_purego_seteuid_call(SB), R1
15+ MOVD (R1), R1
16+ BR R1
17+
18+TEXT _cgo_purego_setgid_trampoline(SB), NOSPLIT|NOFRAME, $0
19+ MOVD ·x_cgo_purego_setgid_call(SB), R1
20+ MOVD (R1), R1
21+ BR R1
22+
23+TEXT _cgo_purego_setregid_trampoline(SB), NOSPLIT|NOFRAME, $0
24+ MOVD ·x_cgo_purego_setregid_call(SB), R1
25+ MOVD (R1), R1
26+ BR R1
27+
28+TEXT _cgo_purego_setresgid_trampoline(SB), NOSPLIT|NOFRAME, $0
29+ MOVD ·x_cgo_purego_setresgid_call(SB), R1
30+ MOVD (R1), R1
31+ BR R1
32+
33+TEXT _cgo_purego_setresuid_trampoline(SB), NOSPLIT|NOFRAME, $0
34+ MOVD ·x_cgo_purego_setresuid_call(SB), R1
35+ MOVD (R1), R1
36+ BR R1
37+
38+TEXT _cgo_purego_setreuid_trampoline(SB), NOSPLIT|NOFRAME, $0
39+ MOVD ·x_cgo_purego_setreuid_call(SB), R1
40+ MOVD (R1), R1
41+ BR R1
42+
43+TEXT _cgo_purego_setuid_trampoline(SB), NOSPLIT|NOFRAME, $0
44+ MOVD ·x_cgo_purego_setuid_call(SB), R1
45+ MOVD (R1), R1
46+ BR R1
47+
48+TEXT _cgo_purego_setgroups_trampoline(SB), NOSPLIT|NOFRAME, $0
49+ MOVD ·x_cgo_purego_setgroups_call(SB), R1
50+ MOVD (R1), R1
51+ BR R1
M internal/fakecgo/trampolines_ppc64le.s +6 -2
@@ -47,15 +47,19 @@ TEXT x_cgo_bindm_trampoline(SB), NOSPLIT, $0-0
4747
4848 // func setg_trampoline(setg uintptr, g uintptr)
4949 TEXT ·setg_trampoline(SB), NOSPLIT, $16-16
50- MOVD R31, 8(R1) // save R31 (load_g clobbers it)
50+ MOVD R31, 8(R1) // save R31
5151
5252 MOVD setg+0(FP), R12
5353 MOVD newg+8(FP), R3
5454
55+ MOVD R3, 16(R1) // save newg before call
56+
5557 MOVD R12, CTR
5658 CALL CTR
5759
58- CALL runtime·load_g(SB)
60+ // Assign g directly instead of calling runtime·load_g
61+ // setg_gcc has already stored newg into TLS; put it in the g register too.
62+ MOVD 16(R1), g
5963
6064 MOVD 8(R1), R31
6165 XOR R0, R0, R0
A internal/fakecgo/trampolines_s390x.s +158 -0
@@ -0,0 +1,158 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build go1.27 && !cgo && linux
5+
6+#include "textflag.h"
7+#include "go_asm.h"
8+
9+// these trampolines map the gcc ABI to Go ABI and then calls into the Go equivalent functions.
10+// Note that C arguments are passed in R2-R6, which matches Go ABIInternal for the first five arguments.
11+// R1 is used as a temporary register.
12+
13+TEXT x_cgo_init_trampoline(SB), NOSPLIT|NOFRAME, $0-0
14+ MOVD R15, R1
15+ SUB $192, R15
16+ MOVD R1, 0(R15) // backchain
17+ MOVD R14, 160(R15) // save R14
18+ MOVD R9, 168(R15) // save R9 (Go runtime needs this preserved)
19+
20+ MOVD ·x_cgo_init_call(SB), R1
21+ MOVD (R1), R1
22+ BL R1
23+
24+ MOVD 168(R15), R9
25+ MOVD 160(R15), R14
26+ ADD $192, R15
27+ BR R14
28+
29+TEXT x_cgo_thread_start_trampoline(SB), NOSPLIT|NOFRAME, $0-0
30+ MOVD R15, R1
31+ SUB $176, R15
32+ MOVD R1, 0(R15) // backchain
33+ MOVD R14, 152(R15) // save R14
34+
35+ MOVD ·x_cgo_thread_start_call(SB), R1
36+ MOVD (R1), R1
37+ BL R1
38+
39+ MOVD 152(R15), R14
40+ ADD $176, R15
41+ BR R14
42+
43+TEXT x_cgo_setenv_trampoline(SB), NOSPLIT|NOFRAME, $0-0
44+ MOVD R15, R1
45+ SUB $176, R15
46+ MOVD R1, 0(R15) // backchain
47+ MOVD R14, 152(R15) // save R14
48+
49+ MOVD ·x_cgo_setenv_call(SB), R1
50+ MOVD (R1), R1
51+ BL R1
52+
53+ MOVD 152(R15), R14
54+ ADD $176, R15
55+ BR R14
56+
57+TEXT x_cgo_unsetenv_trampoline(SB), NOSPLIT|NOFRAME, $0-0
58+ MOVD R15, R1
59+ SUB $176, R15
60+ MOVD R1, 0(R15) // backchain
61+ MOVD R14, 152(R15) // save R14
62+
63+ MOVD ·x_cgo_unsetenv_call(SB), R1
64+ MOVD (R1), R1
65+ BL R1
66+
67+ MOVD 152(R15), R14
68+ ADD $176, R15
69+ BR R14
70+
71+// These just tail-call into Go functions
72+TEXT x_cgo_notify_runtime_init_done_trampoline(SB), NOSPLIT|NOFRAME, $0-0
73+ BR ·x_cgo_notify_runtime_init_done(SB)
74+
75+TEXT x_cgo_bindm_trampoline(SB), NOSPLIT|NOFRAME, $0-0
76+ BR ·x_cgo_bindm(SB)
77+
78+// setg_trampoline(setg uintptr, g uintptr) - called from Go
79+TEXT ·setg_trampoline(SB), NOSPLIT|NOFRAME, $0-16
80+ MOVD 8(R15), R1 // setg function pointer
81+ MOVD 16(R15), R2 // g pointer -> C arg
82+
83+ MOVD R14, R0
84+ MOVD R15, R3
85+ SUB $160, R15
86+ MOVD R3, 0(R15)
87+ MOVD R0, 112(R15)
88+ MOVD R2, 120(R15) // save newg before call
89+
90+ BL R1 // call setg_gcc
91+
92+ // Assign g directly instead of calling runtime·load_g
93+ // setg_gcc has already stored newg into TLS; put it in the g register too.
94+ MOVD 120(R15), g
95+
96+ MOVD 112(R15), R14
97+ ADD $160, R15
98+ BR R14
99+
100+TEXT threadentry_trampoline(SB), NOSPLIT|NOFRAME, $0-0
101+ STMG R6, R15, 48(R15) // C save area
102+ MOVD R15, R1
103+ SUB $176, R15
104+ MOVD R1, 0(R15) // backchain
105+
106+ MOVD ·threadentry_call(SB), R1
107+ MOVD (R1), R1
108+ BL R1
109+
110+ ADD $176, R15
111+ LMG 48(R15), R6, R15
112+ RET
113+
114+TEXT ·call5(SB), NOSPLIT|NOFRAME, $0-56
115+ // Load Go args before modifying R15
116+ MOVD 8(R15), R1 // fn
117+ MOVD 16(R15), R7 // a1
118+ MOVD 24(R15), R8 // a2
119+ MOVD 32(R15), R9 // a3
120+ MOVD 40(R15), R10 // a4
121+ MOVD 48(R15), R11 // a5
122+
123+ // Save state
124+ MOVD R15, R0 // original R15
125+ MOVD R12, R6 // Go's R12
126+ ADD $-128, R15
127+
128+ // Set up C frame with backchain
129+ MOVD R0, 0(R15) // backchain -> original R15
130+ MOVD R0, R3 // R3 = original R15 (can't use R0 as base!)
131+ MOVD 0(R3), R7 // save 0(original R15)
132+ MOVD $0, 0(R3) // terminate backchain
133+
134+ // Save context
135+ MOVD R14, 8(R15)
136+ MOVD R6, 16(R15) // R12
137+ MOVD R0, 24(R15) // original R15
138+ MOVD R7, 32(R15) // saved backchain
139+
140+ // Set up C args (reload a1 since R7 was clobbered)
141+ MOVD 16(R3), R2 // a1 (use R3 as base, not R0!)
142+ MOVD R8, R3 // a2
143+ MOVD R9, R4 // a3
144+ MOVD R10, R5 // a4
145+ MOVD R11, R6 // a5
146+
147+ BL R1
148+
149+ // Store result and restore
150+ MOVD 24(R15), R3 // original R15
151+ MOVD R2, 56(R3) // return value
152+ MOVD 32(R15), R7
153+ MOVD R7, 0(R3) // restore backchain
154+
155+ MOVD 8(R15), R14
156+ MOVD 16(R15), R12
157+ MOVD 24(R15), R15
158+ BR R14
M internal/fakecgo/zsymbols_freebsd.go +10 -10
@@ -16,16 +16,16 @@ import "unsafe"
1616 //go:cgo_import_dynamic purego_sigfillset sigfillset "libc.so.7"
1717 //go:cgo_import_dynamic purego_nanosleep nanosleep "libc.so.7"
1818 //go:cgo_import_dynamic purego_abort abort "libc.so.7"
19-//go:cgo_import_dynamic purego_pthread_attr_init pthread_attr_init "libpthread.so"
20-//go:cgo_import_dynamic purego_pthread_create pthread_create "libpthread.so"
21-//go:cgo_import_dynamic purego_pthread_detach pthread_detach "libpthread.so"
22-//go:cgo_import_dynamic purego_pthread_sigmask pthread_sigmask "libpthread.so"
23-//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "libpthread.so"
24-//go:cgo_import_dynamic purego_pthread_mutex_unlock pthread_mutex_unlock "libpthread.so"
25-//go:cgo_import_dynamic purego_pthread_cond_broadcast pthread_cond_broadcast "libpthread.so"
26-//go:cgo_import_dynamic purego_pthread_setspecific pthread_setspecific "libpthread.so"
27-//go:cgo_import_dynamic purego_pthread_attr_getstacksize pthread_attr_getstacksize "libpthread.so"
28-//go:cgo_import_dynamic purego_pthread_attr_destroy pthread_attr_destroy "libpthread.so"
19+//go:cgo_import_dynamic purego_pthread_attr_init pthread_attr_init "libthr.so.3"
20+//go:cgo_import_dynamic purego_pthread_create pthread_create "libthr.so.3"
21+//go:cgo_import_dynamic purego_pthread_detach pthread_detach "libthr.so.3"
22+//go:cgo_import_dynamic purego_pthread_sigmask pthread_sigmask "libthr.so.3"
23+//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "libthr.so.3"
24+//go:cgo_import_dynamic purego_pthread_mutex_unlock pthread_mutex_unlock "libthr.so.3"
25+//go:cgo_import_dynamic purego_pthread_cond_broadcast pthread_cond_broadcast "libthr.so.3"
26+//go:cgo_import_dynamic purego_pthread_setspecific pthread_setspecific "libthr.so.3"
27+//go:cgo_import_dynamic purego_pthread_attr_getstacksize pthread_attr_getstacksize "libthr.so.3"
28+//go:cgo_import_dynamic purego_pthread_attr_destroy pthread_attr_destroy "libthr.so.3"
2929
3030 //go:nosplit
3131 //go:norace
D internal/xreflect/reflect_go124.go +0 -15
@@ -1,15 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3-
4-//go:build !go1.25
5-
6-package xreflect
7-
8-import "reflect"
9-
10-// TODO: remove this and use Go 1.25's reflect.TypeAssert when minimum go.mod version is 1.25
11-
12-func TypeAssert[T any](v reflect.Value) (T, bool) {
13- v2, ok := v.Interface().(T)
14- return v2, ok
15-}
D internal/xreflect/reflect_go125.go +0 -12
@@ -1,12 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3-
4-//go:build go1.25
5-
6-package xreflect
7-
8-import "reflect"
9-
10-func TypeAssert[T any](v reflect.Value) (T, bool) {
11- return reflect.TypeAssert[T](v)
12-}
A objc/encoding_darwin_test.go +186 -0
@@ -0,0 +1,186 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package objc
5+
6+import (
7+ "fmt"
8+ "os"
9+ "os/exec"
10+ "path/filepath"
11+ "reflect"
12+ "strings"
13+ "structs"
14+ "testing"
15+ "unsafe"
16+)
17+
18+type encodeTypeTestStruct struct {
19+ _ structs.HostLayout
20+ A int32
21+ B float64
22+}
23+
24+var encodeTypeTests = []struct {
25+ typ reflect.Type
26+ cType string
27+ want string
28+}{
29+ {reflect.TypeFor[bool](), "_Bool", "B"},
30+ {reflect.TypeFor[int8](), "signed char", "c"},
31+ {reflect.TypeFor[uint8](), "unsigned char", "C"},
32+ {reflect.TypeFor[int16](), "short", "s"},
33+ {reflect.TypeFor[uint16](), "unsigned short", "S"},
34+ {reflect.TypeFor[int32](), "int", "i"},
35+ {reflect.TypeFor[uint32](), "unsigned int", "I"},
36+ {reflect.TypeFor[int64](), "long long", "q"},
37+ {reflect.TypeFor[uint64](), "unsigned long long", "Q"},
38+ {reflect.TypeFor[int](), "long", "q"},
39+ {reflect.TypeFor[uint](), "unsigned long", "Q"},
40+ {reflect.TypeFor[float32](), "float", "f"},
41+ {reflect.TypeFor[float64](), "double", "d"},
42+ {reflect.TypeFor[string](), "char *", "*"},
43+ {reflect.TypeFor[unsafe.Pointer](), "void *", "^v"},
44+ {reflect.TypeFor[*int32](), "int *", "^i"},
45+ {reflect.TypeFor[**int32](), "int **", "^^i"},
46+ {reflect.TypeFor[ID](), "id", "@"},
47+ {reflect.TypeFor[Class](), "Class", "#"},
48+ {reflect.TypeFor[SEL](), "SEL", ":"},
49+ {reflect.TypeFor[encodeTypeTestStruct](), "struct encodeTypeTestStruct", "{encodeTypeTestStruct=id}"},
50+}
51+
52+func TestEncodeType(t *testing.T) {
53+ for _, tt := range encodeTypeTests {
54+ got, err := encodeType(tt.typ, false)
55+ if err != nil {
56+ t.Errorf("encodeType(%v) returned error: %v", tt.typ, err)
57+ continue
58+ }
59+ if got != tt.want {
60+ t.Errorf("encodeType(%v) = %q; want %q (@encode(%s))", tt.typ, got, tt.want, tt.cType)
61+ }
62+ }
63+}
64+
65+// TestEncodeTypeMatchesClang uses the C compiler as an oracle: it compiles a
66+// program that prints @encode for each C type in encodeTypeTests and checks
67+// that the expected encodings are the ones the compiler actually produces.
68+func TestEncodeTypeMatchesClang(t *testing.T) {
69+ out, err := exec.Command("go", "env", "CC").Output()
70+ if err != nil {
71+ t.Fatalf("go env CC: %v", err)
72+ }
73+ compiler := strings.TrimSpace(string(out))
74+ if compiler == "" {
75+ t.Skip("no C compiler to use as an @encode oracle")
76+ }
77+ if _, err := exec.LookPath(compiler); err != nil {
78+ t.Skipf("no C compiler to use as an @encode oracle: %v", err)
79+ }
80+
81+ var src strings.Builder
82+ src.WriteString("#include <stdio.h>\n#include <objc/objc.h>\n")
83+ src.WriteString("struct encodeTypeTestStruct { int a; double b; };\n")
84+ src.WriteString("int main(void) {\n")
85+ for _, tt := range encodeTypeTests {
86+ fmt.Fprintf(&src, "\tprintf(\"%%s\\n\", @encode(%s));\n", tt.cType)
87+ }
88+ src.WriteString("\treturn 0;\n}\n")
89+
90+ dir := t.TempDir()
91+ srcFile := filepath.Join(dir, "encode.m")
92+ if err := os.WriteFile(srcFile, []byte(src.String()), 0o644); err != nil {
93+ t.Fatal(err)
94+ }
95+
96+ // Built for the host architecture rather than GOARCH, unlike the shared
97+ // libraries in the root package's tests: every darwin target Go supports
98+ // is LP64, so these encodings do not vary by architecture, and building
99+ // for the host avoids needing Rosetta to run an amd64 binary on arm64.
100+ exeFile := filepath.Join(dir, "encode")
101+ cmd := exec.Command(compiler, "-Wall", "-Werror", "-o", exeFile, srcFile)
102+ if out, err := cmd.CombinedOutput(); err != nil {
103+ t.Fatalf("compile oracle: %v\n%q\n%s", err, cmd, out)
104+ }
105+
106+ out, err = exec.Command(exeFile).Output()
107+ if err != nil {
108+ t.Fatalf("run oracle: %v", err)
109+ }
110+ lines := strings.Split(strings.TrimSuffix(string(out), "\n"), "\n")
111+ if len(lines) != len(encodeTypeTests) {
112+ t.Fatalf("oracle printed %d encodings; want %d", len(lines), len(encodeTypeTests))
113+ }
114+ for i, tt := range encodeTypeTests {
115+ if lines[i] != tt.want {
116+ t.Errorf("@encode(%s) = %q; want %q", tt.cType, lines[i], tt.want)
117+ }
118+ }
119+}
120+
121+// TestEncodeFunc checks how encodeFunc assembles a method signature: the
122+// return type, the implicit self and _cmd parameters, and the argument order.
123+// encodeType is checked per type by TestEncodeType, so the cases here vary the
124+// shape of the signature rather than the types in it. The first case is the
125+// signature from issue #493, which was recorded as v@:LLLLLLLLS - declaring
126+// 64-bit arguments as 32-bit made NSInvocation and other consumers of the type
127+// encoding truncate values above 2^32.
128+func TestEncodeFunc(t *testing.T) {
129+ tests := []struct {
130+ name string
131+ fn any
132+ want string
133+ }{
134+ {
135+ name: "void return, 64-bit args",
136+ fn: func(_ ID, _ SEL, a1, a2, a3, a4, a5, a6, a7, a8 uint64, a9 uint16) {},
137+ want: "v@:QQQQQQQQS",
138+ },
139+ {
140+ name: "value return, mixed integer kinds",
141+ fn: func(_ ID, _ SEL, a int, b int64, c uint, d uint64) int { return 0 },
142+ want: "q@:qqQQ",
143+ },
144+ {
145+ name: "no arguments",
146+ fn: func(_ ID, _ SEL) {},
147+ want: "v@:",
148+ },
149+ {
150+ name: "argument order is preserved",
151+ fn: func(_ ID, _ SEL, a int8, b float64, c ID) Class { return 0 },
152+ want: "#@:cd@",
153+ },
154+ }
155+ for _, tt := range tests {
156+ t.Run(tt.name, func(t *testing.T) {
157+ got, err := encodeFunc(tt.fn)
158+ if err != nil {
159+ t.Fatal(err)
160+ }
161+ if got != tt.want {
162+ t.Errorf("encodeFunc = %q; want %q", got, tt.want)
163+ }
164+ })
165+ }
166+}
167+
168+func TestEncodeFuncErrors(t *testing.T) {
169+ tests := []struct {
170+ name string
171+ fn any
172+ }{
173+ {"not a func", 0},
174+ {"too many return values", func(_ ID, _ SEL) (int, int) { return 0, 0 }},
175+ {"missing self and _cmd", func() {}},
176+ {"missing _cmd", func(_ ID) {}},
177+ {"unencodable argument", func(_ ID, _ SEL, c chan int) {}},
178+ }
179+ for _, tt := range tests {
180+ t.Run(tt.name, func(t *testing.T) {
181+ if got, err := encodeFunc(tt.fn); err == nil {
182+ t.Errorf("encodeFunc = %q; want an error", got)
183+ }
184+ })
185+ }
186+}
M objc/objc_block_darwin.go +10 -5
@@ -6,11 +6,11 @@ package objc
66 import (
77 "fmt"
88 "reflect"
9+ "structs"
910 "sync"
1011 "unsafe"
1112
1213 "github.com/bnema/purego"
13- "github.com/bnema/purego/internal/xreflect"
1414 )
1515
1616 const (
@@ -19,7 +19,10 @@ const (
1919 // when the reference count drops to zero, so the associated function is also unreferenced.
2020
2121 // blockBaseClass is the name of the class that block objects will be initialized with.
22- blockBaseClass = "__NSMallocBlock__"
22+ // A new block is a template that Block_copy always relocates to the
23+ // Objective-C heap, so the stack-block class is the accurate name here,
24+ // as the isa of the copied block becomes __NSMallocBlock__ anyway.
25+ blockBaseClass = "__NSStackBlock__"
2326 // blockFlags is the set of flags that block objects will be initialized with.
2427 blockFlags = blockHasCopyDispose | blockHasSignature
2528
@@ -35,6 +38,7 @@ const (
3538 //
3639 // The layout of this struct matches Block_literal_1 described in https://clang.llvm.org/docs/Block-ABI-Apple.html#high-level
3740 type blockDescriptor struct {
41+ _ structs.HostLayout
3842 _ uintptr
3943 size uintptr
4044 _ uintptr
@@ -48,6 +52,7 @@ type blockDescriptor struct {
4852 //
4953 // The layout of this struct matches __block_literal_1 described in https://clang.llvm.org/docs/Block-ABI-Apple.html#high-level
5054 type blockLayout struct {
55+ _ structs.HostLayout
5156 isa Class
5257 flags uint32
5358 _ uint32
@@ -125,7 +130,7 @@ func (*blockCache) encode(typ reflect.Type) *uint8 {
125130 encoding = returnType
126131 }
127132
128- if typ.NumIn() == 0 || typ.In(0) != reflect.TypeOf(Block(0)) {
133+ if typ.NumIn() == 0 || typ.In(0) != reflect.TypeFor[Block]() {
129134 panic(fmt.Sprintf("objc: A Block implementation must take a Block as its first argument; got %v", typ.String()))
130135 }
131136
@@ -168,7 +173,7 @@ func (b *blockCache) getLayout(typ reflect.Type) blockLayout {
168173 reflect.MakeFunc(
169174 typ,
170175 func(args []reflect.Value) (results []reflect.Value) {
171- block, ok := xreflect.TypeAssert[Block](args[0])
176+ block, ok := reflect.TypeAssert[Block](args[0])
172177 if !ok {
173178 panic(fmt.Sprintf("objc: block argument is not a block but %s", args[0].Type().String()))
174179 }
@@ -263,7 +268,7 @@ func InvokeBlock[T any](block Block, args ...any) (result T, err error) {
263268 callResult := fn.Call(reflectedArgs)
264269
265270 var ok bool
266- result, ok = xreflect.TypeAssert[T](callResult[0])
271+ result, ok = reflect.TypeAssert[T](callResult[0])
267272 if !ok {
268273 return result, fmt.Errorf("objc: the returned value type %s was not %T", callResult[0].Type().String(), result)
269274 }
M objc/objc_block_darwin_test.go +2 -0
@@ -5,6 +5,7 @@ package objc_test
55
66 import (
77 "fmt"
8+ "structs"
89 "testing"
910
1011 "github.com/bnema/purego"
@@ -39,6 +40,7 @@ func ExampleNewBlock() {
3940
4041 func ExampleInvokeBlock() {
4142 type vector struct {
43+ _ structs.HostLayout
4244 X, Y, Z float64
4345 }
4446
M objc/objc_runtime_darwin.go +58 -43
@@ -12,15 +12,18 @@ import (
1212 "reflect"
1313 "regexp"
1414 "runtime"
15+ "slices"
1516 stdstrings "strings"
17+ "structs"
1618 "unicode"
1719 "unsafe"
1820
1921 "github.com/bnema/purego"
2022 "github.com/bnema/purego/internal/strings"
21- "github.com/bnema/purego/internal/xreflect"
2223 )
2324
25+var hostLayoutType = reflect.TypeFor[structs.HostLayout]()
26+
2427 // TODO: support try/catch?
2528 // https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
2629 var (
@@ -168,32 +171,44 @@ func Send[T any](id ID, sel SEL, args ...any) T {
168171 return fn(id, sel, args...)
169172 }
170173
171-// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword
172-// as the receiver of a message. It specifies the class definition of the particular superclass that should
173-// be messaged.
174+// objc_super supplies the receiver and defining class to objc_msgSendSuper2,
175+// which begins method lookup in that class's superclass.
174176 type objc_super struct {
177+ _ structs.HostLayout
175178 receiver ID
176179 superClass Class
177180 }
178181
179-// SendSuper is a convenience method for sending message to object's super. This function takes a SEL
180-// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
181-// of RegisterName.
182+// SendSuper sends a message starting lookup in the receiver's runtime superclass.
183+//
184+// Deprecated: Use [ID.SendSuper2] with the class defining the executing method.
182185 func (id ID) SendSuper(sel SEL, args ...any) ID {
186+ return id.SendSuper2(id.Class(), sel, args...)
187+}
188+
189+// SendSuper2 sends a message starting lookup in the superclass of class.
190+// For a super call, class must be the class defining the executing method.
191+func (id ID) SendSuper2(class Class, sel SEL, args ...any) ID {
183192 super := &objc_super{
184193 receiver: id,
185- superClass: id.Class(),
194+ superClass: class,
186195 }
187196 return objc_msgSendSuper2(super, sel, args...)
188197 }
189198
190-// SendSuper is a convenience method for sending message to object's super that can return any type.
191-// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
192-// It is best to cache the result of RegisterName.
199+// SendSuper sends a message starting lookup in the receiver's runtime superclass.
200+//
201+// Deprecated: Use [SendSuper2] with the class defining the executing method.
193202 func SendSuper[T any](id ID, sel SEL, args ...any) T {
203+ return SendSuper2[T](id, id.Class(), sel, args...)
204+}
205+
206+// SendSuper2 sends a message starting lookup in the superclass of class and returns T.
207+// For a super call, class must be the class defining the executing method.
208+func SendSuper2[T any](id ID, class Class, sel SEL, args ...any) T {
194209 super := &objc_super{
195210 receiver: id,
196- superClass: id.Class(),
211+ superClass: class,
197212 }
198213 var fn func(objcSuper *objc_super, sel SEL, args ...any) T
199214 var zero T
@@ -265,7 +280,7 @@ type FieldDef struct {
265280 }
266281
267282 // ivarRegex checks to make sure the Ivar is correctly formatted
268-var ivarRegex = regexp.MustCompile("[a-z_][a-zA-Z0-9_]*")
283+var ivarRegex = regexp.MustCompile(`^[a-z_][a-zA-Z0-9_]*$`)
269284
270285 // RegisterClass takes the name of the class to create, the superclass, a list of protocols this class
271286 // implements, a list of fields this class has and a list of methods. It returns the created class or an error
@@ -322,7 +337,7 @@ func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars [
322337 case ReadWrite:
323338 ty := reflect.FuncOf(
324339 []reflect.Type{
325- reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type,
340+ reflect.TypeFor[ID](), reflect.TypeFor[SEL](), ivar.Type,
326341 },
327342 nil, false,
328343 )
@@ -343,7 +358,7 @@ func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars [
343358 // })(unsafe.Pointer(args[0].Interface().(ID)))).v = 123
344359 //
345360 // However, since the type of the variable is unknown reflection is used to actually assign the value
346- id, ok := xreflect.TypeAssert[ID](args[0])
361+ id, ok := reflect.TypeAssert[ID](args[0])
347362 if !ok {
348363 panic(fmt.Sprintf("objc: id argument is not a ID but %s", args[0].Type().String()))
349364 }
@@ -358,7 +373,7 @@ func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars [
358373 case ReadOnly:
359374 ty := reflect.FuncOf(
360375 []reflect.Type{
361- reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)),
376+ reflect.TypeFor[ID](), reflect.TypeFor[SEL](),
362377 },
363378 []reflect.Type{ivar.Type}, false,
364379 )
@@ -371,7 +386,7 @@ func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars [
371386 if len(args) != 2 {
372387 panic(fmt.Sprintf("objc: incorrect number of args. expected 2 got %d", len(args)))
373388 }
374- id, ok := xreflect.TypeAssert[ID](args[0])
389+ id, ok := reflect.TypeAssert[ID](args[0])
375390 if !ok {
376391 panic(fmt.Sprintf("objc: id argument is not a ID but %s", args[0].Type().String()))
377392 }
@@ -402,8 +417,8 @@ const (
402417 encUShort = "S"
403418 encInt = "i"
404419 encUInt = "I"
405- encLong = "l"
406- encULong = "L"
420+ encLongLong = "q"
421+ encULongLong = "Q"
407422 encFloat = "f"
408423 encDouble = "d"
409424 encBool = "B"
@@ -419,11 +434,11 @@ const (
419434 // Source: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
420435 func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
421436 switch typ {
422- case reflect.TypeOf(Class(0)):
437+ case reflect.TypeFor[Class]():
423438 return encClass, nil
424- case reflect.TypeOf(ID(0)), reflect.TypeOf(Block(0)):
439+ case reflect.TypeFor[ID](), reflect.TypeFor[Block]():
425440 return encId, nil
426- case reflect.TypeOf(SEL(0)):
441+ case reflect.TypeFor[SEL]():
427442 return encSelector, nil
428443 }
429444
@@ -432,7 +447,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
432447 case reflect.Bool:
433448 return encBool, nil
434449 case reflect.Int:
435- return encLong, nil
450+ return encLongLong, nil
436451 case reflect.Int8:
437452 return encChar, nil
438453 case reflect.Int16:
@@ -440,9 +455,9 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
440455 case reflect.Int32:
441456 return encInt, nil
442457 case reflect.Int64:
443- return encULong, nil
458+ return encLongLong, nil
444459 case reflect.Uint:
445- return encULong, nil
460+ return encULongLong, nil
446461 case reflect.Uint8:
447462 return encUChar, nil
448463 case reflect.Uint16:
@@ -450,7 +465,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
450465 case reflect.Uint32:
451466 return encUInt, nil
452467 case reflect.Uint64:
453- return encULong, nil
468+ return encULongLong, nil
454469 case reflect.Uintptr:
455470 return encPtr, nil
456471 case reflect.Float32:
@@ -468,8 +483,15 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
468483 encoding.WriteString(encStructBegin)
469484 encoding.WriteString(typ.Name())
470485 encoding.WriteString("=")
471- for i := 0; i < typ.NumField(); i++ {
486+ for i := range typ.NumField() {
472487 f := typ.Field(i)
488+ if f.Type.ConvertibleTo(hostLayoutType) {
489+ // The structs.HostLayout marker is a signal to the Go compiler
490+ // with no counterpart in C, so it is not a member as far as
491+ // @encode is concerned. Other zero-sized fields are: clang
492+ // encodes a zero-length array member as [0c].
493+ continue
494+ }
473495 tmp, err := encodeType(f.Type, false)
474496 if err != nil {
475497 return "", err
@@ -571,7 +593,9 @@ func (i Ivar) Name() string {
571593 }
572594
573595 // MethodDescription holds the name and type definition of a method.
596+// It matches the Objective-C runtime's struct objc_method_description.
574597 type MethodDescription struct {
598+ _ structs.HostLayout
575599 name, types uintptr
576600 }
577601
@@ -586,7 +610,9 @@ func (m MethodDescription) Types() string {
586610 }
587611
588612 // PropertyAttribute contains the null-terminated Name and Value pair of a Properties internal description.
613+// It matches the Objective-C runtime's objc_property_attribute_t.
589614 type PropertyAttribute struct {
615+ _ structs.HostLayout
590616 Name, Value *byte
591617 }
592618
@@ -626,7 +652,7 @@ func (p *Protocol) Register() {
626652 func (p *Protocol) CopyMethodDescriptionList(isRequiredMethod, isInstanceMethod bool) []MethodDescription {
627653 count := uint32(0)
628654 desc := protocol_copyMethodDescriptionList(p, isRequiredMethod, isInstanceMethod, &count)
629- methods := clone(unsafe.Slice(desc, count))
655+ methods := slices.Clone(unsafe.Slice(desc, count))
630656 free(unsafe.Pointer(desc))
631657 return methods
632658 }
@@ -635,7 +661,7 @@ func (p *Protocol) CopyMethodDescriptionList(isRequiredMethod, isInstanceMethod
635661 func (p *Protocol) CopyProtocolList() []*Protocol {
636662 count := uint32(0)
637663 desc := protocol_copyProtocolList(p, &count)
638- protocols := clone(unsafe.Slice(desc, count))
664+ protocols := slices.Clone(unsafe.Slice(desc, count))
639665 free(unsafe.Pointer(desc))
640666 return protocols
641667 }
@@ -644,7 +670,7 @@ func (p *Protocol) CopyProtocolList() []*Protocol {
644670 func (p *Protocol) CopyPropertyList(isRequiredProperty, isInstanceProperty bool) []Property {
645671 count := uint32(0)
646672 desc := protocol_copyPropertyList2(p, &count, isRequiredProperty, isInstanceProperty)
647- protocols := clone(unsafe.Slice(desc, count))
673+ protocols := slices.Clone(unsafe.Slice(desc, count))
648674 free(unsafe.Pointer(desc))
649675 return protocols
650676 }
@@ -691,21 +717,10 @@ func NewIMP(fn any) IMP {
691717 switch {
692718 case ty.NumIn() < 2:
693719 fallthrough
694- case ty.In(0) != reflect.TypeOf(ID(0)):
720+ case ty.In(0) != reflect.TypeFor[ID]():
695721 fallthrough
696- case ty.In(1) != reflect.TypeOf(SEL(0)):
722+ case ty.In(1) != reflect.TypeFor[SEL]():
697723 panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
698724 }
699725 return IMP(purego.NewCallback(fn))
700726 }
701-
702-// TODO: remove and use slices.Clone when minimum version for purego is 1.21
703-func clone[S ~[]E, E any](s S) S {
704- // Preserve nilness in case it matters.
705- if s == nil {
706- return nil
707- }
708- // Avoid s[:0:0] as it leads to unwanted liveness when cloning a
709- // zero-length slice of a large array; see https://go.dev/issue/68488.
710- return append(S{}, s...)
711-}
M objc/objc_runtime_darwin_test.go +56 -24
@@ -7,6 +7,7 @@ import (
77 "fmt"
88 "log"
99 "reflect"
10+ "structs"
1011 "testing"
1112
1213 "github.com/bnema/purego"
@@ -38,6 +39,7 @@ func ExampleRegisterClass_helloworld() {
3839 }
3940
4041 func ExampleRegisterClass() {
42+ var class objc.Class
4143 var (
4244 sel_new = objc.RegisterName("new")
4345 sel_init = objc.RegisterName("init")
@@ -45,11 +47,12 @@ func ExampleRegisterClass() {
4547 sel_bar = objc.RegisterName("bar")
4648
4749 BarInit = func(id objc.ID, cmd objc.SEL) objc.ID {
48- return id.SendSuper(cmd)
50+ return id.SendSuper2(class, cmd)
4951 }
5052 )
5153
52- class, err := objc.RegisterClass(
54+ var err error
55+ class, err = objc.RegisterClass(
5356 "BarObject",
5457 objc.GetClass("NSObject"),
5558 []*objc.Protocol{
@@ -58,12 +61,12 @@ func ExampleRegisterClass() {
5861 []objc.FieldDef{
5962 {
6063 Name: "bar",
61- Type: reflect.TypeOf(int(0)),
64+ Type: reflect.TypeFor[int](),
6265 Attribute: objc.ReadWrite,
6366 },
6467 {
6568 Name: "foo",
66- Type: reflect.TypeOf(false),
69+ Type: reflect.TypeFor[bool](),
6770 Attribute: objc.ReadWrite,
6871 },
6972 },
@@ -94,17 +97,25 @@ func ExampleIMP() {
9497 // Output: IMP: 105 567 9 2 3 -5 4 8 9
9598 }
9699
97-func ExampleID_SendSuper() {
98- super, err := objc.RegisterClass(
99- "SuperObject",
100+// This example shows three-level inheritance with Base <- Child1 <- Child2.
101+// SendSuper2 takes Child1, the class defining the method.
102+func ExampleID_SendSuper2() {
103+ _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW)
104+ if err != nil {
105+ panic(err)
106+ }
107+
108+ sel := objc.RegisterName("run")
109+ base, err := objc.RegisterClass(
110+ "SuperExampleBase",
100111 objc.GetClass("NSObject"),
101112 nil,
102113 nil,
103114 []objc.MethodDef{
104115 {
105- Cmd: objc.RegisterName("doSomething"),
106- Fn: func(self objc.ID, _cmd objc.SEL) {
107- fmt.Println("In Super!")
116+ Cmd: sel,
117+ Fn: func(self objc.ID, cmd objc.SEL) {
118+ fmt.Println("Base")
108119 },
109120 },
110121 },
@@ -113,17 +124,19 @@ func ExampleID_SendSuper() {
113124 panic(err)
114125 }
115126
116- child, err := objc.RegisterClass(
117- "ChildObject",
118- super,
127+ var child1 objc.Class
128+ child1, err = objc.RegisterClass(
129+ "SuperExampleChild1",
130+ base,
119131 nil,
120132 nil,
121133 []objc.MethodDef{
122134 {
123- Cmd: objc.RegisterName("doSomething"),
124- Fn: func(self objc.ID, _cmd objc.SEL) {
125- fmt.Println("In Child")
126- self.SendSuper(_cmd)
135+ Cmd: sel,
136+ Fn: func(self objc.ID, cmd objc.SEL) {
137+ // Start lookup in the superclass of Child1.
138+ fmt.Print("SendSuper2: ")
139+ self.SendSuper2(child1, cmd)
127140 },
128141 },
129142 },
@@ -132,9 +145,27 @@ func ExampleID_SendSuper() {
132145 panic(err)
133146 }
134147
135- objc.ID(child).Send(objc.RegisterName("new")).Send(objc.RegisterName("doSomething"))
136- // Output: In Child
137- // In Super!
148+ // Child2 inherits Child1's implementation of run.
149+ child2, err := objc.RegisterClass("SuperExampleChild2", child1, nil, nil, nil)
150+ if err != nil {
151+ panic(err)
152+ }
153+
154+ for _, class := range []objc.Class{child1, child2} {
155+ object := objc.ID(class).Send(objc.RegisterName("new"))
156+ defer object.Send(objc.RegisterName("release"))
157+ if class == child1 {
158+ fmt.Println("Child1 instance")
159+ } else {
160+ fmt.Println("Child2 instance")
161+ }
162+ object.Send(sel)
163+ }
164+ // Output:
165+ // Child1 instance
166+ // SendSuper2: Base
167+ // Child2 instance
168+ // SendSuper2: Base
138169 }
139170
140171 func TestSend(t *testing.T) {
@@ -155,6 +186,7 @@ func TestSend(t *testing.T) {
155186
156187 func ExampleSend() {
157188 type NSRange struct {
189+ _ structs.HostLayout
158190 Location, Range uint
159191 }
160192 class_NSString := objc.GetClass("NSString")
@@ -164,11 +196,11 @@ func ExampleSend() {
164196 subString := objc.ID(class_NSString).Send(sel_stringWithUTF8String, "lo, Wor\x00")
165197
166198 r := objc.Send[NSRange](fullString, objc.RegisterName("rangeOfString:"), subString)
167- fmt.Println(r)
168- // Output: {3 7}
199+ fmt.Println(r.Location, r.Range)
200+ // Output: 3 7
169201 }
170202
171-func ExampleSendSuper() {
203+func ExampleSendSuper2() {
172204 super, err := objc.RegisterClass(
173205 "SuperObject2",
174206 objc.GetClass("NSObject"),
@@ -205,7 +237,7 @@ func ExampleSendSuper() {
205237 panic(err)
206238 }
207239
208- res := objc.SendSuper[int](objc.ID(child).Send(objc.RegisterName("new")), objc.RegisterName("doSomething"))
240+ res := objc.SendSuper2[int](objc.ID(child).Send(objc.RegisterName("new")), child, objc.RegisterName("doSomething"))
209241 fmt.Println(res)
210242 // Output: 16
211243 }
A objc/super_darwin_test.go +130 -0
@@ -0,0 +1,130 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package objc_test
5+
6+import (
7+ "fmt"
8+ "os/exec"
9+ "path/filepath"
10+ "runtime"
11+ "structs"
12+ "sync/atomic"
13+ "testing"
14+
15+ "github.com/bnema/purego"
16+ "github.com/bnema/purego/objc"
17+)
18+
19+var superTestClassID atomic.Uint64
20+
21+// TestSendSuperDispatch checks argument and receiver forwarding, return values,
22+// and generic super calls from direct and inherited methods.
23+func TestSendSuperDispatch(t *testing.T) {
24+ if _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil {
25+ t.Fatal(err)
26+ }
27+ prefix := fmt.Sprintf("PuregoSuperTest%d", superTestClassID.Add(1))
28+ sel := objc.RegisterName("probe:")
29+ var baseCalls, child1Calls int
30+ var receiver objc.ID
31+ base, err := objc.RegisterClass(prefix+"Base", objc.GetClass("NSObject"), nil, nil, []objc.MethodDef{
32+ {
33+ Cmd: sel,
34+ Fn: func(self objc.ID, cmd objc.SEL, value int) int {
35+ baseCalls++
36+ receiver = self
37+ return value + 3
38+ },
39+ },
40+ })
41+ if err != nil {
42+ t.Fatal(err)
43+ }
44+ var child1 objc.Class
45+ child1, err = objc.RegisterClass(prefix+"Child1", base, nil, nil, []objc.MethodDef{
46+ {
47+ Cmd: sel,
48+ Fn: func(self objc.ID, cmd objc.SEL, value int) int {
49+ child1Calls++
50+ // Bound recursion so an incorrect class anchor fails without overflowing the stack.
51+ if child1Calls > 1 {
52+ return -1
53+ }
54+
55+ return objc.SendSuper2[int](self, child1, cmd, value) + 5
56+ },
57+ },
58+ })
59+ if err != nil {
60+ t.Fatal(err)
61+ }
62+ child2, err := objc.RegisterClass(prefix+"Child2", child1, nil, nil, nil)
63+ if err != nil {
64+ t.Fatal(err)
65+ }
66+ for _, class := range []objc.Class{child1, child2} {
67+ baseCalls, child1Calls, receiver = 0, 0, 0
68+ object := objc.ID(class).Send(objc.RegisterName("new"))
69+ defer object.Send(objc.RegisterName("release"))
70+ if got := objc.Send[int](object, sel, 34); got != 42 {
71+ t.Errorf("class %v: result = %d, want 42", class, got)
72+ }
73+ if baseCalls != 1 || child1Calls != 1 {
74+ t.Errorf("class %v: base calls = %d, child1 calls = %d; want 1 each", class, baseCalls, child1Calls)
75+ }
76+ if receiver != object {
77+ t.Errorf("base receiver = %v, want %v", receiver, object)
78+ }
79+ // Check forwarding through the ID method too.
80+ receiver = 0
81+ if got := object.SendSuper2(child1, sel, 34); got != 37 {
82+ t.Errorf("class %v: SendSuper2 = %d, want 37", class, got)
83+ }
84+ if receiver != object {
85+ t.Errorf("SendSuper2 receiver = %v, want %v", receiver, object)
86+ }
87+ }
88+ // The legacy helpers retain their direct-instance behavior.
89+ object := objc.ID(child1).Send(objc.RegisterName("new"))
90+ defer object.Send(objc.RegisterName("release"))
91+ if got := object.SendSuper(sel, 34); got != 37 {
92+ t.Errorf("SendSuper = %d, want 37", got)
93+ }
94+ if got := objc.SendSuper[int](object, sel, 34); got != 37 {
95+ t.Errorf("SendSuper[int] = %d, want 37", got)
96+ }
97+}
98+
99+func TestSendSuperStruct(t *testing.T) {
100+ arch := "arm64"
101+ if runtime.GOARCH == "amd64" {
102+ arch = "x86_64"
103+ }
104+ library := filepath.Join(t.TempDir(), "super.dylib")
105+ cmd := exec.Command("clang", "-dynamiclib", "-arch", arch, "-framework", "Foundation", "-o", library, "testdata/super.m")
106+ if out, err := cmd.CombinedOutput(); err != nil {
107+ t.Fatalf("compile super fixture: %v\n%s", err, out)
108+ }
109+ // Objective-C retains the registered classes and their implementations.
110+ if _, err := purego.Dlopen(library, purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil {
111+ t.Fatal(err)
112+ }
113+ type result struct {
114+ _ structs.HostLayout
115+ A, B, C int64
116+ }
117+ want := result{
118+ A: 12,
119+ B: 34,
120+ C: 56,
121+ }
122+ child1 := objc.GetClass("PuregoSuperStructChild1")
123+ for _, class := range []objc.Class{child1, objc.GetClass("PuregoSuperStructChild2")} {
124+ object := objc.ID(class).Send(objc.RegisterName("new"))
125+ defer object.Send(objc.RegisterName("release"))
126+ if got := objc.SendSuper2[result](object, child1, objc.RegisterName("result")); got != want {
127+ t.Errorf("class %v: result = %+v, want %+v", class, got, want)
128+ }
129+ }
130+}
A objc/testdata/super.m +33 -0
@@ -0,0 +1,33 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#import <Foundation/Foundation.h>
5+
6+struct Result {
7+ int64_t a, b, c;
8+};
9+
10+@interface PuregoSuperStructBase : NSObject
11+- (struct Result)result;
12+@end
13+
14+@implementation PuregoSuperStructBase
15+- (struct Result)result {
16+ return (struct Result){.a = 12, .b = 34, .c = 56};
17+}
18+@end
19+
20+@interface PuregoSuperStructChild1 : PuregoSuperStructBase
21+@end
22+
23+@implementation PuregoSuperStructChild1
24+- (struct Result)result {
25+ return (struct Result){.a = 78, .b = 90, .c = 12};
26+}
27+@end
28+
29+@interface PuregoSuperStructChild2 : PuregoSuperStructChild1
30+@end
31+
32+@implementation PuregoSuperStructChild2
33+@end
D struct_386.go +0 -52
@@ -1,52 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3-
4-package purego
5-
6-import (
7- "reflect"
8- "unsafe"
9-)
10-
11-func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
12- panic("purego: struct arguments are not supported")
13-}
14-
15-func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
16- panic("purego: struct returns are not supported")
17-}
18-
19-func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
20- panic("purego: placeRegisters not implemented on 386")
21-}
22-
23-// shouldBundleStackArgs always returns false on 386
24-// since C-style stack argument bundling is only needed on Darwin ARM64.
25-func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
26- return false
27-}
28-
29-// structFitsInRegisters is not used on 386.
30-func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
31- panic("purego: structFitsInRegisters should not be called on 386")
32-}
33-
34-// collectStackArgs is not used on 386.
35-func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
36- keepAlive []any, addInt, addFloat, addStack func(uintptr),
37- pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
38- panic("purego: collectStackArgs should not be called on 386")
39-}
40-
41-// bundleStackArgs is not used on 386.
42-func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
43- panic("purego: bundleStackArgs should not be called on 386")
44-}
45-
46-func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
47- panic("purego: struct callback arguments are not supported on 386")
48-}
49-
50-func setStruct(a *callbackArgs, ret reflect.Value) {
51- panic("purego: struct returns are not supported on 386")
52-}
D struct_abi_helpers.go +0 -38
@@ -1,38 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3-
4-package purego
5-
6-import (
7- "math"
8- "reflect"
9- "unsafe"
10-)
11-
12-func packFloat32Pair(low, high uintptr) uintptr {
13- return uintptr((uint64(high) << 32) | (uint64(low) & math.MaxUint32))
14-}
15-
16-func highFloat32Bits(word uintptr) uint32 {
17- return uint32(uint64(word) >> 32)
18-}
19-
20-func flushPackedWord(val *uint64, shift *byte, flushed *bool, isFloat bool, addFloat, addInt func(uintptr)) {
21- *flushed = true
22- if isFloat {
23- addFloat(uintptr(*val))
24- } else {
25- addInt(uintptr(*val))
26- }
27- *val = 0
28- *shift = 0
29-}
30-
31-func stableValuePointer(v reflect.Value) (unsafe.Pointer, any) {
32- if v.CanAddr() {
33- return v.Addr().UnsafePointer(), nil
34- }
35- tmp := reflect.New(v.Type())
36- tmp.Elem().Set(v)
37- return tmp.UnsafePointer(), tmp.Interface()
38-}
D struct_abi_helpers_test.go +0 -105
@@ -1,105 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3-
4-package purego
5-
6-import (
7- "math"
8- "reflect"
9- "testing"
10-)
11-
12-func TestPackFloat32PairMasksLowWord(t *testing.T) {
13- low := uintptr(0xDEADBEEF00000000 | uint64(math.Float32bits(1.5)))
14- high := uintptr(math.Float32bits(2.5))
15- want := uintptr(uint64(math.Float32bits(2.5))<<32 | uint64(math.Float32bits(1.5)))
16- if got := packFloat32Pair(low, high); got != want {
17- t.Fatalf("packFloat32Pair() = %#x, want %#x", got, want)
18- }
19-}
20-
21-func TestHighFloat32Bits(t *testing.T) {
22- word := uintptr(uint64(math.Float32bits(3.25)) << 32)
23- if got, want := highFloat32Bits(word), math.Float32bits(3.25); got != want {
24- t.Fatalf("highFloat32Bits() = %#x, want %#x", got, want)
25- }
26-}
27-
28-func TestFlushPackedWordResetsState(t *testing.T) {
29- var val uint64 = 0x11223344
30- shift := byte(32)
31- flushed := false
32- var gotFloat uintptr
33- flushPackedWord(&val, &shift, &flushed, true, func(v uintptr) { gotFloat = v }, func(uintptr) {
34- t.Fatal("unexpected integer flush")
35- })
36- if !flushed || val != 0 || shift != 0 {
37- t.Fatalf("flushPackedWord() left state dirty: flushed=%v val=%#x shift=%d", flushed, val, shift)
38- }
39- if gotFloat != 0x11223344 {
40- t.Fatalf("flushPackedWord() flushed %#x, want %#x", gotFloat, uintptr(0x11223344))
41- }
42-}
43-
44-func TestFlushPackedWordResetsStateForIntegerBranch(t *testing.T) {
45- var val uint64 = 0x55667788
46- shift := byte(16)
47- flushed := false
48- var gotInt uintptr
49- flushPackedWord(&val, &shift, &flushed, false, func(uintptr) {
50- t.Fatal("unexpected float flush")
51- }, func(v uintptr) { gotInt = v })
52- if !flushed || val != 0 || shift != 0 {
53- t.Fatalf("flushPackedWord() left state dirty: flushed=%v val=%#x shift=%d", flushed, val, shift)
54- }
55- if gotInt != 0x55667788 {
56- t.Fatalf("flushPackedWord() flushed %#x, want %#x", gotInt, uintptr(0x55667788))
57- }
58-}
59-
60-func TestStableValuePointerCopiesUnaddressableValue(t *testing.T) {
61- type sample struct{ X uint32 }
62- ptr, keepAlive := stableValuePointer(reflect.ValueOf(sample{X: 7}))
63- if keepAlive == nil {
64- t.Fatal("expected keepAlive copy for unaddressable value")
65- }
66- if got := *(*uint32)(ptr); got != 7 {
67- t.Fatalf("stableValuePointer() copied %#x, want %#x", got, uint32(7))
68- }
69-}
70-
71-func TestStableValuePointerUsesAddressableValue(t *testing.T) {
72- type sample struct{ X uint32 }
73- value := sample{X: 9}
74- ptr, keepAlive := stableValuePointer(reflect.ValueOf(&value).Elem())
75- if keepAlive != nil {
76- t.Fatal("expected nil keepAlive for addressable value")
77- }
78- if got := *(*uint32)(ptr); got != 9 {
79- t.Fatalf("stableValuePointer() pointed to %#x, want %#x", got, uint32(9))
80- }
81-}
82-
83-func TestIsAllSameFloatRejectsNestedMixedStruct(t *testing.T) {
84- type inner struct{ X, Y float32 }
85- type mixed struct {
86- Inner inner
87- Tail int32
88- }
89- allFloats, fields := isAllSameFloat(reflect.TypeOf(mixed{}))
90- if allFloats {
91- t.Fatalf("isAllSameFloat() = true for mixed nested struct with %d fields", fields)
92- }
93-}
94-
95-func TestIsAllSameFloatAcceptsNestedFloatStruct(t *testing.T) {
96- type inner struct{ X, Y float32 }
97- type outer struct {
98- Left inner
99- Right inner
100- }
101- allFloats, fields := isAllSameFloat(reflect.TypeOf(outer{}))
102- if !allFloats || fields != 4 {
103- t.Fatalf("isAllSameFloat() = (%v, %d), want (true, 4)", allFloats, fields)
104- }
105-}
M struct_amd64.go +105 -30
@@ -10,8 +10,44 @@ import (
1010 "unsafe"
1111 )
1212
13-func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
13+// structReturnInMemory reports whether a struct return value of the given size
14+// is returned through a caller-allocated hidden pointer passed as the first
15+// integer argument (true) rather than in registers (false).
16+func structReturnInMemory(outType reflect.Type) bool {
17+ size := outType.Size()
18+ if size == 0 {
19+ return false
20+ }
21+ if runtime.GOOS == "windows" {
22+ // The Win64 ABI returns aggregates of exactly 1, 2, 4, or 8 bytes in
23+ // RAX. Every other size is returned through a caller-allocated hidden
24+ // pointer that the callee also returns in RAX.
25+ switch size {
26+ case 1, 2, 4, 8:
27+ return false
28+ default:
29+ return true
30+ }
31+ }
32+ // The System V ABI returns aggregates of up to two eightbytes in registers.
33+ return size > maxRegAllocStructSize
34+}
35+
36+func getStruct(outType reflect.Type, syscall syscallArgs) (v reflect.Value) {
1437 outSize := outType.Size()
38+ if runtime.GOOS == "windows" {
39+ switch {
40+ case outSize == 0:
41+ return reflect.New(outType).Elem()
42+ case structReturnInMemory(outType):
43+ // Returned through the caller-allocated hidden pointer, which the
44+ // callee also returns in RAX.
45+ return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem()
46+ default:
47+ // 1, 2, 4, or 8 byte aggregates are returned in RAX.
48+ return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem()
49+ }
50+ }
1551 switch {
1652 case outSize == 0:
1753 return reflect.New(outType).Elem()
@@ -23,30 +59,36 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
2359 // up to 8 bytes is returned in RAX
2460 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem()
2561 case outSize <= 16:
26- var r1, r2 uintptr
27- var intRegs, floatRegs int
28- for i := 0; i < int((outSize+7)/8); i++ {
29- class := classifyEightbyte(outType, uintptr(i)*8, uintptr(i)*8+8)
30- var reg uintptr
31- if class == _SSE {
32- if floatRegs == 0 {
33- reg = syscall.f1
34- } else {
35- reg = syscall.f2
36- }
37- floatRegs++
38- } else {
39- if intRegs == 0 {
40- reg = syscall.a1
41- } else {
42- reg = syscall.a2
62+ r1, r2 := syscall.a1, syscall.a2
63+ if isAllFloats(outType) {
64+ r1 = syscall.f1
65+ r2 = syscall.f2
66+ } else {
67+ // check first 8 bytes if it's floats
68+ hasFirstFloat := false
69+ numFields := numABIFields(outType)
70+ f1 := abiField(outType, 0).Type
71+ if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && abiField(outType, 1).Type.Kind() == reflect.Float32 {
72+ r1 = syscall.f1
73+ hasFirstFloat = true
74+ }
75+
76+ // find index of the field that starts the second 8 bytes
77+ var i int
78+ for i = 0; i < numFields; i++ {
79+ if abiField(outType, i).Offset == 8 {
80+ break
4381 }
44- intRegs++
4582 }
46- if i == 0 {
47- r1 = reg
48- } else {
49- r2 = reg
83+
84+ // check last 8 bytes if they are floats
85+ f1 = abiField(outType, i).Type
86+ if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && i+1 == numFields {
87+ r2 = syscall.f1
88+ } else if hasFirstFloat {
89+ // if the first field was a float then that means the second integer field
90+ // comes from the first integer register
91+ r2 = syscall.a1
5092 }
5193 }
5294 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem()
@@ -58,8 +100,11 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
58100 }
59101
60102 func isAllFloats(ty reflect.Type) bool {
61- for i := 0; i < ty.NumField(); i++ {
103+ for i := range ty.NumField() {
62104 f := ty.Field(i)
105+ if !isABIField(f) {
106+ continue
107+ }
63108 switch f.Type.Kind() {
64109 case reflect.Float64, reflect.Float32:
65110 default:
@@ -82,6 +127,12 @@ const (
82127 )
83128
84129 func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
130+ if runtime.GOOS == "windows" {
131+ // Win64 still passes an empty struct as an argument slot, so this must
132+ // run before the zero-size early return used by the System V path.
133+ return addStructWindows(v, addInt, keepAlive)
134+ }
135+
85136 if v.Type().Size() == 0 {
86137 return keepAlive
87138 }
@@ -107,6 +158,27 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
107158 return keepAlive
108159 }
109160
161+// addStructWindows passes a struct argument under the Win64 ABI. Aggregates of
162+// exactly 1, 2, 4, or 8 bytes are passed by value in a single integer slot; all
163+// other sizes are passed as a pointer to a caller-allocated copy. Empty structs
164+// fall in the latter group: unlike the System V ABI, Win64 still consumes an
165+// argument slot for them.
166+func addStructWindows(v reflect.Value, addInt func(uintptr), keepAlive []any) []any {
167+ switch v.Type().Size() {
168+ case 1, 2, 4, 8:
169+ var val uintptr
170+ reflect.NewAt(v.Type(), unsafe.Pointer(&val)).Elem().Set(v)
171+ addInt(val)
172+ default:
173+ ptrStruct := reflect.New(v.Type())
174+ ptrStruct.Elem().Set(v)
175+ ptr := ptrStruct.Elem().Addr().UnsafePointer()
176+ keepAlive = append(keepAlive, ptr)
177+ addInt(uintptr(ptr))
178+ }
179+ return keepAlive
180+}
181+
110182 func postMerger(t reflect.Type) (passInMemory bool) {
111183 // (c) If the size of the aggregate exceeds two eightbytes and the first eight- byte isn’t SSE or any other
112184 // eightbyte isn’t SSEUP, the whole argument is passed in memory.
@@ -148,7 +220,10 @@ func tryPlaceRegister(v reflect.Value, addFloat func(uintptr), addInt func(uintp
148220 numFields = v.Type().Len()
149221 }
150222
151- for i := 0; i < numFields; i++ {
223+ for i := range numFields {
224+ if v.Kind() == reflect.Struct && !isABIField(v.Type().Field(i)) {
225+ continue
226+ }
152227 flushed = false
153228 var f reflect.Value
154229 if v.Kind() == reflect.Struct {
@@ -294,7 +369,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
294369 // - If not enough registers for all eightbytes: entire struct goes on the stack
295370 func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
296371 switch runtime.GOOS {
297- case "darwin", "freebsd", "linux", "netbsd":
372+ case "android", "darwin", "freebsd", "ios", "linux", "netbsd":
298373 default:
299374 panic("purego: getCallbackStruct is not supported on " + runtime.GOOS)
300375 }
@@ -315,7 +390,7 @@ func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int,
315390
316391 // Count how many integer and SSE registers this struct needs.
317392 var needInts, needFloats int
318- for i := 0; i < numEightbytes; i++ {
393+ for i := range numEightbytes {
319394 class := classifyEightbyte(inType, uintptr(i)*8, uintptr(i)*8+8)
320395 if class == _SSE {
321396 needFloats++
@@ -333,7 +408,7 @@ func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int,
333408
334409 // Read each eightbyte from its appropriate register class.
335410 var r1, r2 uintptr
336- for i := 0; i < numEightbytes; i++ {
411+ for i := range numEightbytes {
337412 class := classifyEightbyte(inType, uintptr(i)*8, uintptr(i)*8+8)
338413 if class == _SSE {
339414 if i == 0 {
@@ -415,7 +490,7 @@ func doClassifyEightbyte(t reflect.Type, base, start, end uintptr) int {
415490 switch t.Kind() {
416491 case reflect.Struct:
417492 class := _NO_CLASS
418- for i := 0; i < t.NumField(); i++ {
493+ for i := range t.NumField() {
419494 f := t.Field(i)
420495 class |= doClassifyEightbyte(f.Type, base+f.Offset, start, end)
421496 }
@@ -423,7 +498,7 @@ func doClassifyEightbyte(t reflect.Type, base, start, end uintptr) int {
423498 case reflect.Array:
424499 class := _NO_CLASS
425500 elemSize := t.Elem().Size()
426- for i := 0; i < t.Len(); i++ {
501+ for i := range t.Len() {
427502 class |= doClassifyEightbyte(t.Elem(), base+uintptr(i)*elemSize, start, end)
428503 }
429504 return class
D struct_amd64_regression_test.go +0 -21
@@ -1,21 +0,0 @@
1-//go:build amd64 && (darwin || linux)
2-
3-package purego
4-
5-import (
6- "reflect"
7- "testing"
8-)
9-
10-func TestGetStructHandlesSecondEightbyteWithoutOffset8Field(t *testing.T) {
11- type awkward struct {
12- A [3]uint32
13- B uint32
14- }
15- sys := syscall15Args{a1: 0x0000000200000001, a2: 0x0000000400000003}
16- got := getStruct(reflect.TypeOf(awkward{}), sys).Interface().(awkward)
17- want := awkward{A: [3]uint32{1, 2, 3}, B: 4}
18- if got != want {
19- t.Fatalf("got %+v, want %+v", got, want)
20- }
21-}
M struct_arm.go +11 -9
@@ -5,7 +5,6 @@ package purego
55
66 import (
77 "reflect"
8- "runtime"
98 "unsafe"
109 )
1110
@@ -17,10 +16,8 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
1716
1817 // TODO: ARM EABI: small structs are passed in registers or on stack
1918 // For simplicity, pass by pointer for now
20- ptr, live := stableValuePointer(v)
21- if live != nil {
22- keepAlive = append(keepAlive, live)
23- }
19+ ptr := v.Addr().UnsafePointer()
20+ keepAlive = append(keepAlive, ptr)
2421 if *numInts < 4 {
2522 addInt(uintptr(ptr))
2623 *numInts++
@@ -28,11 +25,17 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
2825 addStack(uintptr(ptr))
2926 *numStack++
3027 }
31- runtime.KeepAlive(live)
3228 return keepAlive
3329 }
3430
35-func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
31+// structReturnInMemory always reports false on arm: an indirect struct return
32+// is recovered from the pointer the callee leaves in a1 (see getStruct) rather
33+// than through a caller-allocated hidden first argument.
34+func structReturnInMemory(reflect.Type) bool {
35+ return false
36+}
37+
38+func getStruct(outType reflect.Type, syscall syscallArgs) (v reflect.Value) {
3639 outSize := outType.Size()
3740 if outSize == 0 {
3841 return reflect.New(outType).Elem()
@@ -56,14 +59,13 @@ func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr
5659 if size == 0 {
5760 return
5861 }
59- ptr, live := stableValuePointer(v)
62+ ptr := unsafe.Pointer(v.UnsafeAddr())
6063 if size <= 4 {
6164 addInt(*(*uintptr)(ptr))
6265 } else if size <= 8 {
6366 addInt(*(*uintptr)(ptr))
6467 addInt(*(*uintptr)(unsafe.Add(ptr, 4)))
6568 }
66- runtime.KeepAlive(live)
6769 }
6870
6971 // shouldBundleStackArgs always returns false on arm
M struct_arm64.go +65 -67
@@ -14,7 +14,14 @@ import (
1414 "github.com/bnema/purego/internal/strings"
1515 )
1616
17-func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
17+// structReturnInMemory always reports false on arm64: a struct returned in
18+// memory is passed through the dedicated indirect result register (R8), not as
19+// an ordinary integer argument, so it is handled separately.
20+func structReturnInMemory(reflect.Type) bool {
21+ return false
22+}
23+
24+func getStruct(outType reflect.Type, syscall syscallArgs) (v reflect.Value) {
1825 outSize := outType.Size()
1926 switch {
2027 case outSize == 0:
@@ -78,9 +85,9 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
7885 if hva, hfa, size := isHVA(v.Type()), isHFA(v.Type()), v.Type().Size(); hva || hfa || size <= 16 {
7986 // if this doesn't fit entirely in registers then
8087 // each element goes onto the stack
81- if hfa && *numFloats+v.NumField() > numOfFloatRegisters() {
88+ if hfa && *numFloats+numABIFields(v.Type()) > numOfFloatRegisters() {
8289 *numFloats = numOfFloatRegisters()
83- } else if hva && *numInts+v.NumField() > numOfIntegerRegisters() {
90+ } else if hva && *numInts+numABIFields(v.Type()) > numOfIntegerRegisters() {
8491 *numInts = numOfIntegerRegisters()
8592 }
8693
@@ -92,7 +99,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
9299 }
93100
94101 func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
95- if runtime.GOOS == "darwin" {
102+ if isDarwin {
96103 placeRegistersDarwin(v, addFloat, addInt)
97104 return
98105 }
@@ -112,7 +119,10 @@ func placeRegistersArm64(v reflect.Value, addFloat func(uintptr), addInt func(ui
112119 } else {
113120 numFields = v.Type().Len()
114121 }
115- for k := 0; k < numFields; k++ {
122+ for k := range numFields {
123+ if v.Kind() == reflect.Struct && !isABIField(v.Type().Field(k)) {
124+ continue
125+ }
116126 flushed = false
117127 var f reflect.Value
118128 if v.Kind() == reflect.Struct {
@@ -229,52 +239,39 @@ func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
229239 //
230240 // [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst
231241 func isHFA(t reflect.Type) bool {
242+ // round up struct size to nearest 8 see section B.4
232243 structSize := roundUpTo8(t.Size())
233- if structSize == 0 {
244+ numFields := numABIFields(t)
245+ if structSize == 0 || numFields > 4 {
234246 return false
235247 }
236- var kind reflect.Kind
237- var members int
238- var walk func(reflect.Type) bool
239- walk = func(t reflect.Type) bool {
240- for i := 0; i < t.NumField(); i++ {
241- ft := t.Field(i).Type
242- switch ft.Kind() {
243- case reflect.Float32, reflect.Float64:
244- if kind == 0 {
245- kind = ft.Kind()
246- } else if ft.Kind() != kind {
247- return false
248- }
249- members++
250- if members > 4 {
251- return false
252- }
253- case reflect.Struct:
254- if !walk(ft) {
255- return false
256- }
257- case reflect.Array:
258- elemKind := ft.Elem().Kind()
259- if elemKind != reflect.Float32 && elemKind != reflect.Float64 {
260- return false
261- }
262- if kind == 0 {
263- kind = elemKind
264- } else if elemKind != kind {
265- return false
266- }
267- members += ft.Len()
268- if members > 4 {
269- return false
270- }
271- default:
248+ first := abiField(t, 0)
249+ switch first.Type.Kind() {
250+ case reflect.Float32, reflect.Float64:
251+ firstKind := first.Type.Kind()
252+ for i := range numFields {
253+ if abiField(t, i).Type.Kind() != firstKind {
272254 return false
273255 }
274256 }
275257 return true
258+ case reflect.Array:
259+ switch first.Type.Elem().Kind() {
260+ case reflect.Float32, reflect.Float64:
261+ return true
262+ default:
263+ return false
264+ }
265+ case reflect.Struct:
266+ for range numABIFields(first.Type) {
267+ if !isHFA(first.Type) {
268+ return false
269+ }
270+ }
271+ return true
272+ default:
273+ return false
276274 }
277- return walk(t) && members > 0
278275 }
279276
280277 // isHVA reports a Homogeneous Aggregate with a Fundamental Data Type that is a Short-Vector type
@@ -290,12 +287,13 @@ func isHVA(t reflect.Type) bool {
290287 if structSize == 0 || (structSize != 8 && structSize != 16) {
291288 return false
292289 }
293- first := t.Field(0)
290+ numFields := numABIFields(t)
291+ first := abiField(t, 0)
294292 switch first.Type.Kind() {
295293 case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Int8, reflect.Int16, reflect.Int32:
296294 firstKind := first.Type.Kind()
297- for i := 0; i < t.NumField(); i++ {
298- if t.Field(i).Type.Kind() != firstKind {
295+ for i := range numFields {
296+ if abiField(t, i).Type.Kind() != firstKind {
299297 return false
300298 }
301299 }
@@ -315,7 +313,7 @@ func isHVA(t reflect.Type) bool {
315313 // copyStruct8ByteChunks copies struct memory in 8-byte chunks to the provided callback.
316314 // This is used for Darwin ARM64's byte-level packing of non-HFA/HVA structs.
317315 func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintptr)) {
318- if runtime.GOOS != "darwin" {
316+ if !isDarwin {
319317 panic("purego: should only be called on darwin")
320318 }
321319 for offset := uintptr(0); offset < size; offset += 8 {
@@ -325,7 +323,7 @@ func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintp
325323 chunk = *(*uintptr)(unsafe.Add(ptr, offset))
326324 } else {
327325 // Read byte-by-byte to avoid reading beyond allocation
328- for i := uintptr(0); i < remaining; i++ {
326+ for i := range remaining {
329327 b := *(*byte)(unsafe.Add(ptr, offset+i))
330328 chunk |= uintptr(b) << (i * 8)
331329 }
@@ -342,7 +340,7 @@ func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintp
342340 // For non-HFA/HVA structs, Darwin uses byte-level packing. We copy the struct memory in
343341 // 8-byte chunks, which works correctly for both register and stack placement.
344342 func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
345- if runtime.GOOS != "darwin" {
343+ if !isDarwin {
346344 panic("purego: placeRegistersDarwin should only be called on darwin")
347345 }
348346 // Check if this is an HFA/HVA
@@ -370,7 +368,7 @@ func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(u
370368 // shouldBundleStackArgs determines if we need to start C-style packing for
371369 // Darwin ARM64 stack arguments. This happens when registers are exhausted.
372370 func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
373- if runtime.GOOS != "darwin" {
371+ if !isDarwin {
374372 return false
375373 }
376374
@@ -395,12 +393,12 @@ func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
395393 }
396394
397395 if hfa {
398- need := v.NumField()
396+ need := numABIFields(v.Type())
399397 return numFloats+need > numOfFloatRegisters()
400398 }
401399
402400 if hva {
403- need := v.NumField()
401+ need := numABIFields(v.Type())
404402 return numInts+need > numOfIntegerRegisters()
405403 }
406404
@@ -412,7 +410,7 @@ func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
412410 // registers, used during stack argument bundling to decide if a struct
413411 // should go through normal register allocation or be bundled with stack args.
414412 func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
415- if runtime.GOOS != "darwin" {
413+ if !isDarwin {
416414 panic("purego: structFitsInRegisters should only be called on darwin")
417415 }
418416 hfa := isHFA(val.Type())
@@ -421,13 +419,13 @@ func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (b
421419
422420 if hfa {
423421 // HFA: check if elements fit in float registers
424- if tempNumFloats+val.NumField() <= numOfFloatRegisters() {
425- return true, tempNumInts, tempNumFloats + val.NumField()
422+ if numFields := numABIFields(val.Type()); tempNumFloats+numFields <= numOfFloatRegisters() {
423+ return true, tempNumInts, tempNumFloats + numFields
426424 }
427425 } else if hva {
428426 // HVA: check if elements fit in int registers
429- if tempNumInts+val.NumField() <= numOfIntegerRegisters() {
430- return true, tempNumInts + val.NumField(), tempNumFloats
427+ if numFields := numABIFields(val.Type()); tempNumInts+numFields <= numOfIntegerRegisters() {
428+ return true, tempNumInts + numFields, tempNumFloats
431429 }
432430 } else if size <= 16 {
433431 // Non-HFA/HVA small structs use int registers for byte-packing
@@ -445,7 +443,7 @@ func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (b
445443 func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
446444 keepAlive []any, addInt, addFloat, addStack func(uintptr),
447445 pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
448- if runtime.GOOS != "darwin" {
446+ if !isDarwin {
449447 panic("purego: collectStackArgs should only be called on darwin")
450448 }
451449
@@ -502,7 +500,7 @@ const (
502500 // bundleStackArgs bundles remaining arguments for Darwin ARM64 C-style stack packing.
503501 // It creates a packed struct with proper alignment and copies it to the stack in 8-byte chunks.
504502 func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
505- if runtime.GOOS != "darwin" {
503+ if !isDarwin {
506504 panic("purego: bundleStackArgs should only be called on darwin")
507505 }
508506 if len(stackArgs) == 0 {
@@ -528,7 +526,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
528526 paddingNeeded := uintptr(valAlign) - (currentOffset % uintptr(valAlign))
529527 fields = append(fields, reflect.StructField{
530528 Name: paddingFieldPrefix + strconv.Itoa(fieldIndex),
531- Type: reflect.ArrayOf(int(paddingNeeded), reflect.TypeOf(byte(0))),
529+ Type: reflect.ArrayOf(int(paddingNeeded), reflect.TypeFor[byte]()),
532530 })
533531 currentOffset += paddingNeeded
534532 fieldIndex++
@@ -548,7 +546,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
548546
549547 // Set values (skip padding fields)
550548 argIndex := 0
551- for j := 0; j < structInstance.NumField(); j++ {
549+ for j := range structInstance.NumField() {
552550 fieldName := structType.Field(j).Name
553551 if stdstrings.HasPrefix(fieldName, paddingFieldPrefix) {
554552 continue
@@ -576,7 +574,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
576574 // - Darwin ARM64: byte-level packing on the stack
577575 func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
578576 switch runtime.GOOS {
579- case "darwin", "freebsd", "linux", "netbsd":
577+ case "android", "darwin", "freebsd", "ios", "linux", "netbsd":
580578 default:
581579 panic("purego: getCallbackStruct is not supported on " + runtime.GOOS)
582580 }
@@ -623,8 +621,8 @@ func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int,
623621 }
624622
625623 // Pointer on stack (rare: all integer registers exhausted).
626- if runtime.GOOS == "darwin" {
627- ptrVal := callbackArgFromStack(frame, *stackSlot, stackByteOffset, reflect.TypeOf(uintptr(0)))
624+ if isDarwin {
625+ ptrVal := callbackArgFromStack(frame, *stackSlot, stackByteOffset, reflect.TypeFor[uintptr]())
628626 ptr := uintptr(ptrVal.Uint())
629627 return reflect.NewAt(inType, *(*unsafe.Pointer)(unsafe.Pointer(&ptr))).Elem()
630628 }
@@ -640,9 +638,9 @@ func readHFAFromRegisters(inType reflect.Type, f *[callbackMaxFrame]uintptr, flo
640638 size := inType.Size()
641639
642640 // Determine the element type
643- root := inType.Field(0).Type
641+ root := abiField(inType, 0).Type
644642 for root.Kind() == reflect.Struct {
645- root = root.Field(0).Type
643+ root = abiField(root, 0).Type
646644 }
647645 isFloat32 := root.Kind() == reflect.Float32
648646
@@ -704,7 +702,7 @@ func readHFAFromRegisters(inType reflect.Type, f *[callbackMaxFrame]uintptr, flo
704702 // callback frame. On Darwin ARM64, arguments are byte-packed on the stack.
705703 // On Linux ARM64, arguments are 8-byte aligned.
706704 func readStructFromStackArm64(inType reflect.Type, f *[callbackMaxFrame]uintptr, frame unsafe.Pointer, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
707- if runtime.GOOS == "darwin" {
705+ if isDarwin {
708706 return callbackArgFromStack(frame, *stackSlot, stackByteOffset, inType)
709707 }
710708 // Linux ARM64: 8-byte aligned slots.
D struct_arm64_regression_test.go +0 -37
@@ -1,37 +0,0 @@
1-//go:build arm64 && (darwin || linux)
2-
3-package purego
4-
5-import (
6- "reflect"
7- "testing"
8-)
9-
10-func TestIsHFARejectsNestedMixedStruct(t *testing.T) {
11- type inner struct{ X, Y float32 }
12- type mixed struct {
13- Inner inner
14- Tail int32
15- }
16- if isHFA(reflect.TypeOf(mixed{})) {
17- t.Fatal("expected mixed nested struct to be non-HFA")
18- }
19-}
20-
21-func TestIsHFAAcceptsNestedFloatStructs(t *testing.T) {
22- type inner struct{ X, Y float32 }
23- type outer struct {
24- Left inner
25- Right inner
26- }
27- if !isHFA(reflect.TypeOf(outer{})) {
28- t.Fatal("expected nested float-only struct to be HFA")
29- }
30-}
31-
32-func TestIsHFAAcceptsFloatArrayStruct(t *testing.T) {
33- type arrayed struct{ A [2]float64 }
34- if !isHFA(reflect.TypeOf(arrayed{})) {
35- t.Fatal("expected float array struct to be HFA")
36- }
37-}
D struct_arm_regression_test.go +0 -41
@@ -1,41 +0,0 @@
1-//go:build arm && (freebsd || linux || netbsd || windows)
2-
3-package purego
4-
5-import (
6- "reflect"
7- "testing"
8- "unsafe"
9-)
10-
11-func TestARMAddStructCopiesUnaddressableValue(t *testing.T) {
12- type sample struct{ X uint32 }
13- var numInts, numFloats, numStack int
14- var ptr uintptr
15- keepAlive := addStruct(reflect.ValueOf(sample{X: 7}), &numInts, &numFloats, &numStack, func(v uintptr) {
16- ptr = v
17- }, func(uintptr) {
18- t.Fatal("unexpected float register")
19- }, func(uintptr) {
20- t.Fatal("unexpected stack slot")
21- }, nil)
22- if len(keepAlive) == 0 {
23- t.Fatal("expected copied keepAlive value")
24- }
25- if got := *(*uint32)(unsafe.Pointer(ptr)); got != 7 {
26- t.Fatalf("copied value = %#x, want %#x", got, uint32(7))
27- }
28-}
29-
30-func TestARMPlaceRegistersReadsUnaddressableValue(t *testing.T) {
31- type sample struct{ X uint32 }
32- var ints []uintptr
33- placeRegisters(reflect.ValueOf(sample{X: 7}), func(uintptr) {
34- t.Fatal("unexpected float register")
35- }, func(v uintptr) {
36- ints = append(ints, v)
37- })
38- if len(ints) != 1 || ints[0] != 7 {
39- t.Fatalf("placeRegisters() = %#v, want [7]", ints)
40- }
41-}
M struct_loong64.go +170 -142
@@ -4,172 +4,200 @@
44 package purego
55
66 import (
7- "math"
87 "reflect"
98 "unsafe"
109 )
1110
12-func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
13- outSize := outType.Size()
14- switch {
15- case outSize == 0:
16- return reflect.New(outType).Elem()
17- case outSize <= 8:
18- r1 := syscall.a1
19- if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
20- r1 = syscall.f1
21- if numFields == 2 {
22- r1 = packFloat32Pair(syscall.f1, syscall.f2)
11+// loong64Leaf is a scalar member of an aggregate after flattening nested structs
12+// and arrays. offset is the byte offset of the member within the aggregate.
13+type loong64Leaf struct {
14+ isFloat bool
15+ kind reflect.Kind
16+ offset uintptr
17+ size uintptr
18+}
19+
20+// loong64Flatten appends the scalar leaves of t to leaves, offsetting each member
21+// by base. Nested structs and arrays are expanded into their members.
22+func loong64Flatten(t reflect.Type, base uintptr, leaves *[]loong64Leaf) {
23+ switch t.Kind() {
24+ case reflect.Struct:
25+ for i := range t.NumField() {
26+ f := t.Field(i)
27+ if f.Name == "_" {
28+ // Blank fields are explicit padding to match the C layout, not
29+ // members that the calling convention counts.
30+ continue
2331 }
32+ loong64Flatten(f.Type, base+f.Offset, leaves)
2433 }
25- return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{r1})).Elem()
26- case outSize <= 16:
27- r1, r2 := syscall.a1, syscall.a2
28- if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
29- switch numFields {
30- case 4:
31- r1 = packFloat32Pair(syscall.f1, syscall.f2)
32- r2 = packFloat32Pair(syscall.f3, syscall.f4)
33- case 3:
34- r1 = packFloat32Pair(syscall.f1, syscall.f2)
35- r2 = syscall.f3
36- case 2:
37- r1 = syscall.f1
38- r2 = syscall.f2
39- default:
40- panic("not reached")
41- }
34+ case reflect.Array:
35+ elem := t.Elem()
36+ for i := range t.Len() {
37+ loong64Flatten(elem, base+uintptr(i)*elem.Size(), leaves)
38+ }
39+ default:
40+ k := t.Kind()
41+ *leaves = append(*leaves, loong64Leaf{
42+ isFloat: k == reflect.Float32 || k == reflect.Float64,
43+ kind: k,
44+ offset: base,
45+ size: t.Size(),
46+ })
47+ }
48+}
49+
50+// loong64Classify flattens t and reports whether it is passed and returned through
51+// the floating-point calling convention. Under the LoongArch hard-float ABI an
52+// aggregate uses FP registers only when, after flattening, it has one or two
53+// floating-point members and nothing else, or exactly one floating-point member
54+// together with one integer member.
55+func loong64Classify(t reflect.Type) (leaves []loong64Leaf, useFP bool) {
56+ loong64Flatten(t, 0, &leaves)
57+ var floats, ints int
58+ for _, l := range leaves {
59+ if l.isFloat {
60+ floats++
61+ } else {
62+ ints++
4263 }
43- return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem()
64+ }
65+ switch {
66+ case ints == 0 && (floats == 1 || floats == 2):
67+ return leaves, true
68+ case ints == 1 && floats == 1:
69+ return leaves, true
4470 default:
45- // create struct from the Go pointer created above
46- // weird pointer dereference to circumvent go vet
71+ return leaves, false
72+ }
73+}
74+
75+// structReturnInMemory reports whether a struct return value is returned through
76+// a caller-allocated hidden pointer passed as the first integer argument.
77+// Aggregates larger than two eightbytes are returned in memory.
78+func structReturnInMemory(outType reflect.Type) bool {
79+ return outType.Size() > maxRegAllocStructSize
80+}
81+
82+func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value {
83+ outSize := outType.Size()
84+ if outSize == 0 {
85+ return reflect.New(outType).Elem()
86+ }
87+ if outSize > 16 {
88+ // Returned indirectly through a pointer in the first integer register.
4789 return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem()
4890 }
91+
92+ var buf [16]byte
93+ base := unsafe.Pointer(&buf[0])
94+ if leaves, useFP := loong64Classify(outType); useFP {
95+ floatRegs := [2]uintptr{syscall.f1, syscall.f2}
96+ intRegs := [2]uintptr{syscall.a1, syscall.a2}
97+ var fi, ii int
98+ for _, l := range leaves {
99+ dst := unsafe.Add(base, l.offset)
100+ if !l.isFloat {
101+ loong64StoreInt(dst, l.size, intRegs[ii])
102+ ii++
103+ continue
104+ }
105+ r := floatRegs[fi]
106+ fi++
107+ if l.kind == reflect.Float32 {
108+ // A single-precision value is NaN-boxed in the register.
109+ *(*uint32)(dst) = uint32(r)
110+ } else {
111+ *(*uint64)(dst) = uint64(r)
112+ }
113+ }
114+ } else {
115+ *(*uintptr)(base) = syscall.a1
116+ if outSize > 8 {
117+ *(*uintptr)(unsafe.Add(base, 8)) = syscall.a2
118+ }
119+ }
120+ return reflect.NewAt(outType, base).Elem()
49121 }
50122
51-const (
52- _NO_CLASS = 0b00
53- _FLOAT = 0b01
54- _INT = 0b11
55-)
123+func loong64StoreInt(dst unsafe.Pointer, size uintptr, r uintptr) {
124+ switch size {
125+ case 1:
126+ *(*uint8)(dst) = uint8(r)
127+ case 2:
128+ *(*uint16)(dst) = uint16(r)
129+ case 4:
130+ *(*uint32)(dst) = uint32(r)
131+ default:
132+ *(*uint64)(dst) = uint64(r)
133+ }
134+}
56135
57136 func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
58- if v.Type().Size() == 0 {
137+ size := v.Type().Size()
138+ if size == 0 {
59139 return keepAlive
60140 }
141+ if size > 16 {
142+ return placeStack(v, keepAlive, addInt)
143+ }
61144
62- if size := v.Type().Size(); size <= 16 {
63- placeRegisters(v, addFloat, addInt)
145+ var ptr unsafe.Pointer
146+ if v.CanAddr() {
147+ ptr = v.Addr().UnsafePointer()
64148 } else {
65- keepAlive = placeStack(v, keepAlive, addInt)
149+ tmp := reflect.New(v.Type())
150+ tmp.Elem().Set(v)
151+ ptr = tmp.UnsafePointer()
152+ keepAlive = append(keepAlive, tmp.Interface())
66153 }
67- return keepAlive // the struct was allocated so don't panic
68-}
69154
70-func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
71- var val uint64
72- var shift byte
73- var flushed bool
74- class := _NO_CLASS
75- var place func(v reflect.Value)
76- place = func(v reflect.Value) {
77- var numFields int
78- if v.Kind() == reflect.Struct {
79- numFields = v.Type().NumField()
80- } else {
81- numFields = v.Type().Len()
82- }
83- for k := 0; k < numFields; k++ {
84- flushed = false
85- var f reflect.Value
86- if v.Kind() == reflect.Struct {
87- f = v.Field(k)
88- } else {
89- f = v.Index(k)
90- }
91- align := byte(f.Type().Align()*8 - 1)
92- shift = (shift + align) &^ align
93- if shift >= 64 {
94- flushPackedWord(&val, &shift, &flushed, class == _FLOAT, addFloat, addInt)
95- class = _NO_CLASS
96- }
97- switch f.Type().Kind() {
98- case reflect.Struct:
99- place(f)
100- case reflect.Bool:
101- if f.Bool() {
102- val |= 1 << shift
103- }
104- shift += 8
105- class |= _INT
106- case reflect.Uint8:
107- val |= f.Uint() << shift
108- shift += 8
109- class |= _INT
110- case reflect.Uint16:
111- val |= f.Uint() << shift
112- shift += 16
113- class |= _INT
114- case reflect.Uint32:
115- val |= f.Uint() << shift
116- shift += 32
117- class |= _INT
118- case reflect.Uint64, reflect.Uint, reflect.Uintptr:
119- addInt(uintptr(f.Uint()))
120- shift = 0
121- flushed = true
122- class = _NO_CLASS
123- case reflect.Int8:
124- val |= uint64(f.Int()&0xFF) << shift
125- shift += 8
126- class |= _INT
127- case reflect.Int16:
128- val |= uint64(f.Int()&0xFFFF) << shift
129- shift += 16
130- class |= _INT
131- case reflect.Int32:
132- val |= uint64(f.Int()&0xFFFF_FFFF) << shift
133- shift += 32
134- class |= _INT
135- case reflect.Int64, reflect.Int:
136- addInt(uintptr(f.Int()))
137- shift = 0
138- flushed = true
139- class = _NO_CLASS
140- case reflect.Float32:
141- if class == _FLOAT {
142- addFloat(uintptr(val))
143- val = 0
144- shift = 0
145- }
146- val |= uint64(math.Float32bits(float32(f.Float()))) << shift
147- shift += 32
148- class |= _FLOAT
149- case reflect.Float64:
150- addFloat(uintptr(math.Float64bits(float64(f.Float()))))
151- shift = 0
152- flushed = true
153- class = _NO_CLASS
154- case reflect.Ptr, reflect.UnsafePointer:
155- addInt(f.Pointer())
156- shift = 0
157- flushed = true
158- class = _NO_CLASS
159- case reflect.Array:
160- place(f)
155+ if leaves, useFP := loong64Classify(v.Type()); useFP {
156+ for _, l := range leaves {
157+ src := unsafe.Add(ptr, l.offset)
158+ switch {
159+ case l.isFloat && l.kind == reflect.Float32:
160+ // NaN-box the single-precision value in the 64-bit FP register.
161+ addFloat(uintptr(*(*uint32)(src)) | 0xFFFFFFFF_00000000)
162+ case l.isFloat:
163+ addFloat(uintptr(*(*uint64)(src)))
161164 default:
162- panic("purego: unsupported kind " + f.Kind().String())
165+ addInt(loong64LoadInt(src, l))
163166 }
164167 }
168+ return keepAlive
165169 }
166- place(v)
167- if !flushed {
168- if class == _FLOAT {
169- addFloat(uintptr(val))
170- } else {
171- addInt(uintptr(val))
172- }
170+
171+ // Integer calling convention: pass the raw aggregate in one or two GARs.
172+ var words [16]byte
173+ copy(words[:], unsafe.Slice((*byte)(ptr), size))
174+ addInt(*(*uintptr)(unsafe.Pointer(&words[0])))
175+ if size > 8 {
176+ addInt(*(*uintptr)(unsafe.Pointer(&words[8])))
177+ }
178+ return keepAlive
179+}
180+
181+// loong64LoadInt reads an integer leaf into a register value, sign-extending
182+// signed members and zero-extending the rest.
183+func loong64LoadInt(src unsafe.Pointer, l loong64Leaf) uintptr {
184+ switch l.kind {
185+ case reflect.Int8:
186+ return uintptr(int64(*(*int8)(src)))
187+ case reflect.Int16:
188+ return uintptr(int64(*(*int16)(src)))
189+ case reflect.Int32:
190+ return uintptr(int64(*(*int32)(src)))
191+ case reflect.Int, reflect.Int64:
192+ return uintptr(*(*int64)(src))
193+ case reflect.Bool, reflect.Uint8:
194+ return uintptr(*(*uint8)(src))
195+ case reflect.Uint16:
196+ return uintptr(*(*uint16)(src))
197+ case reflect.Uint32:
198+ return uintptr(*(*uint32)(src))
199+ default:
200+ return uintptr(*(*uint64)(src))
173201 }
174202 }
175203
D struct_loong64_regression_test.go +0 -35
@@ -1,35 +0,0 @@
1-//go:build loong64 && linux
2-
3-package purego
4-
5-import (
6- "math"
7- "reflect"
8- "testing"
9-)
10-
11-func TestLoong64GetStructMasksFloat32Pairs(t *testing.T) {
12- type pair struct{ X, Y float32 }
13- sys := syscall15Args{
14- f1: uintptr(0xDEADBEEF00000000 | uint64(math.Float32bits(1.5))),
15- f2: uintptr(math.Float32bits(2.5)),
16- }
17- got := getStruct(reflect.TypeOf(pair{}), sys).Interface().(pair)
18- if got.X != 1.5 || got.Y != 2.5 {
19- t.Fatalf("got %+v, want {1.5 2.5}", got)
20- }
21-}
22-
23-func TestLoong64PlaceRegistersResetsAfterFlush(t *testing.T) {
24- type triple struct{ A, B, C uint32 }
25- var ints []uintptr
26- placeRegisters(reflect.ValueOf(triple{A: 1, B: 2, C: 3}), func(uintptr) {
27- t.Fatal("unexpected float register")
28- }, func(v uintptr) {
29- ints = append(ints, v)
30- })
31- want := []uintptr{0x0000000200000001, 0x0000000000000003}
32- if len(ints) != len(want) || ints[0] != want[0] || ints[1] != want[1] {
33- t.Fatalf("placeRegisters() = %#v, want %#v", ints, want)
34- }
35-}
A struct_other.go +54 -0
@@ -0,0 +1,54 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build !(amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x)
5+
6+package purego
7+
8+import (
9+ "reflect"
10+ "unsafe"
11+)
12+
13+// This file provides the struct-handling helpers for architectures that do
14+// not support struct arguments or returns: 386, and any architecture reachable
15+// only through the integer-only cgo fallback. Every struct operation panics.
16+
17+func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
18+ panic("purego: struct arguments are not supported on this architecture")
19+}
20+
21+func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value {
22+ panic("purego: struct returns are not supported on this architecture")
23+}
24+
25+// structReturnInMemory reports whether a struct return value is returned through
26+// a caller-allocated hidden pointer. Structs are unsupported on this
27+// architecture, so it always reports false.
28+func structReturnInMemory(reflect.Type) bool {
29+ return false
30+}
31+
32+// shouldBundleStackArgs always returns false because C-style stack argument
33+// bundling is only needed on Darwin ARM64.
34+func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
35+ return false
36+}
37+
38+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
39+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
40+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
41+ panic("purego: collectStackArgs should not be called on this architecture")
42+}
43+
44+func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
45+ panic("purego: bundleStackArgs should not be called on this architecture")
46+}
47+
48+func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
49+ panic("purego: struct callback arguments are not supported on this architecture")
50+}
51+
52+func setStruct(a *callbackArgs, ret reflect.Value) {
53+ panic("purego: struct returns are not supported on this architecture")
54+}
M struct_ppc64le.go +118 -89
@@ -4,77 +4,76 @@
44 package purego
55
66 import (
7+ "math"
78 "reflect"
89 "unsafe"
910 )
1011
11-func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
12- outSize := outType.Size()
13-
14- switch {
15- case outSize == 0:
16- return reflect.New(outType).Elem()
17-
18- case outSize <= 16:
19- // Reconstruct from registers by copying raw bytes
20- var buf [16]byte
21-
22- // Integer registers
23- *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.a1
24- if outSize > 8 {
25- *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.a2
26- }
12+// ppc64leLeaf is a scalar member of a flattened aggregate at byte offset offset.
13+type ppc64leLeaf struct {
14+ kind reflect.Kind
15+ offset uintptr
16+}
2717
28- // Homogeneous float aggregates override integer regs
29- if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
30- if outType.Field(0).Type.Kind() == reflect.Float32 {
31- // float32 values in FP regs
32- f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}
33- for i := 0; i < numFields; i++ {
34- *(*uint32)(unsafe.Pointer(&buf[i*4])) = uint32(f[i])
35- }
36- } else {
37- // float64: whole register value is valid
38- *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.f1
39- if outSize > 8 {
40- *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.f2
41- }
18+// ppc64leFlatten appends the scalar leaves of t to leaves, expanding nested
19+// structs and arrays.
20+func ppc64leFlatten(t reflect.Type, base uintptr, leaves *[]ppc64leLeaf) {
21+ switch t.Kind() {
22+ case reflect.Struct:
23+ for i := range t.NumField() {
24+ f := t.Field(i)
25+ if f.Name == "_" {
26+ // Blank fields are padding, not counted members.
27+ continue
4228 }
29+ ppc64leFlatten(f.Type, base+f.Offset, leaves)
30+ }
31+ case reflect.Array:
32+ elem := t.Elem()
33+ for i := range t.Len() {
34+ ppc64leFlatten(elem, base+uintptr(i)*elem.Size(), leaves)
4335 }
44-
45- return reflect.NewAt(outType, unsafe.Pointer(&buf[0])).Elem()
46-
4736 default:
48- // Returned indirectly via pointer in a1
49- ptr := *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))
50- return reflect.NewAt(outType, ptr).Elem()
37+ *leaves = append(*leaves, ppc64leLeaf{kind: t.Kind(), offset: base})
5138 }
5239 }
5340
54-func addStruct(
55- v reflect.Value,
56- numInts, numFloats, numStack *int,
57- addInt, addFloat, addStack func(uintptr),
58- keepAlive []any,
59-) []any {
60- size := v.Type().Size()
61- if size == 0 {
62- return keepAlive
41+// ppc64leClassifyHFA reports whether t is a homogeneous floating-point
42+// aggregate: one to eight members all of the same floating-point type.
43+func ppc64leClassifyHFA(t reflect.Type) (leaves []ppc64leLeaf, isHFA bool) {
44+ ppc64leFlatten(t, 0, &leaves)
45+ if len(leaves) == 0 || len(leaves) > 8 {
46+ return leaves, false
6347 }
64-
65- if size <= 16 {
66- return placeSmallAggregatePPC64LE(v, addFloat, addInt, keepAlive)
48+ first := leaves[0].kind
49+ if first != reflect.Float32 && first != reflect.Float64 {
50+ return leaves, false
51+ }
52+ for _, l := range leaves {
53+ if l.kind != first {
54+ return leaves, false
55+ }
6756 }
57+ return leaves, true
58+}
6859
69- return placeStack(v, keepAlive, addInt)
60+// structReturnInMemory reports whether a struct return value is returned through
61+// a caller-allocated hidden pointer rather than in registers. A homogeneous
62+// floating-point aggregate is returned in the floating-point registers whatever
63+// its size.
64+func structReturnInMemory(outType reflect.Type) bool {
65+ if outType.Size() <= maxRegAllocStructSize {
66+ return false
67+ }
68+ _, isHFA := ppc64leClassifyHFA(outType)
69+ return !isHFA
7070 }
7171
72-func placeSmallAggregatePPC64LE(
73- v reflect.Value,
74- addFloat, addInt func(uintptr),
75- keepAlive []any,
76-) []any {
72+func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any {
7773 size := v.Type().Size()
74+ if size == 0 {
75+ return keepAlive
76+ }
7877
7978 var ptr unsafe.Pointer
8079 if v.CanAddr() {
@@ -86,58 +85,88 @@ func placeSmallAggregatePPC64LE(
8685 keepAlive = append(keepAlive, tmp.Interface())
8786 }
8887
89- var buf [16]byte
90- src := unsafe.Slice((*byte)(ptr), size)
91- copy(buf[:], src)
92-
93- w0 := *(*uintptr)(unsafe.Pointer(&buf[0]))
94- w1 := uintptr(0)
95- if size > 8 {
96- w1 = *(*uintptr)(unsafe.Pointer(&buf[8]))
88+ // An HFA passes each member in its own floating-point register.
89+ if leaves, isHFA := ppc64leClassifyHFA(v.Type()); isHFA {
90+ for _, l := range leaves {
91+ src := unsafe.Add(ptr, l.offset)
92+ if l.kind == reflect.Float32 {
93+ // Single precision occupies the register in double format.
94+ addFloat(uintptr(math.Float64bits(float64(*(*float32)(src)))))
95+ } else {
96+ addFloat(uintptr(*(*uint64)(src)))
97+ }
98+ }
9799 }
98100
99- if isFloats, _ := isAllSameFloat(v.Type()); isFloats {
100- addFloat(w0)
101- if size > 8 {
102- addFloat(w1)
103- }
104- } else {
105- addInt(w0)
106- if size > 8 {
107- addInt(w1)
101+ // Every aggregate also occupies its size rounded up to doublewords in the
102+ // GPRs r3-r10 / parameter save area: shadow (skipped) for an HFA, the value
103+ // otherwise.
104+ for off := uintptr(0); off < size; off += 8 {
105+ if remaining := size - off; remaining >= 8 {
106+ addInt(*(*uintptr)(unsafe.Add(ptr, off)))
107+ } else {
108+ var word [8]byte
109+ copy(word[:], unsafe.Slice((*byte)(unsafe.Add(ptr, off)), int(remaining)))
110+ addInt(*(*uintptr)(unsafe.Pointer(&word[0])))
108111 }
109112 }
110113
111114 return keepAlive
112115 }
113116
114-// placeStack is a fallback for structs that are too large to fit in registers
115-func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
116- if v.CanAddr() {
117- addInt(v.Addr().Pointer())
118- return keepAlive
117+func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value {
118+ if outType.Size() == 0 {
119+ return reflect.New(outType).Elem()
120+ }
121+
122+ // An HFA returns each member in its own floating-point register.
123+ if leaves, isHFA := ppc64leClassifyHFA(outType); isHFA {
124+ floatRegs := [8]uintptr{
125+ syscall.f1, syscall.f2, syscall.f3, syscall.f4,
126+ syscall.f5, syscall.f6, syscall.f7, syscall.f8,
127+ }
128+ ret := reflect.New(outType)
129+ base := ret.UnsafePointer()
130+ for i, l := range leaves {
131+ dst := unsafe.Add(base, l.offset)
132+ if l.kind == reflect.Float32 {
133+ // The register holds the value in double format.
134+ *(*float32)(dst) = float32(math.Float64frombits(uint64(floatRegs[i])))
135+ } else {
136+ *(*uint64)(dst) = uint64(floatRegs[i])
137+ }
138+ }
139+ return ret.Elem()
140+ }
141+
142+ if outType.Size() > 16 {
143+ // Returned via a caller-allocated buffer whose pointer comes back in r3.
144+ return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem()
145+ }
146+
147+ // Otherwise returned in r3 and r4.
148+ var buf [16]byte
149+ *(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.a1
150+ if outType.Size() > 8 {
151+ *(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.a2
119152 }
120- ptr := reflect.New(v.Type())
121- ptr.Elem().Set(v)
122- addInt(ptr.Pointer())
123- return append(keepAlive, ptr.Interface())
153+ return reflect.NewAt(outType, unsafe.Pointer(&buf[0])).Elem()
124154 }
125155
156+// shouldBundleStackArgs always returns false on ppc64le
157+// since C-style stack argument bundling is only needed on Darwin ARM64.
126158 func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
127- // PPC64LE does not bundle stack args
128159 return false
129160 }
130161
131-func collectStackArgs(
132- args []reflect.Value,
133- i, numInts, numFloats int,
134- keepAlive []any,
135- addInt, addFloat, addStack func(uintptr),
136- numIntsPtr, numFloatsPtr, numStackPtr *int,
137-) ([]reflect.Value, []any) {
138- return nil, keepAlive
162+// collectStackArgs is not used on ppc64le.
163+func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
164+ keepAlive []any, addInt, addFloat, addStack func(uintptr),
165+ pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
166+ panic("purego: collectStackArgs should not be called on ppc64le")
139167 }
140168
169+// bundleStackArgs is not used on ppc64le.
141170 func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
142171 panic("purego: bundleStackArgs should not be called on ppc64le")
143172 }
M struct_riscv64.go +10 -3
@@ -8,7 +8,14 @@ import (
88 "unsafe"
99 )
1010
11-func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
11+// structReturnInMemory reports whether a struct return value is returned through
12+// a caller-allocated hidden pointer passed as the first integer argument.
13+// Aggregates larger than two eightbytes are returned in memory.
14+func structReturnInMemory(outType reflect.Type) bool {
15+ return outType.Size() > maxRegAllocStructSize
16+}
17+
18+func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value {
1219 outSize := outType.Size()
1320
1421 switch {
@@ -27,10 +34,10 @@ func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
2734
2835 // Homogeneous float aggregates override integer regs
2936 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
30- if outType.Field(0).Type.Kind() == reflect.Float32 {
37+ if abiField(outType, 0).Type.Kind() == reflect.Float32 {
3138 // float32 values are NaN-boxed in FP regs; use low 32 bits only
3239 f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}
33- for i := 0; i < numFields; i++ {
40+ for i := range numFields {
3441 *(*uint32)(unsafe.Pointer(&buf[i*4])) = uint32(f[i])
3542 }
3643 } else {
M struct_s390x.go +11 -4
@@ -8,7 +8,14 @@ import (
88 "unsafe"
99 )
1010
11-func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
11+// structReturnInMemory reports whether a struct return value is returned through
12+// a caller-allocated hidden pointer passed as the first integer argument.
13+// Aggregates larger than two eightbytes are returned in memory.
14+func structReturnInMemory(outType reflect.Type) bool {
15+ return outType.Size() > maxRegAllocStructSize
16+}
17+
18+func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value {
1219 outSize := outType.Size()
1320
1421 switch {
@@ -27,11 +34,11 @@ func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
2734
2835 // Homogeneous float aggregates override integer regs
2936 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
30- if outType.Field(0).Type.Kind() == reflect.Float32 {
37+ if abiField(outType, 0).Type.Kind() == reflect.Float32 {
3138 // float32 values in FP regs
3239 f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}
33- for i := 0; i < numFields; i++ {
34- *(*uint32)(unsafe.Pointer(&buf[i*4])) = highFloat32Bits(f[i])
40+ for i := range numFields {
41+ *(*uint32)(unsafe.Pointer(&buf[i*4])) = uint32(f[i])
3542 }
3643 } else {
3744 // float64: whole register value is valid
D struct_s390x_regression_test.go +0 -21
@@ -1,21 +0,0 @@
1-//go:build s390x && linux
2-
3-package purego
4-
5-import (
6- "math"
7- "reflect"
8- "testing"
9-)
10-
11-func TestS390xGetStructUsesHighFloat32Bits(t *testing.T) {
12- type pair struct{ X, Y float32 }
13- sys := syscall15Args{
14- f1: uintptr(uint64(math.Float32bits(1.5)) << 32),
15- f2: uintptr(uint64(math.Float32bits(2.5)) << 32),
16- }
17- got := getStruct(reflect.TypeOf(pair{}), sys).Interface().(pair)
18- if got.X != 1.5 || got.Y != 2.5 {
19- t.Fatalf("got %+v, want {1.5 2.5}", got)
20- }
21-}
M struct_test.go +411 -207
@@ -1,35 +1,44 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2024 The Ebitengine Authors
33
4-//go:build (darwin || linux) && (amd64 || arm64)
4+//go:build (darwin || linux || windows) && (amd64 || arm64 || loong64 || ppc64le)
55
66 package purego_test
77
88 import (
9+ "iter"
910 "math"
1011 "os"
1112 "path/filepath"
1213 "reflect"
1314 "runtime"
15+ "slices"
16+ "structs"
1417 "testing"
1518 "unsafe"
1619
1720 "github.com/bnema/purego"
21+ "github.com/bnema/purego/internal/load"
1822 )
1923
2024 func TestRegisterFunc_structArgs(t *testing.T) {
2125 libFileName := filepath.Join(t.TempDir(), "structtest.so")
2226 t.Logf("Build %v", libFileName)
2327
24- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "structtest", "struct_test.c")); err != nil {
28+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "structtest", "struct_test.c")); err != nil {
2529 t.Fatal(err)
2630 }
2731 defer os.Remove(libFileName)
2832
29- lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
33+ lib, err := load.OpenLibrary(libFileName)
3034 if err != nil {
31- t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
35+ t.Fatalf("OpenLibrary(%q) failed: %v", libFileName, err)
3236 }
37+ defer func() {
38+ if err := load.CloseLibrary(lib); err != nil {
39+ t.Fatalf("failed to close library: %v", err)
40+ }
41+ }()
3342
3443 const (
3544 expectedUnsigned = 0xdeadbeef
@@ -41,8 +50,9 @@ func TestRegisterFunc_structArgs(t *testing.T) {
4150 )
4251
4352 implementations := []struct {
44- name string
45- register func(fptr any, handle uintptr, name string, goFn any)
53+ name string
54+ usesCallbacks bool
55+ register func(fptr any, handle uintptr, name string, goFn any)
4656 }{
4757 {
4858 name: "RegisterLibFunc",
@@ -51,7 +61,8 @@ func TestRegisterFunc_structArgs(t *testing.T) {
5161 },
5262 },
5363 {
54- name: "GoCallbackFunc",
64+ name: "GoCallbackFunc",
65+ usesCallbacks: true,
5566 register: func(fptr any, handle uintptr, name string, goFn any) {
5667 fnType := reflect.TypeOf(fptr).Elem()
5768 if fnType.NumOut() > 0 {
@@ -68,11 +79,20 @@ func TestRegisterFunc_structArgs(t *testing.T) {
6879 },
6980 }
7081 for _, imp := range implementations {
71- imp := imp
82+ if imp.usesCallbacks && runtime.GOOS == "windows" {
83+ // Callbacks on Windows use the stdlib syscall.NewCallback, which does
84+ // not support struct arguments or returns.
85+ continue
86+ }
87+ if imp.usesCallbacks && runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
88+ continue
89+ }
7290 t.Run(imp.name, func(t *testing.T) {
7391 register := imp.register
7492 {
75- type Empty struct{}
93+ type Empty struct {
94+ _ structs.HostLayout
95+ }
7696 var NoStruct func(Empty) int64
7797 register(&NoStruct, lib, "NoStruct", func(Empty) int64 {
7898 return 0xdeadbeef
@@ -82,7 +102,9 @@ func TestRegisterFunc_structArgs(t *testing.T) {
82102 }
83103 }
84104 {
85- type EmptyEmpty struct{}
105+ type EmptyEmpty struct {
106+ _ structs.HostLayout
107+ }
86108 var EmptyEmptyFn func(EmptyEmpty) int64
87109 register(&EmptyEmptyFn, lib, "EmptyEmpty", func(EmptyEmpty) int64 {
88110 return 0xdeadbeef
@@ -100,6 +122,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
100122 }
101123 {
102124 type GreaterThan16Bytes struct {
125+ _ structs.HostLayout
103126 x, y, z *int64
104127 }
105128 var x, y, z int64 = 0xEF, 0xBE00, 0xDEAD0000
@@ -113,7 +136,9 @@ func TestRegisterFunc_structArgs(t *testing.T) {
113136 }
114137 {
115138 type GreaterThan16BytesStruct struct {
139+ _ structs.HostLayout
116140 a struct {
141+ _ structs.HostLayout
117142 x, y, z *int64
118143 }
119144 }
@@ -122,12 +147,16 @@ func TestRegisterFunc_structArgs(t *testing.T) {
122147 register(&GreaterThan16BytesStructFn, lib, "GreaterThan16BytesStruct", func(g GreaterThan16BytesStruct) int64 {
123148 return *g.a.x + *g.a.y + *g.a.z
124149 })
125- if ret := GreaterThan16BytesStructFn(GreaterThan16BytesStruct{a: struct{ x, y, z *int64 }{x: &x, y: &y, z: &z}}); ret != expectedUnsigned {
150+ if ret := GreaterThan16BytesStructFn(GreaterThan16BytesStruct{a: struct {
151+ _ structs.HostLayout
152+ x, y, z *int64
153+ }{x: &x, y: &y, z: &z}}); ret != expectedUnsigned {
126154 t.Fatalf("GreaterThan16BytesStructFn returned %#x wanted %#x", ret, expectedUnsigned)
127155 }
128156 }
129157 {
130158 type GreaterThan16Bytes struct {
159+ _ structs.HostLayout
131160 x, y, z *int64
132161 }
133162 var x, y, z int64 = 0xEF, 0xBE00, 0xDEAD0000
@@ -151,48 +180,52 @@ func TestRegisterFunc_structArgs(t *testing.T) {
151180 register(&BeforeRegisters, lib, "BeforeRegisters", func(bytes GreaterThan16Bytes, a, b int64) uint64 {
152181 return uint64(*bytes.x + *bytes.y + *bytes.z + a + b)
153182 })
154- if ret := BeforeRegisters(GreaterThan16Bytes{&x, &y, &z}, 0x0F, 0xF0); ret != expectedUnsigned {
183+ if ret := BeforeRegisters(GreaterThan16Bytes{x: &x, y: &y, z: &z}, 0x0F, 0xF0); ret != expectedUnsigned {
155184 t.Fatalf("BeforeRegisters returned %#x wanted %#x", ret, expectedUnsigned)
156185 }
157186 }
158187 {
159188 type IntLessThan16Bytes struct {
189+ _ structs.HostLayout
160190 x, y int64
161191 }
162192 var IntLessThan16BytesFn func(bytes IntLessThan16Bytes) int64
163193 register(&IntLessThan16BytesFn, lib, "IntLessThan16Bytes", func(l IntLessThan16Bytes) int64 {
164194 return l.x + l.y
165195 })
166- if ret := IntLessThan16BytesFn(IntLessThan16Bytes{0xDEAD0000, 0xBEEF}); ret != expectedUnsigned {
196+ if ret := IntLessThan16BytesFn(IntLessThan16Bytes{x: 0xDEAD0000, y: 0xBEEF}); ret != expectedUnsigned {
167197 t.Fatalf("IntLessThan16BytesFn returned %#x wanted %#x", ret, expectedUnsigned)
168198 }
169199 }
170200 {
171201 type FloatLessThan16Bytes struct {
202+ _ structs.HostLayout
172203 x, y float32
173204 }
174205 var FloatLessThan16BytesFn func(FloatLessThan16Bytes) float32
175206 register(&FloatLessThan16BytesFn, lib, "FloatLessThan16Bytes", func(f FloatLessThan16Bytes) float32 {
176207 return f.x + f.y
177208 })
178- if ret := FloatLessThan16BytesFn(FloatLessThan16Bytes{3, 7}); ret != expectedFloat {
209+ if ret := FloatLessThan16BytesFn(FloatLessThan16Bytes{x: 3, y: 7}); ret != expectedFloat {
179210 t.Fatalf("FloatLessThan16Bytes returned %f wanted %f", ret, expectedFloat)
180211 }
181212 }
182213 {
183214 type ThreeSmallFields struct {
215+ _ structs.HostLayout
184216 x, y, z float32
185217 }
186218 var ThreeSmallFieldsFn func(ThreeSmallFields) float32
187219 register(&ThreeSmallFieldsFn, lib, "ThreeSmallFields", func(f ThreeSmallFields) float32 {
188220 return f.x + f.y + f.z
189221 })
190- if ret := ThreeSmallFieldsFn(ThreeSmallFields{1, 2, 7}); ret != expectedFloat {
222+ if ret := ThreeSmallFieldsFn(ThreeSmallFields{x: 1, y: 2, z: 7}); ret != expectedFloat {
191223 t.Fatalf("ThreeSmallFields returned %f wanted %f", ret, expectedFloat)
192224 }
193225 }
194226 {
195227 type FloatAndInt struct {
228+ _ structs.HostLayout
196229 x float32
197230 y int32
198231 }
@@ -200,37 +233,41 @@ func TestRegisterFunc_structArgs(t *testing.T) {
200233 register(&FloatAndIntFn, lib, "FloatAndInt", func(f FloatAndInt) float32 {
201234 return f.x + float32(f.y)
202235 })
203- if ret := FloatAndIntFn(FloatAndInt{3, 7}); ret != expectedFloat {
236+ if ret := FloatAndIntFn(FloatAndInt{x: 3, y: 7}); ret != expectedFloat {
204237 t.Fatalf("FloatAndIntFn returned %f wanted %f", ret, expectedFloat)
205238 }
206239 }
207240 {
208241 type DoubleStruct struct {
242+ _ structs.HostLayout
209243 x float64
210244 }
211245 var DoubleStructFn func(DoubleStruct) float64
212246 register(&DoubleStructFn, lib, "DoubleStruct", func(d DoubleStruct) float64 {
213247 return d.x
214248 })
215- if ret := DoubleStructFn(DoubleStruct{10}); ret != expectedDouble {
249+ if ret := DoubleStructFn(DoubleStruct{x: 10}); ret != expectedDouble {
216250 t.Fatalf("DoubleStruct returned %f wanted %f", ret, expectedDouble)
217251 }
218252 }
219253 {
220254 type TwoDoubleStruct struct {
255+ _ structs.HostLayout
221256 x, y float64
222257 }
223258 var TwoDoubleStructFn func(TwoDoubleStruct) float64
224259 register(&TwoDoubleStructFn, lib, "TwoDoubleStruct", func(d TwoDoubleStruct) float64 {
225260 return d.x + d.y
226261 })
227- if ret := TwoDoubleStructFn(TwoDoubleStruct{3, 7}); ret != expectedDouble {
262+ if ret := TwoDoubleStructFn(TwoDoubleStruct{x: 3, y: 7}); ret != expectedDouble {
228263 t.Fatalf("TwoDoubleStruct returned %f wanted %f", ret, expectedDouble)
229264 }
230265 }
231266 {
232267 type TwoDoubleTwoStruct struct {
268+ _ structs.HostLayout
233269 x struct {
270+ _ structs.HostLayout
234271 x, y float64
235272 }
236273 }
@@ -238,69 +275,84 @@ func TestRegisterFunc_structArgs(t *testing.T) {
238275 register(&TwoDoubleTwoStructFn, lib, "TwoDoubleTwoStruct", func(d TwoDoubleTwoStruct) float64 {
239276 return d.x.x + d.x.y
240277 })
241- if ret := TwoDoubleTwoStructFn(TwoDoubleTwoStruct{x: struct{ x, y float64 }{x: 3, y: 7}}); ret != expectedDouble {
278+ if ret := TwoDoubleTwoStructFn(TwoDoubleTwoStruct{x: struct {
279+ _ structs.HostLayout
280+ x, y float64
281+ }{x: 3, y: 7}}); ret != expectedDouble {
242282 t.Fatalf("TwoDoubleTwoStruct returned %f wanted %f", ret, expectedDouble)
243283 }
244284 }
245285 {
246286 type ThreeDoubleStruct struct {
287+ _ structs.HostLayout
247288 x, y, z float64
248289 }
249290 var ThreeDoubleStructFn func(ThreeDoubleStruct) float64
250291 register(&ThreeDoubleStructFn, lib, "ThreeDoubleStruct", func(d ThreeDoubleStruct) float64 {
251292 return d.x + d.y + d.z
252293 })
253- if ret := ThreeDoubleStructFn(ThreeDoubleStruct{1, 3, 6}); ret != expectedDouble {
294+ if ret := ThreeDoubleStructFn(ThreeDoubleStruct{x: 1, y: 3, z: 6}); ret != expectedDouble {
254295 t.Fatalf("ThreeDoubleStructFn returned %f wanted %f", ret, expectedDouble)
255296 }
256297 }
257298 {
258299 type LargeFloatStruct struct {
300+ _ structs.HostLayout
259301 a, b, c, d, e, f float64
260302 }
261303 var LargeFloatStructFn func(LargeFloatStruct) float64
262304 register(&LargeFloatStructFn, lib, "LargeFloatStruct", func(s LargeFloatStruct) float64 {
263305 return s.a + s.b + s.c + s.d + s.e + s.f
264306 })
265- if ret := LargeFloatStructFn(LargeFloatStruct{1, 2, 3, 4, 5, -5}); ret != expectedDouble {
307+ if ret := LargeFloatStructFn(LargeFloatStruct{a: 1, b: 2, c: 3, d: 4, e: 5, f: -5}); ret != expectedDouble {
266308 t.Fatalf("LargeFloatStructFn returned %f wanted %f", ret, expectedFloat)
267309 }
268- var LargeFloatStructWithRegs func(a, b, c float64, s LargeFloatStruct) float64
269- register(&LargeFloatStructWithRegs, lib, "LargeFloatStructWithRegs", func(a, b, c float64, s LargeFloatStruct) float64 {
270- return a + b + c + s.a + s.b + s.c + s.d + s.e + s.f
271- })
272- if ret := LargeFloatStructWithRegs(1, -1, 0, LargeFloatStruct{1, 2, 3, 4, 5, -5}); ret != expectedDouble {
273- t.Fatalf("LargeFloatStructWithRegs returned %f wanted %f", ret, expectedFloat)
310+ if runtime.GOARCH != "ppc64le" {
311+ // Needs 9 float argument registers (3 doubles + a 6-double HFA);
312+ // ppc64le wires 8.
313+ var LargeFloatStructWithRegs func(a, b, c float64, s LargeFloatStruct) float64
314+ register(&LargeFloatStructWithRegs, lib, "LargeFloatStructWithRegs", func(a, b, c float64, s LargeFloatStruct) float64 {
315+ return a + b + c + s.a + s.b + s.c + s.d + s.e + s.f
316+ })
317+ if ret := LargeFloatStructWithRegs(1, -1, 0, LargeFloatStruct{a: 1, b: 2, c: 3, d: 4, e: 5, f: -5}); ret != expectedDouble {
318+ t.Fatalf("LargeFloatStructWithRegs returned %f wanted %f", ret, expectedFloat)
319+ }
274320 }
275321 }
276322 {
277323 type Rect struct {
324+ _ structs.HostLayout
278325 x, y, w, h float64
279326 }
280- var RectangleWithRegs func(a, b, c, d, e float64, rect Rect) float64
281- register(&RectangleWithRegs, lib, "RectangleWithRegs", func(a, b, c, d, e float64, rect Rect) float64 {
282- return a + b + c + d + e + rect.x + rect.y + rect.w + rect.h
283- })
284- if ret := RectangleWithRegs(1, 2, 3, 4, -2, Rect{1, 2, 3, -4}); ret != expectedDouble {
285- t.Fatalf("RectangleWithRegs returned %f wanted %f", ret, expectedDouble)
327+ if runtime.GOARCH != "ppc64le" {
328+ // Needs 9 float argument registers (5 doubles + a 4-double HFA);
329+ // ppc64le wires 8.
330+ var RectangleWithRegs func(a, b, c, d, e float64, rect Rect) float64
331+ register(&RectangleWithRegs, lib, "RectangleWithRegs", func(a, b, c, d, e float64, rect Rect) float64 {
332+ return a + b + c + d + e + rect.x + rect.y + rect.w + rect.h
333+ })
334+ if ret := RectangleWithRegs(1, 2, 3, 4, -2, Rect{x: 1, y: 2, w: 3, h: -4}); ret != expectedDouble {
335+ t.Fatalf("RectangleWithRegs returned %f wanted %f", ret, expectedDouble)
336+ }
286337 }
287338 var RectangleSubtract func(rect Rect) float64
288339 register(&RectangleSubtract, lib, "RectangleSubtract", func(rect Rect) float64 {
289340 return (rect.x + rect.y) - (rect.w + rect.h)
290341 })
291- if ret := RectangleSubtract(Rect{15, 5, 3, 7}); ret != expectedDouble {
342+ if ret := RectangleSubtract(Rect{x: 15, y: 5, w: 3, h: 7}); ret != expectedDouble {
292343 t.Fatalf("RectangleSubtract returned %f wanted %f", ret, expectedDouble)
293344 }
294345 var Rectangle func(rect Rect) float64
295346 register(&Rectangle, lib, "Rectangle", func(rect Rect) float64 {
296347 return rect.x + rect.y + rect.w + rect.h
297348 })
298- if ret := Rectangle(Rect{1, 2, 3, 4}); ret != expectedDouble {
349+ if ret := Rectangle(Rect{x: 1, y: 2, w: 3, h: 4}); ret != expectedDouble {
299350 t.Fatalf("Rectangle returned %f wanted %f", ret, expectedFloat)
300351 }
301352 }
302353 {
303354 type FloatArray struct {
355+ _ structs.HostLayout
304356 a [2]float64
305357 }
306358 var FloatArrayFn func(rect FloatArray) float64
@@ -313,6 +365,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
313365 }
314366 {
315367 type UnsignedChar4Bytes struct {
368+ _ structs.HostLayout
316369 a, b, c, d byte
317370 }
318371 var UnsignedChar4BytesFn func(UnsignedChar4Bytes) uint32
@@ -325,16 +378,21 @@ func TestRegisterFunc_structArgs(t *testing.T) {
325378 }
326379 {
327380 type UnsignedChar4BytesStruct struct {
381+ _ structs.HostLayout
328382 x struct {
383+ _ structs.HostLayout
329384 a byte
330385 }
331386 y struct {
387+ _ structs.HostLayout
332388 b byte
333389 }
334390 z struct {
391+ _ structs.HostLayout
335392 c byte
336393 }
337394 w struct {
395+ _ structs.HostLayout
338396 d byte
339397 }
340398 }
@@ -343,16 +401,29 @@ func TestRegisterFunc_structArgs(t *testing.T) {
343401 return uint32(b.x.a)<<24 | uint32(b.y.b)<<16 | uint32(b.z.c)<<8 | uint32(b.w.d)
344402 })
345403 if ret := UnsignedChar4BytesStructFn(UnsignedChar4BytesStruct{
346- x: struct{ a byte }{a: 0xDE},
347- y: struct{ b byte }{b: 0xAD},
348- z: struct{ c byte }{c: 0xBE},
349- w: struct{ d byte }{d: 0xEF},
404+ x: struct {
405+ _ structs.HostLayout
406+ a byte
407+ }{a: 0xDE},
408+ y: struct {
409+ _ structs.HostLayout
410+ b byte
411+ }{b: 0xAD},
412+ z: struct {
413+ _ structs.HostLayout
414+ c byte
415+ }{c: 0xBE},
416+ w: struct {
417+ _ structs.HostLayout
418+ d byte
419+ }{d: 0xEF},
350420 }); ret != expectedUnsigned {
351421 t.Fatalf("UnsignedChar4BytesStructFn returned %#x wanted %#x", ret, expectedUnsigned)
352422 }
353423 }
354424 {
355425 type Short struct {
426+ _ structs.HostLayout
356427 a, b, c, d uint16
357428 }
358429 var ShortFn func(Short) uint64
@@ -365,6 +436,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
365436 }
366437 {
367438 type Int struct {
439+ _ structs.HostLayout
368440 a, b uint32
369441 }
370442 var IntFn func(Int) uint64
@@ -377,6 +449,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
377449 }
378450 {
379451 type Long struct {
452+ _ structs.HostLayout
380453 a uint64
381454 }
382455 var LongFn func(Long) uint64
@@ -389,6 +462,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
389462 }
390463 {
391464 type Char8Bytes struct {
465+ _ structs.HostLayout
392466 a, b, c, d, e, f, g, h int8
393467 }
394468 var Char8BytesFn func(Char8Bytes) int32
@@ -401,6 +475,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
401475 }
402476 {
403477 type Odd struct {
478+ _ structs.HostLayout
404479 a, b, c byte
405480 }
406481 var OddFn func(Odd) int32
@@ -413,6 +488,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
413488 }
414489 {
415490 type Char2Short1 struct {
491+ _ structs.HostLayout
416492 a, b byte
417493 c uint16
418494 }
@@ -426,6 +502,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
426502 }
427503 {
428504 type SignedChar2Short1 struct {
505+ _ structs.HostLayout
429506 a, b int8
430507 c int16
431508 }
@@ -439,6 +516,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
439516 }
440517 {
441518 type Array4UnsignedChars struct {
519+ _ structs.HostLayout
442520 a [4]uint8
443521 }
444522 var Array4UnsignedCharsFn func(chars Array4UnsignedChars) uint32
@@ -451,6 +529,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
451529 }
452530 {
453531 type Array3UnsignedChar struct {
532+ _ structs.HostLayout
454533 a [3]uint8
455534 }
456535 var Array3UnsignedChars func(chars Array3UnsignedChar) uint32
@@ -463,6 +542,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
463542 }
464543 {
465544 type Array2UnsignedShort struct {
545+ _ structs.HostLayout
466546 a [2]uint16
467547 }
468548 var Array2UnsignedShorts func(chars Array2UnsignedShort) uint32
@@ -475,6 +555,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
475555 }
476556 {
477557 type Array4Chars struct {
558+ _ structs.HostLayout
478559 a [4]int8
479560 }
480561 var Array4CharsFn func(chars Array4Chars) int32
@@ -488,6 +569,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
488569 }
489570 {
490571 type Array2Short struct {
572+ _ structs.HostLayout
491573 a [2]int16
492574 }
493575 var Array2Shorts func(chars Array2Short) int32
@@ -500,6 +582,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
500582 }
501583 {
502584 type Array3Short struct {
585+ _ structs.HostLayout
503586 a [3]int16
504587 }
505588 var Array3Shorts func(chars Array3Short) int32
@@ -512,21 +595,23 @@ func TestRegisterFunc_structArgs(t *testing.T) {
512595 }
513596 {
514597 type BoolStruct struct {
598+ _ structs.HostLayout
515599 b bool
516600 }
517601 var BoolStructFn func(BoolStruct) bool
518602 register(&BoolStructFn, lib, "BoolStruct", func(b BoolStruct) bool {
519603 return b.b
520604 })
521- if ret := BoolStructFn(BoolStruct{true}); ret != true {
605+ if ret := BoolStructFn(BoolStruct{b: true}); ret != true {
522606 t.Fatalf("BoolStructFn returned %v wanted %v", ret, true)
523607 }
524- if ret := BoolStructFn(BoolStruct{false}); ret != false {
608+ if ret := BoolStructFn(BoolStruct{b: false}); ret != false {
525609 t.Fatalf("BoolStructFn returned %v wanted %v", ret, false)
526610 }
527611 }
528612 {
529613 type BoolFloat struct {
614+ _ structs.HostLayout
530615 b bool
531616 _ [3]byte // purego won't do padding for you so make sure it aligns properly with C struct
532617 f float32
@@ -546,9 +631,16 @@ func TestRegisterFunc_structArgs(t *testing.T) {
546631 }
547632 }
548633 {
549- type point struct{ x, y float64 }
550- type size struct{ width, height float64 }
634+ type point struct {
635+ _ structs.HostLayout
636+ x, y float64
637+ }
638+ type size struct {
639+ _ structs.HostLayout
640+ width, height float64
641+ }
551642 type Content struct {
643+ _ structs.HostLayout
552644 point point
553645 size size
554646 }
@@ -564,13 +656,14 @@ func TestRegisterFunc_structArgs(t *testing.T) {
564656 })
565657 if ret := InitWithContentRect(new(int),
566658 // These numbers are created so that when added together and then divided by 11 it produces 0xdeadbeef
567- Content{point{x: 41_000_000_000, y: 95_000_000}, size{width: 214_000, height: 149}},
659+ Content{point: point{x: 41_000_000_000, y: 95_000_000}, size: size{width: 214_000, height: 149}},
568660 15, 4, true); ret != expectedUnsigned {
569661 t.Fatalf("InitWithContentRect returned %d wanted %#x", ret, expectedUnsigned)
570662 }
571663 }
572664 {
573665 type GoInt4 struct {
666+ _ structs.HostLayout
574667 A, B, C, D int
575668 }
576669 var GoInt4Fn func(GoInt4) int
@@ -578,12 +671,13 @@ func TestRegisterFunc_structArgs(t *testing.T) {
578671 return g.A + g.B - g.C + g.D
579672 })
580673 const expected = math.MaxInt - 52 - 3 + 4
581- if ret := GoInt4Fn(GoInt4{math.MaxInt, -52, 3, 4}); ret != expected {
674+ if ret := GoInt4Fn(GoInt4{A: math.MaxInt, B: -52, C: 3, D: 4}); ret != expected {
582675 t.Fatalf("GoInt4Fn returned %d wanted %#x", ret, expected)
583676 }
584677 }
585678 {
586679 type GoUint4 struct {
680+ _ structs.HostLayout
587681 A, B, C, D uint
588682 }
589683 var GoUint4Fn func(GoUint4) uint
@@ -591,23 +685,27 @@ func TestRegisterFunc_structArgs(t *testing.T) {
591685 return g.A + g.B + g.C + g.D
592686 })
593687 const expected = 1_000_000 + 53 + 71 + 8
594- if ret := GoUint4Fn(GoUint4{1_000_000, 53, 71, 8}); ret != expected {
688+ if ret := GoUint4Fn(GoUint4{A: 1_000_000, B: 53, C: 71, D: 8}); ret != expected {
595689 t.Fatalf("GoUint4Fn returned %d wanted %#x", ret, expected)
596690 }
597691 }
598692 {
599- type OneLong struct{ a uintptr }
693+ type OneLong struct {
694+ _ structs.HostLayout
695+ a uintptr
696+ }
600697 var TakeGoUintAndReturn func(a OneLong) uint64
601698 register(&TakeGoUintAndReturn, lib, "TakeGoUintAndReturn", func(a OneLong) uint64 {
602699 return uint64(a.a)
603700 })
604701 expected := uint64(7)
605- if ret := TakeGoUintAndReturn(OneLong{7}); ret != expected {
702+ if ret := TakeGoUintAndReturn(OneLong{a: 7}); ret != expected {
606703 t.Fatalf("TakeGoUintAndReturn returned %+v wanted %+v", ret, expected)
607704 }
608705 }
609706 {
610707 type FloatAndBool struct {
708+ _ structs.HostLayout
611709 x float32
612710 y bool
613711 }
@@ -627,13 +725,14 @@ func TestRegisterFunc_structArgs(t *testing.T) {
627725 }
628726 {
629727 type FourInt32s struct {
728+ _ structs.HostLayout
630729 f0, f1, f2, f3 int32
631730 }
632731 var FourInt32sFn func(FourInt32s) int32
633732 register(&FourInt32sFn, lib, "FourInt32s", func(s FourInt32s) int32 {
634733 return s.f0 + s.f1 + s.f2 + s.f3
635734 })
636- result := FourInt32sFn(FourInt32s{100, -127, 4, -100})
735+ result := FourInt32sFn(FourInt32s{f0: 100, f1: -127, f2: 4, f3: -100})
637736 const want = 100 - 127 + 4 - 100
638737 if result != want {
639738 t.Fatalf("FourInt32s returned %d wanted %d", result, want)
@@ -641,6 +740,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
641740 }
642741 {
643742 type PointerWrapper struct {
743+ _ structs.HostLayout
644744 ctx unsafe.Pointer
645745 }
646746 var ExtractPointer func(wrapper PointerWrapper) uintptr
@@ -658,6 +758,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
658758 }
659759 {
660760 type TwoPointers struct {
761+ _ structs.HostLayout
661762 ptr1, ptr2 unsafe.Pointer
662763 }
663764 var AddPointers func(wrapper TwoPointers) uintptr
@@ -669,7 +770,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
669770 ptr1 := unsafe.Pointer(&v1)
670771 ptr2 := unsafe.Pointer(&v2)
671772 expected := uintptr(ptr1) + uintptr(ptr2)
672- result := AddPointers(TwoPointers{ptr1, ptr2})
773+ result := AddPointers(TwoPointers{ptr1: ptr1, ptr2: ptr2})
673774 if result != expected {
674775 t.Fatalf("AddPointers returned %#x wanted %#x", result, expected)
675776 }
@@ -678,48 +779,60 @@ func TestRegisterFunc_structArgs(t *testing.T) {
678779 // Identity struct arg tests
679780 {
680781 // Single int64: one integer register.
681- type OneInt64 struct{ A int64 }
782+ type OneInt64 struct {
783+ _ structs.HostLayout
784+ A int64
785+ }
682786 var fn func(OneInt64) OneInt64
683787 register(&fn, lib, "IdentityOneInt64", func(s OneInt64) OneInt64 {
684788 return s
685789 })
686- expected := OneInt64{0xDEADBEEF}
790+ expected := OneInt64{A: 0xDEADBEEF}
687791 if ret := fn(expected); ret != expected {
688792 t.Fatalf("IdentityOneInt64 returned %+v wanted %+v", ret, expected)
689793 }
690794 }
691795 {
692796 // Two int64 fields: two integer registers.
693- type IntLessThan16Bytes struct{ X, Y int64 }
797+ type IntLessThan16Bytes struct {
798+ _ structs.HostLayout
799+ X, Y int64
800+ }
694801 var fn func(IntLessThan16Bytes) IntLessThan16Bytes
695802 register(&fn, lib, "IdentityIntLessThan16Bytes", func(s IntLessThan16Bytes) IntLessThan16Bytes {
696803 return s
697804 })
698- expected := IntLessThan16Bytes{0xDEADBEEF, -42}
805+ expected := IntLessThan16Bytes{X: 0xDEADBEEF, Y: -42}
699806 if ret := fn(expected); ret != expected {
700807 t.Fatalf("IdentityIntLessThan16Bytes returned %+v wanted %+v", ret, expected)
701808 }
702809 }
703810 {
704811 // Two float64 fields: SSE on amd64, HFA on arm64.
705- type TwoDoubleStruct struct{ X, Y float64 }
812+ type TwoDoubleStruct struct {
813+ _ structs.HostLayout
814+ X, Y float64
815+ }
706816 var fn func(TwoDoubleStruct) TwoDoubleStruct
707817 register(&fn, lib, "IdentityTwoDoubleStruct", func(s TwoDoubleStruct) TwoDoubleStruct {
708818 return s
709819 })
710- expected := TwoDoubleStruct{3.14, -2.71}
820+ expected := TwoDoubleStruct{X: 3.14, Y: -2.71}
711821 if ret := fn(expected); ret != expected {
712822 t.Fatalf("IdentityTwoDoubleStruct returned %+v wanted %+v", ret, expected)
713823 }
714824 }
715825 {
716826 // Four float32 fields: HFA on arm64, SSE on amd64.
717- type FourFloat32 struct{ A, B, C, D float32 }
827+ type FourFloat32 struct {
828+ _ structs.HostLayout
829+ A, B, C, D float32
830+ }
718831 var fn func(FourFloat32) FourFloat32
719832 register(&fn, lib, "IdentityFourFloat32", func(s FourFloat32) FourFloat32 {
720833 return s
721834 })
722- expected := FourFloat32{1.0, 2.0, 3.0, 4.0}
835+ expected := FourFloat32{A: 1.0, B: 2.0, C: 3.0, D: 4.0}
723836 if ret := fn(expected); ret != expected {
724837 t.Fatalf("IdentityFourFloat32 returned %+v wanted %+v", ret, expected)
725838 }
@@ -727,6 +840,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
727840 {
728841 // Mixed float32 + int32: INTEGER class eightbyte on amd64, non-HFA int regs on arm64.
729842 type FloatAndInt struct {
843+ _ structs.HostLayout
730844 X float32
731845 Y int32
732846 }
@@ -734,19 +848,22 @@ func TestRegisterFunc_structArgs(t *testing.T) {
734848 register(&fn, lib, "IdentityFloatAndInt", func(s FloatAndInt) FloatAndInt {
735849 return s
736850 })
737- expected := FloatAndInt{7.5, 42}
851+ expected := FloatAndInt{X: 7.5, Y: 42}
738852 if ret := fn(expected); ret != expected {
739853 t.Fatalf("IdentityFloatAndInt returned %+v wanted %+v", ret, expected)
740854 }
741855 }
742856 {
743857 // Struct > 16 bytes: hidden pointer on amd64, pointer in int register on arm64.
744- type ThreeInt64 struct{ A, B, C int64 }
858+ type ThreeInt64 struct {
859+ _ structs.HostLayout
860+ A, B, C int64
861+ }
745862 var fn func(ThreeInt64) ThreeInt64
746863 register(&fn, lib, "IdentityThreeInt64", func(s ThreeInt64) ThreeInt64 {
747864 return s
748865 })
749- expected := ThreeInt64{1, 2, 3}
866+ expected := ThreeInt64{A: 1, B: 2, C: 3}
750867 if ret := fn(expected); ret != expected {
751868 t.Fatalf("IdentityThreeInt64 returned %+v wanted %+v", ret, expected)
752869 }
@@ -754,6 +871,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
754871 {
755872 // Struct > 16 bytes with pointer fields.
756873 type PtrInt64Ptr struct {
874+ _ structs.HostLayout
757875 A *int64
758876 B int64
759877 C *int64
@@ -764,7 +882,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
764882 register(&fn, lib, "IdentityPtrInt64Ptr", func(s PtrInt64Ptr) PtrInt64Ptr {
765883 return s
766884 })
767- expected := PtrInt64Ptr{a, 200, c}
885+ expected := PtrInt64Ptr{A: a, B: 200, C: c}
768886 ret := fn(expected)
769887 if *ret.A != *expected.A || ret.B != expected.B || *ret.C != *expected.C {
770888 t.Fatalf("IdentityPtrInt64Ptr returned %+v wanted %+v", ret, expected)
@@ -772,26 +890,34 @@ func TestRegisterFunc_structArgs(t *testing.T) {
772890 runtime.KeepAlive(a)
773891 runtime.KeepAlive(c)
774892 }
775- {
893+ if runtime.GOARCH != "ppc64le" {
776894 // Struct arg after some primitive args: register offset correctness.
777- type IntLessThan16Bytes struct{ X, Y int64 }
895+ // ppc64le is skipped: a float argument shadows a GPR, which purego
896+ // does not model for scalars, so the trailing struct is misplaced.
897+ type IntLessThan16Bytes struct {
898+ _ structs.HostLayout
899+ X, Y int64
900+ }
778901 var fn func(int64, float64, IntLessThan16Bytes) IntLessThan16Bytes
779902 register(&fn, lib, "IdentityTwoInt64AfterPrims", func(x int64, y float64, s IntLessThan16Bytes) IntLessThan16Bytes {
780903 return s
781904 })
782- expected := IntLessThan16Bytes{100, 200}
905+ expected := IntLessThan16Bytes{X: 100, Y: 200}
783906 if ret := fn(1, 2.0, expected); ret != expected {
784907 t.Fatalf("IdentityTwoInt64AfterPrims returned %+v wanted %+v", ret, expected)
785908 }
786909 }
787910 {
788911 // Float struct arg after float primitive args: float register offset.
789- type FloatLessThan16Bytes struct{ X, Y float32 }
912+ type FloatLessThan16Bytes struct {
913+ _ structs.HostLayout
914+ X, Y float32
915+ }
790916 var fn func(float64, float64, FloatLessThan16Bytes) FloatLessThan16Bytes
791917 register(&fn, lib, "IdentityTwoFloat32AfterFloats", func(x, y float64, s FloatLessThan16Bytes) FloatLessThan16Bytes {
792918 return s
793919 })
794- expected := FloatLessThan16Bytes{5.0, 6.0}
920+ expected := FloatLessThan16Bytes{X: 5.0, Y: 6.0}
795921 if ret := fn(1.0, 2.0, expected); ret != expected {
796922 t.Fatalf("IdentityTwoFloat32AfterFloats returned %+v wanted %+v", ret, expected)
797923 }
@@ -799,6 +925,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
799925 {
800926 // Mixed struct with pointer: pointer + int32 + float32 + int32.
801927 type Mixed5Args struct {
928+ _ structs.HostLayout
802929 A *int64
803930 B int32
804931 C float32
@@ -810,7 +937,7 @@ func TestRegisterFunc_structArgs(t *testing.T) {
810937 register(&fn, lib, "IdentityMixed5Args", func(s Mixed5Args) Mixed5Args {
811938 return s
812939 })
813- expected := Mixed5Args{ptr, 1, 7.2, 9}
940+ expected := Mixed5Args{A: ptr, B: 1, C: 7.2, D: 9}
814941 ret := fn(expected)
815942 if ret.A != expected.A || ret.B != expected.B || ret.C != expected.C || ret.D != expected.D {
816943 t.Fatalf("IdentityMixed5Args returned %+v wanted %+v", ret, expected)
@@ -821,38 +948,23 @@ func TestRegisterFunc_structArgs(t *testing.T) {
821948 }
822949 }
823950
824-// TODO: this could use the iter.Seq interface when purego supports Go 1.23
825-func nextFieldFn(v reflect.Value) func() (reflect.Value, bool) {
826- var fieldIndex int
827- var tracker func() (reflect.Value, bool)
828- return func() (reflect.Value, bool) {
829- if v.NumField() == 0 {
830- return reflect.Value{}, false
831- }
832- if tracker != nil {
833- if field, ok := tracker(); ok {
834- return field, ok
835- }
836- tracker = nil
837- }
838- for fieldIndex < v.NumField() {
839- if v.Type().Field(fieldIndex).Name == "_" {
840- fieldIndex++
951+func fields(v reflect.Value) iter.Seq[reflect.Value] {
952+ return func(yield func(reflect.Value) bool) {
953+ for i := range v.NumField() {
954+ if v.Type().Field(i).Name == "_" {
841955 continue
842956 }
843- field := v.Field(fieldIndex)
844- fieldIndex++
957+ field := v.Field(i)
845958 if field.Kind() == reflect.Struct {
846- tracker = nextFieldFn(field)
847- if inner, ok := tracker(); ok {
848- return inner, ok
959+ for inner := range fields(field) {
960+ if !yield(inner) {
961+ return
962+ }
849963 }
850- tracker = nil
851- } else {
852- return field, true
964+ } else if !yield(field) {
965+ return
853966 }
854967 }
855- return reflect.Value{}, false
856968 }
857969 }
858970
@@ -860,36 +972,42 @@ func TestRegisterFunc_structReturns(t *testing.T) {
860972 libFileName := filepath.Join(t.TempDir(), "structreturntest.so")
861973 t.Logf("Build %v", libFileName)
862974
863- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "structtest", "structreturn_test.c")); err != nil {
975+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "structtest", "structreturn_test.c")); err != nil {
864976 t.Fatal(err)
865977 }
866978 defer os.Remove(libFileName)
867979
868- lib, err := purego.Dlopen(libFileName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
980+ lib, err := load.OpenLibrary(libFileName)
869981 if err != nil {
870- t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
982+ t.Fatalf("OpenLibrary(%q) failed: %v", libFileName, err)
871983 }
984+ defer func() {
985+ if err := load.CloseLibrary(lib); err != nil {
986+ t.Fatalf("failed to close library: %v", err)
987+ }
988+ }()
872989 implementations := []struct {
873- name string
874- register func(fptr any, handle uintptr, name string)
990+ name string
991+ usesCallbacks bool
992+ register func(fptr any, handle uintptr, name string)
875993 }{
876994 {
877995 name: "RegisterLibFunc",
878996 register: purego.RegisterLibFunc,
879997 },
880998 {
881- name: "GoCallbackFunc",
999+ name: "GoCallbackFunc",
1000+ usesCallbacks: true,
8821001 register: func(fptr any, _ uintptr, _ string) {
8831002 fn := reflect.MakeFunc(reflect.TypeOf(fptr).Elem(), func(args []reflect.Value) []reflect.Value {
8841003 retType := reflect.TypeOf(fptr).Elem().Out(0)
8851004 ret := reflect.New(retType).Elem()
886- next := nextFieldFn(ret)
887- for _, a := range args {
888- field, ok := next()
889- if !ok {
1005+ leaves := slices.Collect(fields(ret))
1006+ for i, a := range args {
1007+ if i >= len(leaves) {
8901008 panic("purego: no more fields")
8911009 }
892- field.Set(a)
1010+ leaves[i].Set(a)
8931011 }
8941012 return []reflect.Value{ret}
8951013 })
@@ -898,170 +1016,239 @@ func TestRegisterFunc_structReturns(t *testing.T) {
8981016 },
8991017 }
9001018 for _, imp := range implementations {
901- imp := imp
1019+ if imp.usesCallbacks && runtime.GOOS == "windows" {
1020+ // Callbacks on Windows use the stdlib syscall.NewCallback, which does
1021+ // not support struct arguments or returns.
1022+ continue
1023+ }
1024+ if imp.usesCallbacks && runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
1025+ continue
1026+ }
9021027 t.Run(imp.name, func(t *testing.T) {
9031028 register := imp.register
9041029 {
905- type Empty struct{}
1030+ type Empty struct {
1031+ _ structs.HostLayout
1032+ }
9061033 var ReturnEmpty func() Empty
9071034 register(&ReturnEmpty, lib, "ReturnEmpty")
9081035 ret := ReturnEmpty()
9091036 _ = ret
9101037 }
9111038 {
912- type inner struct{ A int16 }
1039+ type inner struct {
1040+ _ structs.HostLayout
1041+ A int16
1042+ }
9131043 type StructInStruct struct {
1044+ _ structs.HostLayout
9141045 A inner
9151046 B inner
9161047 C inner
9171048 }
9181049 var ReturnStructInStruct func(a, b, c int16) StructInStruct
9191050 register(&ReturnStructInStruct, lib, "ReturnStructInStruct")
920- expected := StructInStruct{inner{^int16(0)}, inner{2}, inner{3}}
1051+ expected := StructInStruct{A: inner{A: ^int16(0)}, B: inner{A: 2}, C: inner{A: 3}}
9211052 if ret := ReturnStructInStruct(^int16(0), 2, 3); ret != expected {
9221053 t.Fatalf("StructInStruct returned %+v wanted %+v", ret, expected)
9231054 }
9241055 }
9251056 {
926- type ThreeShorts struct{ A, B, C int16 }
1057+ type ThreeShorts struct {
1058+ _ structs.HostLayout
1059+ A, B, C int16
1060+ }
9271061 var ReturnThreeShorts func(a, b, c int16) ThreeShorts
9281062 register(&ReturnThreeShorts, lib, "ReturnThreeShorts")
929- expected := ThreeShorts{^int16(0), 2, 3}
1063+ expected := ThreeShorts{A: ^int16(0), B: 2, C: 3}
9301064 if ret := ReturnThreeShorts(^int16(0), 2, 3); ret != expected {
9311065 t.Fatalf("ReturnThreeShorts returned %+v wanted %+v", ret, expected)
9321066 }
9331067 }
9341068 {
935- type FourShorts struct{ A, B, C, D int16 }
1069+ type FourShorts struct {
1070+ _ structs.HostLayout
1071+ A, B, C, D int16
1072+ }
9361073 var ReturnFourShorts func(a, b, c, d int16) FourShorts
9371074 register(&ReturnFourShorts, lib, "ReturnFourShorts")
938- expected := FourShorts{^int16(0), 2, 3, 4}
1075+ expected := FourShorts{A: ^int16(0), B: 2, C: 3, D: 4}
9391076 if ret := ReturnFourShorts(^int16(0), 2, 3, 4); ret != expected {
9401077 t.Fatalf("ReturnFourShorts returned %+v wanted %+v", ret, expected)
9411078 }
9421079 }
9431080 {
944- type OneLong struct{ A int64 }
1081+ type OneLong struct {
1082+ _ structs.HostLayout
1083+ A int64
1084+ }
9451085 var ReturnOneLong func(a int64) OneLong
9461086 register(&ReturnOneLong, lib, "ReturnOneLong")
947- expected := OneLong{5}
1087+ expected := OneLong{A: 5}
9481088 if ret := ReturnOneLong(5); ret != expected {
9491089 t.Fatalf("ReturnOneLong returned %+v wanted %+v", ret, expected)
9501090 }
9511091 }
9521092 {
953- type TwoLongs struct{ A, B int64 }
1093+ type TwoLongs struct {
1094+ _ structs.HostLayout
1095+ A, B int64
1096+ }
9541097 var ReturnTwoLongs func(a, b int64) TwoLongs
9551098 register(&ReturnTwoLongs, lib, "ReturnTwoLongs")
956- expected := TwoLongs{1, 2}
1099+ expected := TwoLongs{A: 1, B: 2}
9571100 if ret := ReturnTwoLongs(1, 2); ret != expected {
9581101 t.Fatalf("ReturnTwoLongs returned %+v wanted %+v", ret, expected)
9591102 }
9601103 }
9611104 {
962- type ThreeLongs struct{ A, B, C int64 }
1105+ type ThreeLongs struct {
1106+ _ structs.HostLayout
1107+ A, B, C int64
1108+ }
9631109 var ReturnThreeLongs func(a, b, c int64) ThreeLongs
9641110 register(&ReturnThreeLongs, lib, "ReturnThreeLongs")
965- expected := ThreeLongs{1, 2, 3}
1111+ expected := ThreeLongs{A: 1, B: 2, C: 3}
9661112 if ret := ReturnThreeLongs(1, 2, 3); ret != expected {
9671113 t.Fatalf("ReturnThreeLongs returned %+v wanted %+v", ret, expected)
9681114 }
9691115 }
9701116 {
971- type OneFloat struct{ A float32 }
1117+ type OneFloat struct {
1118+ _ structs.HostLayout
1119+ A float32
1120+ }
9721121 var ReturnOneFloat func(a float32) OneFloat
9731122 register(&ReturnOneFloat, lib, "ReturnOneFloat")
974- expected := OneFloat{1}
1123+ expected := OneFloat{A: 1}
9751124 if ret := ReturnOneFloat(1); ret != expected {
9761125 t.Fatalf("ReturnOneFloat returned %+v wanted %+v", ret, expected)
9771126 }
9781127 }
9791128 {
980- type TwoFloats struct{ A, B float32 }
1129+ type TwoFloats struct {
1130+ _ structs.HostLayout
1131+ A, B float32
1132+ }
9811133 var ReturnTwoFloats func(a, b float32) TwoFloats
9821134 register(&ReturnTwoFloats, lib, "ReturnTwoFloats")
983- expected := TwoFloats{5, 2}
1135+ expected := TwoFloats{A: 5, B: 2}
9841136 if ret := ReturnTwoFloats(5, 2); ret != expected {
9851137 t.Fatalf("ReturnTwoFloats returned %+v wanted %+v", ret, expected)
9861138 }
9871139 }
9881140 {
989- type ThreeFloats struct{ A, B, C float32 }
1141+ type ThreeFloats struct {
1142+ _ structs.HostLayout
1143+ A, B, C float32
1144+ }
9901145 var ReturnThreeFloats func(a, b, c float32) ThreeFloats
9911146 register(&ReturnThreeFloats, lib, "ReturnThreeFloats")
992- expected := ThreeFloats{1, 2, 3}
1147+ expected := ThreeFloats{A: 1, B: 2, C: 3}
9931148 if ret := ReturnThreeFloats(1, 2, 3); ret != expected {
9941149 t.Fatalf("ReturnThreeFloats returned %+v wanted %+v", ret, expected)
9951150 }
9961151 }
9971152 {
998- type FourFloats struct{ A, B, C, D float32 }
1153+ type FourFloats struct {
1154+ _ structs.HostLayout
1155+ A, B, C, D float32
1156+ }
9991157 var ReturnFourFloats func(a, b, c, d float32) FourFloats
10001158 register(&ReturnFourFloats, lib, "ReturnFourFloats")
1001- expected := FourFloats{1, 2, 3, 4}
1159+ expected := FourFloats{A: 1, B: 2, C: 3, D: 4}
10021160 if ret := ReturnFourFloats(1, 2, 3, 4); ret != expected {
10031161 t.Fatalf("ReturnFourFloats returned %+v wanted %+v", ret, expected)
10041162 }
10051163 }
10061164 {
1007- type OneDouble struct{ A float64 }
1165+ type OneDouble struct {
1166+ _ structs.HostLayout
1167+ A float64
1168+ }
10081169 var ReturnOneDouble func(a float64) OneDouble
10091170 register(&ReturnOneDouble, lib, "ReturnOneDouble")
1010- expected := OneDouble{1}
1171+ expected := OneDouble{A: 1}
10111172 if ret := ReturnOneDouble(1); ret != expected {
10121173 t.Fatalf("ReturnOneDouble returned %+v wanted %+v", ret, expected)
10131174 }
10141175 }
10151176 {
1016- type TwoDoubles struct{ A, B float64 }
1177+ type TwoDoubles struct {
1178+ _ structs.HostLayout
1179+ A, B float64
1180+ }
10171181 var ReturnTwoDoubles func(a, b float64) TwoDoubles
10181182 register(&ReturnTwoDoubles, lib, "ReturnTwoDoubles")
1019- expected := TwoDoubles{7, 3}
1183+ expected := TwoDoubles{A: 7, B: 3}
10201184 if ret := ReturnTwoDoubles(7, 3); ret != expected {
10211185 t.Fatalf("ReturnTwoDoubles returned %+v wanted %+v", ret, expected)
10221186 }
10231187 }
10241188 {
1025- type ThreeDoubles struct{ A, B, C float64 }
1189+ type ThreeDoubles struct {
1190+ _ structs.HostLayout
1191+ A, B, C float64
1192+ }
10261193 var ReturnThreeDoubles func(a, b, c float64) ThreeDoubles
10271194 register(&ReturnThreeDoubles, lib, "ReturnThreeDoubles")
1028- expected := ThreeDoubles{1, 2, 3}
1195+ expected := ThreeDoubles{A: 1, B: 2, C: 3}
10291196 if ret := ReturnThreeDoubles(1, 2, 3); ret != expected {
10301197 t.Fatalf("ReturnThreeDoubles returned %+v wanted %+v", ret, expected)
10311198 }
10321199 }
10331200 {
1034- type FourDoubles struct{ A, B, C, D float64 }
1201+ type FourDoubles struct {
1202+ _ structs.HostLayout
1203+ A, B, C, D float64
1204+ }
10351205 var ReturnFourDoubles func(a, b, c, d float64) FourDoubles
10361206 register(&ReturnFourDoubles, lib, "ReturnFourDoubles")
1037- expected := FourDoubles{1, 2, 3, 4}
1207+ expected := FourDoubles{A: 1, B: 2, C: 3, D: 4}
10381208 if ret := ReturnFourDoubles(1, 2, 3, 4); ret != expected {
10391209 t.Fatalf("ReturnFourDoubles returned %+v wanted %+v", ret, expected)
10401210 }
10411211 }
10421212 {
10431213 type FourDoublesInternal struct {
1044- F struct{ A, B float64 }
1045- G struct{ C, D float64 }
1214+ _ structs.HostLayout
1215+ F struct {
1216+ _ structs.HostLayout
1217+ A, B float64
1218+ }
1219+ G struct {
1220+ _ structs.HostLayout
1221+ C, D float64
1222+ }
10461223 }
10471224 var ReturnFourDoublesInternal func(a, b, c, d float64) FourDoublesInternal
10481225 register(&ReturnFourDoublesInternal, lib, "ReturnFourDoublesInternal")
1049- expected := FourDoublesInternal{F: struct{ A, B float64 }{A: 1, B: 2}, G: struct{ C, D float64 }{C: 3, D: 4}}
1226+ expected := FourDoublesInternal{F: struct {
1227+ _ structs.HostLayout
1228+ A, B float64
1229+ }{A: 1, B: 2}, G: struct {
1230+ _ structs.HostLayout
1231+ C, D float64
1232+ }{C: 3, D: 4}}
10501233 if ret := ReturnFourDoublesInternal(1, 2, 3, 4); ret != expected {
10511234 t.Fatalf("ReturnFourDoublesInternal returned %+v wanted %+v", ret, expected)
10521235 }
10531236 }
10541237 {
1055- type FiveDoubles struct{ A, B, C, D, E float64 }
1238+ type FiveDoubles struct {
1239+ _ structs.HostLayout
1240+ A, B, C, D, E float64
1241+ }
10561242 var ReturnFiveDoubles func(a, b, c, d, e float64) FiveDoubles
10571243 register(&ReturnFiveDoubles, lib, "ReturnFiveDoubles")
1058- expected := FiveDoubles{1, 2, 3, 4, 5}
1244+ expected := FiveDoubles{A: 1, B: 2, C: 3, D: 4, E: 5}
10591245 if ret := ReturnFiveDoubles(1, 2, 3, 4, 5); ret != expected {
10601246 t.Fatalf("ReturnFiveDoubles returned %+v wanted %+v", ret, expected)
10611247 }
10621248 }
10631249 {
10641250 type OneFloatOneDouble struct {
1251+ _ structs.HostLayout
10651252 A float32
10661253 _ float32
10671254 B float64
@@ -1075,18 +1262,20 @@ func TestRegisterFunc_structReturns(t *testing.T) {
10751262 }
10761263 {
10771264 type OneDoubleOneFloat struct {
1265+ _ structs.HostLayout
10781266 A float64
10791267 B float32
10801268 }
10811269 var ReturnOneDoubleOneFloat func(a float64, b float32) OneDoubleOneFloat
10821270 register(&ReturnOneDoubleOneFloat, lib, "ReturnOneDoubleOneFloat")
1083- expected := OneDoubleOneFloat{1, 2}
1271+ expected := OneDoubleOneFloat{A: 1, B: 2}
10841272 if ret := ReturnOneDoubleOneFloat(1, 2); ret != expected {
10851273 t.Fatalf("ReturnOneDoubleOneFloat returned %+v wanted %+v", ret, expected)
10861274 }
10871275 }
10881276 {
10891277 type Unaligned1 struct {
1278+ _ structs.HostLayout
10901279 A int8
10911280 _ [1]int8
10921281 B int16
@@ -1101,76 +1290,88 @@ func TestRegisterFunc_structReturns(t *testing.T) {
11011290 }
11021291 }
11031292
1104- {
1105- type Mixed1 struct {
1106- A float32
1107- B int32
1108- }
1109- var ReturnMixed1 func(a float32, b int32) Mixed1
1110- register(&ReturnMixed1, lib, "ReturnMixed1")
1111- expected := Mixed1{1, 2}
1112- if ret := ReturnMixed1(1, 2); ret != expected {
1113- t.Fatalf("ReturnMixed1 returned %+v wanted %+v", ret, expected)
1114- }
1115- }
1116- {
1117- type Mixed2 struct {
1118- A float32
1119- B int32
1120- C float32
1121- D int32
1122- }
1123- var ReturnMixed2 func(a float32, b int32, c float32, d int32) Mixed2
1124- register(&ReturnMixed2, lib, "ReturnMixed2")
1125- expected := Mixed2{1, 2, 3, 4}
1126- if ret := ReturnMixed2(1, 2, 3, 4); ret != expected {
1127- t.Fatalf("ReturnMixed2 returned %+v wanted %+v", ret, expected)
1128- }
1129- }
1130- {
1131- type Mixed3 struct {
1132- A float32
1133- B uint32
1134- C float64
1135- }
1136- var ReturnMixed3 func(a float32, b uint32, c float64) Mixed3
1137- register(&ReturnMixed3, lib, "ReturnMixed3")
1138- expected := Mixed3{1, 2, 3}
1139- if ret := ReturnMixed3(1, 2, 3); ret != expected {
1140- t.Fatalf("ReturnMixed3 returned %+v wanted %+v", ret, expected)
1293+ if runtime.GOARCH != "ppc64le" {
1294+ // ppc64le is skipped: a float argument shadows a GPR, which purego
1295+ // does not model for scalars, so a trailing integer arg is misplaced.
1296+ // TODO: Support mixed scalar floating-point and integer arguments on
1297+ // ppc64le by modeling the ELFv2 general-purpose register shadowing.
1298+ {
1299+ type Mixed1 struct {
1300+ _ structs.HostLayout
1301+ A float32
1302+ B int32
1303+ }
1304+ var ReturnMixed1 func(a float32, b int32) Mixed1
1305+ register(&ReturnMixed1, lib, "ReturnMixed1")
1306+ expected := Mixed1{A: 1, B: 2}
1307+ if ret := ReturnMixed1(1, 2); ret != expected {
1308+ t.Fatalf("ReturnMixed1 returned %+v wanted %+v", ret, expected)
1309+ }
11411310 }
1142- }
1143- {
1144- type Mixed4 struct {
1145- A float64
1146- B uint32
1147- C float32
1311+ {
1312+ type Mixed2 struct {
1313+ _ structs.HostLayout
1314+ A float32
1315+ B int32
1316+ C float32
1317+ D int32
1318+ }
1319+ var ReturnMixed2 func(a float32, b int32, c float32, d int32) Mixed2
1320+ register(&ReturnMixed2, lib, "ReturnMixed2")
1321+ expected := Mixed2{A: 1, B: 2, C: 3, D: 4}
1322+ if ret := ReturnMixed2(1, 2, 3, 4); ret != expected {
1323+ t.Fatalf("ReturnMixed2 returned %+v wanted %+v", ret, expected)
1324+ }
11481325 }
1149- var ReturnMixed4 func(a float64, b uint32, c float32) Mixed4
1150- register(&ReturnMixed4, lib, "ReturnMixed4")
1151- expected := Mixed4{1, 2, 3}
1152- if ret := ReturnMixed4(1, 2, 3); ret != expected {
1153- t.Fatalf("ReturnMixed4 returned %+v wanted %+v", ret, expected)
1326+ {
1327+ type Mixed3 struct {
1328+ _ structs.HostLayout
1329+ A float32
1330+ B uint32
1331+ C float64
1332+ }
1333+ var ReturnMixed3 func(a float32, b uint32, c float64) Mixed3
1334+ register(&ReturnMixed3, lib, "ReturnMixed3")
1335+ expected := Mixed3{A: 1, B: 2, C: 3}
1336+ if ret := ReturnMixed3(1, 2, 3); ret != expected {
1337+ t.Fatalf("ReturnMixed3 returned %+v wanted %+v", ret, expected)
1338+ }
11541339 }
1155- }
1156- {
1157- type Mixed5 struct {
1158- A *int64
1159- B int32
1160- C float32
1161- D int32
1340+ {
1341+ type Mixed4 struct {
1342+ _ structs.HostLayout
1343+ A float64
1344+ B uint32
1345+ C float32
1346+ }
1347+ var ReturnMixed4 func(a float64, b uint32, c float32) Mixed4
1348+ register(&ReturnMixed4, lib, "ReturnMixed4")
1349+ expected := Mixed4{A: 1, B: 2, C: 3}
1350+ if ret := ReturnMixed4(1, 2, 3); ret != expected {
1351+ t.Fatalf("ReturnMixed4 returned %+v wanted %+v", ret, expected)
1352+ }
11621353 }
1163- var ReturnMixed5 func(a *int64, b int32, c float32, d int32) Mixed5
1164- register(&ReturnMixed5, lib, "ReturnMixed5")
1165- ptr := new(int64)
1166- expected := Mixed5{ptr, 1, 7.2, 9}
1167- if ret := ReturnMixed5(ptr, 1, 7.2, 9); ret != expected {
1168- t.Fatalf("ReturnMixed5 returned %+v wanted %+v", ret, expected)
1354+ {
1355+ type Mixed5 struct {
1356+ _ structs.HostLayout
1357+ A *int64
1358+ B int32
1359+ C float32
1360+ D int32
1361+ }
1362+ var ReturnMixed5 func(a *int64, b int32, c float32, d int32) Mixed5
1363+ register(&ReturnMixed5, lib, "ReturnMixed5")
1364+ ptr := new(int64)
1365+ expected := Mixed5{A: ptr, B: 1, C: 7.2, D: 9}
1366+ if ret := ReturnMixed5(ptr, 1, 7.2, 9); ret != expected {
1367+ t.Fatalf("ReturnMixed5 returned %+v wanted %+v", ret, expected)
1368+ }
1369+ runtime.KeepAlive(ptr)
11691370 }
1170- runtime.KeepAlive(ptr)
11711371 }
11721372 {
11731373 type Mixed5 struct {
1374+ _ structs.HostLayout
11741375 A *int64
11751376 B int32
11761377 C float32
@@ -1182,7 +1383,7 @@ func TestRegisterFunc_structReturns(t *testing.T) {
11821383 // Struct callback args are tested in TestRegisterFunc_structArgs.
11831384 purego.RegisterLibFunc(&IdentityMixed5, lib, "IdentityMixed5")
11841385 ptr := new(int64)
1185- expected := Mixed5{ptr, 1, 7.2, 9}
1386+ expected := Mixed5{A: ptr, B: 1, C: 7.2, D: 9}
11861387 if ret := IdentityMixed5(expected); ret != expected {
11871388 t.Fatalf("IdentityMixed5 returned %+v wanted %+v", ret, expected)
11881389 }
@@ -1190,19 +1391,21 @@ func TestRegisterFunc_structReturns(t *testing.T) {
11901391 }
11911392 {
11921393 type SmallBool struct {
1394+ _ structs.HostLayout
11931395 A bool
11941396 B int32
11951397 C int64
11961398 }
11971399 var ReturnSmallBool func(a bool, b int32, c int64) SmallBool
11981400 register(&ReturnSmallBool, lib, "ReturnSmallBool")
1199- expected := SmallBool{true, 42, 123456789}
1401+ expected := SmallBool{A: true, B: 42, C: 123456789}
12001402 if ret := ReturnSmallBool(true, 42, 123456789); ret != expected {
12011403 t.Fatalf("ReturnSmallBool returned %+v wanted %+v", ret, expected)
12021404 }
12031405 }
12041406 {
12051407 type LargeBool struct {
1408+ _ structs.HostLayout
12061409 A bool
12071410 B int32
12081411 C int64
@@ -1210,20 +1413,21 @@ func TestRegisterFunc_structReturns(t *testing.T) {
12101413 }
12111414 var ReturnLargeBool func(a bool, b int32, c int64, d int64) LargeBool
12121415 register(&ReturnLargeBool, lib, "ReturnLargeBool")
1213- expected := LargeBool{false, -99, 987654321, 111222333444}
1416+ expected := LargeBool{A: false, B: -99, C: 987654321, D: 111222333444}
12141417 if ret := ReturnLargeBool(false, -99, 987654321, 111222333444); ret != expected {
12151418 t.Fatalf("ReturnLargeBool returned %+v wanted %+v", ret, expected)
12161419 }
12171420 }
12181421 {
12191422 type Ptr1 struct {
1423+ _ structs.HostLayout
12201424 a *int64
12211425 b unsafe.Pointer
12221426 }
12231427 var ReturnPtr1 func(a *int64, b unsafe.Pointer) Ptr1
12241428 purego.RegisterLibFunc(&ReturnPtr1, lib, "ReturnPtr1")
12251429 a, b := new(int64), new(struct{})
1226- expected := Ptr1{a, unsafe.Pointer(b)}
1430+ expected := Ptr1{a: a, b: unsafe.Pointer(b)}
12271431 if ret := ReturnPtr1(a, unsafe.Pointer(b)); ret != expected {
12281432 t.Fatalf("ReturnPtr1 returned %+v wanted %+v", ret, expected)
12291433 }
M sys_386.s +44 -43
@@ -10,8 +10,8 @@
1010 #define STACK_SIZE 160
1111 #define PTR_ADDRESS (STACK_SIZE - 4)
1212
13-// syscall15X calls a function in libc on behalf of the syscall package.
14-// syscall15X takes a pointer to a struct like:
13+// syscallX calls a function in libc on behalf of the syscall package.
14+// syscallX takes a pointer to a struct like:
1515 // struct {
1616 // fn uintptr
1717 // a1 uintptr
@@ -22,18 +22,18 @@
2222 // f16 uintptr
2323 // arm64_r8 uintptr
2424 // }
25-// syscall15X must be called on the g0 stack with the
25+// syscallX must be called on the g0 stack with the
2626 // C calling convention (use libcCall).
2727 //
2828 // On i386 System V ABI, all arguments are passed on the stack.
2929 // Return value is in EAX (and EDX for 64-bit values).
30-GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $4
31-DATA ·syscall15XABI0(SB)/4, $syscall15X(SB)
32-TEXT syscall15X(SB), NOSPLIT|NOFRAME, $0-0
30+GLOBL ·syscallXABI0(SB), NOPTR|RODATA, $4
31+DATA ·syscallXABI0(SB)/4, $syscallX(SB)
32+TEXT syscallX(SB), NOSPLIT|NOFRAME, $0-0
3333 // Called via C calling convention: argument pointer at 4(SP)
3434 // NOT via Go calling convention
3535 // On i386, the first argument is at 4(SP) after CALL pushes return address
36- MOVL 4(SP), AX // get pointer to syscall15Args
36+ MOVL 4(SP), AX // get pointer to syscallArgs
3737
3838 // Save callee-saved registers
3939 PUSHL BP
@@ -50,75 +50,75 @@ TEXT syscall15X(SB), NOSPLIT|NOFRAME, $0-0
5050 MOVL BX, PTR_ADDRESS(SP) // save args pointer
5151
5252 // Load function pointer
53- MOVL syscall15Args_fn(BX), AX
53+ MOVL syscallArgs_fn(BX), AX
5454 MOVL AX, (PTR_ADDRESS-4)(SP) // save fn pointer
5555
5656 // Push all integer arguments onto the stack (a1-a32)
5757 // i386 SysV ABI: arguments pushed right-to-left, but we're
5858 // setting up the stack from low to high addresses
59- MOVL syscall15Args_a1(BX), AX
59+ MOVL syscallArgs_a1(BX), AX
6060 MOVL AX, 0(SP)
61- MOVL syscall15Args_a2(BX), AX
61+ MOVL syscallArgs_a2(BX), AX
6262 MOVL AX, 4(SP)
63- MOVL syscall15Args_a3(BX), AX
63+ MOVL syscallArgs_a3(BX), AX
6464 MOVL AX, 8(SP)
65- MOVL syscall15Args_a4(BX), AX
65+ MOVL syscallArgs_a4(BX), AX
6666 MOVL AX, 12(SP)
67- MOVL syscall15Args_a5(BX), AX
67+ MOVL syscallArgs_a5(BX), AX
6868 MOVL AX, 16(SP)
69- MOVL syscall15Args_a6(BX), AX
69+ MOVL syscallArgs_a6(BX), AX
7070 MOVL AX, 20(SP)
71- MOVL syscall15Args_a7(BX), AX
71+ MOVL syscallArgs_a7(BX), AX
7272 MOVL AX, 24(SP)
73- MOVL syscall15Args_a8(BX), AX
73+ MOVL syscallArgs_a8(BX), AX
7474 MOVL AX, 28(SP)
75- MOVL syscall15Args_a9(BX), AX
75+ MOVL syscallArgs_a9(BX), AX
7676 MOVL AX, 32(SP)
77- MOVL syscall15Args_a10(BX), AX
77+ MOVL syscallArgs_a10(BX), AX
7878 MOVL AX, 36(SP)
79- MOVL syscall15Args_a11(BX), AX
79+ MOVL syscallArgs_a11(BX), AX
8080 MOVL AX, 40(SP)
81- MOVL syscall15Args_a12(BX), AX
81+ MOVL syscallArgs_a12(BX), AX
8282 MOVL AX, 44(SP)
83- MOVL syscall15Args_a13(BX), AX
83+ MOVL syscallArgs_a13(BX), AX
8484 MOVL AX, 48(SP)
85- MOVL syscall15Args_a14(BX), AX
85+ MOVL syscallArgs_a14(BX), AX
8686 MOVL AX, 52(SP)
87- MOVL syscall15Args_a15(BX), AX
87+ MOVL syscallArgs_a15(BX), AX
8888 MOVL AX, 56(SP)
89- MOVL syscall15Args_a16(BX), AX
89+ MOVL syscallArgs_a16(BX), AX
9090 MOVL AX, 60(SP)
91- MOVL syscall15Args_a17(BX), AX
91+ MOVL syscallArgs_a17(BX), AX
9292 MOVL AX, 64(SP)
93- MOVL syscall15Args_a18(BX), AX
93+ MOVL syscallArgs_a18(BX), AX
9494 MOVL AX, 68(SP)
95- MOVL syscall15Args_a19(BX), AX
95+ MOVL syscallArgs_a19(BX), AX
9696 MOVL AX, 72(SP)
97- MOVL syscall15Args_a20(BX), AX
97+ MOVL syscallArgs_a20(BX), AX
9898 MOVL AX, 76(SP)
99- MOVL syscall15Args_a21(BX), AX
99+ MOVL syscallArgs_a21(BX), AX
100100 MOVL AX, 80(SP)
101- MOVL syscall15Args_a22(BX), AX
101+ MOVL syscallArgs_a22(BX), AX
102102 MOVL AX, 84(SP)
103- MOVL syscall15Args_a23(BX), AX
103+ MOVL syscallArgs_a23(BX), AX
104104 MOVL AX, 88(SP)
105- MOVL syscall15Args_a24(BX), AX
105+ MOVL syscallArgs_a24(BX), AX
106106 MOVL AX, 92(SP)
107- MOVL syscall15Args_a25(BX), AX
107+ MOVL syscallArgs_a25(BX), AX
108108 MOVL AX, 96(SP)
109- MOVL syscall15Args_a26(BX), AX
109+ MOVL syscallArgs_a26(BX), AX
110110 MOVL AX, 100(SP)
111- MOVL syscall15Args_a27(BX), AX
111+ MOVL syscallArgs_a27(BX), AX
112112 MOVL AX, 104(SP)
113- MOVL syscall15Args_a28(BX), AX
113+ MOVL syscallArgs_a28(BX), AX
114114 MOVL AX, 108(SP)
115- MOVL syscall15Args_a29(BX), AX
115+ MOVL syscallArgs_a29(BX), AX
116116 MOVL AX, 112(SP)
117- MOVL syscall15Args_a30(BX), AX
117+ MOVL syscallArgs_a30(BX), AX
118118 MOVL AX, 116(SP)
119- MOVL syscall15Args_a31(BX), AX
119+ MOVL syscallArgs_a31(BX), AX
120120 MOVL AX, 120(SP)
121- MOVL syscall15Args_a32(BX), AX
121+ MOVL syscallArgs_a32(BX), AX
122122 MOVL AX, 124(SP)
123123
124124 // Call the C function
@@ -127,13 +127,14 @@ TEXT syscall15X(SB), NOSPLIT|NOFRAME, $0-0
127127
128128 // Get args pointer back and save results
129129 MOVL PTR_ADDRESS(SP), BX
130- MOVL AX, syscall15Args_a1(BX) // return value r1
131- MOVL DX, syscall15Args_a2(BX) // return value r2 (for 64-bit returns)
130+ MOVL AX, syscallArgs_a1(BX) // return value r1
131+ MOVL DX, syscallArgs_a2(BX) // return value r2 (for 64-bit returns)
132+ MOVL $0, syscallArgs_a3(BX) // no errno here: clear the input argument
132133
133134 // Save x87 FPU return value (ST0) to f1 field
134135 // On i386 System V ABI, float/double returns are in ST(0)
135136 // We save as float64 (8 bytes) to preserve precision
136- FMOVDP F0, syscall15Args_f1(BX)
137+ FMOVDP F0, syscallArgs_f1(BX)
137138
138139 // Clean up stack
139140 ADDL $STACK_SIZE, SP
M sys_amd64.s +73 -37
@@ -8,11 +8,11 @@
88 #include "go_asm.h"
99 #include "funcdata.h"
1010
11-#define STACK_SIZE 80
11+#define STACK_SIZE 224
1212 #define PTR_ADDRESS (STACK_SIZE - 8)
1313
14-// syscall15X calls a function in libc on behalf of the syscall package.
15-// syscall15X takes a pointer to a struct like:
14+// syscallX calls a function in libc on behalf of the syscall package.
15+// syscallX takes a pointer to a struct like:
1616 // struct {
1717 // fn uintptr
1818 // a1 uintptr
@@ -34,65 +34,101 @@
3434 // r2 uintptr
3535 // err uintptr
3636 // }
37-// syscall15X must be called on the g0 stack with the
37+// syscallX must be called on the g0 stack with the
3838 // C calling convention (use libcCall).
39-GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8
40-DATA ·syscall15XABI0(SB)/8, $syscall15X(SB)
41-TEXT syscall15X(SB), NOSPLIT, $STACK_SIZE
39+GLOBL ·syscallXABI0(SB), NOPTR|RODATA, $8
40+DATA ·syscallXABI0(SB)/8, $syscallX(SB)
41+TEXT syscallX(SB), NOSPLIT, $STACK_SIZE
4242 MOVQ DI, PTR_ADDRESS(SP) // save the pointer
4343 MOVQ DI, R11
4444
45- MOVQ syscall15Args_f1(R11), X0 // f1
46- MOVQ syscall15Args_f2(R11), X1 // f2
47- MOVQ syscall15Args_f3(R11), X2 // f3
48- MOVQ syscall15Args_f4(R11), X3 // f4
49- MOVQ syscall15Args_f5(R11), X4 // f5
50- MOVQ syscall15Args_f6(R11), X5 // f6
51- MOVQ syscall15Args_f7(R11), X6 // f7
52- MOVQ syscall15Args_f8(R11), X7 // f8
53-
54- MOVQ syscall15Args_a1(R11), DI // a1
55- MOVQ syscall15Args_a2(R11), SI // a2
56- MOVQ syscall15Args_a3(R11), DX // a3
57- MOVQ syscall15Args_a4(R11), CX // a4
58- MOVQ syscall15Args_a5(R11), R8 // a5
59- MOVQ syscall15Args_a6(R11), R9 // a6
45+ MOVQ syscallArgs_f1(R11), X0 // f1
46+ MOVQ syscallArgs_f2(R11), X1 // f2
47+ MOVQ syscallArgs_f3(R11), X2 // f3
48+ MOVQ syscallArgs_f4(R11), X3 // f4
49+ MOVQ syscallArgs_f5(R11), X4 // f5
50+ MOVQ syscallArgs_f6(R11), X5 // f6
51+ MOVQ syscallArgs_f7(R11), X6 // f7
52+ MOVQ syscallArgs_f8(R11), X7 // f8
53+
54+ MOVQ syscallArgs_a1(R11), DI // a1
55+ MOVQ syscallArgs_a2(R11), SI // a2
56+ MOVQ syscallArgs_a3(R11), DX // a3
57+ MOVQ syscallArgs_a4(R11), CX // a4
58+ MOVQ syscallArgs_a5(R11), R8 // a5
59+ MOVQ syscallArgs_a6(R11), R9 // a6
6060
6161 // push the remaining parameters onto the stack
62- MOVQ syscall15Args_a7(R11), R12
62+ MOVQ syscallArgs_a7(R11), R12
6363 MOVQ R12, 0(SP) // push a7
64- MOVQ syscall15Args_a8(R11), R12
64+ MOVQ syscallArgs_a8(R11), R12
6565 MOVQ R12, 8(SP) // push a8
66- MOVQ syscall15Args_a9(R11), R12
66+ MOVQ syscallArgs_a9(R11), R12
6767 MOVQ R12, 16(SP) // push a9
68- MOVQ syscall15Args_a10(R11), R12
68+ MOVQ syscallArgs_a10(R11), R12
6969 MOVQ R12, 24(SP) // push a10
70- MOVQ syscall15Args_a11(R11), R12
70+ MOVQ syscallArgs_a11(R11), R12
7171 MOVQ R12, 32(SP) // push a11
72- MOVQ syscall15Args_a12(R11), R12
72+ MOVQ syscallArgs_a12(R11), R12
7373 MOVQ R12, 40(SP) // push a12
74- MOVQ syscall15Args_a13(R11), R12
74+ MOVQ syscallArgs_a13(R11), R12
7575 MOVQ R12, 48(SP) // push a13
76- MOVQ syscall15Args_a14(R11), R12
76+ MOVQ syscallArgs_a14(R11), R12
7777 MOVQ R12, 56(SP) // push a14
78- MOVQ syscall15Args_a15(R11), R12
78+ MOVQ syscallArgs_a15(R11), R12
7979 MOVQ R12, 64(SP) // push a15
80+ MOVQ syscallArgs_a16(R11), R12
81+ MOVQ R12, 72(SP) // push a16
82+ MOVQ syscallArgs_a17(R11), R12
83+ MOVQ R12, 80(SP) // push a17
84+ MOVQ syscallArgs_a18(R11), R12
85+ MOVQ R12, 88(SP) // push a18
86+ MOVQ syscallArgs_a19(R11), R12
87+ MOVQ R12, 96(SP) // push a19
88+ MOVQ syscallArgs_a20(R11), R12
89+ MOVQ R12, 104(SP) // push a20
90+ MOVQ syscallArgs_a21(R11), R12
91+ MOVQ R12, 112(SP) // push a21
92+ MOVQ syscallArgs_a22(R11), R12
93+ MOVQ R12, 120(SP) // push a22
94+ MOVQ syscallArgs_a23(R11), R12
95+ MOVQ R12, 128(SP) // push a23
96+ MOVQ syscallArgs_a24(R11), R12
97+ MOVQ R12, 136(SP) // push a24
98+ MOVQ syscallArgs_a25(R11), R12
99+ MOVQ R12, 144(SP) // push a25
100+ MOVQ syscallArgs_a26(R11), R12
101+ MOVQ R12, 152(SP) // push a26
102+ MOVQ syscallArgs_a27(R11), R12
103+ MOVQ R12, 160(SP) // push a27
104+ MOVQ syscallArgs_a28(R11), R12
105+ MOVQ R12, 168(SP) // push a28
106+ MOVQ syscallArgs_a29(R11), R12
107+ MOVQ R12, 176(SP) // push a29
108+ MOVQ syscallArgs_a30(R11), R12
109+ MOVQ R12, 184(SP) // push a30
110+ MOVQ syscallArgs_a31(R11), R12
111+ MOVQ R12, 192(SP) // push a31
112+ MOVQ syscallArgs_a32(R11), R12
113+ MOVQ R12, 200(SP) // push a32
80114 XORL AX, AX // vararg: say "no float args"
81115
82- MOVQ syscall15Args_fn(R11), R10 // fn
116+ MOVQ syscallArgs_fn(R11), R10 // fn
83117 CALL R10
84118
85119 MOVQ PTR_ADDRESS(SP), DI // get the pointer back
86- MOVQ AX, syscall15Args_a1(DI) // r1
87- MOVQ DX, syscall15Args_a2(DI) // r2
88- MOVQ X0, syscall15Args_f1(DI) // f1
89- MOVQ X1, syscall15Args_f2(DI) // f2
120+ MOVQ AX, syscallArgs_a1(DI) // r1
121+ MOVQ DX, syscallArgs_a2(DI) // r2
122+ MOVQ X0, syscallArgs_f1(DI) // f1
123+ MOVQ X1, syscallArgs_f2(DI) // f2
90124
91125 #ifdef GOOS_darwin
92126 CALL purego_error(SB)
93127 MOVQ PTR_ADDRESS(SP), DI // reload (DI clobbered by call)
94128 MOVQ (AX), AX
95- MOVQ AX, syscall15Args_a3(DI) // save errno
129+ MOVQ AX, syscallArgs_a3(DI) // save errno
130+#else
131+ MOVQ $0, syscallArgs_a3(DI) // no errno here: clear the input argument
96132 #endif
97133
98134 XORL AX, AX // no error (it's ignored anyway)
M sys_arm.s +65 -54
@@ -10,8 +10,8 @@
1010 #define STACK_SIZE 128
1111 #define PTR_ADDRESS (STACK_SIZE - 4)
1212
13-// syscall15X calls a function in libc on behalf of the syscall package.
14-// syscall15X takes a pointer to a struct like:
13+// syscallX calls a function in libc on behalf of the syscall package.
14+// syscallX takes a pointer to a struct like:
1515 // struct {
1616 // fn uintptr
1717 // a1 uintptr
@@ -22,12 +22,12 @@
2222 // f16 uintptr
2323 // arm64_r8 uintptr
2424 // }
25-// syscall15X must be called on the g0 stack with the
25+// syscallX must be called on the g0 stack with the
2626 // 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
27+GLOBL ·syscallXABI0(SB), NOPTR|RODATA, $4
28+DATA ·syscallXABI0(SB)/4, $syscallX(SB)
29+TEXT syscallX(SB), NOSPLIT|NOFRAME, $0-0
30+ // Called via C calling convention: R0 = pointer to syscallArgs
3131 // NOT via Go calling convention
3232 // Save link register and callee-saved registers first
3333 MOVW.W R14, -4(R13) // save LR (decrement and store)
@@ -38,82 +38,87 @@ TEXT syscall15X(SB), NOSPLIT|NOFRAME, $0-0
3838 MOVW R8, PTR_ADDRESS(R13)
3939
4040 // Load function pointer first (before anything can corrupt R8)
41- MOVW syscall15Args_fn(R8), R5
41+ MOVW syscallArgs_fn(R8), R5
4242 MOVW R5, (PTR_ADDRESS-4)(R13) // save fn at offset 56
4343
44+ // Skip floating point registers if runtime.goarmsoftfp!=0.
45+ MOVB runtime·goarmsoftfp(SB), R11
46+ CMP $0, R11
47+ BNE skipfpload
4448 // Load floating point arguments
4549 // 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
50+ MOVD syscallArgs_f1(R8), F0 // f1+f2 -> D0
51+ MOVD syscallArgs_f3(R8), F1 // f3+f4 -> D1
52+ MOVD syscallArgs_f5(R8), F2 // f5+f6 -> D2
53+ MOVD syscallArgs_f7(R8), F3 // f7+f8 -> D3
54+ MOVD syscallArgs_f9(R8), F4 // f9+f10 -> D4
55+ MOVD syscallArgs_f11(R8), F5 // f11+f12 -> D5
56+ MOVD syscallArgs_f13(R8), F6 // f13+f14 -> D6
57+ MOVD syscallArgs_f15(R8), F7 // f15+f16 -> D7
5458
59+skipfpload:
5560 // 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
61+ MOVW syscallArgs_a1(R8), R0 // a1
62+ MOVW syscallArgs_a2(R8), R1 // a2
63+ MOVW syscallArgs_a3(R8), R2 // a3
64+ MOVW syscallArgs_a4(R8), R3 // a4
6065
6166 // push a5-a32 onto stack
62- MOVW syscall15Args_a5(R8), R4
67+ MOVW syscallArgs_a5(R8), R4
6368 MOVW R4, 0(R13)
64- MOVW syscall15Args_a6(R8), R4
69+ MOVW syscallArgs_a6(R8), R4
6570 MOVW R4, 4(R13)
66- MOVW syscall15Args_a7(R8), R4
71+ MOVW syscallArgs_a7(R8), R4
6772 MOVW R4, 8(R13)
68- MOVW syscall15Args_a8(R8), R4
73+ MOVW syscallArgs_a8(R8), R4
6974 MOVW R4, 12(R13)
70- MOVW syscall15Args_a9(R8), R4
75+ MOVW syscallArgs_a9(R8), R4
7176 MOVW R4, 16(R13)
72- MOVW syscall15Args_a10(R8), R4
77+ MOVW syscallArgs_a10(R8), R4
7378 MOVW R4, 20(R13)
74- MOVW syscall15Args_a11(R8), R4
79+ MOVW syscallArgs_a11(R8), R4
7580 MOVW R4, 24(R13)
76- MOVW syscall15Args_a12(R8), R4
81+ MOVW syscallArgs_a12(R8), R4
7782 MOVW R4, 28(R13)
78- MOVW syscall15Args_a13(R8), R4
83+ MOVW syscallArgs_a13(R8), R4
7984 MOVW R4, 32(R13)
80- MOVW syscall15Args_a14(R8), R4
85+ MOVW syscallArgs_a14(R8), R4
8186 MOVW R4, 36(R13)
82- MOVW syscall15Args_a15(R8), R4
87+ MOVW syscallArgs_a15(R8), R4
8388 MOVW R4, 40(R13)
84- MOVW syscall15Args_a16(R8), R4
89+ MOVW syscallArgs_a16(R8), R4
8590 MOVW R4, 44(R13)
86- MOVW syscall15Args_a17(R8), R4
91+ MOVW syscallArgs_a17(R8), R4
8792 MOVW R4, 48(R13)
88- MOVW syscall15Args_a18(R8), R4
93+ MOVW syscallArgs_a18(R8), R4
8994 MOVW R4, 52(R13)
90- MOVW syscall15Args_a19(R8), R4
95+ MOVW syscallArgs_a19(R8), R4
9196 MOVW R4, 56(R13)
92- MOVW syscall15Args_a20(R8), R4
97+ MOVW syscallArgs_a20(R8), R4
9398 MOVW R4, 60(R13)
94- MOVW syscall15Args_a21(R8), R4
99+ MOVW syscallArgs_a21(R8), R4
95100 MOVW R4, 64(R13)
96- MOVW syscall15Args_a22(R8), R4
101+ MOVW syscallArgs_a22(R8), R4
97102 MOVW R4, 68(R13)
98- MOVW syscall15Args_a23(R8), R4
103+ MOVW syscallArgs_a23(R8), R4
99104 MOVW R4, 72(R13)
100- MOVW syscall15Args_a24(R8), R4
105+ MOVW syscallArgs_a24(R8), R4
101106 MOVW R4, 76(R13)
102- MOVW syscall15Args_a25(R8), R4
107+ MOVW syscallArgs_a25(R8), R4
103108 MOVW R4, 80(R13)
104- MOVW syscall15Args_a26(R8), R4
109+ MOVW syscallArgs_a26(R8), R4
105110 MOVW R4, 84(R13)
106- MOVW syscall15Args_a27(R8), R4
111+ MOVW syscallArgs_a27(R8), R4
107112 MOVW R4, 88(R13)
108- MOVW syscall15Args_a28(R8), R4
113+ MOVW syscallArgs_a28(R8), R4
109114 MOVW R4, 92(R13)
110- MOVW syscall15Args_a29(R8), R4
115+ MOVW syscallArgs_a29(R8), R4
111116 MOVW R4, 96(R13)
112- MOVW syscall15Args_a30(R8), R4
117+ MOVW syscallArgs_a30(R8), R4
113118 MOVW R4, 100(R13)
114- MOVW syscall15Args_a31(R8), R4
119+ MOVW syscallArgs_a31(R8), R4
115120 MOVW R4, 104(R13)
116- MOVW syscall15Args_a32(R8), R4
121+ MOVW syscallArgs_a32(R8), R4
117122 MOVW R4, 108(R13)
118123
119124 // Load saved function pointer and call
@@ -128,15 +133,21 @@ TEXT syscall15X(SB), NOSPLIT|NOFRAME, $0-0
128133 ADD $STACK_SIZE, R13
129134
130135 // save R0, R1
131- MOVW R0, syscall15Args_a1(R8)
132- MOVW R1, syscall15Args_a2(R8)
136+ MOVW R0, syscallArgs_a1(R8)
137+ MOVW R1, syscallArgs_a2(R8)
138+ MOVW $0, R1
139+ MOVW R1, syscallArgs_a3(R8) // no errno here: clear the input argument
133140
141+ MOVB runtime·goarmsoftfp(SB), R11
142+ CMP $0, R11
143+ BNE skipfpsave
134144 // 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)
145+ MOVD F0, syscallArgs_f1(R8)
146+ MOVD F1, syscallArgs_f3(R8)
147+ MOVD F2, syscallArgs_f5(R8)
148+ MOVD F3, syscallArgs_f7(R8)
139149
150+skipfpsave:
140151 // Restore callee-saved registers and return
141152 MOVM.IA.W (R13), [R4, R5, R6, R7, R8, R9, R11]
142153 MOVW.P 4(R13), R15 // pop LR into PC (return)
M sys_arm64.s +75 -40
@@ -7,11 +7,11 @@
77 #include "go_asm.h"
88 #include "funcdata.h"
99
10-#define STACK_SIZE 64
10+#define STACK_SIZE 208
1111 #define PTR_ADDRESS (STACK_SIZE - 8)
1212
13-// syscall15X calls a function in libc on behalf of the syscall package.
14-// syscall15X takes a pointer to a struct like:
13+// syscallX calls a function in libc on behalf of the syscall package.
14+// syscallX takes a pointer to a struct like:
1515 // struct {
1616 // fn uintptr
1717 // a1 uintptr
@@ -33,66 +33,101 @@
3333 // r2 uintptr
3434 // err uintptr
3535 // }
36-// syscall15X must be called on the g0 stack with the
36+// syscallX must be called on the g0 stack with the
3737 // C calling convention (use libcCall).
38-GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8
39-DATA ·syscall15XABI0(SB)/8, $syscall15X(SB)
40-TEXT syscall15X(SB), NOSPLIT, $0
38+GLOBL ·syscallXABI0(SB), NOPTR|RODATA, $8
39+DATA ·syscallXABI0(SB)/8, $syscallX(SB)
40+TEXT syscallX(SB), NOSPLIT, $0
4141 SUB $STACK_SIZE, RSP // push structure pointer
4242 MOVD R0, PTR_ADDRESS(RSP)
4343 MOVD R0, R9
4444
45- FMOVD syscall15Args_f1(R9), F0 // f1
46- FMOVD syscall15Args_f2(R9), F1 // f2
47- FMOVD syscall15Args_f3(R9), F2 // f3
48- FMOVD syscall15Args_f4(R9), F3 // f4
49- FMOVD syscall15Args_f5(R9), F4 // f5
50- FMOVD syscall15Args_f6(R9), F5 // f6
51- FMOVD syscall15Args_f7(R9), F6 // f7
52- FMOVD syscall15Args_f8(R9), F7 // f8
45+ FMOVD syscallArgs_f1(R9), F0 // f1
46+ FMOVD syscallArgs_f2(R9), F1 // f2
47+ FMOVD syscallArgs_f3(R9), F2 // f3
48+ FMOVD syscallArgs_f4(R9), F3 // f4
49+ FMOVD syscallArgs_f5(R9), F4 // f5
50+ FMOVD syscallArgs_f6(R9), F5 // f6
51+ FMOVD syscallArgs_f7(R9), F6 // f7
52+ FMOVD syscallArgs_f8(R9), F7 // f8
5353
54- MOVD syscall15Args_a1(R9), R0 // a1
55- MOVD syscall15Args_a2(R9), R1 // a2
56- MOVD syscall15Args_a3(R9), R2 // a3
57- MOVD syscall15Args_a4(R9), R3 // a4
58- MOVD syscall15Args_a5(R9), R4 // a5
59- MOVD syscall15Args_a6(R9), R5 // a6
60- MOVD syscall15Args_a7(R9), R6 // a7
61- MOVD syscall15Args_a8(R9), R7 // a8
62- MOVD syscall15Args_arm64_r8(R9), R8 // r8
54+ MOVD syscallArgs_a1(R9), R0 // a1
55+ MOVD syscallArgs_a2(R9), R1 // a2
56+ MOVD syscallArgs_a3(R9), R2 // a3
57+ MOVD syscallArgs_a4(R9), R3 // a4
58+ MOVD syscallArgs_a5(R9), R4 // a5
59+ MOVD syscallArgs_a6(R9), R5 // a6
60+ MOVD syscallArgs_a7(R9), R6 // a7
61+ MOVD syscallArgs_a8(R9), R7 // a8
62+ MOVD syscallArgs_arm64_r8(R9), R8 // r8
6363
64- MOVD syscall15Args_a9(R9), R10
64+ MOVD syscallArgs_a9(R9), R10
6565 MOVD R10, 0(RSP) // push a9 onto stack
66- MOVD syscall15Args_a10(R9), R10
66+ MOVD syscallArgs_a10(R9), R10
6767 MOVD R10, 8(RSP) // push a10 onto stack
68- MOVD syscall15Args_a11(R9), R10
68+ MOVD syscallArgs_a11(R9), R10
6969 MOVD R10, 16(RSP) // push a11 onto stack
70- MOVD syscall15Args_a12(R9), R10
70+ MOVD syscallArgs_a12(R9), R10
7171 MOVD R10, 24(RSP) // push a12 onto stack
72- MOVD syscall15Args_a13(R9), R10
72+ MOVD syscallArgs_a13(R9), R10
7373 MOVD R10, 32(RSP) // push a13 onto stack
74- MOVD syscall15Args_a14(R9), R10
74+ MOVD syscallArgs_a14(R9), R10
7575 MOVD R10, 40(RSP) // push a14 onto stack
76- MOVD syscall15Args_a15(R9), R10
76+ MOVD syscallArgs_a15(R9), R10
7777 MOVD R10, 48(RSP) // push a15 onto stack
78+ MOVD syscallArgs_a16(R9), R10
79+ MOVD R10, 56(RSP) // push a16 onto stack
80+ MOVD syscallArgs_a17(R9), R10
81+ MOVD R10, 64(RSP) // push a17 onto stack
82+ MOVD syscallArgs_a18(R9), R10
83+ MOVD R10, 72(RSP) // push a18 onto stack
84+ MOVD syscallArgs_a19(R9), R10
85+ MOVD R10, 80(RSP) // push a19 onto stack
86+ MOVD syscallArgs_a20(R9), R10
87+ MOVD R10, 88(RSP) // push a20 onto stack
88+ MOVD syscallArgs_a21(R9), R10
89+ MOVD R10, 96(RSP) // push a21 onto stack
90+ MOVD syscallArgs_a22(R9), R10
91+ MOVD R10, 104(RSP) // push a22 onto stack
92+ MOVD syscallArgs_a23(R9), R10
93+ MOVD R10, 112(RSP) // push a23 onto stack
94+ MOVD syscallArgs_a24(R9), R10
95+ MOVD R10, 120(RSP) // push a24 onto stack
96+ MOVD syscallArgs_a25(R9), R10
97+ MOVD R10, 128(RSP) // push a25 onto stack
98+ MOVD syscallArgs_a26(R9), R10
99+ MOVD R10, 136(RSP) // push a26 onto stack
100+ MOVD syscallArgs_a27(R9), R10
101+ MOVD R10, 144(RSP) // push a27 onto stack
102+ MOVD syscallArgs_a28(R9), R10
103+ MOVD R10, 152(RSP) // push a28 onto stack
104+ MOVD syscallArgs_a29(R9), R10
105+ MOVD R10, 160(RSP) // push a29 onto stack
106+ MOVD syscallArgs_a30(R9), R10
107+ MOVD R10, 168(RSP) // push a30 onto stack
108+ MOVD syscallArgs_a31(R9), R10
109+ MOVD R10, 176(RSP) // push a31 onto stack
110+ MOVD syscallArgs_a32(R9), R10
111+ MOVD R10, 184(RSP) // push a32 onto stack
78112
79- MOVD syscall15Args_fn(R9), R10 // fn
113+ MOVD syscallArgs_fn(R9), R10 // fn
80114 BL (R10)
81115
82116 MOVD PTR_ADDRESS(RSP), R2 // pop structure pointer
83117 ADD $STACK_SIZE, RSP
84118
85- MOVD R0, syscall15Args_a1(R2) // save r1
86- MOVD R1, syscall15Args_a2(R2) // save r3
87- FMOVD F0, syscall15Args_f1(R2) // save f0
88- FMOVD F1, syscall15Args_f2(R2) // save f1
89- FMOVD F2, syscall15Args_f3(R2) // save f2
90- FMOVD F3, syscall15Args_f4(R2) // save f3
119+ MOVD R0, syscallArgs_a1(R2) // save r1
120+ MOVD R1, syscallArgs_a2(R2) // save r3
121+ FMOVD F0, syscallArgs_f1(R2) // save f0
122+ FMOVD F1, syscallArgs_f2(R2) // save f1
123+ FMOVD F2, syscallArgs_f3(R2) // save f2
124+ FMOVD F3, syscallArgs_f4(R2) // save f3
91125
92126 #ifdef GOOS_darwin
93127 BL purego_error(SB)
94128 MOVD (R0), R0
95- MOVD R0, syscall15Args_a3(R2) // save errno
96-
129+ MOVD R0, syscallArgs_a3(R2) // save errno
130+#else
131+ MOVD $0, syscallArgs_a3(R2) // no errno here: clear the input argument
97132 #endif
98133 RET
M sys_loong64.s +72 -37
@@ -7,11 +7,11 @@
77 #include "go_asm.h"
88 #include "funcdata.h"
99
10-#define STACK_SIZE 64
10+#define STACK_SIZE 208
1111 #define PTR_ADDRESS (STACK_SIZE - 8)
1212
13-// syscall15X calls a function in libc on behalf of the syscall package.
14-// syscall15X takes a pointer to a struct like:
13+// syscallX calls a function in libc on behalf of the syscall package.
14+// syscallX takes a pointer to a struct like:
1515 // struct {
1616 // fn uintptr
1717 // a1 uintptr
@@ -33,51 +33,85 @@
3333 // r2 uintptr
3434 // err uintptr
3535 // }
36-// syscall15X must be called on the g0 stack with the
36+// syscallX must be called on the g0 stack with the
3737 // C calling convention (use libcCall).
38-GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8
39-DATA ·syscall15XABI0(SB)/8, $syscall15X(SB)
40-TEXT syscall15X(SB), NOSPLIT, $0
38+GLOBL ·syscallXABI0(SB), NOPTR|RODATA, $8
39+DATA ·syscallXABI0(SB)/8, $syscallX(SB)
40+TEXT syscallX(SB), NOSPLIT, $0
4141 // push structure pointer
4242 SUBV $STACK_SIZE, R3
4343 MOVV R4, PTR_ADDRESS(R3)
4444 MOVV R4, R13
4545
46- MOVD syscall15Args_f1(R13), F0 // f1
47- MOVD syscall15Args_f2(R13), F1 // f2
48- MOVD syscall15Args_f3(R13), F2 // f3
49- MOVD syscall15Args_f4(R13), F3 // f4
50- MOVD syscall15Args_f5(R13), F4 // f5
51- MOVD syscall15Args_f6(R13), F5 // f6
52- MOVD syscall15Args_f7(R13), F6 // f7
53- MOVD syscall15Args_f8(R13), F7 // f8
46+ MOVD syscallArgs_f1(R13), F0 // f1
47+ MOVD syscallArgs_f2(R13), F1 // f2
48+ MOVD syscallArgs_f3(R13), F2 // f3
49+ MOVD syscallArgs_f4(R13), F3 // f4
50+ MOVD syscallArgs_f5(R13), F4 // f5
51+ MOVD syscallArgs_f6(R13), F5 // f6
52+ MOVD syscallArgs_f7(R13), F6 // f7
53+ MOVD syscallArgs_f8(R13), F7 // f8
5454
55- MOVV syscall15Args_a1(R13), R4 // a1
56- MOVV syscall15Args_a2(R13), R5 // a2
57- MOVV syscall15Args_a3(R13), R6 // a3
58- MOVV syscall15Args_a4(R13), R7 // a4
59- MOVV syscall15Args_a5(R13), R8 // a5
60- MOVV syscall15Args_a6(R13), R9 // a6
61- MOVV syscall15Args_a7(R13), R10 // a7
62- MOVV syscall15Args_a8(R13), R11 // a8
55+ MOVV syscallArgs_a1(R13), R4 // a1
56+ MOVV syscallArgs_a2(R13), R5 // a2
57+ MOVV syscallArgs_a3(R13), R6 // a3
58+ MOVV syscallArgs_a4(R13), R7 // a4
59+ MOVV syscallArgs_a5(R13), R8 // a5
60+ MOVV syscallArgs_a6(R13), R9 // a6
61+ MOVV syscallArgs_a7(R13), R10 // a7
62+ MOVV syscallArgs_a8(R13), R11 // a8
6363
6464 // push a9-a15 onto stack
65- MOVV syscall15Args_a9(R13), R12
65+ MOVV syscallArgs_a9(R13), R12
6666 MOVV R12, 0(R3)
67- MOVV syscall15Args_a10(R13), R12
67+ MOVV syscallArgs_a10(R13), R12
6868 MOVV R12, 8(R3)
69- MOVV syscall15Args_a11(R13), R12
69+ MOVV syscallArgs_a11(R13), R12
7070 MOVV R12, 16(R3)
71- MOVV syscall15Args_a12(R13), R12
71+ MOVV syscallArgs_a12(R13), R12
7272 MOVV R12, 24(R3)
73- MOVV syscall15Args_a13(R13), R12
73+ MOVV syscallArgs_a13(R13), R12
7474 MOVV R12, 32(R3)
75- MOVV syscall15Args_a14(R13), R12
75+ MOVV syscallArgs_a14(R13), R12
7676 MOVV R12, 40(R3)
77- MOVV syscall15Args_a15(R13), R12
77+ MOVV syscallArgs_a15(R13), R12
7878 MOVV R12, 48(R3)
79+ MOVV syscallArgs_a16(R13), R12
80+ MOVV R12, 56(R3)
81+ MOVV syscallArgs_a17(R13), R12
82+ MOVV R12, 64(R3)
83+ MOVV syscallArgs_a18(R13), R12
84+ MOVV R12, 72(R3)
85+ MOVV syscallArgs_a19(R13), R12
86+ MOVV R12, 80(R3)
87+ MOVV syscallArgs_a20(R13), R12
88+ MOVV R12, 88(R3)
89+ MOVV syscallArgs_a21(R13), R12
90+ MOVV R12, 96(R3)
91+ MOVV syscallArgs_a22(R13), R12
92+ MOVV R12, 104(R3)
93+ MOVV syscallArgs_a23(R13), R12
94+ MOVV R12, 112(R3)
95+ MOVV syscallArgs_a24(R13), R12
96+ MOVV R12, 120(R3)
97+ MOVV syscallArgs_a25(R13), R12
98+ MOVV R12, 128(R3)
99+ MOVV syscallArgs_a26(R13), R12
100+ MOVV R12, 136(R3)
101+ MOVV syscallArgs_a27(R13), R12
102+ MOVV R12, 144(R3)
103+ MOVV syscallArgs_a28(R13), R12
104+ MOVV R12, 152(R3)
105+ MOVV syscallArgs_a29(R13), R12
106+ MOVV R12, 160(R3)
107+ MOVV syscallArgs_a30(R13), R12
108+ MOVV R12, 168(R3)
109+ MOVV syscallArgs_a31(R13), R12
110+ MOVV R12, 176(R3)
111+ MOVV syscallArgs_a32(R13), R12
112+ MOVV R12, 184(R3)
79113
80- MOVV syscall15Args_fn(R13), R12
114+ MOVV syscallArgs_fn(R13), R12
81115 JAL (R12)
82116
83117 // pop structure pointer
@@ -85,12 +119,13 @@ TEXT syscall15X(SB), NOSPLIT, $0
85119 ADDV $STACK_SIZE, R3
86120
87121 // save R4, R5
88- MOVV R4, syscall15Args_a1(R13)
89- MOVV R5, syscall15Args_a2(R13)
122+ MOVV R4, syscallArgs_a1(R13)
123+ MOVV R5, syscallArgs_a2(R13)
124+ MOVV R0, syscallArgs_a3(R13) // no errno here: clear the input argument
90125
91126 // save f0-f3
92- MOVD F0, syscall15Args_f1(R13)
93- MOVD F1, syscall15Args_f2(R13)
94- MOVD F2, syscall15Args_f3(R13)
95- MOVD F3, syscall15Args_f4(R13)
127+ MOVD F0, syscallArgs_f1(R13)
128+ MOVD F1, syscallArgs_f2(R13)
129+ MOVD F2, syscallArgs_f3(R13)
130+ MOVD F3, syscallArgs_f4(R13)
96131 RET
M sys_ppc64le.s +42 -36
@@ -35,10 +35,10 @@
3535 #define TOC_SAVE 160
3636 #define ARGP_SAVE 168
3737
38-GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8
39-DATA ·syscall15XABI0(SB)/8, $syscall15X(SB)
38+GLOBL ·syscallXABI0(SB), NOPTR|RODATA, $8
39+DATA ·syscallXABI0(SB)/8, $syscallX(SB)
4040
41-TEXT syscall15X(SB), NOSPLIT, $0
41+TEXT syscallX(SB), NOSPLIT, $0
4242 // Prologue: create stack frame
4343 // R3 contains the args pointer on entry
4444 MOVD R1, R12 // save old SP
@@ -51,49 +51,49 @@ TEXT syscall15X(SB), NOSPLIT, $0
5151 // Save args pointer (in R3)
5252 MOVD R3, ARGP_SAVE(R1)
5353
54- // R11 := args pointer (syscall15Args*)
54+ // R11 := args pointer (syscallArgs*)
5555 MOVD R3, R11
5656
5757 // Load float args into F1-F8
58- FMOVD syscall15Args_f1(R11), F1
59- FMOVD syscall15Args_f2(R11), F2
60- FMOVD syscall15Args_f3(R11), F3
61- FMOVD syscall15Args_f4(R11), F4
62- FMOVD syscall15Args_f5(R11), F5
63- FMOVD syscall15Args_f6(R11), F6
64- FMOVD syscall15Args_f7(R11), F7
65- FMOVD syscall15Args_f8(R11), F8
58+ FMOVD syscallArgs_f1(R11), F1
59+ FMOVD syscallArgs_f2(R11), F2
60+ FMOVD syscallArgs_f3(R11), F3
61+ FMOVD syscallArgs_f4(R11), F4
62+ FMOVD syscallArgs_f5(R11), F5
63+ FMOVD syscallArgs_f6(R11), F6
64+ FMOVD syscallArgs_f7(R11), F7
65+ FMOVD syscallArgs_f8(R11), F8
6666
6767 // Load integer args into R3-R10
68- MOVD syscall15Args_a1(R11), R3
69- MOVD syscall15Args_a2(R11), R4
70- MOVD syscall15Args_a3(R11), R5
71- MOVD syscall15Args_a4(R11), R6
72- MOVD syscall15Args_a5(R11), R7
73- MOVD syscall15Args_a6(R11), R8
74- MOVD syscall15Args_a7(R11), R9
75- MOVD syscall15Args_a8(R11), R10
68+ MOVD syscallArgs_a1(R11), R3
69+ MOVD syscallArgs_a2(R11), R4
70+ MOVD syscallArgs_a3(R11), R5
71+ MOVD syscallArgs_a4(R11), R6
72+ MOVD syscallArgs_a5(R11), R7
73+ MOVD syscallArgs_a6(R11), R8
74+ MOVD syscallArgs_a7(R11), R9
75+ MOVD syscallArgs_a8(R11), R10
7676
7777 // Spill a9-a15 onto the stack (stack parameters start at 96(R1))
7878 // Per ELFv2: parameter save area is 32-95, stack args start at 96
7979 MOVD ARGP_SAVE(R1), R11 // reload args pointer
80- MOVD syscall15Args_a9(R11), R12
80+ MOVD syscallArgs_a9(R11), R12
8181 MOVD R12, 96(R1) // a9 at 96(R1)
82- MOVD syscall15Args_a10(R11), R12
82+ MOVD syscallArgs_a10(R11), R12
8383 MOVD R12, 104(R1) // a10 at 104(R1)
84- MOVD syscall15Args_a11(R11), R12
84+ MOVD syscallArgs_a11(R11), R12
8585 MOVD R12, 112(R1) // a11 at 112(R1)
86- MOVD syscall15Args_a12(R11), R12
86+ MOVD syscallArgs_a12(R11), R12
8787 MOVD R12, 120(R1) // a12 at 120(R1)
88- MOVD syscall15Args_a13(R11), R12
88+ MOVD syscallArgs_a13(R11), R12
8989 MOVD R12, 128(R1) // a13 at 128(R1)
90- MOVD syscall15Args_a14(R11), R12
90+ MOVD syscallArgs_a14(R11), R12
9191 MOVD R12, 136(R1) // a14 at 136(R1)
92- MOVD syscall15Args_a15(R11), R12
92+ MOVD syscallArgs_a15(R11), R12
9393 MOVD R12, 144(R1) // a15 at 144(R1)
9494
9595 // Call function: load fn and call
96- MOVD syscall15Args_fn(R11), R12
96+ MOVD syscallArgs_fn(R11), R12
9797 MOVD R12, CTR
9898 BL (CTR)
9999
@@ -104,14 +104,20 @@ TEXT syscall15X(SB), NOSPLIT, $0
104104 MOVD ARGP_SAVE(R1), R11
105105
106106 // Store integer results back (R3, R4)
107- MOVD R3, syscall15Args_a1(R11)
108- MOVD R4, syscall15Args_a2(R11)
109-
110- // Store float return values (F1-F4)
111- FMOVD F1, syscall15Args_f1(R11)
112- FMOVD F2, syscall15Args_f2(R11)
113- FMOVD F3, syscall15Args_f3(R11)
114- FMOVD F4, syscall15Args_f4(R11)
107+ MOVD R3, syscallArgs_a1(R11)
108+ MOVD R4, syscallArgs_a2(R11)
109+ MOVD $0, R3
110+ MOVD R3, syscallArgs_a3(R11) // no errno here: clear the input argument
111+
112+ // Store float return values F1-F8 (an HFA may span up to eight).
113+ FMOVD F1, syscallArgs_f1(R11)
114+ FMOVD F2, syscallArgs_f2(R11)
115+ FMOVD F3, syscallArgs_f3(R11)
116+ FMOVD F4, syscallArgs_f4(R11)
117+ FMOVD F5, syscallArgs_f5(R11)
118+ FMOVD F6, syscallArgs_f6(R11)
119+ FMOVD F7, syscallArgs_f7(R11)
120+ FMOVD F8, syscallArgs_f8(R11)
115121
116122 // Epilogue: restore and return
117123 MOVD LR_SAVE(R1), R12
M sys_riscv64.s +84 -50
@@ -8,22 +8,21 @@
88 #include "funcdata.h"
99
1010 // Stack usage:
11-// 0(SP) - 56(SP): stack args a9-a15 (7 * 8 bytes = 56)
12-// 56(SP) - 64(SP): saved RA (x1)
13-// 64(SP) - 72(SP): saved X9 (s1)
14-// 72(SP) - 80(SP): saved X18 (s2)
15-// 80(SP) - 88(SP): saved args pointer (original X10)
16-// 88(SP) - 96(SP): padding
17-#define STACK_SIZE 96
18-#define SAVE_RA 56
19-#define SAVE_X9 64
20-#define SAVE_X18 72
21-#define SAVE_ARGP 80
22-
23-GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8
24-DATA ·syscall15XABI0(SB)/8, $syscall15X(SB)
25-
26-TEXT syscall15X(SB), NOSPLIT, $0
11+// 0(SP) - 192(SP): stack args a9-a32 (24 * 8 bytes)
12+// 192(SP) - 200(SP): saved RA (x1)
13+// 200(SP) - 208(SP): saved X9 (s1)
14+// 208(SP) - 216(SP): saved X18 (s2)
15+// 216(SP) - 224(SP): saved args pointer (original X10)
16+#define STACK_SIZE 224
17+#define SAVE_RA 192
18+#define SAVE_X9 200
19+#define SAVE_X18 208
20+#define SAVE_ARGP 216
21+
22+GLOBL ·syscallXABI0(SB), NOPTR|RODATA, $8
23+DATA ·syscallXABI0(SB)/8, $syscallX(SB)
24+
25+TEXT syscallX(SB), NOSPLIT, $0
2726 // Allocate stack frame (keeps 16-byte alignment)
2827 SUB $STACK_SIZE, SP
2928
@@ -35,62 +34,97 @@ TEXT syscall15X(SB), NOSPLIT, $0
3534 // Save original args pointer (in a0/X10)
3635 MOV X10, SAVE_ARGP(SP)
3736
38- // X9 := args pointer (syscall15Args*)
37+ // X9 := args pointer (syscallArgs*)
3938 MOV X10, X9
4039
4140 // Load float args into fa0-fa7 (F10-F17)
42- MOVD syscall15Args_f1(X9), F10
43- MOVD syscall15Args_f2(X9), F11
44- MOVD syscall15Args_f3(X9), F12
45- MOVD syscall15Args_f4(X9), F13
46- MOVD syscall15Args_f5(X9), F14
47- MOVD syscall15Args_f6(X9), F15
48- MOVD syscall15Args_f7(X9), F16
49- MOVD syscall15Args_f8(X9), F17
41+ MOVD syscallArgs_f1(X9), F10
42+ MOVD syscallArgs_f2(X9), F11
43+ MOVD syscallArgs_f3(X9), F12
44+ MOVD syscallArgs_f4(X9), F13
45+ MOVD syscallArgs_f5(X9), F14
46+ MOVD syscallArgs_f6(X9), F15
47+ MOVD syscallArgs_f7(X9), F16
48+ MOVD syscallArgs_f8(X9), F17
5049
5150 // Load integer args into a0-a7 (X10-X17)
52- MOV syscall15Args_a1(X9), X10
53- MOV syscall15Args_a2(X9), X11
54- MOV syscall15Args_a3(X9), X12
55- MOV syscall15Args_a4(X9), X13
56- MOV syscall15Args_a5(X9), X14
57- MOV syscall15Args_a6(X9), X15
58- MOV syscall15Args_a7(X9), X16
59- MOV syscall15Args_a8(X9), X17
60-
61- // Spill a9-a15 onto the stack (C ABI)
62- MOV syscall15Args_a9(X9), X18
51+ MOV syscallArgs_a1(X9), X10
52+ MOV syscallArgs_a2(X9), X11
53+ MOV syscallArgs_a3(X9), X12
54+ MOV syscallArgs_a4(X9), X13
55+ MOV syscallArgs_a5(X9), X14
56+ MOV syscallArgs_a6(X9), X15
57+ MOV syscallArgs_a7(X9), X16
58+ MOV syscallArgs_a8(X9), X17
59+
60+ // Spill a9-a32 onto the stack (C ABI)
61+ MOV syscallArgs_a9(X9), X18
6362 MOV X18, 0(SP)
64- MOV syscall15Args_a10(X9), X18
63+ MOV syscallArgs_a10(X9), X18
6564 MOV X18, 8(SP)
66- MOV syscall15Args_a11(X9), X18
65+ MOV syscallArgs_a11(X9), X18
6766 MOV X18, 16(SP)
68- MOV syscall15Args_a12(X9), X18
67+ MOV syscallArgs_a12(X9), X18
6968 MOV X18, 24(SP)
70- MOV syscall15Args_a13(X9), X18
69+ MOV syscallArgs_a13(X9), X18
7170 MOV X18, 32(SP)
72- MOV syscall15Args_a14(X9), X18
71+ MOV syscallArgs_a14(X9), X18
7372 MOV X18, 40(SP)
74- MOV syscall15Args_a15(X9), X18
73+ MOV syscallArgs_a15(X9), X18
7574 MOV X18, 48(SP)
75+ MOV syscallArgs_a16(X9), X18
76+ MOV X18, 56(SP)
77+ MOV syscallArgs_a17(X9), X18
78+ MOV X18, 64(SP)
79+ MOV syscallArgs_a18(X9), X18
80+ MOV X18, 72(SP)
81+ MOV syscallArgs_a19(X9), X18
82+ MOV X18, 80(SP)
83+ MOV syscallArgs_a20(X9), X18
84+ MOV X18, 88(SP)
85+ MOV syscallArgs_a21(X9), X18
86+ MOV X18, 96(SP)
87+ MOV syscallArgs_a22(X9), X18
88+ MOV X18, 104(SP)
89+ MOV syscallArgs_a23(X9), X18
90+ MOV X18, 112(SP)
91+ MOV syscallArgs_a24(X9), X18
92+ MOV X18, 120(SP)
93+ MOV syscallArgs_a25(X9), X18
94+ MOV X18, 128(SP)
95+ MOV syscallArgs_a26(X9), X18
96+ MOV X18, 136(SP)
97+ MOV syscallArgs_a27(X9), X18
98+ MOV X18, 144(SP)
99+ MOV syscallArgs_a28(X9), X18
100+ MOV X18, 152(SP)
101+ MOV syscallArgs_a29(X9), X18
102+ MOV X18, 160(SP)
103+ MOV syscallArgs_a30(X9), X18
104+ MOV X18, 168(SP)
105+ MOV syscallArgs_a31(X9), X18
106+ MOV X18, 176(SP)
107+ MOV syscallArgs_a32(X9), X18
108+ MOV X18, 184(SP)
76109
77110 // Call fn
78111 // IMPORTANT: preserve RA across this call (we saved it above)
79- MOV syscall15Args_fn(X9), X18
112+ MOV syscallArgs_fn(X9), X18
80113 CALL X18
81114
82- // Restore args pointer (syscall15Args*) for storing results
115+ // Restore args pointer (syscallArgs*) for storing results
83116 MOV SAVE_ARGP(SP), X9
84117
85118 // Store results back
86- MOV X10, syscall15Args_a1(X9)
87- MOV X11, syscall15Args_a2(X9)
119+ MOV X10, syscallArgs_a1(X9)
120+ MOV X11, syscallArgs_a2(X9)
121+ MOV X0, syscallArgs_a3(X9) // no errno here: clear the input argument
88122
89123 // Store back float return regs if used by your ABI contract
90- MOVD F10, syscall15Args_f1(X9)
91- MOVD F11, syscall15Args_f2(X9)
92- MOVD F12, syscall15Args_f3(X9)
93- MOVD F13, syscall15Args_f4(X9)
124+ MOVD F10, syscallArgs_f1(X9)
125+ MOVD F11, syscallArgs_f2(X9)
126+ MOVD F12, syscallArgs_f3(X9)
127+ MOVD F13, syscallArgs_f4(X9)
94128
95129 // Restore callee-saved regs and return address
96130 MOV SAVE_X18(SP), X18
M sys_s390x.s +70 -36
@@ -24,19 +24,18 @@
2424 //
2525 // We need space for:
2626 // - 160 bytes standard frame (with register save area)
27-// - Stack args a6-a15 (10 * 8 = 80 bytes)
27+// - Stack args a6-a32 (27 * 8 = 216 bytes)
2828 // - Saved args pointer (8 bytes)
29-// - Padding for alignment
30-// Total: 264 bytes (rounded to 8-byte alignment)
29+// Total: 384 bytes
3130
32-#define STACK_SIZE 264
31+#define STACK_SIZE 384
3332 #define STACK_ARGS 160
34-#define ARGP_SAVE 248
33+#define ARGP_SAVE 376
3534
36-GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8
37-DATA ·syscall15XABI0(SB)/8, $syscall15X(SB)
35+GLOBL ·syscallXABI0(SB), NOPTR|RODATA, $8
36+DATA ·syscallXABI0(SB)/8, $syscallX(SB)
3837
39-TEXT syscall15X(SB), NOSPLIT, $0
38+TEXT syscallX(SB), NOSPLIT, $0
4039 // On entry, R2 contains the args pointer
4140 // Save callee-saved registers in caller's frame (per ABI)
4241 STMG R6, R15, 48(R15)
@@ -49,61 +48,96 @@ TEXT syscall15X(SB), NOSPLIT, $0
4948 // Save args pointer
5049 MOVD R2, ARGP_SAVE(R15)
5150
52- // R9 := args pointer (syscall15Args*)
51+ // R9 := args pointer (syscallArgs*)
5352 MOVD R2, R9
5453
5554 // Load float args into F0, F2, F4, F6 (s390x uses even-numbered FPRs)
56- FMOVD syscall15Args_f1(R9), F0
57- FMOVD syscall15Args_f2(R9), F2
58- FMOVD syscall15Args_f3(R9), F4
59- FMOVD syscall15Args_f4(R9), F6
55+ FMOVD syscallArgs_f1(R9), F0
56+ FMOVD syscallArgs_f2(R9), F2
57+ FMOVD syscallArgs_f3(R9), F4
58+ FMOVD syscallArgs_f4(R9), F6
6059
6160 // Load integer args into R2-R6 (5 registers)
62- MOVD syscall15Args_a1(R9), R2
63- MOVD syscall15Args_a2(R9), R3
64- MOVD syscall15Args_a3(R9), R4
65- MOVD syscall15Args_a4(R9), R5
66- MOVD syscall15Args_a5(R9), R6
61+ MOVD syscallArgs_a1(R9), R2
62+ MOVD syscallArgs_a2(R9), R3
63+ MOVD syscallArgs_a3(R9), R4
64+ MOVD syscallArgs_a4(R9), R5
65+ MOVD syscallArgs_a5(R9), R6
6766
68- // Spill remaining args (a6-a15) onto the stack at 160(R15)
67+ // Spill remaining args (a6-a32) onto the stack at 160(R15)
6968 MOVD ARGP_SAVE(R15), R9 // reload args pointer
70- MOVD syscall15Args_a6(R9), R1
69+ MOVD syscallArgs_a6(R9), R1
7170 MOVD R1, (STACK_ARGS+0*8)(R15)
72- MOVD syscall15Args_a7(R9), R1
71+ MOVD syscallArgs_a7(R9), R1
7372 MOVD R1, (STACK_ARGS+1*8)(R15)
74- MOVD syscall15Args_a8(R9), R1
73+ MOVD syscallArgs_a8(R9), R1
7574 MOVD R1, (STACK_ARGS+2*8)(R15)
76- MOVD syscall15Args_a9(R9), R1
75+ MOVD syscallArgs_a9(R9), R1
7776 MOVD R1, (STACK_ARGS+3*8)(R15)
78- MOVD syscall15Args_a10(R9), R1
77+ MOVD syscallArgs_a10(R9), R1
7978 MOVD R1, (STACK_ARGS+4*8)(R15)
80- MOVD syscall15Args_a11(R9), R1
79+ MOVD syscallArgs_a11(R9), R1
8180 MOVD R1, (STACK_ARGS+5*8)(R15)
82- MOVD syscall15Args_a12(R9), R1
81+ MOVD syscallArgs_a12(R9), R1
8382 MOVD R1, (STACK_ARGS+6*8)(R15)
84- MOVD syscall15Args_a13(R9), R1
83+ MOVD syscallArgs_a13(R9), R1
8584 MOVD R1, (STACK_ARGS+7*8)(R15)
86- MOVD syscall15Args_a14(R9), R1
85+ MOVD syscallArgs_a14(R9), R1
8786 MOVD R1, (STACK_ARGS+8*8)(R15)
88- MOVD syscall15Args_a15(R9), R1
87+ MOVD syscallArgs_a15(R9), R1
8988 MOVD R1, (STACK_ARGS+9*8)(R15)
89+ MOVD syscallArgs_a16(R9), R1
90+ MOVD R1, (STACK_ARGS+10*8)(R15)
91+ MOVD syscallArgs_a17(R9), R1
92+ MOVD R1, (STACK_ARGS+11*8)(R15)
93+ MOVD syscallArgs_a18(R9), R1
94+ MOVD R1, (STACK_ARGS+12*8)(R15)
95+ MOVD syscallArgs_a19(R9), R1
96+ MOVD R1, (STACK_ARGS+13*8)(R15)
97+ MOVD syscallArgs_a20(R9), R1
98+ MOVD R1, (STACK_ARGS+14*8)(R15)
99+ MOVD syscallArgs_a21(R9), R1
100+ MOVD R1, (STACK_ARGS+15*8)(R15)
101+ MOVD syscallArgs_a22(R9), R1
102+ MOVD R1, (STACK_ARGS+16*8)(R15)
103+ MOVD syscallArgs_a23(R9), R1
104+ MOVD R1, (STACK_ARGS+17*8)(R15)
105+ MOVD syscallArgs_a24(R9), R1
106+ MOVD R1, (STACK_ARGS+18*8)(R15)
107+ MOVD syscallArgs_a25(R9), R1
108+ MOVD R1, (STACK_ARGS+19*8)(R15)
109+ MOVD syscallArgs_a26(R9), R1
110+ MOVD R1, (STACK_ARGS+20*8)(R15)
111+ MOVD syscallArgs_a27(R9), R1
112+ MOVD R1, (STACK_ARGS+21*8)(R15)
113+ MOVD syscallArgs_a28(R9), R1
114+ MOVD R1, (STACK_ARGS+22*8)(R15)
115+ MOVD syscallArgs_a29(R9), R1
116+ MOVD R1, (STACK_ARGS+23*8)(R15)
117+ MOVD syscallArgs_a30(R9), R1
118+ MOVD R1, (STACK_ARGS+24*8)(R15)
119+ MOVD syscallArgs_a31(R9), R1
120+ MOVD R1, (STACK_ARGS+25*8)(R15)
121+ MOVD syscallArgs_a32(R9), R1
122+ MOVD R1, (STACK_ARGS+26*8)(R15)
90123
91124 // Call function
92- MOVD syscall15Args_fn(R9), R1
125+ MOVD syscallArgs_fn(R9), R1
93126 BL (R1)
94127
95128 // Restore args pointer for storing results
96129 MOVD ARGP_SAVE(R15), R9
97130
98131 // Store integer results back (R2, R3)
99- MOVD R2, syscall15Args_a1(R9)
100- MOVD R3, syscall15Args_a2(R9)
132+ MOVD R2, syscallArgs_a1(R9)
133+ MOVD R3, syscallArgs_a2(R9)
134+ MOVD $0, syscallArgs_a3(R9) // no errno here: clear the input argument
101135
102136 // Store float return values (F0, F2, F4, F6)
103- FMOVD F0, syscall15Args_f1(R9)
104- FMOVD F2, syscall15Args_f2(R9)
105- FMOVD F4, syscall15Args_f3(R9)
106- FMOVD F6, syscall15Args_f4(R9)
137+ FMOVD F0, syscallArgs_f1(R9)
138+ FMOVD F2, syscallArgs_f2(R9)
139+ FMOVD F4, syscallArgs_f3(R9)
140+ FMOVD F6, syscallArgs_f4(R9)
107141
108142 // Deallocate stack frame
109143 ADD $STACK_SIZE, R15
M sys_unix_386.s +82 -74
@@ -29,9 +29,10 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
2929 // 0-15: saved callee-saved registers (BX, SI, DI, BP)
3030 // 16-19: saved callback index
3131 // 20-23: saved return address
32- // 24-35: callbackArgs struct (12 bytes)
33- // 36-291: copy of C arguments (256 bytes for 64 args, matching callbackMaxFrame)
34- // Total: 292 bytes, round up to 304 for alignment
32+ // 24-47: callbackArgs struct (24 bytes: index, args, result[4],
33+ // matching Go's callbackArgs with room for 64-bit returns)
34+ // 48-303: copy of C arguments (256 bytes for 64 args, matching callbackMaxFrame)
35+ // Total: 304 bytes, already 16-byte aligned
3536 SUBL $304, SP
3637
3738 // Save callee-saved registers
@@ -46,148 +47,152 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
4647
4748 // Copy C arguments from original stack location to our frame
4849 // Original args start at 304+4(SP) = 308(SP) (past our frame + original return addr)
49- // Copy to our frame at 36(SP)
50+ // Copy to our frame at 48(SP)
5051 // Copy 64 arguments (256 bytes, matching callbackMaxFrame = 64 * ptrSize)
5152 MOVL 308(SP), AX
52- MOVL AX, 36(SP)
53+ MOVL AX, 48(SP)
5354 MOVL 312(SP), AX
54- MOVL AX, 40(SP)
55+ MOVL AX, 52(SP)
5556 MOVL 316(SP), AX
56- MOVL AX, 44(SP)
57+ MOVL AX, 56(SP)
5758 MOVL 320(SP), AX
58- MOVL AX, 48(SP)
59+ MOVL AX, 60(SP)
5960 MOVL 324(SP), AX
60- MOVL AX, 52(SP)
61+ MOVL AX, 64(SP)
6162 MOVL 328(SP), AX
62- MOVL AX, 56(SP)
63+ MOVL AX, 68(SP)
6364 MOVL 332(SP), AX
64- MOVL AX, 60(SP)
65+ MOVL AX, 72(SP)
6566 MOVL 336(SP), AX
66- MOVL AX, 64(SP)
67+ MOVL AX, 76(SP)
6768 MOVL 340(SP), AX
68- MOVL AX, 68(SP)
69+ MOVL AX, 80(SP)
6970 MOVL 344(SP), AX
70- MOVL AX, 72(SP)
71+ MOVL AX, 84(SP)
7172 MOVL 348(SP), AX
72- MOVL AX, 76(SP)
73+ MOVL AX, 88(SP)
7374 MOVL 352(SP), AX
74- MOVL AX, 80(SP)
75+ MOVL AX, 92(SP)
7576 MOVL 356(SP), AX
76- MOVL AX, 84(SP)
77+ MOVL AX, 96(SP)
7778 MOVL 360(SP), AX
78- MOVL AX, 88(SP)
79+ MOVL AX, 100(SP)
7980 MOVL 364(SP), AX
80- MOVL AX, 92(SP)
81+ MOVL AX, 104(SP)
8182 MOVL 368(SP), AX
82- MOVL AX, 96(SP)
83+ MOVL AX, 108(SP)
8384 MOVL 372(SP), AX
84- MOVL AX, 100(SP)
85+ MOVL AX, 112(SP)
8586 MOVL 376(SP), AX
86- MOVL AX, 104(SP)
87+ MOVL AX, 116(SP)
8788 MOVL 380(SP), AX
88- MOVL AX, 108(SP)
89+ MOVL AX, 120(SP)
8990 MOVL 384(SP), AX
90- MOVL AX, 112(SP)
91+ MOVL AX, 124(SP)
9192 MOVL 388(SP), AX
92- MOVL AX, 116(SP)
93+ MOVL AX, 128(SP)
9394 MOVL 392(SP), AX
94- MOVL AX, 120(SP)
95+ MOVL AX, 132(SP)
9596 MOVL 396(SP), AX
96- MOVL AX, 124(SP)
97+ MOVL AX, 136(SP)
9798 MOVL 400(SP), AX
98- MOVL AX, 128(SP)
99+ MOVL AX, 140(SP)
99100 MOVL 404(SP), AX
100- MOVL AX, 132(SP)
101+ MOVL AX, 144(SP)
101102 MOVL 408(SP), AX
102- MOVL AX, 136(SP)
103+ MOVL AX, 148(SP)
103104 MOVL 412(SP), AX
104- MOVL AX, 140(SP)
105+ MOVL AX, 152(SP)
105106 MOVL 416(SP), AX
106- MOVL AX, 144(SP)
107+ MOVL AX, 156(SP)
107108 MOVL 420(SP), AX
108- MOVL AX, 148(SP)
109+ MOVL AX, 160(SP)
109110 MOVL 424(SP), AX
110- MOVL AX, 152(SP)
111+ MOVL AX, 164(SP)
111112 MOVL 428(SP), AX
112- MOVL AX, 156(SP)
113+ MOVL AX, 168(SP)
113114 MOVL 432(SP), AX
114- MOVL AX, 160(SP)
115+ MOVL AX, 172(SP)
115116 MOVL 436(SP), AX
116- MOVL AX, 164(SP)
117+ MOVL AX, 176(SP)
117118 MOVL 440(SP), AX
118- MOVL AX, 168(SP)
119+ MOVL AX, 180(SP)
119120 MOVL 444(SP), AX
120- MOVL AX, 172(SP)
121+ MOVL AX, 184(SP)
121122 MOVL 448(SP), AX
122- MOVL AX, 176(SP)
123+ MOVL AX, 188(SP)
123124 MOVL 452(SP), AX
124- MOVL AX, 180(SP)
125+ MOVL AX, 192(SP)
125126 MOVL 456(SP), AX
126- MOVL AX, 184(SP)
127+ MOVL AX, 196(SP)
127128 MOVL 460(SP), AX
128- MOVL AX, 188(SP)
129+ MOVL AX, 200(SP)
129130 MOVL 464(SP), AX
130- MOVL AX, 192(SP)
131+ MOVL AX, 204(SP)
131132 MOVL 468(SP), AX
132- MOVL AX, 196(SP)
133+ MOVL AX, 208(SP)
133134 MOVL 472(SP), AX
134- MOVL AX, 200(SP)
135+ MOVL AX, 212(SP)
135136 MOVL 476(SP), AX
136- MOVL AX, 204(SP)
137+ MOVL AX, 216(SP)
137138 MOVL 480(SP), AX
138- MOVL AX, 208(SP)
139+ MOVL AX, 220(SP)
139140 MOVL 484(SP), AX
140- MOVL AX, 212(SP)
141+ MOVL AX, 224(SP)
141142 MOVL 488(SP), AX
142- MOVL AX, 216(SP)
143+ MOVL AX, 228(SP)
143144 MOVL 492(SP), AX
144- MOVL AX, 220(SP)
145+ MOVL AX, 232(SP)
145146 MOVL 496(SP), AX
146- MOVL AX, 224(SP)
147+ MOVL AX, 236(SP)
147148 MOVL 500(SP), AX
148- MOVL AX, 228(SP)
149+ MOVL AX, 240(SP)
149150 MOVL 504(SP), AX
150- MOVL AX, 232(SP)
151+ MOVL AX, 244(SP)
151152 MOVL 508(SP), AX
152- MOVL AX, 236(SP)
153+ MOVL AX, 248(SP)
153154 MOVL 512(SP), AX
154- MOVL AX, 240(SP)
155+ MOVL AX, 252(SP)
155156 MOVL 516(SP), AX
156- MOVL AX, 244(SP)
157+ MOVL AX, 256(SP)
157158 MOVL 520(SP), AX
158- MOVL AX, 248(SP)
159+ MOVL AX, 260(SP)
159160 MOVL 524(SP), AX
160- MOVL AX, 252(SP)
161+ MOVL AX, 264(SP)
161162 MOVL 528(SP), AX
162- MOVL AX, 256(SP)
163+ MOVL AX, 268(SP)
163164 MOVL 532(SP), AX
164- MOVL AX, 260(SP)
165+ MOVL AX, 272(SP)
165166 MOVL 536(SP), AX
166- MOVL AX, 264(SP)
167+ MOVL AX, 276(SP)
167168 MOVL 540(SP), AX
168- MOVL AX, 268(SP)
169+ MOVL AX, 280(SP)
169170 MOVL 544(SP), AX
170- MOVL AX, 272(SP)
171+ MOVL AX, 284(SP)
171172 MOVL 548(SP), AX
172- MOVL AX, 276(SP)
173+ MOVL AX, 288(SP)
173174 MOVL 552(SP), AX
174- MOVL AX, 280(SP)
175+ MOVL AX, 292(SP)
175176 MOVL 556(SP), AX
176- MOVL AX, 284(SP)
177+ MOVL AX, 296(SP)
177178 MOVL 560(SP), AX
178- MOVL AX, 288(SP)
179+ MOVL AX, 300(SP)
179180
180181 // Set up callbackArgs struct at 24(SP)
181182 // struct callbackArgs {
182- // index uintptr // offset 0
183- // args *byte // offset 4
184- // result uintptr // offset 8
183+ // index uintptr // offset 0
184+ // args unsafe.Pointer // offset 4
185+ // result [4]uintptr // offset 8: result[0] and result[1] hold
186+ // a 64-bit return, result[2:] are unused
185187 // }
186188 MOVL 16(SP), AX // callback index
187189 MOVL AX, 24(SP) // callbackArgs.index
188- LEAL 36(SP), AX // pointer to copied arguments
190+ LEAL 48(SP), AX // pointer to copied arguments
189191 MOVL AX, 28(SP) // callbackArgs.args
190- MOVL $0, 32(SP) // callbackArgs.result = 0
192+ MOVL $0, 32(SP) // callbackArgs.result[0] = 0
193+ MOVL $0, 36(SP) // callbackArgs.result[1] = 0
194+ MOVL $0, 40(SP) // callbackArgs.result[2] = 0
195+ MOVL $0, 44(SP) // callbackArgs.result[3] = 0
191196
192197 // Call crosscall2(fn, frame, 0, ctxt)
193198 // crosscall2 expects arguments on stack:
@@ -209,8 +214,11 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
209214
210215 ADDL $16, SP
211216
212- // Get result from callbackArgs.result
217+ // Get result from callbackArgs.result. i386 cdecl returns 64-bit
218+ // values in EDX:EAX, so forward both halves written by
219+ // setInt64Result/setUint64Result.
213220 MOVL 32(SP), AX
221+ MOVL 36(SP), DX
214222
215223 // Restore callee-saved registers
216224 MOVL 0(SP), BX
M sys_unix_arm.s +11 -1
@@ -33,8 +33,11 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
3333 MOVW R2, 136(R13)
3434 MOVW R3, 140(R13)
3535
36+ // Skip floating point registers if runtime.goarmsoftfp!=0.
37+ MOVB runtime·goarmsoftfp(SB), R11
38+ CMP $0, R11
39+ BNE skipfpsave
3640 // Save floating point registers F0-F7 at SP+64 (frame[0..15])
37- // Note: We always save these since we target hard-float ABI.
3841 MOVD F0, 64(R13)
3942 MOVD F1, 72(R13)
4043 MOVD F2, 80(R13)
@@ -44,6 +47,7 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
4447 MOVD F6, 112(R13)
4548 MOVD F7, 120(R13)
4649
50+skipfpsave:
4751 // Set up callbackArgs at SP+48
4852 MOVW 36(R13), R4
4953 MOVW R4, 48(R13)
@@ -51,6 +55,7 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
5155 MOVW R4, 52(R13)
5256 MOVW $0, R4
5357 MOVW R4, 56(R13)
58+ MOVW R4, 60(R13) // high word of a 64-bit return
5459
5560 // Call crosscall2(fn, frame, 0, ctxt)
5661 MOVW ·callbackWrap_call(SB), R0
@@ -63,7 +68,11 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
6368
6469 // Get result
6570 MOVW 56(R13), R0
71+ MOVW 60(R13), R1 // high word of a 64-bit return
6672
73+ MOVB runtime·goarmsoftfp(SB), R11
74+ CMP $0, R11
75+ BNE skipfprest
6776 // Restore float registers
6877 MOVD 64(R13), F0
6978 MOVD 72(R13), F1
@@ -74,6 +83,7 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
7483 MOVD 112(R13), F6
7584 MOVD 120(R13), F7
7685
86+skipfprest:
7787 // Restore callee-saved registers
7888 MOVW 0(R13), R4
7989 MOVW 4(R13), R5
M sys_unix_s390x.s +10 -8
@@ -18,22 +18,24 @@
1818 //
1919 // S390X uses R2-R6 for integer arguments (5 registers) and F0,F2,F4,F6 for floats (4 registers).
2020 //
21-// Our frame layout (total 264 bytes, 8-byte aligned):
21+// Our frame layout (total 200 bytes, 8-byte aligned). To stay under the
22+// 800-byte nosplit limit on Go 1.27 the callbackArgs struct is stored in the
23+// caller's free linkage slot (old_R15+16..48) instead of our own frame:
2224 // 0(R15) - back chain
2325 // 48(R15) - saved R6-R15 (done by STMG)
24-// 160(R15) - callbackArgs struct (32 bytes: index, args, result, stackArgs)
25-// 192(R15) - args array start
26+// 128(R15) - args array start
2627 //
2728 // Args array layout:
2829 // - floats F0,F2,F4,F6 (32 bytes)
2930 // - ints R2-R6 (40 bytes)
30-// Total args array: 72 bytes, ends at 264
31+// Total args array: 72 bytes, ends at 200
3132 //
33+// callbackArgs struct lives at old_R15+16 (32 bytes: index, args, result, stackArgs)
3234 // Stack args in caller's frame start at old_R15+160
3335
34-#define FRAME_SIZE 264
35-#define CB_ARGS 160
36-#define ARGS_ARRAY 192
36+#define FRAME_SIZE 200
37+#define ARGS_ARRAY 128
38+#define CB_ARGS (FRAME_SIZE+16)
3739 #define FLOAT_OFF 0
3840 #define INT_OFF 32
3941
@@ -71,7 +73,7 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0
7173 MOVD R5, (ARGS_ARRAY+INT_OFF+3*8)(R15)
7274
7375 // R6 (5th int arg) was saved at 48(old_R15) by STMG
74- // old_R15 = current R15 + FRAME_SIZE, so R6 is at 48+FRAME_SIZE(R15) = 312(R15)
76+ // old_R15 = current R15 + FRAME_SIZE
7577 MOVD (48+FRAME_SIZE)(R15), R1
7678 MOVD R1, (ARGS_ARRAY+INT_OFF+4*8)(R15)
7779
M syscall.go +56 -65
@@ -1,66 +1,65 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-//go:build !386 && !arm && (darwin || freebsd || linux || netbsd || windows)
4+//go:build !386 && !arm && !ppc64le && (darwin || freebsd || linux || netbsd || windows)
55
66 package purego
77
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{}
8+import (
9+ "runtime"
10+ "structs"
11+ "unsafe"
12+)
1413
1514 const (
16- maxArgs = 15
15+ maxArgs = 32
1716 )
1817
19-type syscall15Args struct {
20- _ hostLayout
21- fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
22- f1, f2, f3, f4, f5, f6, f7, f8 uintptr
23- arm64_r8 uintptr
18+// syscallArgs is the argument block handed to the syscall15 trampoline. On
19+// platforms that go through internal/cgo it is passed to C as a
20+// struct syscallArgs, so its layout must match the C ABI.
21+//
22+// The trampoline reports its results through this block: the return values go
23+// to a1 and a2 and, where the platform captures errno, the error code goes to
24+// a3. A trampoline that cannot capture errno must clear a3.
25+type syscallArgs struct {
26+ _ structs.HostLayout
27+
28+ fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
29+ a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr
30+ f1, f2, f3, f4, f5, f6, f7, f8 uintptr
31+ arm64_r8 uintptr
2432 }
2533
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
34+func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintptr) *syscallArgs {
35+ s := thePool.Get().(*syscallArgs)
36+ *s = syscallArgs{
37+ fn: fn,
38+ a1: sysargs[0], a2: sysargs[1], a3: sysargs[2], a4: sysargs[3],
39+ a5: sysargs[4], a6: sysargs[5], a7: sysargs[6], a8: sysargs[7],
40+ a9: sysargs[8], a10: sysargs[9], a11: sysargs[10], a12: sysargs[11],
41+ a13: sysargs[12], a14: sysargs[13], a15: sysargs[14], a16: sysargs[15],
42+ a17: sysargs[16], a18: sysargs[17], a19: sysargs[18], a20: sysargs[19],
43+ a21: sysargs[20], a22: sysargs[21], a23: sysargs[22], a24: sysargs[23],
44+ a25: sysargs[24], a26: sysargs[25], a27: sysargs[26], a28: sysargs[27],
45+ a29: sysargs[28], a30: sysargs[29], a31: sysargs[30], a32: sysargs[31],
46+ f1: floats[0], f2: floats[1], f3: floats[2], f4: floats[3],
47+ f5: floats[4], f6: floats[5], f7: floats[6], f8: floats[7],
48+ arm64_r8: r8,
49+ }
50+ runtime_cgocall(syscallXABI0, unsafe.Pointer(s))
51+ return s
5252 }
5353
5454 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
55-// There is an internal maximum number of arguments that SyscallN can take.
56-// On 386 and arm, SyscallN supports at most 15 uintptr arguments.
57-// It panics when the maximum is exceeded. It returns the result and the libc error code if there is one.
55+// There is an internal maximum number of arguments that SyscallN can take. It panics
56+// when the maximum is exceeded. It returns the result and GetLastError on Windows or the errno on
57+// platforms that capture it, otherwise 0.
5858 //
5959 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
6060 // especially point 4.
6161 //
6262 // NOTE: SyscallN does not properly call functions that have both integer and float parameters.
63-// Use RegisterFunc instead when the target function mixes integer and floating-point arguments.
6463 // See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607
6564 // for an explanation of why that is.
6665 //
@@ -79,30 +78,22 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
7978 if len(args) > maxArgs {
8079 panic("purego: too many arguments to SyscallN")
8180 }
82- // add padding so there is no out-of-bounds slicing
81+
82+ // Add padding so there is no out-of-bounds slicing.
8383 var tmp [maxArgs]uintptr
84- copy(tmp[:], args)
85- 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])
84+ n := copy(tmp[:], args)
85+ return syscallPadded(fn, &tmp, n)
8686 }
8787
88-// SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.
89-// It is designed for method-like calls where the first argument is always a receiver pointer.
90-// It shares SyscallN's argument-placement limits and mixed integer/float restriction.
91-// On 386 and arm, the total count including self is limited to 15 uintptr arguments.
92-// This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).
93-//
94-//go:uintptrescapes
95-func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) {
96- if fn == 0 {
97- panic("purego: fn is nil")
98- }
99- if len(args) > maxArgs-1 {
100- panic("purego: too many arguments to SyscallSelf")
88+// syscallPadded calls fn with the first n values of tmp as arguments.
89+func syscallPadded(fn uintptr, tmp *[maxArgs]uintptr, n int) (r1, r2, err uintptr) {
90+ // Windows uses syscall.SyscallN in syscall_windows.go.
91+ if runtime.GOOS == "windows" {
92+ return syscall_syscallN(fn, tmp[:n]...)
10193 }
102- // Use a stack-allocated array to prepend self without heap allocation.
103- // self goes in tmp[0], remaining args follow.
104- var tmp [maxArgs]uintptr
105- tmp[0] = self
106- copy(tmp[1:], args)
107- 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])
94+ var floats [maxArgs]uintptr
95+ copy(floats[:], tmp[:])
96+ s := syscall_SyscallN(fn, tmp[:], floats[:], 0)
97+ defer thePool.Put(s)
98+ return s.a1, s.a2, s.a3
10899 }
M syscall_32bit.go +53 -67
@@ -5,69 +5,63 @@
55
66 package purego
77
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{}
8+import (
9+ "runtime"
10+ "structs"
11+ "unsafe"
12+)
1413
1514 const (
16- maxArgs = 15
15+ maxArgs = 32
1716 )
1817
19-type syscall15Args struct {
20- _ hostLayout
18+// syscallArgs is the argument block handed to the syscall15 trampoline. On
19+// platforms that go through internal/cgo it is passed to C as a
20+// struct syscallArgs, so its layout must match the C ABI.
21+//
22+// The trampoline reports its results through this block: the return values go
23+// to a1 and a2 and, where the platform captures errno, the error code goes to
24+// a3. A trampoline that cannot capture errno must clear a3.
25+type syscallArgs struct {
26+ _ structs.HostLayout
27+
2128 fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
2229 a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr
2330 f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16 uintptr
2431 arm64_r8 uintptr
2532 }
2633
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.f1 = floats[0]
45- s.f2 = floats[1]
46- s.f3 = floats[2]
47- s.f4 = floats[3]
48- s.f5 = floats[4]
49- s.f6 = floats[5]
50- s.f7 = floats[6]
51- s.f8 = floats[7]
52- s.f9 = floats[8]
53- s.f10 = floats[9]
54- s.f11 = floats[10]
55- s.f12 = floats[11]
56- s.f13 = floats[12]
57- s.f14 = floats[13]
58- s.f15 = floats[14]
59- s.arm64_r8 = r8
34+func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintptr) *syscallArgs {
35+ s := thePool.Get().(*syscallArgs)
36+ *s = syscallArgs{
37+ fn: fn,
38+ a1: sysargs[0], a2: sysargs[1], a3: sysargs[2], a4: sysargs[3],
39+ a5: sysargs[4], a6: sysargs[5], a7: sysargs[6], a8: sysargs[7],
40+ a9: sysargs[8], a10: sysargs[9], a11: sysargs[10], a12: sysargs[11],
41+ a13: sysargs[12], a14: sysargs[13], a15: sysargs[14], a16: sysargs[15],
42+ a17: sysargs[16], a18: sysargs[17], a19: sysargs[18], a20: sysargs[19],
43+ a21: sysargs[20], a22: sysargs[21], a23: sysargs[22], a24: sysargs[23],
44+ a25: sysargs[24], a26: sysargs[25], a27: sysargs[26], a28: sysargs[27],
45+ a29: sysargs[28], a30: sysargs[29], a31: sysargs[30], a32: sysargs[31],
46+ f1: floats[0], f2: floats[1], f3: floats[2], f4: floats[3],
47+ f5: floats[4], f6: floats[5], f7: floats[6], f8: floats[7],
48+ f9: floats[8], f10: floats[9], f11: floats[10], f12: floats[11],
49+ f13: floats[12], f14: floats[13], f15: floats[14], f16: floats[15],
50+ arm64_r8: r8,
51+ }
52+ runtime_cgocall(syscallXABI0, unsafe.Pointer(s))
53+ return s
6054 }
6155
6256 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
63-// On 386 and arm, SyscallN supports at most 15 uintptr arguments.
64-// It panics when the maximum is exceeded. It returns the result and the libc error code if there is one.
57+// There is an internal maximum number of arguments that SyscallN can take. It panics
58+// when the maximum is exceeded. It returns the result and GetLastError on Windows or the errno on
59+// platforms that capture it, otherwise 0.
6560 //
6661 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
6762 // especially point 4.
6863 //
6964 // NOTE: SyscallN does not properly call functions that have both integer and float parameters.
70-// Use RegisterFunc instead when the target function mixes integer and floating-point arguments.
7165 // See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607
7266 // for an explanation of why that is.
7367 //
@@ -86,30 +80,22 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
8680 if len(args) > maxArgs {
8781 panic("purego: too many arguments to SyscallN")
8882 }
89- // add padding so there is no out-of-bounds slicing
83+
84+ // Add padding so there is no out-of-bounds slicing.
9085 var tmp [maxArgs]uintptr
91- copy(tmp[:], args)
92- 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])
86+ n := copy(tmp[:], args)
87+ return syscallPadded(fn, &tmp, n)
9388 }
9489
95-// SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.
96-// It is designed for method-like calls where the first argument is always a receiver pointer.
97-// It shares SyscallN's argument-placement limits and mixed integer/float restriction.
98-// On 386 and arm, the total count including self is limited to 15 uintptr arguments.
99-// This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).
100-//
101-//go:uintptrescapes
102-func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) {
103- if fn == 0 {
104- panic("purego: fn is nil")
105- }
106- if len(args) > maxArgs-1 {
107- panic("purego: too many arguments to SyscallSelf")
90+// syscallPadded calls fn with the first n values of tmp as arguments.
91+func syscallPadded(fn uintptr, tmp *[maxArgs]uintptr, n int) (r1, r2, err uintptr) {
92+ // Windows uses syscall.SyscallN in syscall_windows.go.
93+ if runtime.GOOS == "windows" {
94+ return syscall_syscallN(fn, tmp[:n]...)
10895 }
109- // Use a stack-allocated array to prepend self without heap allocation.
110- // self goes in tmp[0], remaining args follow.
111- var tmp [maxArgs]uintptr
112- tmp[0] = self
113- copy(tmp[1:], args)
114- 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])
96+ var floats [16]uintptr
97+ copy(floats[:], tmp[:16])
98+ s := syscall_SyscallN(fn, tmp[:], floats[:], 0)
99+ defer thePool.Put(s)
100+ return s.a1, s.a2, s.a3
115101 }
D syscall_32bit_internal_test.go +0 -16
@@ -1,16 +0,0 @@
1-//go:build (386 || arm) && (freebsd || linux || netbsd || windows)
2-
3-package purego
4-
5-import "testing"
6-
7-func TestSyscall15ArgsSetDoesNotPanicWithMaxArgs(t *testing.T) {
8- defer func() {
9- if r := recover(); r != nil {
10- t.Fatalf("Set panicked: %v", r)
11- }
12- }()
13-
14- var s syscall15Args
15- s.Set(1, make([]uintptr, maxArgs), make([]uintptr, maxArgs), 8)
16-}
D syscall_32bit_test.go +0 -36
@@ -1,36 +0,0 @@
1-//go:build (386 || arm) && (freebsd || linux || netbsd || windows)
2-
3-package purego_test
4-
5-import (
6- "fmt"
7- "testing"
8-
9- "github.com/bnema/purego"
10-)
11-
12-func TestSyscallN32BitPanicsAt16Args(t *testing.T) {
13- defer func() {
14- r := recover()
15- if r == nil {
16- t.Fatal("expected panic")
17- }
18- if got := fmt.Sprint(r); got != "purego: too many arguments to SyscallN" {
19- t.Fatalf("panic = %q, want %q", got, "purego: too many arguments to SyscallN")
20- }
21- }()
22- purego.SyscallN(uintptr(1), make([]uintptr, 16)...)
23-}
24-
25-func TestSyscallSelf32BitPanicsAt16Args(t *testing.T) {
26- defer func() {
27- r := recover()
28- if r == nil {
29- t.Fatal("expected panic")
30- }
31- if got := fmt.Sprint(r); got != "purego: too many arguments to SyscallSelf" {
32- t.Fatalf("panic = %q, want %q", got, "purego: too many arguments to SyscallSelf")
33- }
34- }()
35- purego.SyscallSelf(uintptr(1), 1, make([]uintptr, 15)...)
36-}
M syscall_bench_test.go +11 -11
@@ -37,7 +37,7 @@ func BenchmarkCallingMethods(b *testing.B) {
3737
3838 // Build C library for benchmarking
3939 libFileName := filepath.Join(b.TempDir(), "libbenchmark.so")
40- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "benchmarktest", "benchmark.c")); err != nil {
40+ if err := buildSharedLib(b, "CC", libFileName, filepath.Join("testdata", "benchmarktest", "benchmark.c")); err != nil {
4141 b.Fatalf("Failed to build C library: %v", err)
4242 }
4343 b.Cleanup(func() {
@@ -116,7 +116,7 @@ func BenchmarkCallingMethods(b *testing.B) {
116116 args := int64sToUintptrs(tc.args)
117117 var result uintptr
118118 b.ResetTimer()
119- for i := 0; i < b.N; i++ {
119+ for range b.N {
120120 result, _, _ = purego.SyscallN(tc.goFnPtr, args...)
121121 }
122122 b.StopTimer()
@@ -135,7 +135,7 @@ func BenchmarkCallingMethods(b *testing.B) {
135135 args := int64sToUintptrs(tc.args)
136136 var result uintptr
137137 b.ResetTimer()
138- for i := 0; i < b.N; i++ {
138+ for range b.N {
139139 result, _, _ = purego.SyscallN(tc.cFnPtr, args...)
140140 }
141141 b.StopTimer()
@@ -165,7 +165,7 @@ func BenchmarkCallingMethods(b *testing.B) {
165165
166166 var result uintptr
167167 b.ResetTimer()
168- for i := 0; i < b.N; i++ {
168+ for range b.N {
169169 result, _, _ = purego.SyscallN(tc.cCallbackPtr, callbackArgs...)
170170 }
171171 b.StopTimer()
@@ -205,40 +205,40 @@ func callRegisterFunc(registerFn any, n int, args []int64, iterations int) int64
205205 switch n {
206206 case 1:
207207 f := registerFn.(*func(int64) int64)
208- for i := 0; i < iterations; i++ {
208+ for range iterations {
209209 result = (*f)(args[0])
210210 }
211211 case 2:
212212 f := registerFn.(*func(int64, int64) int64)
213- for i := 0; i < iterations; i++ {
213+ for range iterations {
214214 result = (*f)(args[0], args[1])
215215 }
216216 case 3:
217217 f := registerFn.(*func(int64, int64, int64) int64)
218- for i := 0; i < iterations; i++ {
218+ for range iterations {
219219 result = (*f)(args[0], args[1], args[2])
220220 }
221221 case 5:
222222 f := registerFn.(*func(int64, int64, int64, int64, int64) int64)
223- for i := 0; i < iterations; i++ {
223+ for range iterations {
224224 result = (*f)(args[0], args[1], args[2], args[3], args[4])
225225 }
226226 case 10:
227227 f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
228- for i := 0; i < iterations; i++ {
228+ for range iterations {
229229 result = (*f)(args[0], args[1], args[2], args[3], args[4],
230230 args[5], args[6], args[7], args[8], args[9])
231231 }
232232 case 14:
233233 f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
234- for i := 0; i < iterations; i++ {
234+ for range iterations {
235235 result = (*f)(args[0], args[1], args[2], args[3], args[4],
236236 args[5], args[6], args[7], args[8], args[9],
237237 args[10], args[11], args[12], args[13])
238238 }
239239 case 15:
240240 f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
241- for i := 0; i < iterations; i++ {
241+ for range iterations {
242242 result = (*f)(args[0], args[1], args[2], args[3], args[4],
243243 args[5], args[6], args[7], args[8], args[9],
244244 args[10], args[11], args[12], args[13], args[14])
M syscall_cgo_linux.go +5 -6
@@ -9,13 +9,12 @@ import (
99 "github.com/bnema/purego/internal/cgo"
1010 )
1111
12-var syscall15XABI0 = uintptr(cgo.Syscall15XABI0)
13-
14-//go:nosplit
15-func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) {
16- return cgo.Syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15)
17-}
12+var syscallXABI0 = uintptr(cgo.SyscallXABI0)
1813
1914 func NewCallback(_ any) uintptr {
2015 panic("purego: NewCallback on Linux is only supported on 386/amd64/arm64/arm/loong64/ppc64le/riscv64/s390x")
2116 }
17+
18+func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
19+ panic("purego: syscall_syscallN is only supported on windows")
20+}
M syscall_linux_test.go +7 -7
@@ -10,7 +10,7 @@ import (
1010 "os"
1111 "path/filepath"
1212 "runtime"
13- "sort"
13+ "slices"
1414 "strings"
1515 "syscall"
1616 "testing"
@@ -119,8 +119,8 @@ func compareStatus(filter, expect string) error {
119119 // "... : bad file descriptor.
120120 continue
121121 }
122- lines := strings.Split(string(d), "\n")
123- for _, line := range lines {
122+ lines := strings.SplitSeq(string(d), "\n")
123+ for line := range lines {
124124 // Different kernel vintages pad differently.
125125 line = strings.TrimSpace(line)
126126 if strings.HasPrefix(line, "Pid:\t") {
@@ -149,7 +149,7 @@ func compareStatus(filter, expect string) error {
149149 // https://github.com/golang/go/issues/46145
150150 // Containers don't reliably output this line in sorted order so manually sort and compare that.
151151 a := strings.Split(line[8:], " ")
152- sort.Slice(a, func(i, j int) bool { return a[i] < a[j] })
152+ slices.Sort(a)
153153 got := strings.Join(a, " ")
154154 if got == expected[8:] {
155155 foundAThread = true
@@ -173,7 +173,7 @@ func compareStatus(filter, expect string) error {
173173 func TestDlopenThenAllThreadsSyscall(t *testing.T) {
174174 // Step 1: Build and load a shared C library that calls back into Go.
175175 libFileName := filepath.Join(t.TempDir(), "libcbtest.so")
176- if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {
176+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "libcbtest", "callback_test.c")); err != nil {
177177 t.Fatal(err)
178178 }
179179 defer os.Remove(libFileName)
@@ -192,12 +192,12 @@ func TestDlopenThenAllThreadsSyscall(t *testing.T) {
192192 }
193193
194194 cb := purego.NewCallback(goFunc)
195- for i := 0; i < 10; i++ {
195+ for range 10 {
196196 callCallback(cb, "hello")
197197 }
198198
199199 // Step 2: Generate thread churn + AllThreadsSyscall (via Setuid).
200- for i := 0; i < 20; i++ {
200+ for range 20 {
201201 c := make(chan struct{})
202202 go func() {
203203 runtime.LockOSThread()
M syscall_notstackargs.go +18 -1
@@ -17,7 +17,6 @@ import "unsafe"
1717 // the runtime's own callback mechanism, so this type is compiled but unused,
1818 // serving only as a stub to satisfy cross-platform compilation.
1919 type callbackArgs struct {
20- _ hostLayout
2120 index uintptr
2221 // args points to the argument block.
2322 //
@@ -36,3 +35,21 @@ type callbackArgs struct {
3635 func (c *callbackArgs) stackFrame() unsafe.Pointer {
3736 return nil
3837 }
38+
39+func (c *callbackArgs) intFrame() unsafe.Pointer {
40+ return nil
41+}
42+
43+func (c *callbackArgs) setInt64Result(result int64) {
44+ c.result[0] = uintptr(result)
45+ if unsafe.Sizeof(uintptr(0)) == 4 {
46+ c.result[1] = uintptr(result >> 32)
47+ }
48+}
49+
50+func (c *callbackArgs) setUint64Result(result uint64) {
51+ c.result[0] = uintptr(result)
52+ if unsafe.Sizeof(uintptr(0)) == 4 {
53+ c.result[1] = uintptr(result >> 32)
54+ }
55+}
A syscall_ppc64le.go +79 -0
@@ -0,0 +1,79 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build linux
5+
6+package purego
7+
8+import (
9+ "unsafe"
10+)
11+
12+const (
13+ maxArgs = 15
14+)
15+
16+type syscallArgs struct {
17+ fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
18+ f1, f2, f3, f4, f5, f6, f7, f8 uintptr
19+ arm64_r8 uintptr
20+}
21+
22+func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintptr) *syscallArgs {
23+ s := thePool.Get().(*syscallArgs)
24+ *s = syscallArgs{
25+ fn: fn,
26+ a1: sysargs[0], a2: sysargs[1], a3: sysargs[2], a4: sysargs[3],
27+ a5: sysargs[4], a6: sysargs[5], a7: sysargs[6], a8: sysargs[7],
28+ a9: sysargs[8], a10: sysargs[9], a11: sysargs[10], a12: sysargs[11],
29+ a13: sysargs[12], a14: sysargs[13], a15: sysargs[14],
30+ f1: floats[0], f2: floats[1], f3: floats[2], f4: floats[3],
31+ f5: floats[4], f6: floats[5], f7: floats[6], f8: floats[7],
32+ arm64_r8: r8,
33+ }
34+ runtime_cgocall(syscallXABI0, unsafe.Pointer(s))
35+ return s
36+}
37+
38+// SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
39+// There is an internal maximum number of arguments that SyscallN can take. It panics
40+// when the maximum is exceeded. It returns the result. This file builds only for
41+// linux/ppc64le, where the trampoline does not capture errno, so err is always 0.
42+//
43+// In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
44+// especially point 4.
45+//
46+// NOTE: SyscallN does not properly call functions that have both integer and float parameters.
47+// See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607
48+// for an explanation of why that is.
49+//
50+// On amd64, if there are more than 8 floats the 9th and so on will be placed incorrectly on the
51+// stack.
52+//
53+// The pragma go:nosplit is not needed at this function declaration because it uses go:uintptrescapes
54+// which forces all the objects that the uintptrs point to onto the heap where a stack split won't affect
55+// their memory location.
56+//
57+//go:uintptrescapes
58+func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
59+ if fn == 0 {
60+ panic("purego: fn is nil")
61+ }
62+ if len(args) > maxArgs {
63+ panic("purego: too many arguments to SyscallN")
64+ }
65+
66+ // Add padding so there is no out-of-bounds slicing.
67+ var tmp [maxArgs]uintptr
68+ n := copy(tmp[:], args)
69+ return syscallPadded(fn, &tmp, n)
70+}
71+
72+// syscallPadded calls fn with the first n values of tmp as arguments.
73+func syscallPadded(fn uintptr, tmp *[maxArgs]uintptr, n int) (r1, r2, err uintptr) {
74+ var floats [maxArgs]uintptr
75+ copy(floats[:], tmp[:])
76+ s := syscall_SyscallN(fn, tmp[:], floats[:], 0)
77+ defer thePool.Put(s)
78+ return s.a1, s.a2, s.a3
79+}
A syscall_self.go +24 -0
@@ -0,0 +1,24 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || linux || netbsd || windows
5+
6+package purego
7+
8+// SyscallSelf calls fn with self as the first argument followed by args.
9+// It is equivalent to SyscallN(fn, append([]uintptr{self}, args...)...)
10+// without allocating the combined argument slice.
11+//
12+//go:uintptrescapes
13+func SyscallSelf(fn, self uintptr, args ...uintptr) (r1, r2, err uintptr) {
14+ if fn == 0 {
15+ panic("purego: fn is nil")
16+ }
17+ if len(args) > maxArgs-1 {
18+ panic("purego: too many arguments to SyscallSelf")
19+ }
20+ var tmp [maxArgs]uintptr
21+ tmp[0] = self
22+ n := copy(tmp[1:], args)
23+ return syscallPadded(fn, &tmp, n+1)
24+}
A syscall_self_test.go +69 -0
@@ -0,0 +1,69 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2022 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || linux || netbsd || windows
5+
6+package purego_test
7+
8+import (
9+ "fmt"
10+ "testing"
11+
12+ "github.com/bnema/purego"
13+)
14+
15+func TestSyscallSelf(t *testing.T) {
16+ sum2 := purego.NewCallback(func(self, a1 uintptr) uintptr { return self + a1 })
17+ sum4 := purego.NewCallback(func(self, a1, a2, a3 uintptr) uintptr { return self + a1 + a2 + a3 })
18+ triple := purego.NewCallback(func(self uintptr) uintptr { return self * 3 })
19+
20+ tests := []struct {
21+ name string
22+ fn uintptr
23+ self uintptr
24+ args []uintptr
25+ want uintptr
26+ }{
27+ {"one arg", sum2, 10, []uintptr{20}, 30},
28+ {"zero self", sum2, 0, []uintptr{42}, 42},
29+ {"multiple args", sum4, 1, []uintptr{2, 3, 4}, 10},
30+ {"no variadic args", triple, 7, nil, 21},
31+ }
32+ for _, tc := range tests {
33+ t.Run(tc.name, func(t *testing.T) {
34+ if r1, _, _ := purego.SyscallSelf(tc.fn, tc.self, tc.args...); r1 != tc.want {
35+ t.Errorf("SyscallSelf returned %d, want %d", r1, tc.want)
36+ }
37+ })
38+ }
39+}
40+
41+func TestSyscallSelfPanics(t *testing.T) {
42+ t.Run("nil fn", func(t *testing.T) {
43+ defer func() {
44+ if recover() == nil {
45+ t.Error("expected panic for nil fn")
46+ }
47+ }()
48+ purego.SyscallSelf(0, 1)
49+ })
50+
51+ t.Run("too many args", func(t *testing.T) {
52+ defer func() {
53+ if got := fmt.Sprint(recover()); got != "purego: too many arguments to SyscallSelf" {
54+ t.Errorf("unexpected panic: %s", got)
55+ }
56+ }()
57+ fn := purego.NewCallback(func(uintptr) uintptr { return 0 })
58+ purego.SyscallSelf(fn, 1, make([]uintptr, purego.MaxArgs)...)
59+ })
60+}
61+
62+func TestSyscallSelfAllocs(t *testing.T) {
63+ fn := purego.NewCallback(func(self, a1, a2 uintptr) uintptr { return self + a1 + a2 })
64+ directAllocs := testing.AllocsPerRun(100, func() { purego.SyscallN(fn, 1, 2, 3) })
65+ selfAllocs := testing.AllocsPerRun(100, func() { purego.SyscallSelf(fn, 1, 2, 3) })
66+ if selfAllocs > directAllocs {
67+ t.Fatalf("SyscallSelf allocs = %v, SyscallN allocs = %v", selfAllocs, directAllocs)
68+ }
69+}
D syscall_stackargs.go +0 -43
@@ -1,43 +0,0 @@
1-// SPDX-License-Identifier: Apache-2.0
2-// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3-
4-//go:build ppc64le || s390x
5-
6-package purego
7-
8-import "unsafe"
9-
10-// callbackArgs is the argument block passed from the assembly trampoline
11-// to callbackWrap when C code calls a Go callback registered with NewCallback.
12-// The assembly fills in the fields before calling callbackWrap, which uses
13-// them to determine which Go function to invoke and where to read its
14-// arguments from, and writes the return value back into result.
15-//
16-// callbackArgs is only used on Unix. On Windows, callbacks are handled by
17-// the runtime's own callback mechanism, so this type is compiled but unused,
18-// serving only as a stub to satisfy cross-platform compilation.
19-type callbackArgs struct {
20- _ hostLayout
21- index uintptr
22- // args points to the argument block.
23- //
24- // The structure of the arguments goes
25- // float registers followed by the
26- // integer registers followed by the stack.
27- //
28- // This variable is treated as a contiguous
29- // block of memory containing all of the arguments
30- // for this callback.
31- args unsafe.Pointer
32- // Below are out-args from callbackWrap
33- result [1]uintptr
34- // stackArgs points to stack-passed arguments for architectures where
35- // they can't be made contiguous with register args (e.g., ppc64le).
36- // On other architectures, this is nil and stack args are read from
37- // the end of the args block.
38- stackArgs unsafe.Pointer
39-}
40-
41-func (c *callbackArgs) stackFrame() unsafe.Pointer {
42- return c.stackArgs
43-}
A syscall_stackargs_ppc64le.go +38 -0
@@ -0,0 +1,38 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+import "unsafe"
7+
8+// callbackArgs is the argument block passed from the assembly trampoline
9+// to callbackWrap when C code calls a Go callback registered with NewCallback.
10+type callbackArgs struct {
11+ index uintptr
12+ // args points to the argument block.
13+ //
14+ // The structure of the arguments goes
15+ // float registers followed by the
16+ // integer registers.
17+ args unsafe.Pointer
18+ // Below are out-args from callbackWrap.
19+ result [1]uintptr
20+ // stackArgs points to stack-passed arguments.
21+ stackArgs unsafe.Pointer
22+}
23+
24+func (c *callbackArgs) stackFrame() unsafe.Pointer {
25+ return c.stackArgs
26+}
27+
28+func (c *callbackArgs) intFrame() unsafe.Pointer {
29+ return nil
30+}
31+
32+func (c *callbackArgs) setInt64Result(result int64) {
33+ c.result[0] = uintptr(result)
34+}
35+
36+func (c *callbackArgs) setUint64Result(result uint64) {
37+ c.result[0] = uintptr(result)
38+}
A syscall_stackargs_s390x.go +38 -0
@@ -0,0 +1,38 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+import "unsafe"
7+
8+// callbackArgs is the argument block passed from the assembly trampoline
9+// to callbackWrap when C code calls a Go callback registered with NewCallback.
10+type callbackArgs struct {
11+ index uintptr
12+ // args points to the argument block.
13+ //
14+ // The structure of the arguments goes
15+ // float registers followed by the
16+ // integer registers followed by the stack.
17+ args unsafe.Pointer
18+ // Below are out-args from callbackWrap.
19+ result [1]uintptr
20+ // stackArgs points to stack-passed arguments.
21+ stackArgs unsafe.Pointer
22+}
23+
24+func (c *callbackArgs) stackFrame() unsafe.Pointer {
25+ return c.stackArgs
26+}
27+
28+func (c *callbackArgs) intFrame() unsafe.Pointer {
29+ return nil
30+}
31+
32+func (c *callbackArgs) setInt64Result(result int64) {
33+ c.result[0] = uintptr(result)
34+}
35+
36+func (c *callbackArgs) setUint64Result(result uint64) {
37+ c.result[0] = uintptr(result)
38+}
M syscall_test.go +36 -83
@@ -4,11 +4,10 @@
44 package purego_test
55
66 import (
7- "fmt"
87 "os"
8+ "path/filepath"
99 "runtime"
1010 "testing"
11- "unsafe"
1211
1312 "github.com/bnema/purego"
1413 "github.com/bnema/purego/internal/load"
@@ -26,24 +25,54 @@ func TestOS(t *testing.T) {
2625 }
2726 }
2827
28+// errnoIsCaptured reports whether SyscallN returns the libc errno as its third
29+// value on this platform.
30+func errnoIsCaptured() bool {
31+ switch runtime.GOOS {
32+ case "darwin":
33+ return true
34+ case "linux":
35+ switch runtime.GOARCH {
36+ case "mips", "mipsle", "mips64", "mips64le", "ppc64":
37+ return true
38+ }
39+ }
40+ return false
41+}
42+
2943 func TestErrno(t *testing.T) {
30- if runtime.GOOS != "darwin" {
44+ if !errnoIsCaptured() {
3145 t.Skip("platform does not support returning errno from syscall")
3246 }
3347
34- libc, err := load.OpenLibrary("/usr/lib/libSystem.B.dylib")
48+ libFileName := filepath.Join(t.TempDir(), "liberrnotest.so")
49+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "liberrnotest", "errno_test.c")); err != nil {
50+ t.Fatal(err)
51+ }
52+ defer os.Remove(libFileName)
53+
54+ lib, err := load.OpenLibrary(libFileName)
3555 if err != nil {
3656 t.Fatal(err)
3757 }
3858
39- openSym, err := load.OpenSymbol(libc, "open")
59+ setErrno, err := load.OpenSymbol(lib, "setErrno")
4060 if err != nil {
4161 t.Fatal(err)
4262 }
4363
44- r1, _, errno := purego.SyscallN(openSym, uintptr(unsafe.Pointer(&[]byte("_file_that_does_not_exist_\x00")[0])), uintptr(os.O_RDWR))
64+ r1, _, errno := purego.SyscallN(setErrno)
4565 if int32(r1) != -1 {
46- t.Errorf("open returned %d, wanted -1", r1)
66+ t.Errorf("setErrno returned %d, wanted -1", r1)
67+ }
68+
69+ libcName, err := getSystemLibrary()
70+ if err != nil {
71+ t.Fatal(err)
72+ }
73+ libc, err := load.OpenLibrary(libcName)
74+ if err != nil {
75+ t.Fatal(err)
4776 }
4877
4978 var strerror func(int32) string
@@ -55,79 +84,3 @@ func TestErrno(t *testing.T) {
5584 t.Errorf("strerror returned %q, wanted \"%s\"", got, expected)
5685 }
5786 }
58-
59-func TestSyscallSelf(t *testing.T) {
60- // Test that SyscallSelf(fn, self, args...) produces the same result as
61- // SyscallN(fn, self, args...) for various argument counts.
62-
63- // sum2 takes (self, a1) and returns self + a1
64- sum2 := purego.NewCallback(func(self, a1 uintptr) uintptr {
65- return self + a1
66- })
67- // sum4 takes (self, a1, a2, a3) and returns self + a1 + a2 + a3
68- sum4 := purego.NewCallback(func(self, a1, a2, a3 uintptr) uintptr {
69- return self + a1 + a2 + a3
70- })
71-
72- tests := []struct {
73- name string
74- fn uintptr
75- self uintptr
76- args []uintptr
77- want uintptr
78- }{
79- {"no extra args", sum2, 10, []uintptr{20}, 30},
80- {"zero self", sum2, 0, []uintptr{42}, 42},
81- {"multiple args", sum4, 1, []uintptr{2, 3, 4}, 10},
82- {"no variadic args",
83- purego.NewCallback(func(self uintptr) uintptr { return self * 3 }),
84- 7, nil, 21},
85- }
86-
87- for _, tc := range tests {
88- t.Run(tc.name, func(t *testing.T) {
89- r1, _, _ := purego.SyscallSelf(tc.fn, tc.self, tc.args...)
90- if r1 != tc.want {
91- t.Errorf("SyscallSelf returned %d, want %d", r1, tc.want)
92- }
93-
94- // Verify it matches SyscallN with manually prepended self
95- allArgs := make([]uintptr, 0, 1+len(tc.args))
96- allArgs = append(allArgs, tc.self)
97- allArgs = append(allArgs, tc.args...)
98- r1n, _, _ := purego.SyscallN(tc.fn, allArgs...)
99- if r1 != r1n {
100- t.Errorf("SyscallSelf result %d differs from SyscallN result %d", r1, r1n)
101- }
102- })
103- }
104-}
105-
106-func TestSyscallSelfPanics(t *testing.T) {
107- t.Run("nil fn", func(t *testing.T) {
108- defer func() {
109- if r := recover(); r == nil {
110- t.Error("expected panic for nil fn")
111- }
112- }()
113- purego.SyscallSelf(0, 1)
114- })
115-
116- t.Run("too many args", func(t *testing.T) {
117- defer func() {
118- r := recover()
119- if r == nil {
120- t.Error("expected panic for too many args")
121- }
122- msg := fmt.Sprint(r)
123- if msg != "purego: too many arguments to SyscallSelf" {
124- t.Errorf("unexpected panic message: %s", msg)
125- }
126- }()
127- // Create a valid callback so we don't panic on nil fn
128- fn := purego.NewCallback(func(uintptr) uintptr { return 0 })
129- // Pass more args than maxArgs-1 allows (maxArgs is 15 on 64-bit)
130- args := make([]uintptr, 15)
131- purego.SyscallSelf(fn, 1, args...)
132- })
133-}
M syscall_unix.go +105 -62
@@ -1,8 +1,7 @@
11 // SPDX-License-Identifier: Apache-2.0
22 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors
33
4-// TODO: remove s390x cgo dependency once golang/go#77449 is resolved
5-//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (cgo && s390x))) || netbsd
4+//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (s390x && (cgo || go1.27)))) || netbsd
65
76 package purego
87
@@ -10,6 +9,7 @@ import (
109 "encoding/json"
1110 "errors"
1211 "fmt"
12+ "math"
1313 "os"
1414 "reflect"
1515 "runtime"
@@ -22,31 +22,31 @@ import (
2222 internalstrings "github.com/bnema/purego/internal/strings"
2323 )
2424
25-var syscall15XABI0 uintptr
25+var syscallXABI0 uintptr
2626
27-func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) {
28- args := &syscall15Args{
29- fn: fn,
30- a1: a1, a2: a2, a3: a3, a4: a4, a5: a5, a6: a6, a7: a7, a8: a8,
31- a9: a9, a10: a10, a11: a11, a12: a12, a13: a13, a14: a14, a15: a15,
32- f1: a1, f2: a2, f3: a3, f4: a4, f5: a5, f6: a6, f7: a7, f8: a8,
33- }
34-
35- runtime_cgocall(syscall15XABI0, unsafe.Pointer(args))
36- return args.a1, args.a2, args.a3
27+func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
28+ panic("purego: syscall_syscallN is only supported on windows")
3729 }
3830
3931 // NewCallback converts a Go function to a function pointer conforming to the C calling convention.
4032 // This is useful when interoperating with C code requiring callbacks. The argument is expected to be a
4133 // function with zero or one uintptr-sized result. The function must not have arguments with size larger than the size
4234 // of uintptr. Only a limited number of callbacks may be created in a single Go process, and any memory allocated
43-// for these callbacks is never released. At least 2000 callbacks can always be created. Although this function
35+// for these callbacks is never released. At least 1024 callbacks can always be created. Although this function
4436 // provides similar functionality to windows.NewCallback it is distinct.
37+//
38+// Every call to NewCallback creates a new callback even for the same function value, so passing a Go callback to C
39+// inside a loop (e.g. a qsort comparator) keeps consuming callbacks and eventually panics once they are exhausted.
40+// The same happens when a func value is passed to a C function, as [RegisterFunc] creates a new callback for each
41+// call. Create the callback once with NewCallback and reuse the returned pointer instead.
4542 func NewCallback(fn any) uintptr {
4643 ty := reflect.TypeOf(fn)
47- for i := 0; i < ty.NumIn(); i++ {
44+ if ty == nil || ty.Kind() != reflect.Func {
45+ panic("purego: the type must be a function but was not")
46+ }
47+ for i := range ty.NumIn() {
4848 in := ty.In(i)
49- if !in.AssignableTo(reflect.TypeOf(CDecl{})) {
49+ if !in.AssignableTo(reflect.TypeFor[CDecl]()) {
5050 continue
5151 }
5252 if i != 0 {
@@ -101,7 +101,6 @@ func UnrefCallback(cb uintptr) error {
101101 delete(cbs.knownIdx, cb)
102102 cbs.holes[idx] = struct{}{}
103103 cbs.funcs[idx] = reflect.Value{}
104- cbs.argPools[idx] = nil
105104 recordCallbackLedger("release", idx, cb, key, val, false, "")
106105 return nil
107106 }
@@ -134,7 +133,6 @@ func UnrefCallbackFnPtr(fnPtr any) error {
134133 cbs.fnPtrKeys[idx] = 0
135134 cbs.holes[idx] = struct{}{}
136135 cbs.funcs[idx] = reflect.Value{}
137- cbs.argPools[idx] = nil
138136 recordCallbackLedger("release-fnptr", idx, addr, val.Pointer(), callbackVal, false, "")
139137 return nil
140138 }
@@ -148,7 +146,6 @@ var cbs = struct {
148146 numFn int // the highest allocated callback index + 1
149147 holes map[int]struct{} // reusable callback slots
150148 funcs [maxCB]reflect.Value // the saved callbacks
151- argPools [maxCB]*sync.Pool // pre-allocated argument buffers per callback
152149 knownIdx map[uintptr]int // callback address -> slot index
153150 knownFnPtr map[uintptr]uintptr // function pointer variable address -> callback address
154151 fnPtrKeys [maxCB]uintptr // slot index -> function pointer variable address
@@ -342,10 +339,6 @@ func traceCallbackAllocation(event string, val reflect.Value, remaining int) {
342339 if traceFile == "" {
343340 traceFile = fmt.Sprintf("/tmp/purego-callback-trace-%d.log", os.Getpid())
344341 }
345- // traceCallbackAllocation intentionally accepts the opt-in
346- // PUREGO_CALLBACK_TRACE_FILE path. Users must set PUREGO_CALLBACK_TRACE=1 to
347- // enable tracing, and callers who override the path control their debugging
348- // environment. This documents the deliberate gosec G304 tradeoff for audits.
349342 if f, err := os.OpenFile(traceFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); err == nil {
350343 _, _ = f.WriteString(message)
351344 _ = f.Close()
@@ -361,14 +354,14 @@ func compileCallback(fn any) uintptr {
361354 panic("purego: function must not be nil")
362355 }
363356 ty := val.Type()
364- for i := 0; i < ty.NumIn(); i++ {
357+ for i := range ty.NumIn() {
365358 in := ty.In(i)
366359 switch in.Kind() {
367360 case reflect.Struct:
368- if i == 0 && in.AssignableTo(reflect.TypeOf(CDecl{})) {
361+ if i == 0 && in.AssignableTo(reflect.TypeFor[CDecl]()) {
369362 continue
370363 }
371- ensureStructSupported()
364+ ensureCallbackStructSupported()
372365 checkStructFieldsSupported(in)
373366 continue
374367 case reflect.Interface, reflect.Func, reflect.Slice,
@@ -381,9 +374,13 @@ output:
381374 switch {
382375 case ty.NumOut() == 1:
383376 switch ty.Out(0).Kind() {
377+ case reflect.Struct:
378+ ensureCallbackStructSupported()
379+ checkStructFieldsSupported(ty.Out(0))
380+ break output
384381 case reflect.Pointer, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
385382 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
386- reflect.Bool, reflect.UnsafePointer, reflect.Struct:
383+ reflect.Bool, reflect.UnsafePointer:
387384 break output
388385 }
389386 panic("purego: unsupported return type: " + ty.String())
@@ -414,12 +411,6 @@ output:
414411 cbs.numFn++
415412 }
416413 cbs.funcs[index] = val
417- numIn := ty.NumIn()
418- cbs.argPools[index] = &sync.Pool{
419- New: func() any {
420- return make([]reflect.Value, numIn)
421- },
422- }
423414 addr := callbackasmAddr(index)
424415 cbs.knownIdx[addr] = index
425416 if reused {
@@ -449,26 +440,23 @@ var callbackWrap_call = callbackWrap
449440 // callbackWrap is called by assembly code which determines which Go function to call.
450441 // This function takes the arguments and passes them to the Go function and returns the result.
451442 func callbackWrap(a *callbackArgs) {
452- cbs.lock.RLock()
443+ cbs.lock.Lock()
453444 fn := cbs.funcs[a.index]
454- pool := cbs.argPools[a.index]
455- cbs.lock.RUnlock()
445+ cbs.lock.Unlock()
456446 fnType := fn.Type()
457- args := pool.Get().([]reflect.Value)
458- defer func() {
459- for i := range args {
460- args[i] = reflect.Value{}
461- }
462- pool.Put(args)
463- }()
447+ args := make([]reflect.Value, fnType.NumIn())
464448 frame := (*[callbackMaxFrame]uintptr)(a.args)
465449 // stackFrame points to stack-passed arguments. On most architectures this is
466450 // contiguous with frame (after register args), but on ppc64le it's separate.
467451 var stackFrame *[callbackMaxFrame]uintptr
452+ var intFrame *[callbackMaxFrame]uintptr
468453 if sf := a.stackFrame(); sf != nil {
469454 // Only ppc64le uses separate stackArgs pointer due to NOSPLIT constraints
470455 stackFrame = (*[callbackMaxFrame]uintptr)(sf)
471456 }
457+ if intf := a.intFrame(); intf != nil {
458+ intFrame = (*[callbackMaxFrame]uintptr)(intf)
459+ }
472460 // floatsN and intsN track the number of register slots used, not argument count.
473461 // This distinction matters on ARM32 where float64 uses 2 slots (32-bit registers).
474462 var floatsN int
@@ -476,7 +464,7 @@ func callbackWrap(a *callbackArgs) {
476464 // On amd64/loong64/ppc64le/riscv64/s390x, when returning a struct larger than
477465 // maxRegAllocStructSize, the caller passes a hidden pointer in the first integer
478466 // register. Skip it to avoid misreading it as the first function argument.
479- if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") &&
467+ if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") &&
480468 fnType.NumOut() == 1 && fnType.Out(0).Kind() == reflect.Struct &&
481469 fnType.Out(0).Size() > maxRegAllocStructSize {
482470 intsN = 1
@@ -494,33 +482,62 @@ func callbackWrap(a *callbackArgs) {
494482 stackByteOffset := uintptr(0)
495483 for i := range args {
496484 // slots is the number of pointer-sized slots the argument takes
497- var slots int
498485 inType := fnType.In(i)
486+ slots := int((inType.Size() + ptrSize - 1) / ptrSize)
499487 switch inType.Kind() {
500488 case reflect.Float32, reflect.Float64:
501- slots = int((fnType.In(i).Size() + ptrSize - 1) / ptrSize)
489+ if isARMSoftFloat() {
490+ // we should restore from integer slot, can skip unnecessary branching here
491+ if isARMPaddingNeeded(inType, -1, intsN) {
492+ intsN++
493+ }
494+ if intsN+slots <= numOfIntegerRegisters() {
495+ // the integers begin after the floats in frame
496+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[intsN+numOfFloatRegisters()])).Elem()
497+ intsN += slots
498+ continue
499+ }
500+ if isARMPaddingNeeded(inType, -1, stackSlot) {
501+ stackSlot++
502+ }
503+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
504+ stackSlot += slots
505+ intsN += slots
506+ continue
507+ }
508+
502509 if floatsN+slots > numOfFloatRegisters() {
503- if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
510+ if isDarwin && runtime.GOARCH == "arm64" {
504511 // Darwin ARM64: read from packed stack with proper alignment
505512 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
506513 } else if stackFrame != nil {
507514 // ppc64le/s390x: stack args are in separate stackFrame
508- if runtime.GOARCH == "s390x" {
515+ switch runtime.GOARCH {
516+ case "ppc64le":
517+ args[i] = callbackFloatFromDoubleSlot(unsafe.Pointer(&stackFrame[stackSlot]), inType)
518+ case "s390x":
509519 // s390x big-endian: sub-8-byte values are right-justified
510520 args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&stackFrame[stackSlot]), inType)
511- } else {
521+ default:
512522 args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()
513523 }
514524 stackSlot += slots
525+ } else if isARMFloatPaddingNeeded(inType, -1, stackSlot) {
526+ stackSlot++
527+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
528+ stackSlot += slots
515529 } else {
516530 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
517531 stackSlot += slots
518532 }
519533 } else {
520- if runtime.GOARCH == "s390x" {
534+ switch runtime.GOARCH {
535+ case "ppc64le":
536+ args[i] = callbackFloatFromDoubleSlot(unsafe.Pointer(&frame[floatsN]), inType)
537+ case "s390x":
521538 // s390x big-endian: float32 is right-justified in 8-byte FPR slot
522539 args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&frame[floatsN]), inType)
523- } else {
540+ default:
524541 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[floatsN])).Elem()
525542 }
526543 }
@@ -550,7 +567,7 @@ func callbackWrap(a *callbackArgs) {
550567 args[i] = reflect.ValueOf(internalstrings.GoString(ptr))
551568 continue
552569 case reflect.Struct:
553- if i == 0 && inType.AssignableTo(reflect.TypeOf(CDecl{})) {
570+ if i == 0 && inType.AssignableTo(reflect.TypeFor[CDecl]()) {
554571 args[i] = reflect.Zero(inType)
555572 continue
556573 }
@@ -561,9 +578,11 @@ func callbackWrap(a *callbackArgs) {
561578 args[i] = getCallbackStruct(inType, a.args, &floatsN, &intsN, &stackSlot, &stackByteOffset)
562579 continue
563580 default:
564- slots = int((inType.Size() + ptrSize - 1) / ptrSize)
581+ if isARMPaddingNeeded(inType, -1, intsN) {
582+ intsN++
583+ }
565584 if intsN+slots > numOfIntegerRegisters() {
566- if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
585+ if isDarwin && runtime.GOARCH == "arm64" {
567586 // Darwin ARM64: read from packed stack with proper alignment
568587 args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
569588 } else if stackFrame != nil {
@@ -575,18 +594,26 @@ func callbackWrap(a *callbackArgs) {
575594 args[i] = reflect.NewAt(inType, unsafe.Pointer(&stackFrame[stackSlot])).Elem()
576595 }
577596 stackSlot += slots
597+ } else if isARMPaddingNeeded(inType, -1, stackSlot) {
598+ stackSlot++
599+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
600+ stackSlot += slots
578601 } else {
579602 args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[stackSlot])).Elem()
580603 stackSlot += slots
581604 }
582605 } else {
583- // the integers begin after the floats in frame
584- pos := intsN + numOfFloatRegisters()
585- if runtime.GOARCH == "s390x" {
586- // s390x big-endian: sub-8-byte values are right-justified in GPR slot
587- args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&frame[pos]), inType)
606+ if intFrame != nil {
607+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&intFrame[intsN])).Elem()
588608 } else {
589- args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()
609+ // the integers begin after the floats in frame
610+ pos := intsN + numOfFloatRegisters()
611+ if runtime.GOARCH == "s390x" {
612+ // s390x big-endian: sub-8-byte values are right-justified in GPR slot
613+ args[i] = callbackArgFromSlotBigEndian(unsafe.Pointer(&frame[pos]), inType)
614+ } else {
615+ args[i] = reflect.NewAt(inType, unsafe.Pointer(&frame[pos])).Elem()
616+ }
590617 }
591618 }
592619 intsN += slots
@@ -595,9 +622,13 @@ func callbackWrap(a *callbackArgs) {
595622 ret := fn.Call(args)
596623 if len(ret) > 0 {
597624 switch k := ret[0].Kind(); k {
598- case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uintptr:
625+ case reflect.Uint64:
626+ a.setUint64Result(ret[0].Uint())
627+ case reflect.Uint, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uintptr:
599628 a.result[0] = uintptr(ret[0].Uint())
600- case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
629+ case reflect.Int64:
630+ a.setInt64Result(ret[0].Int())
631+ case reflect.Int, reflect.Int32, reflect.Int16, reflect.Int8:
601632 a.result[0] = uintptr(ret[0].Int())
602633 case reflect.Bool:
603634 if ret[0].Bool() {
@@ -640,6 +671,18 @@ func callbackArgFromStack(argsBase unsafe.Pointer, stackSlot int, stackByteOffse
640671 return reflect.NewAt(inType, ptr).Elem()
641672 }
642673
674+// callbackFloatFromDoubleSlot reads a floating-point callback argument from an
675+// 8-byte register or stack slot on ppc64le, where a single-precision value is
676+// held in double-precision format.
677+func callbackFloatFromDoubleSlot(slotPtr unsafe.Pointer, inType reflect.Type) reflect.Value {
678+ if inType.Kind() != reflect.Float32 {
679+ return reflect.NewAt(inType, slotPtr).Elem()
680+ }
681+ v := reflect.New(inType).Elem()
682+ v.SetFloat(math.Float64frombits(*(*uint64)(slotPtr)))
683+ return v
684+}
685+
643686 // callbackArgFromSlotBigEndian reads an argument from an 8-byte slot on big-endian architectures.
644687 // On s390x:
645688 // - Integer types are right-justified in GPRs: sub-8-byte values are at offset (8 - size)
M syscall_windows.go +14 -6
@@ -11,10 +11,10 @@ import (
1111 "unsafe"
1212 )
1313
14-var syscall15XABI0 uintptr
14+var syscallXABI0 uintptr
1515
16-func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) {
17- r1, r2, errno := syscall.Syscall15(fn, 15, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15)
16+func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
17+ r1, r2, errno := syscall.SyscallN(fn, args...)
1818 return r1, r2, uintptr(errno)
1919 }
2020
@@ -23,14 +23,22 @@ func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a
2323 // function with one uintptr-sized result. The function must not have arguments with size larger than the
2424 // size of uintptr. Only a limited number of callbacks may be created in a single Go process, and any memory
2525 // allocated for these callbacks is never released. Between NewCallback and NewCallbackCDecl, at least 1024
26-// callbacks can always be created. Although this function is similiar to the darwin version it may act
26+// callbacks can always be created. Although this function is similar to the darwin version it may act
2727 // differently.
28+//
29+// Every call to NewCallback creates a new callback even for the same function value, so passing a Go callback to C
30+// inside a loop (e.g. a qsort comparator) keeps consuming callbacks and eventually panics once they are exhausted.
31+// The same happens when a func value is passed to a C function, as [RegisterFunc] creates a new callback for each
32+// call. Create the callback once with NewCallback and reuse the returned pointer instead.
2833 func NewCallback(fn any) uintptr {
2934 isCDecl := false
3035 ty := reflect.TypeOf(fn)
31- for i := 0; i < ty.NumIn(); i++ {
36+ if ty == nil || ty.Kind() != reflect.Func {
37+ panic("purego: the type must be a function but was not")
38+ }
39+ for i := range ty.NumIn() {
3240 in := ty.In(i)
33- if !in.AssignableTo(reflect.TypeOf(CDecl{})) {
41+ if !in.AssignableTo(reflect.TypeFor[CDecl]()) {
3442 continue
3543 }
3644 if i != 0 {
M testdata/abitest/abi_test.c +82 -0
@@ -129,3 +129,85 @@ void stack_25_int64_exceeds(char *buf, size_t bufsize, int64_t a1, int64_t a2, i
129129 snprintf(buf, bufsize, "%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64 ":%" PRId64,
130130 a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25);
131131 }
132+
133+uintptr_t stack_20_uintptr(
134+ uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, uintptr_t a5,
135+ uintptr_t a6, uintptr_t a7, uintptr_t a8, uintptr_t a9, uintptr_t a10,
136+ uintptr_t a11, uintptr_t a12, uintptr_t a13, uintptr_t a14, uintptr_t a15,
137+ uintptr_t a16, uintptr_t a17, uintptr_t a18, uintptr_t a19, uintptr_t a20
138+) {
139+ return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 +
140+ a11 + a12 + a13 + a14 + a15 + a16 + a17 + a18 + a19 + a20;
141+}
142+
143+uintptr_t stack_32_uintptr(
144+ uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, uintptr_t a5, uintptr_t a6, uintptr_t a7, uintptr_t a8,
145+ uintptr_t a9, uintptr_t a10, uintptr_t a11, uintptr_t a12, uintptr_t a13, uintptr_t a14, uintptr_t a15, uintptr_t a16,
146+ uintptr_t a17, uintptr_t a18, uintptr_t a19, uintptr_t a20, uintptr_t a21, uintptr_t a22, uintptr_t a23, uintptr_t a24,
147+ uintptr_t a25, uintptr_t a26, uintptr_t a27, uintptr_t a28, uintptr_t a29, uintptr_t a30, uintptr_t a31, uintptr_t a32
148+) {
149+ return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 +
150+ a9 + a10 + a11 + a12 + a13 + a14 + a15 + a16 +
151+ a17 + a18 + a19 + a20 + a21 + a22 + a23 + a24 +
152+ a25 + a26 + a27 + a28 + a29 + a30 + a31 + a32;
153+}
154+
155+double stack_32_mixed_int_float(
156+ uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, uintptr_t a5, uintptr_t a6, uintptr_t a7, uintptr_t a8,
157+ uintptr_t a9, uintptr_t a10, uintptr_t a11, uintptr_t a12, uintptr_t a13, uintptr_t a14, uintptr_t a15, uintptr_t a16,
158+ double f1, double f2, double f3, double f4, double f5, double f6, double f7, double f8,
159+ double f9, double f10, double f11, double f12, double f13, double f14, double f15, double f16
160+) {
161+ return (double)a1 * 1 + (double)a2 * 2 + (double)a3 * 3 + (double)a4 * 4 +
162+ (double)a5 * 5 + (double)a6 * 6 + (double)a7 * 7 + (double)a8 * 8 +
163+ (double)a9 * 9 + (double)a10 * 10 + (double)a11 * 11 + (double)a12 * 12 +
164+ (double)a13 * 13 + (double)a14 * 14 + (double)a15 * 15 + (double)a16 * 16 +
165+ f1 * 17 + f2 * 18 + f3 * 19 + f4 * 20 +
166+ f5 * 21 + f6 * 22 + f7 * 23 + f8 * 24 +
167+ f9 * 25 + f10 * 26 + f11 * 27 + f12 * 28 +
168+ f13 * 29 + f14 * 30 + f15 * 31 + f16 * 32;
169+}
170+
171+int64_t arm_int64_unaligned_in_registers(uintptr_t a1, int64_t a2) {
172+ return (int64_t)a1 * 123 + a2;
173+}
174+
175+int64_t arm_int64_unaligned_on_stack(uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, uintptr_t a5, int64_t a6) {
176+ return (int64_t)a1 * 1 + (int64_t)a2 * 2 + (int64_t)a3 * 3 + (int64_t)a4 * 4 +
177+ (int64_t)a5 * 5 + a6;
178+}
179+
180+int64_t arm_int64_unaligned_on_stack_after_floats(
181+ uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, /* these args are going to integer registers */
182+ float f1, float f2, float f3, float f4, /* these args are going to float registers */
183+ float f5, float f6, float f7, float f8,
184+ float f9, float f10, float f11, float f12,
185+ float f13, float f14, float f15, float f16,
186+ float f17, /* this is a first arg on stack */
187+ int64_t a5 /* this arg must be padded */
188+) {
189+ return (int64_t)a1 * 1 + (int64_t)a2 * 2 + (int64_t)a3 * 3 + (int64_t)a4 * 4 + a5;
190+}
191+
192+double arm_float64_unaligned_in_registers(uintptr_t a1, double a2) {
193+ return (double)a1 * 123.5 + a2;
194+}
195+
196+double arm_float64_unaligned_on_stack(uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, uintptr_t a5, double a6) {
197+ return (double)a1 * 1 + (double)a2 * 2 + (double)a3 * 3 + (double)a4 * 4 +
198+ (double)a5 * 5 + a6;
199+}
200+
201+typedef int32_t (*AddFunc)(int32_t, int32_t);
202+
203+int32_t returned_add(int32_t a, int32_t b) {
204+ return a + b;
205+}
206+
207+AddFunc return_func_ptr(void) {
208+ return returned_add;
209+}
210+
211+AddFunc return_null_func_ptr(void) {
212+ return NULL;
213+}
A testdata/liberrnotest/errno_test.c +10 -0
@@ -0,0 +1,10 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#include <errno.h>
5+
6+// setErrno fails the way a C library call does: it returns -1 and sets errno.
7+int setErrno(void) {
8+ errno = ENOENT;
9+ return -1;
10+}
A testdata/slicetest/slice_test.c +34 -0
@@ -0,0 +1,34 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#include <stddef.h>
5+#include <string.h>
6+
7+static char buf[256];
8+
9+const char *join_strings(const char **strs) {
10+ buf[0] = '\0';
11+ if (strs == NULL) {
12+ return "(null)";
13+ }
14+ for (size_t i = 0; strs[i] != NULL; i++) {
15+ if (i > 0) {
16+ strcat(buf, ",");
17+ }
18+ strcat(buf, strs[i]);
19+ }
20+ return buf;
21+}
22+
23+static const char *items[] = {"alpha", "beta", "gamma", NULL};
24+
25+const char **get_strings(int empty) {
26+ if (empty) {
27+ return NULL;
28+ }
29+ return items;
30+}
31+
32+float sum_floats4(const float v[4]) {
33+ return v[0] + v[1] + v[2] + v[3];
34+}
M testdata/structtest/struct_test.c +33 -38
@@ -5,16 +5,11 @@
55 #include <stdio.h>
66 #include <stdlib.h>
77
8-#if defined(__x86_64__) || defined(__aarch64__)
9-typedef int64_t GoInt;
10-typedef uint64_t GoUint;
11-#endif
12-
138 // Empty is empty
149 struct Empty {};
1510
1611 // NoStruct tests that an empty struct doesn't cause issues
17-unsigned long NoStruct(struct Empty e) {
12+uint64_t NoStruct(struct Empty e) {
1813 return 0xdeadbeef;
1914 }
2015
@@ -22,28 +17,28 @@ struct EmptyEmpty {
2217 struct Empty x;
2318 };
2419
25-unsigned long EmptyEmpty(struct EmptyEmpty e) {
20+uint64_t EmptyEmpty(struct EmptyEmpty e) {
2621 return 0xdeadbeef;
2722 }
2823
29-unsigned long EmptyEmptyWithReg(unsigned int x, struct EmptyEmpty e, unsigned int y) {
24+uint64_t EmptyEmptyWithReg(unsigned int x, struct EmptyEmpty e, unsigned int y) {
3025 return (x << 16) | y;
3126 }
3227
3328 // GreaterThan16Bytes is 24 bytes on 64 bit systems
3429 struct GreaterThan16Bytes {
35- long *x, *y, *z;
30+ int64_t *x, *y, *z;
3631 };
3732
3833 // GreaterThan16Bytes is a basic test for structs bigger than 16 bytes
39-unsigned long GreaterThan16Bytes(struct GreaterThan16Bytes g) {
34+uint64_t GreaterThan16Bytes(struct GreaterThan16Bytes g) {
4035 return *g.x + *g.y + *g.z;
4136 }
4237
4338 // AfterRegisters tests to make sure that structs placed on the stack work properly
44-unsigned long AfterRegisters(long a, long b, long c, long d, long e, long f, long g, long h, struct GreaterThan16Bytes bytes) {
45- long registers = a + b + c + d + e + f + g + h;
46- long stack = *bytes.x + *bytes.y + *bytes.z;
39+uint64_t AfterRegisters(intptr_t a, intptr_t b, intptr_t c, intptr_t d, intptr_t e, intptr_t f, intptr_t g, intptr_t h, struct GreaterThan16Bytes bytes) {
40+ intptr_t registers = a + b + c + d + e + f + g + h;
41+ int64_t stack = *bytes.x + *bytes.y + *bytes.z;
4742 if (registers != stack) {
4843 return 0xbadbad;
4944 }
@@ -53,25 +48,25 @@ unsigned long AfterRegisters(long a, long b, long c, long d, long e, long f, lon
5348 return stack;
5449 }
5550
56-unsigned long BeforeRegisters(struct GreaterThan16Bytes bytes, long a, long b) {
51+uint64_t BeforeRegisters(struct GreaterThan16Bytes bytes, int64_t a, int64_t b) {
5752 return *bytes.x + *bytes.y + *bytes.z + a + b;
5853 }
5954
6055 struct GreaterThan16BytesStruct {
6156 struct {
62- long *x, *y, *z;
57+ int64_t *x, *y, *z;
6358 } a ;
6459 };
6560
66-unsigned long GreaterThan16BytesStruct(struct GreaterThan16BytesStruct g) {
61+uint64_t GreaterThan16BytesStruct(struct GreaterThan16BytesStruct g) {
6762 return *(g.a.x) + *(g.a.y) + *(g.a.z);
6863 }
6964
7065 struct IntLessThan16Bytes {
71- long x, y;
66+ int64_t x, y;
7267 };
7368
74-unsigned long IntLessThan16Bytes(struct IntLessThan16Bytes l) {
69+uint64_t IntLessThan16Bytes(struct IntLessThan16Bytes l) {
7570 return l.x + l.y;
7671 }
7772
@@ -201,23 +196,23 @@ struct Short {
201196 unsigned short a, b, c, d;
202197 };
203198
204-unsigned long Short(struct Short s) {
205- return (long)s.a << 48 | (long)s.b << 32 | (long)s.c << 16 | (long)s.d << 0;
199+uint64_t Short(struct Short s) {
200+ return (uint64_t)s.a << 48 | (uint64_t)s.b << 32 | (uint64_t)s.c << 16 | (uint64_t)s.d << 0;
206201 }
207202
208203 struct Int {
209204 unsigned int a, b;
210205 };
211206
212-unsigned long Int(struct Int i) {
213- return (long)i.a << 32 | (long)i.b << 0;
207+uint64_t Int(struct Int i) {
208+ return (uint64_t)i.a << 32 | (uint64_t)i.b << 0;
214209 }
215210
216211 struct Long {
217- unsigned long a;
212+ uint64_t a;
218213 };
219214
220-unsigned long Long(struct Long l) {
215+uint64_t Long(struct Long l) {
221216 return l.a;
222217 }
223218
@@ -327,31 +322,31 @@ struct Content {
327322 struct { double width, height; } size;
328323 };
329324
330-unsigned long InitWithContentRect(int *win, struct Content c, int style, int backing, _Bool flag) {
325+uint64_t InitWithContentRect(int *win, struct Content c, int style, int backing, _Bool flag) {
331326 if (win == 0)
332327 return 0xBAD;
333328 if (!flag)
334329 return 0xF1A6; // FLAG
335- return (unsigned long)(c.point.x + c.point.y + c.size.width + c.size.height) / (style - backing);
330+ return (uint64_t)(c.point.x + c.point.y + c.size.width + c.size.height) / (style - backing);
336331 }
337332
338333 struct GoInt4 {
339- GoInt a, b, c, d;
334+ intptr_t a, b, c, d;
340335 };
341336
342-GoInt GoInt4(struct GoInt4 g) {
337+intptr_t GoInt4(struct GoInt4 g) {
343338 return g.a + g.b - g.c + g.d;
344339 }
345340
346341 struct GoUint4 {
347- GoUint a, b, c, d;
342+ uintptr_t a, b, c, d;
348343 };
349344
350-GoUint GoUint4(struct GoUint4 g) {
345+uintptr_t GoUint4(struct GoUint4 g) {
351346 return g.a + g.b + g.c + g.d;
352347 }
353348
354-GoUint TakeGoUintAndReturn(GoUint a) {
349+uintptr_t TakeGoUintAndReturn(uintptr_t a) {
355350 return a;
356351 }
357352
@@ -395,7 +390,7 @@ uintptr_t AddPointers(struct TwoPointers wrapper) {
395390 // Identity functions for round-trip testing of struct arguments
396391
397392 struct OneInt64 {
398- long long a;
393+ int64_t a;
399394 };
400395
401396 struct OneInt64 IdentityOneInt64(struct OneInt64 s) {
@@ -423,7 +418,7 @@ struct FloatAndInt IdentityFloatAndInt(struct FloatAndInt s) {
423418 }
424419
425420 struct ThreeInt64 {
426- long long a, b, c;
421+ int64_t a, b, c;
427422 };
428423
429424 struct ThreeInt64 IdentityThreeInt64(struct ThreeInt64 s) {
@@ -431,16 +426,16 @@ struct ThreeInt64 IdentityThreeInt64(struct ThreeInt64 s) {
431426 }
432427
433428 struct PtrInt64Ptr {
434- long long *a;
435- long long b;
436- long long *c;
429+ int64_t *a;
430+ int64_t b;
431+ int64_t *c;
437432 };
438433
439434 struct PtrInt64Ptr IdentityPtrInt64Ptr(struct PtrInt64Ptr s) {
440435 return s;
441436 }
442437
443-struct IntLessThan16Bytes IdentityTwoInt64AfterPrims(long long x, double y, struct IntLessThan16Bytes s) {
438+struct IntLessThan16Bytes IdentityTwoInt64AfterPrims(int64_t x, double y, struct IntLessThan16Bytes s) {
444439 return s;
445440 }
446441
@@ -449,7 +444,7 @@ struct FloatLessThan16Bytes IdentityTwoFloat32AfterFloats(double x, double y, st
449444 }
450445
451446 struct Mixed5Args {
452- long long *a;
447+ int64_t *a;
453448 int32_t b;
454449 float c;
455450 int32_t d;
M wincallback.go +8 -8
@@ -35,7 +35,7 @@ func genasm386() {
3535
3636 TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
3737 `)
38- for i := 0; i < maxCallback; i++ {
38+ for i := range maxCallback {
3939 fmt.Fprintf(&buf, "\tMOVL $%d, CX\n", i)
4040 buf.WriteString("\tJMP callbackasm1(SB)\n")
4141 }
@@ -63,7 +63,7 @@ func genasmAmd64() {
6363
6464 TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
6565 `)
66- for i := 0; i < maxCallback; i++ {
66+ for range maxCallback {
6767 buf.WriteString("\tCALL callbackasm1(SB)\n")
6868 }
6969 if err := os.WriteFile("zcallback_amd64.s", buf.Bytes(), 0644); err != nil {
@@ -90,7 +90,7 @@ func genasmArm() {
9090
9191 TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
9292 `)
93- for i := 0; i < maxCallback; i++ {
93+ for i := range maxCallback {
9494 fmt.Fprintf(&buf, "\tMOVW $%d, R12\n", i)
9595 buf.WriteString("\tB callbackasm1(SB)\n")
9696 }
@@ -118,7 +118,7 @@ func genasmArm64() {
118118
119119 TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
120120 `)
121- for i := 0; i < maxCallback; i++ {
121+ for i := range maxCallback {
122122 fmt.Fprintf(&buf, "\tMOVD $%d, R12\n", i)
123123 buf.WriteString("\tB callbackasm1(SB)\n")
124124 }
@@ -146,7 +146,7 @@ func genasmLoong64() {
146146
147147 TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
148148 `)
149- for i := 0; i < maxCallback; i++ {
149+ for i := range maxCallback {
150150 fmt.Fprintf(&buf, "\tMOVV $%d, R12\n", i)
151151 buf.WriteString("\tJMP callbackasm1(SB)\n")
152152 }
@@ -174,7 +174,7 @@ func genasmPpc64le() {
174174
175175 TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
176176 `)
177- for i := 0; i < maxCallback; i++ {
177+ for i := range maxCallback {
178178 fmt.Fprintf(&buf, "\tMOVD $%d, R11\n", i)
179179 buf.WriteString("\tBR callbackasm1(SB)\n")
180180 }
@@ -206,7 +206,7 @@ func genasmRiscv64() {
206206
207207 TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
208208 `)
209- for i := 0; i < maxCallback; i++ {
209+ for i := range maxCallback {
210210 space := " "
211211 if i <= 32 {
212212 fmt.Fprintf(&buf, "\tPCALIGN $8\n")
@@ -240,7 +240,7 @@ func genasmS390x() {
240240
241241 TEXT callbackasm(SB), NOSPLIT|NOFRAME, $0
242242 `)
243- for i := 0; i < maxCallback; i++ {
243+ for i := range maxCallback {
244244 fmt.Fprintf(&buf, "\tMOVD $%d, R0\n", i)
245245 buf.WriteString("\tBR callbackasm1(SB)\n")
246246 }

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