From 9e11b135f9f7d18d32a6d2bc6b8c993d65ae09bb Mon Sep 17 00:00:00 2001 From: George Adams Date: Wed, 25 Feb 2026 13:39:48 +0000 Subject: [PATCH 1/5] fakecgo: add s390x support for CGO_ENABLED=0 --- README.md | 3 +- internal/fakecgo/trampolines_linux_s390x.s | 51 +++++++ internal/fakecgo/trampolines_s390x.s | 154 +++++++++++++++++++++ syscall_unix.go | 3 +- 4 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 internal/fakecgo/trampolines_linux_s390x.s create mode 100644 internal/fakecgo/trampolines_s390x.s diff --git a/README.md b/README.md index d431def1..c4c9212d 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o - **Android**: 3861, arm1 - **FreeBSD**: amd642, arm642 -- **Linux**: 386, arm, loong64, ppc64le, riscv64, s390x1 +- **Linux**: 386, arm, loong64, ppc64le, riscv64, s390x5 - **NetBSD**: amd642, arm642 - **Windows**: 3863, arm3,4 @@ -52,6 +52,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o 2. These architectures require the special flag `-gcflags="github.com/ebitengine/purego/internal/fakecgo=-std"` to compile with CGO_ENABLED=0 3. These architectures only support `SyscallN` and `NewCallback` 4. These architectures are no longer supported as of Go 1.26 +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 ## Example diff --git a/internal/fakecgo/trampolines_linux_s390x.s b/internal/fakecgo/trampolines_linux_s390x.s new file mode 100644 index 00000000..2ef29dbb --- /dev/null +++ b/internal/fakecgo/trampolines_linux_s390x.s @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build go1.27 && !cgo + +#include "textflag.h" + +TEXT _cgo_purego_setegid_trampoline(SB), NOSPLIT|NOFRAME, $0 + MOVD ·x_cgo_purego_setegid_call(SB), R1 + MOVD (R1), R1 + BR R1 + +TEXT _cgo_purego_seteuid_trampoline(SB), NOSPLIT|NOFRAME, $0 + MOVD ·x_cgo_purego_seteuid_call(SB), R1 + MOVD (R1), R1 + BR R1 + +TEXT _cgo_purego_setgid_trampoline(SB), NOSPLIT|NOFRAME, $0 + MOVD ·x_cgo_purego_setgid_call(SB), R1 + MOVD (R1), R1 + BR R1 + +TEXT _cgo_purego_setregid_trampoline(SB), NOSPLIT|NOFRAME, $0 + MOVD ·x_cgo_purego_setregid_call(SB), R1 + MOVD (R1), R1 + BR R1 + +TEXT _cgo_purego_setresgid_trampoline(SB), NOSPLIT|NOFRAME, $0 + MOVD ·x_cgo_purego_setresgid_call(SB), R1 + MOVD (R1), R1 + BR R1 + +TEXT _cgo_purego_setresuid_trampoline(SB), NOSPLIT|NOFRAME, $0 + MOVD ·x_cgo_purego_setresuid_call(SB), R1 + MOVD (R1), R1 + BR R1 + +TEXT _cgo_purego_setreuid_trampoline(SB), NOSPLIT|NOFRAME, $0 + MOVD ·x_cgo_purego_setreuid_call(SB), R1 + MOVD (R1), R1 + BR R1 + +TEXT _cgo_purego_setuid_trampoline(SB), NOSPLIT|NOFRAME, $0 + MOVD ·x_cgo_purego_setuid_call(SB), R1 + MOVD (R1), R1 + BR R1 + +TEXT _cgo_purego_setgroups_trampoline(SB), NOSPLIT|NOFRAME, $0 + MOVD ·x_cgo_purego_setgroups_call(SB), R1 + MOVD (R1), R1 + BR R1 diff --git a/internal/fakecgo/trampolines_s390x.s b/internal/fakecgo/trampolines_s390x.s new file mode 100644 index 00000000..2b68d438 --- /dev/null +++ b/internal/fakecgo/trampolines_s390x.s @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors + +//go:build go1.27 && !cgo && linux + +#include "textflag.h" +#include "go_asm.h" + +// these trampolines map the gcc ABI to Go ABI and then calls into the Go equivalent functions. +// Note that C arguments are passed in R2-R6, which matches Go ABIInternal for the first five arguments. +// R1 is used as a temporary register. + +TEXT x_cgo_init_trampoline(SB), NOSPLIT|NOFRAME, $0-0 + MOVD R15, R1 + SUB $192, R15 + MOVD R1, 0(R15) // backchain + MOVD R14, 160(R15) // save R14 + MOVD R9, 168(R15) // save R9 (Go runtime needs this preserved) + + MOVD ·x_cgo_init_call(SB), R1 + MOVD (R1), R1 + BL R1 + + MOVD 168(R15), R9 + MOVD 160(R15), R14 + ADD $192, R15 + BR R14 + +TEXT x_cgo_thread_start_trampoline(SB), NOSPLIT|NOFRAME, $0-0 + MOVD R15, R1 + SUB $176, R15 + MOVD R1, 0(R15) // backchain + MOVD R14, 152(R15) // save R14 + + MOVD ·x_cgo_thread_start_call(SB), R1 + MOVD (R1), R1 + BL R1 + + MOVD 152(R15), R14 + ADD $176, R15 + BR R14 + +TEXT x_cgo_setenv_trampoline(SB), NOSPLIT|NOFRAME, $0-0 + MOVD R15, R1 + SUB $176, R15 + MOVD R1, 0(R15) // backchain + MOVD R14, 152(R15) // save R14 + + MOVD ·x_cgo_setenv_call(SB), R1 + MOVD (R1), R1 + BL R1 + + MOVD 152(R15), R14 + ADD $176, R15 + BR R14 + +TEXT x_cgo_unsetenv_trampoline(SB), NOSPLIT|NOFRAME, $0-0 + MOVD R15, R1 + SUB $176, R15 + MOVD R1, 0(R15) // backchain + MOVD R14, 152(R15) // save R14 + + MOVD ·x_cgo_unsetenv_call(SB), R1 + MOVD (R1), R1 + BL R1 + + MOVD 152(R15), R14 + ADD $176, R15 + BR R14 + +// These just tail-call into Go functions +TEXT x_cgo_notify_runtime_init_done_trampoline(SB), NOSPLIT|NOFRAME, $0-0 + BR ·x_cgo_notify_runtime_init_done(SB) + +TEXT x_cgo_bindm_trampoline(SB), NOSPLIT|NOFRAME, $0-0 + BR ·x_cgo_bindm(SB) + +// setg_trampoline(setg uintptr, g uintptr) - called from Go +TEXT ·setg_trampoline(SB), NOSPLIT|NOFRAME, $0-16 + MOVD 8(R15), R1 // setg function pointer + MOVD 16(R15), R2 // g pointer -> C arg + + MOVD R14, R0 + MOVD R15, R3 + SUB $160, R15 + MOVD R3, 0(R15) + MOVD R0, 112(R15) + + BL R1 // call setg_gcc + BL runtime·load_g(SB) + + MOVD 112(R15), R14 + ADD $160, R15 + BR R14 + +TEXT threadentry_trampoline(SB), NOSPLIT|NOFRAME, $0-0 + STMG R6, R15, 48(R15) // C save area + MOVD R15, R1 + SUB $176, R15 + MOVD R1, 0(R15) // backchain + + MOVD ·threadentry_call(SB), R1 + MOVD (R1), R1 + BL R1 + + ADD $176, R15 + LMG 48(R15), R6, R15 + RET + +TEXT ·call5(SB), NOSPLIT|NOFRAME, $0-56 + // Load Go args before modifying R15 + MOVD 8(R15), R1 // fn + MOVD 16(R15), R7 // a1 + MOVD 24(R15), R8 // a2 + MOVD 32(R15), R9 // a3 + MOVD 40(R15), R10 // a4 + MOVD 48(R15), R11 // a5 + + // Save state + MOVD R15, R0 // original R15 + MOVD R12, R6 // Go's R12 + ADD $-128, R15 + + // Set up C frame with backchain + MOVD R0, 0(R15) // backchain -> original R15 + MOVD R0, R3 // R3 = original R15 (can't use R0 as base!) + MOVD 0(R3), R7 // save 0(original R15) + MOVD $0, 0(R3) // terminate backchain + + // Save context + MOVD R14, 8(R15) + MOVD R6, 16(R15) // R12 + MOVD R0, 24(R15) // original R15 + MOVD R7, 32(R15) // saved backchain + + // Set up C args (reload a1 since R7 was clobbered) + MOVD 16(R3), R2 // a1 (use R3 as base, not R0!) + MOVD R8, R3 // a2 + MOVD R9, R4 // a3 + MOVD R10, R5 // a4 + MOVD R11, R6 // a5 + + BL R1 + + // Store result and restore + MOVD 24(R15), R3 // original R15 + MOVD R2, 56(R3) // return value + MOVD 32(R15), R7 + MOVD R7, 0(R3) // restore backchain + + MOVD 8(R15), R14 + MOVD 16(R15), R12 + MOVD 24(R15), R15 + BR R14 diff --git a/syscall_unix.go b/syscall_unix.go index 87030f8c..a3caec7e 100644 --- a/syscall_unix.go +++ b/syscall_unix.go @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors -// TODO: remove s390x cgo dependency once golang/go#77449 is resolved -//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (cgo && s390x))) || netbsd +//go:build darwin || freebsd || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || (s390x && (cgo || go1.27)))) || netbsd package purego From d665b39b0580943a40ae20be9cc0888f6cff0d28 Mon Sep 17 00:00:00 2001 From: George Adams Date: Wed, 27 May 2026 12:32:57 +0100 Subject: [PATCH 2/5] remove load_g symbol --- internal/fakecgo/trampolines_s390x.s | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/fakecgo/trampolines_s390x.s b/internal/fakecgo/trampolines_s390x.s index 2b68d438..258c4a33 100644 --- a/internal/fakecgo/trampolines_s390x.s +++ b/internal/fakecgo/trampolines_s390x.s @@ -85,9 +85,13 @@ TEXT ·setg_trampoline(SB), NOSPLIT|NOFRAME, $0-16 SUB $160, R15 MOVD R3, 0(R15) MOVD R0, 112(R15) + MOVD R2, 120(R15) // save newg before call - BL R1 // call setg_gcc - BL runtime·load_g(SB) + BL R1 // call setg_gcc + + // Assign g directly instead of calling runtime·load_g + // setg_gcc has already stored newg into TLS; put it in the g register too. + MOVD 120(R15), g MOVD 112(R15), R14 ADD $160, R15 From f713819f290f5db7403b2fb7de27d51b094c186d Mon Sep 17 00:00:00 2001 From: George Adams Date: Mon, 29 Jun 2026 10:58:27 +0100 Subject: [PATCH 3/5] add RC --- .github/workflows/test.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c10b6dbf..0f70ad00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest, windows-11-arm] - go: ['1.25.x', '1.26.x'] + go: ['1.25.x', '1.26.x', '1.27.0-rc.1'] name: Test with Go ${{ matrix.go }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} defaults: @@ -75,13 +75,13 @@ jobs: - name: go build (Linux minor architectures) # Test them only on the latest Go to reduce CI time. - # s390x cannot be tested here as it requires Cgo. - if: startsWith(matrix.go, '1.26.') + if: startsWith(matrix.go, '1.27.0-rc.1') run: | # Check cross-compiling Linux binaries for minor architectures. env GOOS=linux GOARCH=loong64 go build -v ./... env GOOS=linux GOARCH=ppc64le go build -v ./... env GOOS=linux GOARCH=riscv64 go build -v ./... + env GOOS=linux GOARCH=s390x go build -v ./... - name: go build (Linux mips, Cgo) # mips builds only via the Cgo fallback (CGO_ENABLED=1). See #460. @@ -191,7 +191,7 @@ jobs: minor-arches: strategy: matrix: - go: ['1.25.x', '1.26.x'] + go: ['1.25.x', '1.26.x', '1.27.0-rc.1'] name: Test with Go ${{ matrix.go }} on Linux minor architectures runs-on: ubuntu-latest defaults: @@ -278,6 +278,11 @@ jobs: run: | go env -w CC=s390x-linux-gnu-gcc go env -w CXX=s390x-linux-gnu-g++ + # CGO_ENABLED=0 is only supported on Go 1.27+ for s390x. + if [[ "${{ matrix.go }}" != '1.25.x' && "${{ matrix.go }}" != '1.26.x' ]]; then + env GOOS=linux GOARCH=s390x CGO_ENABLED=0 go test -c -o=purego-test-nocgo . + env QEMU_LD_PREFIX=/usr/s390x-linux-gnu qemu-s390x ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10 + fi env GOOS=linux GOARCH=s390x CGO_ENABLED=1 go test -c -o=purego-test-cgo . env QEMU_LD_PREFIX=/usr/s390x-linux-gnu qemu-s390x ./purego-test-cgo -test.shuffle=on -test.v -test.count=10 go env -u CC @@ -287,7 +292,7 @@ jobs: strategy: matrix: os: ['FreeBSD', 'NetBSD'] - go: ['1.25.8', '1.26.1'] + go: ['1.25.8', '1.26.1', '1.27.0-rc.1'] name: Test with Go ${{ matrix.go }} on ${{ matrix.os }} runs-on: ubuntu-22.04 defaults: @@ -326,7 +331,7 @@ jobs: strategy: fail-fast: false matrix: - go: ['1.25.x', '1.26.x'] + go: ['1.25.x', '1.26.x', '1.27.0-rc.1'] name: Test with Go ${{ matrix.go }} on Android amd64 runs-on: ubuntu-latest defaults: From 48e0d42a22d76a4a9cd880be7eceea443a26b34b Mon Sep 17 00:00:00 2001 From: George Adams Date: Mon, 29 Jun 2026 11:34:09 +0100 Subject: [PATCH 4/5] shrink allbackasm1 to 200 bytes --- .github/workflows/test.yml | 2 +- sys_unix_s390x.s | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f70ad00..b62321f2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -292,7 +292,7 @@ jobs: strategy: matrix: os: ['FreeBSD', 'NetBSD'] - go: ['1.25.8', '1.26.1', '1.27.0-rc.1'] + go: ['1.25.8', '1.26.1'] name: Test with Go ${{ matrix.go }} on ${{ matrix.os }} runs-on: ubuntu-22.04 defaults: diff --git a/sys_unix_s390x.s b/sys_unix_s390x.s index 9eed6d29..d32221d6 100644 --- a/sys_unix_s390x.s +++ b/sys_unix_s390x.s @@ -18,22 +18,24 @@ // // S390X uses R2-R6 for integer arguments (5 registers) and F0,F2,F4,F6 for floats (4 registers). // -// Our frame layout (total 264 bytes, 8-byte aligned): +// Our frame layout (total 200 bytes, 8-byte aligned). To stay under the +// 800-byte nosplit limit on Go 1.27 the callbackArgs struct is stored in the +// caller's free linkage slot (old_R15+16..48) instead of our own frame: // 0(R15) - back chain // 48(R15) - saved R6-R15 (done by STMG) -// 160(R15) - callbackArgs struct (32 bytes: index, args, result, stackArgs) -// 192(R15) - args array start +// 128(R15) - args array start // // Args array layout: // - floats F0,F2,F4,F6 (32 bytes) // - ints R2-R6 (40 bytes) -// Total args array: 72 bytes, ends at 264 +// Total args array: 72 bytes, ends at 200 // +// callbackArgs struct lives at old_R15+16 (32 bytes: index, args, result, stackArgs) // Stack args in caller's frame start at old_R15+160 -#define FRAME_SIZE 264 -#define CB_ARGS 160 -#define ARGS_ARRAY 192 +#define FRAME_SIZE 200 +#define ARGS_ARRAY 128 +#define CB_ARGS (FRAME_SIZE+16) #define FLOAT_OFF 0 #define INT_OFF 32 @@ -71,7 +73,7 @@ TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 MOVD R5, (ARGS_ARRAY+INT_OFF+3*8)(R15) // R6 (5th int arg) was saved at 48(old_R15) by STMG - // old_R15 = current R15 + FRAME_SIZE, so R6 is at 48+FRAME_SIZE(R15) = 312(R15) + // old_R15 = current R15 + FRAME_SIZE MOVD (48+FRAME_SIZE)(R15), R1 MOVD R1, (ARGS_ARRAY+INT_OFF+4*8)(R15) From 5b6f5bce4b541a68335997c41b41735b500e6992 Mon Sep 17 00:00:00 2001 From: George Adams Date: Mon, 29 Jun 2026 13:17:37 +0100 Subject: [PATCH 5/5] review fixup --- .github/workflows/test.yml | 4 ++-- README.md | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b62321f2..5c334cac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,7 +75,7 @@ jobs: - name: go build (Linux minor architectures) # Test them only on the latest Go to reduce CI time. - if: startsWith(matrix.go, '1.27.0-rc.1') + if: startsWith(matrix.go, '1.27.') run: | # Check cross-compiling Linux binaries for minor architectures. env GOOS=linux GOARCH=loong64 go build -v ./... @@ -279,7 +279,7 @@ jobs: go env -w CC=s390x-linux-gnu-gcc go env -w CXX=s390x-linux-gnu-g++ # CGO_ENABLED=0 is only supported on Go 1.27+ for s390x. - if [[ "${{ matrix.go }}" != '1.25.x' && "${{ matrix.go }}" != '1.26.x' ]]; then + if [[ "${{ matrix.go }}" != 1.25.* && "${{ matrix.go }}" != 1.26.* ]]; then env GOOS=linux GOARCH=s390x CGO_ENABLED=0 go test -c -o=purego-test-nocgo . env QEMU_LD_PREFIX=/usr/s390x-linux-gnu qemu-s390x ./purego-test-nocgo -test.shuffle=on -test.v -test.count=10 fi diff --git a/README.md b/README.md index 3afa776f..3239ab50 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o - **Android**: 3861,3, arm1,3 - **FreeBSD**: amd643,4, arm643,4 -- **Linux**: 3863, arm3, loong643, ppc64le3, riscv643, s390x3, 7 +- **Linux**: 3863, arm3, loong643, ppc64le3, riscv643, s390x3, 5 - **NetBSD**: amd643,4, arm643,4 -- **Windows**: 3863,5, arm3,5,6 +- **Windows**: 3863,6, arm3,6,7 #### Support Notes @@ -52,9 +52,9 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o 2. These architectures support passing structs by value as arguments and return values when calling C functions, but not in callbacks created with `NewCallback` 3. These architectures do not support passing structs by value as arguments or return values 4. These architectures require the special flag `-gcflags="github.com/ebitengine/purego/internal/fakecgo=-std"` to compile with CGO_ENABLED=0 -5. These architectures only support `SyscallN` and `NewCallback` -6. These architectures are no longer supported as of Go 1.26 -7. 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 +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 +6. These architectures only support `SyscallN` and `NewCallback` +7. These architectures are no longer supported as of Go 1.26 ## Example