diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c10b6dbf..5c334cac 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.')
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.* && "${{ 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
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
@@ -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:
diff --git a/README.md b/README.md
index d7e904d7..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, s390x1,3
+- **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,8 +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
+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
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..258c4a33
--- /dev/null
+++ b/internal/fakecgo/trampolines_s390x.s
@@ -0,0 +1,158 @@
+// 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)
+ MOVD R2, 120(R15) // save newg before call
+
+ 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
+ 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/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)
diff --git a/syscall_unix.go b/syscall_unix.go
index 2c904371..7a7f9e7e 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