Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ 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.')
run: |
# Check cross-compiling Linux binaries for minor architectures.
Expand Down Expand Up @@ -179,6 +180,7 @@ jobs:
gcc-14-loongarch64-linux-gnu g++-14-loongarch64-linux-gnu \
gcc-powerpc64le-linux-gnu g++-powerpc64le-linux-gnu \
gcc-riscv64-linux-gnu g++-riscv64-linux-gnu \
gcc-s390x-linux-gnu g++-s390x-linux-gnu \
qemu-user

- name: go test (Linux loong64)
Expand Down Expand Up @@ -216,6 +218,15 @@ jobs:
go env -u CC
go env -u CXX

- name: go test (Linux s390x)
run: |
go env -w CC=s390x-linux-gnu-gcc
go env -w CXX=s390x-linux-gnu-g++
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
go env -u CXX

bsd:
strategy:
matrix:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o

- **Android**: 386<sup>1</sup>, arm<sup>1</sup>
- **FreeBSD**: amd64<sup>2</sup>, arm64<sup>2</sup>
- **Linux**: 386, arm, loong64, ppc64le, riscv64
- **Linux**: 386, arm, loong64, ppc64le, riscv64, s390x<sup>1</sup>
- **Windows**: 386<sup>3</sup>, arm<sup>3,4</sup>

#### Support Notes
Expand Down
7 changes: 6 additions & 1 deletion callback_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 The Ebitengine Authors

//go:build darwin || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64))
//go:build darwin || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x))

package purego_test

Expand Down Expand Up @@ -126,6 +126,11 @@ func TestNewCallbackFloat32(t *testing.T) {
}

func TestNewCallbackFloat32AndFloat64(t *testing.T) {
if runtime.GOARCH == "s390x" {
// S390X has only 4 float registers (F0,F2,F4,F6), so 15 floats requires
// 11 stack slots, but only 10 are available (maxArgs - numIntRegs = 15 - 5)
t.Skip("Test requires too many stack arguments for s390x")
}
// This tests that calling a function with a mix of float32 and float64 arguments works
const (
expectedCbTotalF32 = float32(72)
Expand Down
22 changes: 18 additions & 4 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
panic("purego: cfn is nil")
}
if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) &&
runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {
runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
panic("purego: float returns are not supported")
}
{
Expand Down Expand Up @@ -273,7 +273,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
var arm64_r8 uintptr
if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct {
outType := ty.Out(0)
if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64") && outType.Size() > maxRegAllocStructSize {
if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") && outType.Size() > maxRegAllocStructSize {
val := reflect.New(outType)
keepAlive = append(keepAlive, val)
addInt(val.Pointer())
Expand Down Expand Up @@ -313,7 +313,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
syscall := thePool.Get().(*syscall15Args)
defer thePool.Put(syscall)

if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" {
if runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x" {
syscall.Set(cfn, sysargs[:], floats[:], 0)
runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall))
} else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" {
Expand Down Expand Up @@ -356,11 +356,15 @@ func RegisterFunc(fptr any, cfn uintptr) {
// On 32bit platforms syscall.r2 is the upper part of a 64bit return.
// On 386, x87 FPU returns floats as float64 in ST(0), so we read as float64 and convert.
// On PPC64LE, C ABI converts float32 to double in FPR, so we read as float64.
// On S390X (big-endian), float32 is in upper 32 bits of the 64-bit FP register.
switch runtime.GOARCH {
case "386":
v.SetFloat(math.Float64frombits(uint64(syscall.f1) | (uint64(syscall.f2) << 32)))
case "ppc64le":
v.SetFloat(math.Float64frombits(uint64(syscall.f1)))
case "s390x":
// S390X is big-endian: float32 in upper 32 bits of 64-bit register
v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32))))
default:
v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1))))
}
Expand Down Expand Up @@ -411,7 +415,12 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
addInt(0)
}
case reflect.Float32:
addFloat(uintptr(math.Float32bits(float32(v.Float()))))
// On S390X big-endian, float32 goes in upper 32 bits of 64-bit FP register
if runtime.GOARCH == "s390x" {
addFloat(uintptr(math.Float32bits(float32(v.Float()))) << 32)
} else {
addFloat(uintptr(math.Float32bits(float32(v.Float()))))
}
case reflect.Float64:
if is32bit {
bits := math.Float64bits(v.Float())
Expand Down Expand Up @@ -498,6 +507,8 @@ func numOfFloatRegisters() int {
switch runtime.GOARCH {
case "amd64", "arm64", "loong64", "ppc64le", "riscv64":
return 8
case "s390x":
return 4
case "arm":
return 16
case "386":
Expand All @@ -516,6 +527,9 @@ func numOfIntegerRegisters() int {
return 8
case "amd64":
return 6
case "s390x":
// S390X uses R2-R6 for integer arguments
return 5
case "arm":
return 4
case "386":
Expand Down
8 changes: 4 additions & 4 deletions func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestRegisterFunc(t *testing.T) {
}

func Test_qsort(t *testing.T) {
if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {
if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
t.Skip("Platform doesn't support Floats")
return
}
Expand Down Expand Up @@ -79,7 +79,7 @@ func Test_qsort(t *testing.T) {
}

func TestRegisterFunc_Floats(t *testing.T) {
if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {
if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
t.Skip("Platform doesn't support Floats")
return
}
Expand Down Expand Up @@ -121,7 +121,7 @@ func TestRegisterFunc_Floats(t *testing.T) {
}

func TestRegisterLibFunc_Bool(t *testing.T) {
if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" {
if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
t.Skip("Platform doesn't support callbacks")
return
}
Expand Down Expand Up @@ -376,7 +376,7 @@ func TestABI_ArgumentPassing(t *testing.T) {
if tt.name == "20_int32" && (runtime.GOOS != "darwin" || runtime.GOARCH != "arm64") {
t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
}
if tt.name == "10_float32" && (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64") {
if tt.name == "10_float32" && (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") {
t.Skip("float32 stack arguments not yet supported on this platform")
}
// Struct tests require Darwin ARM64 or AMD64
Expand Down
55 changes: 55 additions & 0 deletions internal/fakecgo/asm_s390x.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include "textflag.h"

// Called by C code generated by cmd/cgo.
// func crosscall2(fn, a unsafe.Pointer, n int32, ctxt uintptr)
// Saves C callee-saved registers and calls cgocallback with three arguments.
// fn is the PC of a func(a unsafe.Pointer) function.
TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0
// Start with standard C stack frame layout and linkage.

// Save R6-R15 in the register save area of the calling function.
STMG R6, R15, 48(R15)

// Allocate 96 bytes on the stack.
MOVD $-96(R15), R15

// Save F8-F15 in our stack frame.
FMOVD F8, 32(R15)
FMOVD F9, 40(R15)
FMOVD F10, 48(R15)
FMOVD F11, 56(R15)
FMOVD F12, 64(R15)
FMOVD F13, 72(R15)
FMOVD F14, 80(R15)
FMOVD F15, 88(R15)

// Initialize Go ABI environment.
BL runtime·load_g(SB)

MOVD R2, 8(R15) // fn unsafe.Pointer
MOVD R3, 16(R15) // a unsafe.Pointer

// Skip R4 = n uint32
MOVD R5, 24(R15) // ctxt uintptr
BL runtime·cgocallback(SB)

FMOVD 32(R15), F8
FMOVD 40(R15), F9
FMOVD 48(R15), F10
FMOVD 56(R15), F11
FMOVD 64(R15), F12
FMOVD 72(R15), F13
FMOVD 80(R15), F14
FMOVD 88(R15), F15

// De-allocate stack frame.
MOVD $96(R15), R15

// Restore R6-R15.
LMG 48(R15), R6, R15

RET
143 changes: 143 additions & 0 deletions struct_s390x.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 The Ebitengine Authors

package purego

import (
"reflect"
"unsafe"
)

func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
outSize := outType.Size()

switch {
case outSize == 0:
return reflect.New(outType).Elem()

case outSize <= 16:
// Reconstruct from registers by copying raw bytes
var buf [16]byte

// Integer registers
*(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.a1
if outSize > 8 {
*(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.a2
}

// Homogeneous float aggregates override integer regs
if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
if outType.Field(0).Type.Kind() == reflect.Float32 {
// float32 values in FP regs
f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}
for i := 0; i < numFields; i++ {
*(*uint32)(unsafe.Pointer(&buf[i*4])) = uint32(f[i])
}
} else {
// float64: whole register value is valid
*(*uintptr)(unsafe.Pointer(&buf[0])) = syscall.f1
if outSize > 8 {
*(*uintptr)(unsafe.Pointer(&buf[8])) = syscall.f2
}
}
}

return reflect.NewAt(outType, unsafe.Pointer(&buf[0])).Elem()

default:
// Returned indirectly via pointer in a1
ptr := *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))
return reflect.NewAt(outType, ptr).Elem()
}
}

func addStruct(
v reflect.Value,
numInts, numFloats, numStack *int,
addInt, addFloat, addStack func(uintptr),
keepAlive []any,
) []any {
size := v.Type().Size()
if size == 0 {
return keepAlive
}

if size <= 16 {
return placeSmallAggregateS390X(v, addFloat, addInt, keepAlive)
}

return placeStack(v, keepAlive, addInt)
}

func placeSmallAggregateS390X(
v reflect.Value,
addFloat, addInt func(uintptr),
keepAlive []any,
) []any {
size := v.Type().Size()

var ptr unsafe.Pointer
if v.CanAddr() {
ptr = v.Addr().UnsafePointer()
} else {
tmp := reflect.New(v.Type())
tmp.Elem().Set(v)
ptr = tmp.UnsafePointer()
keepAlive = append(keepAlive, tmp.Interface())
}

var buf [16]byte
src := unsafe.Slice((*byte)(ptr), size)
copy(buf[:], src)

w0 := *(*uintptr)(unsafe.Pointer(&buf[0]))
w1 := uintptr(0)
if size > 8 {
w1 = *(*uintptr)(unsafe.Pointer(&buf[8]))
}

if isFloats, _ := isAllSameFloat(v.Type()); isFloats {
addFloat(w0)
if size > 8 {
addFloat(w1)
}
} else {
addInt(w0)
if size > 8 {
addInt(w1)
}
}

return keepAlive
}

// placeStack is a fallback for structs that are too large to fit in registers
func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
if v.CanAddr() {
addInt(v.Addr().Pointer())
return keepAlive
}
ptr := reflect.New(v.Type())
ptr.Elem().Set(v)
addInt(ptr.Pointer())
return append(keepAlive, ptr.Interface())
}

func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
// S390X does not bundle stack args
return false
}

func collectStackArgs(
args []reflect.Value,
i, numInts, numFloats int,
keepAlive []any,
addInt, addFloat, addStack func(uintptr),
numIntsPtr, numFloatsPtr, numStackPtr *int,
) ([]reflect.Value, []any) {
return nil, keepAlive
}

func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
panic("bundleStackArgs not supported on S390X")
}
Loading
Loading