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
74 changes: 74 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,77 @@ jobs:
rm -fr /usr/local/go && tar -C /usr/local -xf go${{matrix.go}}.netbsd-amd64.tar.gz
chmod +x $GITHUB_WORKSPACE/.github/scripts/bsd_tests.sh
run: $GITHUB_WORKSPACE/.github/scripts/bsd_tests.sh

# Android is tested on amd64 only. The emulator requires the system image
# architecture to match the host, and GitHub's arm64 hosted runners do not
# provide a usable KVM device, so there is no hosted hardware-accelerated path
# to an Android arm64 emulator. The arm64 struct ABI is already covered by the
# linux/arm64 `test` job, and Android's Bionic/cgo integration by this job.
android:
strategy:
fail-fast: false
matrix:
go: ['1.25.x', '1.26.x']
Comment thread
hajimehoshi marked this conversation as resolved.
name: Test with Go ${{ matrix.go }} on Android amd64
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v5

- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}

# The tests compile their C libraries at run time via `go env CC`, but the
# emulator has no C toolchain. Every library the tests load must therefore be
# cross-compiled here and loaded as a prebuilt library at run time instead.
- name: Cross-compile the test binary and C libraries
run: |
set -eux
NDK="$ANDROID_NDK_LATEST_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
CC="$NDK/x86_64-linux-android21-clang"
mkdir -p prebuilt
# Build every library the tests load. Keep this in sync with the
# buildSharedLib callers in the tests; a missing entry surfaces as a
# "prebuilt lib ... no such file" failure on the emulator.
"$CC" -shared -Wall -Werror -fPIC -o prebuilt/structtest.so testdata/structtest/struct_test.c
"$CC" -shared -Wall -Werror -fPIC -o prebuilt/structreturntest.so testdata/structtest/structreturn_test.c
"$CC" -shared -Wall -Werror -fPIC -o prebuilt/libcbtest.so testdata/libcbtest/callback_test.c
"$CC" -shared -Wall -Werror -fPIC -o prebuilt/abitest.so testdata/abitest/abi_test.c
env GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC="$CC" go test -c -o purego.test .

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Run the test suite on the Android emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
target: google_apis
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
script: |
set -eux
adb push purego.test /data/local/tmp/purego.test
adb push prebuilt /data/local/tmp/prebuilt
adb shell "chmod 755 /data/local/tmp/purego.test"
# TestSetuidEtc needs root and glibc (not Bionic) semantics; TestNestedDlopenCall
# needs the C++ runtime present on the device. Both are covered by the Linux
# runners, so skip them here rather than special-casing the emulator.
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
grep -q '^PASS' result.txt
! grep -q '^FAIL' result.txt

# TODO: Add an iOS job that runs the test suite on the iOS Simulator.
# iOS has no emulator runner equivalent: on a macOS runner the test binary must
# be wrapped into a .app and launched via `xcrun simctl` (or x/mobile's
# go_ios_exec), with the C libraries cross-compiled against the iphonesimulator
# SDK and loaded through PUREGO_TEST_PREBUILT_LIBDIR (as the android job does).
# Deprioritized because the iOS arm64 ABI matches macOS arm64, which the `test`
# job already runs.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,27 @@ except for float arguments and return values.

Tier 1 platforms are the primary targets officially supported by PureGo. When a new version of PureGo is released, any critical bugs found on Tier 1 platforms are treated as release blockers. The release will be postponed until such issues are resolved.

- **Android**: amd64<sup>1,2</sup>, arm64<sup>1,2</sup>
- **iOS**: amd64<sup>1,2</sup>, arm64<sup>1,2</sup>
- **Android**: amd64<sup>1</sup>, arm64<sup>1</sup>
- **iOS**: amd64<sup>1</sup>, arm64<sup>1</sup>
- **Linux**: amd64, arm64
- **macOS**: amd64, arm64
- **Windows**: amd64<sup>3</sup>, arm64<sup>3</sup>
- **Windows**: amd64<sup>2</sup>, arm64<sup>2</sup>

### Tier 2

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.

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

#### Support Notes

1. These architectures require CGO_ENABLED=1 to compile
2. These architectures do not support passing structs by value as arguments or return values
3. These architectures support passing structs by value as arguments and return values when calling C functions, but not in callbacks created with `NewCallback`
2. These architectures support passing structs by value as arguments and return values when calling C functions, but not in callbacks created with `NewCallback`
Comment thread
hajimehoshi marked this conversation as resolved.
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
Expand Down
20 changes: 13 additions & 7 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
// int64 <=> int64_t
// float32 <=> float
// float64 <=> double
// struct <=> struct (darwin amd64/arm64, linux amd64/arm64, windows amd64/arm64)
// struct <=> struct (android, darwin, ios, linux, and windows on amd64/arm64)
// func <=> C function
// unsafe.Pointer, *T <=> void*
// []T => void*
Expand Down Expand Up @@ -99,8 +99,8 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
// it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure
// that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example.
//
// On Darwin ARM64, purego handles proper alignment of struct arguments when passing them on the stack,
// following the C ABI's byte-level packing rules.
// On Apple ARM64 platforms (macOS and iOS), purego handles proper alignment of struct arguments
// when passing them on the stack, following the C ABI's byte-level packing rules.
//
// On Windows, struct arguments and returns are supported on amd64 and arm64 when calling C functions.
// Passing or returning structs in callbacks created with [NewCallback] is not supported on Windows.
Expand Down Expand Up @@ -213,7 +213,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
if ints+floats+stack > argsLimit {
panic("purego: too many stack arguments")
}
} else if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
} else if isDarwin && runtime.GOARCH == "arm64" {
// On Darwin ARM64, use byte-based validation since arguments pack efficiently.
// See https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms
stackBytes := estimateStackBytes(ty)
Expand Down Expand Up @@ -313,7 +313,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
continue
}
// Check if we need to start Darwin ARM64 C-style stack packing
if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && shouldBundleStackArgs(v, numInts, numFloats) {
if runtime.GOARCH == "arm64" && isDarwin && shouldBundleStackArgs(v, numInts, numFloats) {
// Collect and separate remaining args into register vs stack
stackArgs, newKeepAlive := collectStackArgs(args, i, numInts, numFloats,
keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack)
Expand Down Expand Up @@ -505,11 +505,17 @@ func ensureStructSupported() {
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
panic("purego: struct arguments/returns are only supported on amd64 and arm64")
}
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" && runtime.GOOS != "windows" {
panic("purego: struct arguments/returns are only supported on darwin, linux, and windows")
switch runtime.GOOS {
case "android", "darwin", "ios", "linux", "windows":
default:
panic("purego: struct arguments/returns are only supported on android, darwin, ios, linux, and windows")
}
}

// isDarwin is true on platforms that use Apple's calling convention.
// iOS (GOOS=ios) shares it with macOS (GOOS=darwin).
const isDarwin = runtime.GOOS == "darwin" || runtime.GOOS == "ios"

// amd64StructReturnInMemory reports whether a struct return value of the given
// size is returned through a caller-allocated hidden pointer (true) rather than
// in registers (false). It must only be consulted on amd64.
Expand Down
20 changes: 19 additions & 1 deletion func_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
Expand All @@ -21,7 +22,9 @@ import (

func getSystemLibrary() (string, error) {
switch runtime.GOOS {
case "darwin":
case "android":
return "libc.so", nil
case "darwin", "ios":
return "/usr/lib/libSystem.B.dylib", nil
case "freebsd":
return "libc.so.7", nil
Expand Down Expand Up @@ -601,6 +604,21 @@ func TestABI_TooManyArguments(t *testing.T) {
}

func buildSharedLib(compilerEnv, libFile string, sources ...string) error {
// When PUREGO_TEST_PREBUILT_LIBDIR is set, the shared library has been
// cross-compiled ahead of time and placed in that directory under the
// base name of libFile. This allows running the tests on a target that
// has no C toolchain, such as an Android emulator.
if dir := os.Getenv("PUREGO_TEST_PREBUILT_LIBDIR"); dir != "" {
data, err := os.ReadFile(filepath.Join(dir, filepath.Base(libFile)))
if err != nil {
return fmt.Errorf("prebuilt lib: %w", err)
}
if err := os.WriteFile(libFile, data, 0o755); err != nil {
return fmt.Errorf("prebuilt lib: %w", err)
}
return nil
}

out, err := exec.Command("go", "env", compilerEnv).Output()
if err != nil {
return fmt.Errorf("go env %s error: %w", compilerEnv, err)
Expand Down
2 changes: 1 addition & 1 deletion struct_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
// - If not enough registers for all eightbytes: entire struct goes on the stack
func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
switch runtime.GOOS {
case "darwin", "freebsd", "linux", "netbsd":
case "android", "darwin", "freebsd", "ios", "linux", "netbsd":
default:
panic("purego: getCallbackStruct is not supported on " + runtime.GOOS)
}
Expand Down
20 changes: 10 additions & 10 deletions struct_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
}

func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
if runtime.GOOS == "darwin" {
if isDarwin {
placeRegistersDarwin(v, addFloat, addInt)
return
}
Expand Down Expand Up @@ -301,7 +301,7 @@ func isHVA(t reflect.Type) bool {
// copyStruct8ByteChunks copies struct memory in 8-byte chunks to the provided callback.
// This is used for Darwin ARM64's byte-level packing of non-HFA/HVA structs.
func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintptr)) {
if runtime.GOOS != "darwin" {
if !isDarwin {
panic("purego: should only be called on darwin")
}
for offset := uintptr(0); offset < size; offset += 8 {
Expand All @@ -328,7 +328,7 @@ func copyStruct8ByteChunks(ptr unsafe.Pointer, size uintptr, addChunk func(uintp
// For non-HFA/HVA structs, Darwin uses byte-level packing. We copy the struct memory in
// 8-byte chunks, which works correctly for both register and stack placement.
func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) {
if runtime.GOOS != "darwin" {
if !isDarwin {
panic("purego: placeRegistersDarwin should only be called on darwin")
}
// Check if this is an HFA/HVA
Expand Down Expand Up @@ -356,7 +356,7 @@ func placeRegistersDarwin(v reflect.Value, addFloat func(uintptr), addInt func(u
// shouldBundleStackArgs determines if we need to start C-style packing for
// Darwin ARM64 stack arguments. This happens when registers are exhausted.
func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
if runtime.GOOS != "darwin" {
if !isDarwin {
return false
}

Expand Down Expand Up @@ -398,7 +398,7 @@ func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool {
// registers, used during stack argument bundling to decide if a struct
// should go through normal register allocation or be bundled with stack args.
func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (bool, int, int) {
if runtime.GOOS != "darwin" {
if !isDarwin {
panic("purego: structFitsInRegisters should only be called on darwin")
}
hfa := isHFA(val.Type())
Expand Down Expand Up @@ -431,7 +431,7 @@ func structFitsInRegisters(val reflect.Value, tempNumInts, tempNumFloats int) (b
func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int,
keepAlive []any, addInt, addFloat, addStack func(uintptr),
pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) {
if runtime.GOOS != "darwin" {
if !isDarwin {
panic("purego: collectStackArgs should only be called on darwin")
}

Expand Down Expand Up @@ -488,7 +488,7 @@ const (
// bundleStackArgs bundles remaining arguments for Darwin ARM64 C-style stack packing.
// It creates a packed struct with proper alignment and copies it to the stack in 8-byte chunks.
func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
if runtime.GOOS != "darwin" {
if !isDarwin {
panic("purego: bundleStackArgs should only be called on darwin")
}
if len(stackArgs) == 0 {
Expand Down Expand Up @@ -562,7 +562,7 @@ func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) {
// - Darwin ARM64: byte-level packing on the stack
func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
switch runtime.GOOS {
case "darwin", "freebsd", "linux", "netbsd":
case "android", "darwin", "freebsd", "ios", "linux", "netbsd":
default:
panic("purego: getCallbackStruct is not supported on " + runtime.GOOS)
}
Expand Down Expand Up @@ -609,7 +609,7 @@ func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int,
}

// Pointer on stack (rare: all integer registers exhausted).
if runtime.GOOS == "darwin" {
if isDarwin {
ptrVal := callbackArgFromStack(frame, *stackSlot, stackByteOffset, reflect.TypeOf(uintptr(0)))
ptr := uintptr(ptrVal.Uint())
return reflect.NewAt(inType, *(*unsafe.Pointer)(unsafe.Pointer(&ptr))).Elem()
Expand Down Expand Up @@ -690,7 +690,7 @@ func readHFAFromRegisters(inType reflect.Type, f *[callbackMaxFrame]uintptr, flo
// callback frame. On Darwin ARM64, arguments are byte-packed on the stack.
// On Linux ARM64, arguments are 8-byte aligned.
func readStructFromStackArm64(inType reflect.Type, f *[callbackMaxFrame]uintptr, frame unsafe.Pointer, stackSlot *int, stackByteOffset *uintptr) reflect.Value {
if runtime.GOOS == "darwin" {
if isDarwin {
return callbackArgFromStack(frame, *stackSlot, stackByteOffset, inType)
}
// Linux ARM64: 8-byte aligned slots.
Expand Down
4 changes: 2 additions & 2 deletions syscall_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func callbackWrap(a *callbackArgs) {
case reflect.Float32, reflect.Float64:
slots = int((fnType.In(i).Size() + ptrSize - 1) / ptrSize)
if floatsN+slots > numOfFloatRegisters() {
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
if isDarwin && runtime.GOARCH == "arm64" {
// Darwin ARM64: read from packed stack with proper alignment
args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
} else if stackFrame != nil {
Expand Down Expand Up @@ -203,7 +203,7 @@ func callbackWrap(a *callbackArgs) {
default:
slots = int((inType.Size() + ptrSize - 1) / ptrSize)
if intsN+slots > numOfIntegerRegisters() {
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
if isDarwin && runtime.GOARCH == "arm64" {
// Darwin ARM64: read from packed stack with proper alignment
args[i] = callbackArgFromStack(a.args, stackSlot, &stackByteOffset, inType)
} else if stackFrame != nil {
Expand Down