From b7a3a76f0d339bc193537bd039178f85a81ff8bf Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 15 Jul 2026 21:00:53 +0300 Subject: [PATCH 1/4] feat: add Android Bionic loader and errno support --- ffi/dl_android.go | 60 ++++++++++ ffi/dl_unix.go | 2 +- internal/arch/arm64/abi_capture_test.go | 2 +- internal/dl/dl_android.go | 23 ++++ internal/dl/dl_android_cgo.go | 100 ++++++++++++++++ internal/dl/dl_android_nocgo.go | 148 ++++++++++++++++++++++++ internal/dl/dl_android_nocgo_arm64.s | 122 +++++++++++++++++++ internal/dl/dl_linux.go | 2 +- internal/dl/dl_stubs_arm64.s | 2 +- internal/dl/dl_unix.go | 2 +- internal/dl/dl_wrappers_arm64.s | 2 +- internal/syscall/errno_android.go | 12 ++ internal/syscall/errno_android_cgo.go | 14 +++ internal/syscall/errno_linux.go | 2 +- internal/syscall/errno_stubs_amd64.s | 2 +- internal/syscall/errno_stubs_arm64.s | 5 +- internal/syscall/errno_unix.go | 8 +- 17 files changed, 495 insertions(+), 13 deletions(-) create mode 100644 ffi/dl_android.go create mode 100644 internal/dl/dl_android.go create mode 100644 internal/dl/dl_android_cgo.go create mode 100644 internal/dl/dl_android_nocgo.go create mode 100644 internal/dl/dl_android_nocgo_arm64.s create mode 100644 internal/syscall/errno_android.go create mode 100644 internal/syscall/errno_android_cgo.go diff --git a/ffi/dl_android.go b/ffi/dl_android.go new file mode 100644 index 0000000..c227a54 --- /dev/null +++ b/ffi/dl_android.go @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build android && arm64 + +package ffi + +import ( + "fmt" + "unsafe" + + "github.com/go-webgpu/goffi/internal/dl" +) + +// Bionic keeps the object mapped after dlclose when RTLD_NODELETE is set. +// This matches goffi's process-lifetime function-pointer policy. +const ( + RTLD_NOW = dl.RTLD_NOW + RTLD_LOCAL = dl.RTLD_LOCAL + RTLD_NODELETE = dl.RTLD_NODELETE +) + +// LoadLibrary loads a public Android shared library with eager, private +// symbol resolution. Android support is arm64/API 29+ only. +func LoadLibrary(name string) (unsafe.Pointer, error) { + handle, err := dl.Dlopen(name, RTLD_NOW|RTLD_LOCAL|RTLD_NODELETE) + if err != nil { + return nil, &LibraryError{Operation: "load", Name: name, Err: err} + } + // Reinterpret the opaque loader value without a uintptr-to-pointer + // conversion, which would make go vet assume a hidden Go heap pointer. + return *(*unsafe.Pointer)(unsafe.Pointer(&handle)), nil +} + +// GetSymbol retrieves a function or data pointer from an Android library. +func GetSymbol(handle unsafe.Pointer, name string) (unsafe.Pointer, error) { + fnPtr, err := dl.Dlsym(uintptr(handle), name) + if err != nil { + return nil, &LibraryError{Operation: "symbol", Name: name, Err: err} + } + if fnPtr == 0 { + return nil, &LibraryError{ + Operation: "symbol", + Name: name, + Err: fmt.Errorf("symbol not found"), + } + } + // dlsym returns an address in native code, not a Go heap pointer. Preserve + // its bits through the same vet-safe representation used by the Unix path. + return *(*unsafe.Pointer)(unsafe.Pointer(&fnPtr)), nil +} + +// FreeLibrary accepts a handle for API symmetry. internal/dl deliberately +// retains Android mappings for process lifetime, so this is safe to defer. +func FreeLibrary(handle unsafe.Pointer) error { + if handle == nil { + return nil + } + return dl.Dlclose(uintptr(handle)) +} diff --git a/ffi/dl_unix.go b/ffi/dl_unix.go index 9e92913..f30ceee 100644 --- a/ffi/dl_unix.go +++ b/ffi/dl_unix.go @@ -1,4 +1,4 @@ -//go:build (linux || freebsd) && (amd64 || arm64) +//go:build ((linux && !android) || freebsd) && (amd64 || arm64) // Unix library loading via dlopen - OUR OWN implementation (NO dependencies!) // diff --git a/internal/arch/arm64/abi_capture_test.go b/internal/arch/arm64/abi_capture_test.go index 5275eea..b29bab8 100644 --- a/internal/arch/arm64/abi_capture_test.go +++ b/internal/arch/arm64/abi_capture_test.go @@ -42,7 +42,7 @@ func captureCall(t *testing.T, argTypes []*types.TypeDescriptor, args []unsafe.P } var impl Implementation - if err := impl.Execute(cif, fnPtr, nil, args); err != nil { + if _, err := impl.Execute(cif, fnPtr, nil, args, 0); err != nil { t.Fatalf("Execute failed: %v", err) } diff --git a/internal/dl/dl_android.go b/internal/dl/dl_android.go new file mode 100644 index 0000000..dbcdc57 --- /dev/null +++ b/internal/dl/dl_android.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build android && arm64 + +// Android/Bionic dynamic-loader declarations. +package dl + +// Android exposes the POSIX dlfcn entry points from libdl.so. It does not +// provide glibc's libdl.so.2 soname, and RTLD_NODELETE is the supported way to +// keep function pointers valid when the caller retains them for the process +// lifetime. + +const ( + RTLD_LAZY = 0x00001 + RTLD_NOW = 0x00002 + RTLD_GLOBAL = 0x00100 + RTLD_LOCAL = 0x00000 + RTLD_NODELETE = 0x01000 +) + +// RTLD_DEFAULT is Android's default lookup pseudo-handle. +const RTLD_DEFAULT = 0x00000 diff --git a/internal/dl/dl_android_cgo.go b/internal/dl/dl_android_cgo.go new file mode 100644 index 0000000..8adfa8c --- /dev/null +++ b/internal/dl/dl_android_cgo.go @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build android && cgo && arm64 + +package dl + +/* +#cgo android LDFLAGS: -ldl +#include +#include +#include +#include + +typedef struct { + void* value; + char* error; +} goffi_android_dl_result; + +static void goffi_android_capture_error(goffi_android_dl_result* result) { + const char* message = dlerror(); + result->error = message == NULL ? NULL : strdup(message); +} + +static void goffi_android_dlopen(const char* path, int mode, goffi_android_dl_result* result) { + result->value = dlopen(path, mode); + result->error = NULL; + if (result->value == NULL) { + goffi_android_capture_error(result); + } +} + +static void goffi_android_dlsym(uintptr_t handle, const char* name, goffi_android_dl_result* result) { + // Clear a prior loader error before resolving this symbol. + dlerror(); + result->value = dlsym((void*)handle, name); + result->error = NULL; + if (result->value == NULL) { + goffi_android_capture_error(result); + } +} + +static void goffi_android_free(char* value) { + free(value); +} + +static uintptr_t goffi_android_errno_addr(void) { + return (uintptr_t)dlsym(RTLD_DEFAULT, "__errno"); +} +*/ +import "C" + +import "fmt" + +// Dlopen uses an ordinary cgo call on Android when cgo is enabled. This +// avoids passing an external-linker SDYNIMPORT through a hand-written branch +// relocation while retaining the same API and error semantics. +func Dlopen(path string, mode int) (uintptr, error) { + cpath := C.CString(path) + defer C.goffi_android_free(cpath) + + var result C.goffi_android_dl_result + C.goffi_android_dlopen(cpath, C.int(mode), &result) + if result.value == nil { + return 0, fmt.Errorf("dlopen failed: %s", takeAndroidDlerror(result.error)) + } + return uintptr(result.value), nil +} + +// Dlsym returns the address of a symbol in a loaded Android library. +func Dlsym(handle uintptr, name string) (uintptr, error) { + cname := C.CString(name) + defer C.goffi_android_free(cname) + + var result C.goffi_android_dl_result + C.goffi_android_dlsym(C.uintptr_t(handle), cname, &result) + if result.value == nil { + return 0, fmt.Errorf("dlsym failed: %s", takeAndroidDlerror(result.error)) + } + return uintptr(result.value), nil +} + +// Dlclose intentionally retains process-lifetime mappings, matching the +// no-cgo implementation's RTLD_NODELETE policy. +func Dlclose(uintptr) error { return nil } + +func takeAndroidDlerror(msg *C.char) string { + if msg == nil { + return "unknown error" + } + defer C.goffi_android_free(msg) + return C.GoString(msg) +} + +// AndroidErrnoAddr resolves Bionic's __errno accessor for the syscall package. +// The returned address is called by syscallN immediately after the target C +// function, while execution remains on the same OS thread. +func AndroidErrnoAddr() uintptr { + return uintptr(C.goffi_android_errno_addr()) +} diff --git a/internal/dl/dl_android_nocgo.go b/internal/dl/dl_android_nocgo.go new file mode 100644 index 0000000..758182d --- /dev/null +++ b/internal/dl/dl_android_nocgo.go @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build android && !cgo && arm64 + +package dl + +import ( + "fmt" + "structs" + "unsafe" +) + +// Keep the no-cgo path on the Go linker's dynamic-import route. The cgo path +// uses ordinary C wrappers instead; external Android linking cannot resolve +// AAPCS64 branch relocations directly to SDYNIMPORT symbols. +// +//go:cgo_import_dynamic goffi_android_dlopen dlopen "libdl.so" +//go:cgo_import_dynamic goffi_android_dlsym dlsym "libdl.so" +//go:cgo_import_dynamic goffi_android_dlerror dlerror "libdl.so" +//go:cgo_import_dynamic goffi_android_strdup strdup "libc.so" +//go:cgo_import_dynamic goffi_android_free free "libc.so" + +// Force dependencies on Android's public loader and C libraries. +//go:cgo_import_dynamic _ _ "libdl.so" +//go:cgo_import_dynamic _ _ "libc.so" + +//go:linkname runtime_cgocall runtime.cgocall +//go:noescape +func runtime_cgocall(fn uintptr, arg unsafe.Pointer) int32 + +// The loader wrappers call dlerror and strdup before returning from the same +// runtime.cgocall. dlerror state is thread-local, so querying it from a second +// cgocall could observe a different OS thread. +type androidDlopenArgs struct { + _ structs.HostLayout + fn uintptr + errorFn uintptr + strdupFn uintptr + path *byte + mode int + result uintptr + error uintptr +} + +type androidDlsymArgs struct { + _ structs.HostLayout + fn uintptr + errorFn uintptr + strdupFn uintptr + handle uintptr + name *byte + result uintptr + error uintptr +} + +type androidFreeArgs struct { + _ structs.HostLayout + fn uintptr + ptr uintptr +} + +//go:linkname android_dlopen_stub android_dlopen_stub +var android_dlopen_stub byte + +//go:linkname android_dlsym_stub android_dlsym_stub +var android_dlsym_stub byte + +//go:linkname android_dlerror_stub android_dlerror_stub +var android_dlerror_stub byte + +//go:linkname android_strdup_stub android_strdup_stub +var android_strdup_stub byte + +//go:linkname android_free_stub android_free_stub +var android_free_stub byte + +var ( + androidDlopenStubABI0 = uintptr(unsafe.Pointer(&android_dlopen_stub)) + androidDlsymStubABI0 = uintptr(unsafe.Pointer(&android_dlsym_stub)) + androidDlerrorStubABI0 = uintptr(unsafe.Pointer(&android_dlerror_stub)) + androidStrdupStubABI0 = uintptr(unsafe.Pointer(&android_strdup_stub)) + androidFreeStubABI0 = uintptr(unsafe.Pointer(&android_free_stub)) +) + +func androidDlopenWrapper(unsafe.Pointer) +func androidDlsymWrapper(unsafe.Pointer) +func androidFreeWrapper(unsafe.Pointer) + +var ( + androidDlopenWrapperABI0 uintptr + androidDlsymWrapperABI0 uintptr + androidFreeWrapperABI0 uintptr +) + +func Dlopen(path string, mode int) (uintptr, error) { + pathBytes := append([]byte(path), 0) + args := androidDlopenArgs{ + fn: androidDlopenStubABI0, + errorFn: androidDlerrorStubABI0, + strdupFn: androidStrdupStubABI0, + path: &pathBytes[0], + mode: mode, + } + runtime_cgocall(androidDlopenWrapperABI0, unsafe.Pointer(&args)) + if args.result == 0 { + return 0, fmt.Errorf("dlopen failed: %s", takeAndroidDlerror(args.error)) + } + return args.result, nil +} + +func Dlsym(handle uintptr, name string) (uintptr, error) { + nameBytes := append([]byte(name), 0) + args := androidDlsymArgs{ + fn: androidDlsymStubABI0, + errorFn: androidDlerrorStubABI0, + strdupFn: androidStrdupStubABI0, + handle: handle, + name: &nameBytes[0], + } + runtime_cgocall(androidDlsymWrapperABI0, unsafe.Pointer(&args)) + if args.result == 0 { + return 0, fmt.Errorf("dlsym failed: %s", takeAndroidDlerror(args.error)) + } + return args.result, nil +} + +// Dlclose deliberately retains mappings for process-lifetime function +// pointers, matching the RTLD_NODELETE mode used by ffi.LoadLibrary. +func Dlclose(uintptr) error { return nil } + +func takeAndroidDlerror(ptr uintptr) string { + if ptr == 0 { + return "unknown error" + } + // ptr names strdup-owned native memory. Reinterpret its bits without a + // uintptr-to-pointer conversion so vet does not mistake it for a hidden Go + // heap pointer. + messagePtr := *(*unsafe.Pointer)(unsafe.Pointer(&ptr)) + length := 0 + for *(*byte)(unsafe.Add(messagePtr, length)) != 0 { + length++ + } + message := string(unsafe.Slice((*byte)(messagePtr), length)) + args := androidFreeArgs{fn: androidFreeStubABI0, ptr: ptr} + runtime_cgocall(androidFreeWrapperABI0, unsafe.Pointer(&args)) + return message +} diff --git a/internal/dl/dl_android_nocgo_arm64.s b/internal/dl/dl_android_nocgo_arm64.s new file mode 100644 index 0000000..868e5f1 --- /dev/null +++ b/internal/dl/dl_android_nocgo_arm64.s @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build android && !cgo && arm64 + +#include "textflag.h" + +TEXT android_dlopen_stub(SB), NOSPLIT|NOFRAME, $0-0 + B goffi_android_dlopen(SB) + +TEXT android_dlsym_stub(SB), NOSPLIT|NOFRAME, $0-0 + B goffi_android_dlsym(SB) + +TEXT android_dlerror_stub(SB), NOSPLIT|NOFRAME, $0-0 + B goffi_android_dlerror(SB) + +TEXT android_strdup_stub(SB), NOSPLIT|NOFRAME, $0-0 + B goffi_android_strdup(SB) + +TEXT android_free_stub(SB), NOSPLIT|NOFRAME, $0-0 + B goffi_android_free(SB) + +// androidDlopenArgs offsets: fn=0, errorFn=8, strdupFn=16, path=24, +// mode=32, result=40, error=48. +GLOBL ·androidDlopenWrapperABI0(SB), NOPTR|RODATA, $8 +DATA ·androidDlopenWrapperABI0(SB)/8, $androidDlopenWrapper(SB) + +TEXT androidDlopenWrapper(SB), NOSPLIT|NOFRAME, $0 + SUB $32, RSP, RSP + MOVD R29, 0(RSP) + MOVD R30, 8(RSP) + MOVD R0, 16(RSP) + MOVD RSP, R29 + + MOVD R0, R9 + MOVD 24(R9), R0 + MOVD 32(R9), R1 + MOVD 0(R9), R10 + BL (R10) + MOVD 16(RSP), R9 + MOVD R0, 40(R9) + CBNZ R0, android_dlopen_done + + MOVD 8(R9), R10 + BL (R10) + CBZ R0, android_dlopen_done + MOVD 16(RSP), R9 + MOVD 16(R9), R10 + BL (R10) + MOVD 16(RSP), R9 + MOVD R0, 48(R9) + +android_dlopen_done: + MOVD 8(RSP), R30 + MOVD 0(RSP), R29 + ADD $32, RSP, RSP + MOVD $0, R0 + RET + +// androidDlsymArgs offsets: fn=0, errorFn=8, strdupFn=16, handle=24, +// name=32, result=40, error=48. +GLOBL ·androidDlsymWrapperABI0(SB), NOPTR|RODATA, $8 +DATA ·androidDlsymWrapperABI0(SB)/8, $androidDlsymWrapper(SB) + +TEXT androidDlsymWrapper(SB), NOSPLIT|NOFRAME, $0 + SUB $32, RSP, RSP + MOVD R29, 0(RSP) + MOVD R30, 8(RSP) + MOVD R0, 16(RSP) + MOVD RSP, R29 + + // Clear the caller thread's prior loader error before dlsym. + MOVD R0, R9 + MOVD 8(R9), R10 + BL (R10) + MOVD 16(RSP), R9 + + MOVD 24(R9), R0 + MOVD 32(R9), R1 + MOVD 0(R9), R10 + BL (R10) + MOVD 16(RSP), R9 + MOVD R0, 40(R9) + CBNZ R0, android_dlsym_done + + MOVD 8(R9), R10 + BL (R10) + CBZ R0, android_dlsym_done + MOVD 16(RSP), R9 + MOVD 16(R9), R10 + BL (R10) + MOVD 16(RSP), R9 + MOVD R0, 48(R9) + +android_dlsym_done: + MOVD 8(RSP), R30 + MOVD 0(RSP), R29 + ADD $32, RSP, RSP + MOVD $0, R0 + RET + +// androidFreeArgs offsets: fn=0, ptr=8. +GLOBL ·androidFreeWrapperABI0(SB), NOPTR|RODATA, $8 +DATA ·androidFreeWrapperABI0(SB)/8, $androidFreeWrapper(SB) + +TEXT androidFreeWrapper(SB), NOSPLIT|NOFRAME, $0 + SUB $32, RSP, RSP + MOVD R29, 0(RSP) + MOVD R30, 8(RSP) + MOVD R0, 16(RSP) + MOVD RSP, R29 + + MOVD R0, R9 + MOVD 8(R9), R0 + MOVD 0(R9), R10 + BL (R10) + + MOVD 8(RSP), R30 + MOVD 0(RSP), R29 + ADD $32, RSP, RSP + MOVD $0, R0 + RET diff --git a/internal/dl/dl_linux.go b/internal/dl/dl_linux.go index ee1b6f9..bec6a9b 100644 --- a/internal/dl/dl_linux.go +++ b/internal/dl/dl_linux.go @@ -1,4 +1,4 @@ -//go:build linux +//go:build linux && !android // Linux-specific constants for dynamic library loading. // diff --git a/internal/dl/dl_stubs_arm64.s b/internal/dl/dl_stubs_arm64.s index 3547a04..cec205d 100644 --- a/internal/dl/dl_stubs_arm64.s +++ b/internal/dl/dl_stubs_arm64.s @@ -1,4 +1,4 @@ -//go:build (linux || darwin || freebsd) && arm64 +//go:build ((linux && !android) || darwin || freebsd) && arm64 #include "textflag.h" diff --git a/internal/dl/dl_unix.go b/internal/dl/dl_unix.go index 3308796..7299aed 100644 --- a/internal/dl/dl_unix.go +++ b/internal/dl/dl_unix.go @@ -1,4 +1,4 @@ -//go:build linux || darwin || freebsd +//go:build (linux && !android) || darwin || freebsd // OUR OWN Dlopen/Dlsym implementation - NO dependencies! // Uses runtime.cgocall approach similar to syscall6. diff --git a/internal/dl/dl_wrappers_arm64.s b/internal/dl/dl_wrappers_arm64.s index f49a987..d90a72f 100644 --- a/internal/dl/dl_wrappers_arm64.s +++ b/internal/dl/dl_wrappers_arm64.s @@ -1,4 +1,4 @@ -//go:build (linux || darwin || freebsd) && arm64 +//go:build ((linux && !android) || darwin || freebsd) && arm64 #include "textflag.h" diff --git a/internal/syscall/errno_android.go b/internal/syscall/errno_android.go new file mode 100644 index 0000000..ff59c46 --- /dev/null +++ b/internal/syscall/errno_android.go @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build android && !cgo && arm64 + +package syscall + +// Bionic exports __errno from libc.so. __errno_location is a glibc name and +// must never appear in an Android artifact. +// +//go:cgo_import_dynamic goffi_errno_location __errno "libc.so" +//go:cgo_import_dynamic _ _ "libc.so" diff --git a/internal/syscall/errno_android_cgo.go b/internal/syscall/errno_android_cgo.go new file mode 100644 index 0000000..5cf3ce4 --- /dev/null +++ b/internal/syscall/errno_android_cgo.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build android && cgo && arm64 + +package syscall + +import "github.com/go-webgpu/goffi/internal/dl" + +// ErrnoFnAddr returns Bionic's __errno function address. A zero result means +// that the platform did not expose the accessor; syscallN then skips capture. +func ErrnoFnAddr() uintptr { + return dl.AndroidErrnoAddr() +} diff --git a/internal/syscall/errno_linux.go b/internal/syscall/errno_linux.go index 87f969e..5836294 100644 --- a/internal/syscall/errno_linux.go +++ b/internal/syscall/errno_linux.go @@ -1,4 +1,4 @@ -//go:build linux && (amd64 || arm64) +//go:build linux && !android && (amd64 || arm64) package syscall diff --git a/internal/syscall/errno_stubs_amd64.s b/internal/syscall/errno_stubs_amd64.s index 04c4632..bf45d73 100644 --- a/internal/syscall/errno_stubs_amd64.s +++ b/internal/syscall/errno_stubs_amd64.s @@ -1,4 +1,4 @@ -//go:build (linux || darwin || freebsd) && amd64 +//go:build ((linux && !android) || darwin || freebsd) && amd64 #include "textflag.h" diff --git a/internal/syscall/errno_stubs_arm64.s b/internal/syscall/errno_stubs_arm64.s index 4e4eacc..24f9ba8 100644 --- a/internal/syscall/errno_stubs_arm64.s +++ b/internal/syscall/errno_stubs_arm64.s @@ -1,9 +1,10 @@ -//go:build (linux || darwin || freebsd) && arm64 +//go:build ((linux && !android) || (android && !cgo) || darwin || freebsd) && arm64 #include "textflag.h" // goffi_errno_location_stub: B to the dynamically linked errno function. -// On Linux/FreeBSD: __errno_location (from libc.so.6 / libc.so.7) +// On glibc Linux: __errno_location (from libc.so.6); on Android: __errno +// (from libc.so); on FreeBSD: __error (from libc.so.7). // On macOS: __error (from libSystem.B.dylib) // In all cases the dynamic symbol is imported as goffi_errno_location. TEXT goffi_errno_location_stub(SB), NOSPLIT|NOFRAME, $0-0 diff --git a/internal/syscall/errno_unix.go b/internal/syscall/errno_unix.go index cde7b61..f815301 100644 --- a/internal/syscall/errno_unix.go +++ b/internal/syscall/errno_unix.go @@ -1,11 +1,12 @@ -//go:build (linux || darwin || freebsd) && (amd64 || arm64) +//go:build ((linux && !android) || (android && !cgo) || darwin || freebsd) && (amd64 || arm64) package syscall import "unsafe" // goffi_errno_location_stub is the JMP trampoline that forwards calls to the -// dynamically linked __errno_location (Linux/FreeBSD) or __error (macOS). +// dynamically linked __errno_location (glibc Linux), __errno (Bionic), or +// __error (macOS/FreeBSD). // The symbol is defined in errno_stubs_amd64.s / errno_stubs_arm64.s and // jumps to the goffi_errno_location dynamic symbol imported via // //go:cgo_import_dynamic in errno_linux.go / errno_darwin.go / errno_freebsd.go. @@ -20,7 +21,8 @@ var goffi_errno_location_stub byte var errnoFnABI0 = uintptr(unsafe.Pointer(&goffi_errno_location_stub)) //nolint:govet // unsafe.Pointer-to-uintptr is intentional: this is an assembly stub address, not a GC-managed pointer. // ErrnoFnAddr returns the address of the platform's errno-location function -// (__errno_location on Linux/FreeBSD, __error on macOS). This address is +// (__errno_location on glibc Linux, __errno on Bionic, __error on +// macOS/FreeBSD). This address is // passed to CallNFloatErrno to enable in-trampoline errno capture. // // Returns 0 if errno capture is not supported on the current platform. From 0cb3ecb9ba84705b6d51f8b2e3d1bd5e684d25af Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 15 Jul 2026 21:01:26 +0300 Subject: [PATCH 2/4] feat: add Android arm64 fakecgo startup guard --- ffi/callback_android_arm64.go | 14 ++ ffi/callback_android_arm64_test.go | 18 ++ ffi/callback_arm64.go | 2 +- ffi/callback_arm64.s | 2 +- ffi/callback_cthread_test.go | 2 +- ffi/callback_struct_args_test.go | 2 +- ffi/callback_test.go | 2 +- internal/fakecgo/android_dl.go | 65 ++++++ internal/fakecgo/android_dl_stubs_arm64.s | 14 ++ internal/fakecgo/gen.go | 120 ++++++++--- internal/fakecgo/go_android_arm64.go | 129 ++++++++++++ internal/fakecgo/go_linux_amd64.go | 2 +- internal/fakecgo/go_linux_arm64.go | 2 +- internal/fakecgo/libcgo.go | 2 +- internal/fakecgo/libcgo_android.go | 62 ++++++ internal/fakecgo/libcgo_linux.go | 2 +- internal/fakecgo/symbols.go | 2 +- internal/fakecgo/symbols_android.go | 211 +++++++++++++++++++ internal/fakecgo/symbols_android_imports.go | 28 +++ internal/fakecgo/symbols_linux.go | 2 +- internal/fakecgo/trampolines_stubs.s | 2 +- internal/fakecgo/trampolines_stubs_android.s | 86 ++++++++ 22 files changed, 733 insertions(+), 38 deletions(-) create mode 100644 ffi/callback_android_arm64.go create mode 100644 ffi/callback_android_arm64_test.go create mode 100644 internal/fakecgo/android_dl.go create mode 100644 internal/fakecgo/android_dl_stubs_arm64.s create mode 100644 internal/fakecgo/go_android_arm64.go create mode 100644 internal/fakecgo/libcgo_android.go create mode 100644 internal/fakecgo/symbols_android.go create mode 100644 internal/fakecgo/symbols_android_imports.go create mode 100644 internal/fakecgo/trampolines_stubs_android.s diff --git a/ffi/callback_android_arm64.go b/ffi/callback_android_arm64.go new file mode 100644 index 0000000..29f2d8b --- /dev/null +++ b/ffi/callback_android_arm64.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build android && arm64 + +package ffi + +// NewCallback is intentionally unavailable on Android until the foreign +// thread callback path has physical arm64 evidence. Failing before inspecting +// or retaining fn prevents callers from accidentally passing a bogus pointer +// into a Vulkan driver. +func NewCallback(any) uintptr { + panic("ffi: callbacks are unsupported on Android") +} diff --git a/ffi/callback_android_arm64_test.go b/ffi/callback_android_arm64_test.go new file mode 100644 index 0000000..eb0c4ea --- /dev/null +++ b/ffi/callback_android_arm64_test.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build android && arm64 + +package ffi + +import "testing" + +func TestNewCallbackFailsExplicitlyOnAndroid(t *testing.T) { + defer func() { + got := recover() + if got != "ffi: callbacks are unsupported on Android" { + t.Fatalf("NewCallback panic = %v, want stable unsupported message", got) + } + }() + NewCallback(func() {}) +} diff --git a/ffi/callback_arm64.go b/ffi/callback_arm64.go index 931324f..d3edee8 100644 --- a/ffi/callback_arm64.go +++ b/ffi/callback_arm64.go @@ -1,4 +1,4 @@ -//go:build (linux || darwin || freebsd) && arm64 +//go:build ((linux && !android) || darwin || freebsd) && arm64 // Package ffi provides callback support for Foreign Function Interface (ARM64 Unix version). // This file implements Go function registration as C callbacks using diff --git a/ffi/callback_arm64.s b/ffi/callback_arm64.s index 81740d9..81821a7 100644 --- a/ffi/callback_arm64.s +++ b/ffi/callback_arm64.s @@ -1,4 +1,4 @@ -//go:build (linux || darwin || freebsd) && arm64 +//go:build ((linux && !android) || darwin || freebsd) && arm64 #include "textflag.h" #include "go_asm.h" diff --git a/ffi/callback_cthread_test.go b/ffi/callback_cthread_test.go index 6ef7069..4bd79c2 100644 --- a/ffi/callback_cthread_test.go +++ b/ffi/callback_cthread_test.go @@ -1,4 +1,4 @@ -//go:build (linux || darwin || freebsd) && (amd64 || arm64) +//go:build ((linux && !android) || darwin || freebsd) && (amd64 || arm64) package ffi diff --git a/ffi/callback_struct_args_test.go b/ffi/callback_struct_args_test.go index 7c99067..82c0201 100644 --- a/ffi/callback_struct_args_test.go +++ b/ffi/callback_struct_args_test.go @@ -1,4 +1,4 @@ -//go:build (linux || darwin || freebsd) && amd64 +//go:build ((linux && !android) || darwin || freebsd) && amd64 package ffi diff --git a/ffi/callback_test.go b/ffi/callback_test.go index d80904b..9b89b08 100644 --- a/ffi/callback_test.go +++ b/ffi/callback_test.go @@ -1,4 +1,4 @@ -//go:build (linux || darwin || freebsd) && (amd64 || arm64) +//go:build ((linux && !android) || darwin || freebsd) && (amd64 || arm64) package ffi diff --git a/internal/fakecgo/android_dl.go b/internal/fakecgo/android_dl.go new file mode 100644 index 0000000..99b4169 --- /dev/null +++ b/internal/fakecgo/android_dl.go @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build !cgo && android && arm64 + +package fakecgo + +import "unsafe" + +// x_cgo_init runs before the Go runtime has enabled runtime.cgocall. Resolve +// the API-level marker with direct AAPCS64 calls instead of internal/dl so the +// pre-Q guard cannot recurse through an uninitialized cgocall path. +// +//go:cgo_import_dynamic purego_android_dlopen dlopen "libdl.so" +//go:cgo_import_dynamic purego_android_dlsym dlsym "libdl.so" +//go:cgo_import_dynamic _ _ "libdl.so" + +// The assembly stubs are kept separate from the generated libc wrappers: the +// latter intentionally contain only the runtime symbols needed after startup. +// +//go:linkname _android_dlopen _android_dlopen +var _android_dlopen byte + +//go:linkname _android_dlsym _android_dlsym +var _android_dlsym byte + +var ( + androidDlopenABI0 = uintptr(unsafe.Pointer(&_android_dlopen)) + androidDlsymABI0 = uintptr(unsafe.Pointer(&_android_dlsym)) +) + +var ( + androidLibcName = [...]byte{'l', 'i', 'b', 'c', '.', 's', 'o', 0} + androidAPISymbol = [...]byte{ + 'a', 'n', 'd', 'r', 'o', 'i', 'd', '_', 'g', 'e', 't', '_', + 'd', 'e', 'v', 'i', 'c', 'e', '_', 'a', 'p', 'i', '_', 'l', + 'e', 'v', 'e', 'l', 0, + } +) + +const androidRTLDNow = uintptr(0x00002) +const androidMinAPI = uintptr(29) + +// androidAPI29 reports whether libc exposes the API-29 marker and returns a +// sufficiently new level. It uses only direct call5/assembly calls and is +// safe to invoke from x_cgo_init before ordinary runtime.cgocall exists. +// +//go:nosplit +func androidAPI29() bool { + handle := call5(androidDlopenABI0, + uintptr(unsafe.Pointer(&androidLibcName[0])), androidRTLDNow, 0, 0, 0) + if handle == 0 { + return false + } + apiFn := call5(androidDlsymABI0, handle, + uintptr(unsafe.Pointer(&androidAPISymbol[0])), 0, 0, 0) + if apiFn == 0 { + return false + } + api := call5(apiFn, 0, 0, 0, 0, 0) + // Do not close libc here. The runtime's Android path keeps libc resident, + // and avoiding a second loader transition keeps this pre-runtime probe + // deterministic. + return api >= androidMinAPI +} diff --git a/internal/fakecgo/android_dl_stubs_arm64.s b/internal/fakecgo/android_dl_stubs_arm64.s new file mode 100644 index 0000000..2573877 --- /dev/null +++ b/internal/fakecgo/android_dl_stubs_arm64.s @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build !cgo && android && arm64 + +#include "textflag.h" + +TEXT _android_dlopen(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_android_dlopen(SB) + RET + +TEXT _android_dlsym(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_android_dlsym(SB) + RET diff --git a/internal/fakecgo/gen.go b/internal/fakecgo/gen.go index 5407e79..1a80e7d 100644 --- a/internal/fakecgo/gen.go +++ b/internal/fakecgo/gen.go @@ -22,7 +22,7 @@ const templateSymbols = `// Code generated by 'go generate' with gen.go. DO NOT // SPDX-FileCopyrightText: 2022 The Ebitengine Authors // SPDX-FileCopyrightText: 2025-2026 Andrey Kolkov and GoGPU Contributors -//go:build !cgo && (darwin || freebsd || linux || netbsd) +//go:build !cgo && (darwin || freebsd || linux || netbsd) && !android package fakecgo @@ -90,7 +90,7 @@ const templateTrampolinesStubs = `// Code generated by 'go generate' with gen.go // SPDX-FileCopyrightText: 2022 The Ebitengine Authors // SPDX-FileCopyrightText: 2025-2026 Andrey Kolkov and GoGPU Contributors -//go:build !cgo && (darwin || freebsd || linux || netbsd) +//go:build !cgo && (darwin || freebsd || linux || netbsd) && !android #include "textflag.h" @@ -135,6 +135,20 @@ type LocatedSymbols struct { Symbols []Symbol } +func writeFormatted(path string, source []byte) error { + formatted, err := format.Source(source) + if err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + _, err = f.Write(formatted) + return err +} + var ( libcSymbols = []Symbol{ {"malloc", [5]Arg{{"size", "uintptr"}}, "unsafe.Pointer"}, @@ -146,6 +160,11 @@ var ( {"abort", [5]Arg{}, ""}, {"sigaltstack", [5]Arg{{"ss", "*stack_t"}, {"old_ss", "*stack_t"}}, "int32"}, } + androidLibcSymbols = append(append([]Symbol{}, libcSymbols...), Symbol{ + "write", + [5]Arg{{"fd", "int32"}, {"buf", "unsafe.Pointer"}, {"count", "size_t"}}, + "int", + }) pthreadSymbols = []Symbol{ {"pthread_attr_init", [5]Arg{{"attr", "*pthread_attr_t"}}, "int32"}, {"pthread_create", [5]Arg{{"thread", "*pthread_t"}, {"attr", "*pthread_attr_t"}, {"start", "unsafe.Pointer"}, {"arg", "unsafe.Pointer"}}, "int32"}, @@ -161,6 +180,18 @@ var ( {"pthread_cond_broadcast", [5]Arg{{"cond", "*pthread_cond_t"}}, "int32"}, {"pthread_setspecific", [5]Arg{{"key", "pthread_key_t"}, {"value", "unsafe.Pointer"}}, "int32"}, } + androidPthreadSymbols = []Symbol{ + {"pthread_attr_init", [5]Arg{{"attr", "*pthread_attr_t"}}, "int32"}, + {"pthread_create", [5]Arg{{"thread", "*pthread_t"}, {"attr", "*pthread_attr_t"}, {"start", "unsafe.Pointer"}, {"arg", "unsafe.Pointer"}}, "int32"}, + {"pthread_detach", [5]Arg{{"thread", "pthread_t"}}, "int32"}, + {"pthread_sigmask", [5]Arg{{"how", "sighow"}, {"ign", "*sigset_t"}, {"oset", "*sigset_t"}}, "int32"}, + {"pthread_attr_getstacksize", [5]Arg{{"attr", "*pthread_attr_t"}, {"stacksize", "*size_t"}}, "int32"}, + {"pthread_attr_destroy", [5]Arg{{"attr", "*pthread_attr_t"}}, "int32"}, + {"pthread_mutex_lock", [5]Arg{{"mutex", "*pthread_mutex_t"}}, "int32"}, + {"pthread_mutex_unlock", [5]Arg{{"mutex", "*pthread_mutex_t"}}, "int32"}, + {"pthread_cond_broadcast", [5]Arg{{"cond", "*pthread_cond_t"}}, "int32"}, + {"pthread_setspecific", [5]Arg{{"key", "pthread_key_t"}, {"value", "unsafe.Pointer"}}, "int32"}, + } ) var funcs = map[string]any{ @@ -172,45 +203,30 @@ func run() error { if err != nil { return err } - f, err := os.Create("symbols.go") - defer f.Close() - if err != nil { - return err - } allSymbols := append(append([]Symbol{}, libcSymbols...), pthreadSymbols...) buf := new(bytes.Buffer) if err := t.Execute(buf, allSymbols); err != nil { return err } - source, err := format.Source(buf.Bytes()) - if err != nil { - return err - } - if _, err = f.Write(source); err != nil { + if err := writeFormatted("symbols.go", buf.Bytes()); err != nil { return err } t, err = template.New("trampolines_stubs.s").Funcs(funcs).Parse(templateTrampolinesStubs) if err != nil { return err } - f, err = os.Create("trampolines_stubs.s") - defer f.Close() - if err != nil { + buf.Reset() + if err := t.Execute(buf, allSymbols); err != nil { return err } - if err := t.Execute(f, allSymbols); err != nil { + if err := os.WriteFile("trampolines_stubs.s", buf.Bytes(), 0o644); err != nil { return err } t, err = template.New("symbols_goos.go").Parse(templateSymbolsGoos) if err != nil { return err } - for _, goos := range []string{"darwin", "freebsd", "linux", "netbsd"} { - f, err = os.Create(fmt.Sprintf("symbols_%s.go", goos)) - defer f.Close() - if err != nil { - return err - } + for _, goos := range []string{"darwin", "freebsd", "linux", "netbsd", "android"} { b := &bytes.Buffer{} var libcSO, pthreadSO string switch goos { @@ -226,14 +242,34 @@ func run() error { case "netbsd": libcSO = "libc.so" pthreadSO = "libpthread.so" + case "android": + libcSO = "libc.so" + pthreadSO = "libc.so" default: return fmt.Errorf("unsupported OS: %s", goos) } + pthread := pthreadSymbols + libc := libcSymbols + if goos == "android" { + libc = androidLibcSymbols + pthread = androidPthreadSymbols + } located := []LocatedSymbols{ - {SharedObject: libcSO, Symbols: libcSymbols}, - {SharedObject: pthreadSO, Symbols: pthreadSymbols}, + {SharedObject: libcSO, Symbols: libc}, + {SharedObject: pthreadSO, Symbols: pthread}, + } + goosTemplate := t + switch goos { + case "linux": + // The go command also satisfies the linux build tag on Android, + // including for _linux.go files. Keep glibc imports out of Bionic. + goosTemplate = template.Must(template.New("symbols_linux.go").Parse(strings.Replace(templateSymbolsGoos, "//go:build !cgo", "//go:build !cgo && !android", 1))) + case "android": + // Android uses a distinct build selector and symbol set. Keep the + // generated generic Linux imports out of the Android ELF. + goosTemplate = template.Must(template.New("symbols_android.go").Parse(strings.Replace(templateSymbolsGoos, "//go:build !cgo", "//go:build !cgo && android && arm64", 1))) } - if err = t.Execute(b, located); err != nil { + if err = goosTemplate.Execute(b, located); err != nil { return err } var src []byte @@ -241,10 +277,42 @@ func run() error { if err != nil { return err } - if _, err = f.Write(src); err != nil { + name := fmt.Sprintf("symbols_%s.go", goos) + if goos == "android" { + name = "symbols_android_imports.go" + } + if err = os.WriteFile(name, src, 0o644); err != nil { return err } } + + // The Android wrappers and assembly stubs are generated from the same + // restricted symbol set as the imports above. + androidSymbols := append(append([]Symbol{}, androidLibcSymbols...), androidPthreadSymbols...) + androidTemplate := strings.Replace(templateSymbols, "//go:build !cgo && (darwin || freebsd || linux || netbsd) && !android", "//go:build !cgo && android && arm64", 1) + t, err = template.New("symbols_android.go").Funcs(funcs).Parse(androidTemplate) + if err != nil { + return err + } + buf.Reset() + if err := t.Execute(buf, androidSymbols); err != nil { + return err + } + if err := writeFormatted("symbols_android.go", buf.Bytes()); err != nil { + return err + } + androidStubTemplate := strings.Replace(templateTrampolinesStubs, "//go:build !cgo && (darwin || freebsd || linux || netbsd) && !android", "//go:build !cgo && android && arm64", 1) + t, err = template.New("trampolines_stubs_android.s").Funcs(funcs).Parse(androidStubTemplate) + if err != nil { + return err + } + buf.Reset() + if err := t.Execute(buf, androidSymbols); err != nil { + return err + } + if err := os.WriteFile("trampolines_stubs_android.s", buf.Bytes(), 0o644); err != nil { + return err + } return nil } diff --git a/internal/fakecgo/go_android_arm64.go b/internal/fakecgo/go_android_arm64.go new file mode 100644 index 0000000..e31285c --- /dev/null +++ b/internal/fakecgo/go_android_arm64.go @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2011 The Go Authors +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build !cgo && android && arm64 + +package fakecgo + +import "unsafe" + +// Android reserves TLS_SLOT_APP (slot 2) for the runtime's g pointer on +// API-29+ arm64. Keep this as a package constant so the ABI test and startup +// guard cannot silently drift apart. +const androidTLSGOffset = uintptr(2 * unsafe.Sizeof(uintptr(0))) + +// _cgo_sys_thread_start is the Android arm64 half of the fakecgo thread +// startup path. It intentionally mirrors runtime/cgo/gcc_linux_arm64.c while +// using Bionic's libc-only pthread symbols and Android-verified layouts. +// +//go:nosplit +func _cgo_sys_thread_start(ts *ThreadStart) { + var attr pthread_attr_t + var ign, oset sigset_t + var p pthread_t + var size size_t + var err int + + sigfillset(&ign) + pthread_sigmask(SIG_SETMASK, &ign, &oset) + + if pthread_attr_init(&attr) != 0 { + androidFatal("fakecgo: pthread_attr_init failed on Android") + } + if pthread_attr_getstacksize(&attr, &size) != 0 { + androidFatal("fakecgo: pthread_attr_getstacksize failed on Android") + } + // Leave stacklo=0 and set stackhi=size; mstart will do the rest. + ts.g.stackhi = uintptr(size) + + err = _cgo_try_pthread_create(&p, &attr, unsafe.Pointer(threadentry_trampolineABI0), ts) + + pthread_sigmask(SIG_SETMASK, &oset, nil) + + if err != 0 { + androidFatal("fakecgo: pthread_create failed on Android") + } +} + +// threadentry_trampolineABI0 maps the C ABI to Go ABI and then calls the Go +// function. The trampoline is supplied by the shared fakecgo arm64 assembly. +// +//go:linkname x_threadentry_trampoline threadentry_trampoline +var x_threadentry_trampoline byte +var threadentry_trampolineABI0 = &x_threadentry_trampoline + +//go:nosplit +func threadentry(v unsafe.Pointer) unsafe.Pointer { + ts := *(*ThreadStart)(v) + free(v) + + setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g))) + + fn := uintptr(unsafe.Pointer(&ts.fn)) + (*(*func())(unsafe.Pointer(&fn)))() + + return nil +} + +// setg_func stores the runtime-provided setg_gcc callback for new pthreads. +var setg_func uintptr + +// androidFatal is deliberately direct and nosplit: x_cgo_init may call it +// before normal runtime initialization, when panic, Go printing, and ordinary +// cgocall are not safe. It writes directly through Bionic and then aborts. +var androidFatalNewline = [1]byte{'\n'} + +//go:nosplit +func androidFatal(message string) { + if len(message) != 0 { + write(2, unsafe.Pointer(unsafe.StringData(message)), size_t(len(message))) + } + write(2, unsafe.Pointer(&androidFatalNewline[0]), 1) + abort() +} + +// x_cgo_inittls validates the API-29+ Android arm64 TLS contract used by the +// pinned Go runtime. Android Q reserves TLS_SLOT_APP (slot 2), so runtime.tls_g +// must contain 2*sizeof(void*) == 16. The API-level guard runs first and uses +// direct dlsym/call5 calls; this function never enters runtime.cgocall. +// +//go:nosplit +func x_cgo_inittls(tlsg *uintptr, tlsbase unsafe.Pointer) { + _ = tlsbase // The runtime supplies the base for the C implementation; slot 2 is fixed. + if !androidAPI29() { + androidFatal("fakecgo: Android API 29 or newer is required") + } + if tlsg == nil || *tlsg != androidTLSGOffset { + androidFatal("fakecgo: Android runtime.tls_g offset mismatch") + } +} + +// x_cgo_init matches the four-argument Android arm64 entry point in the +// pinned Go runtime: (G*, setg_gcc, &runtime.tls_g, TLS base). +// +//go:nosplit +func x_cgo_init(g *G, setg uintptr, tlsg *uintptr, tlsbase unsafe.Pointer) { + setg_func = setg + + // The API/TLS guard must execute before touching the slot or entering any + // ordinary cgocall path. + x_cgo_inittls(tlsg, tlsbase) + + var size size_t + var attr *pthread_attr_t + + attr = (*pthread_attr_t)(malloc(unsafe.Sizeof(*attr))) + if attr == nil { + androidFatal("fakecgo: malloc failed while initializing Android cgo") + } + if pthread_attr_init(attr) != 0 || pthread_attr_getstacksize(attr, &size) != 0 { + androidFatal("fakecgo: pthread stack initialization failed on Android") + } + // runtime/cgo uses _cgo_set_stacklo with a malloc-backed probe. The + // fakecgo path keeps the established Linux calculation, but applies it only + // after the Bionic attr layout has been validated above. + g.stacklo = uintptr(unsafe.Pointer(&size)) - uintptr(size) + 4096 + pthread_attr_destroy(attr) + free(unsafe.Pointer(attr)) +} diff --git a/internal/fakecgo/go_linux_amd64.go b/internal/fakecgo/go_linux_amd64.go index c9ff715..198f2dc 100644 --- a/internal/fakecgo/go_linux_amd64.go +++ b/internal/fakecgo/go_linux_amd64.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !cgo +//go:build !cgo && linux && !android package fakecgo diff --git a/internal/fakecgo/go_linux_arm64.go b/internal/fakecgo/go_linux_arm64.go index a3b1cca..fb45f64 100644 --- a/internal/fakecgo/go_linux_arm64.go +++ b/internal/fakecgo/go_linux_arm64.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !cgo +//go:build !cgo && linux && !android package fakecgo diff --git a/internal/fakecgo/libcgo.go b/internal/fakecgo/libcgo.go index f50e835..d78d6d0 100644 --- a/internal/fakecgo/libcgo.go +++ b/internal/fakecgo/libcgo.go @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2022 The Ebitengine Authors // SPDX-FileCopyrightText: 2025-2026 Andrey Kolkov and GoGPU Contributors -//go:build !cgo && (darwin || freebsd || linux || netbsd) +//go:build !cgo && (darwin || freebsd || linux || netbsd) && !android package fakecgo diff --git a/internal/fakecgo/libcgo_android.go b/internal/fakecgo/libcgo_android.go new file mode 100644 index 0000000..f3d9def --- /dev/null +++ b/internal/fakecgo/libcgo_android.go @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build !cgo && android && arm64 + +package fakecgo + +import "structs" + +// These layouts mirror Android NDK LP64 pthread_types.h. They intentionally +// do not reuse the glibc Linux definitions: Android's pthread_attr_t is 56 +// bytes and pthread_mutex_t is 40 bytes on arm64, while pthread_key_t is a +// 32-bit int and pthread_t is a C long. +type ( + size_t uintptr + // Bionic LP64 uses the asm-generic sigset_t typedef: one unsigned long. + // Keep its natural 8-byte alignment because pthread_sigmask receives this + // object by pointer and the ABI probe asserts the native layout. + sigset_t uint64 + // Use word arrays to preserve Bionic's native alignment as well as size. + // pthread_attr_t contains pointers/size_t and is 8-byte aligned on LP64. + pthread_attr_t [7]uint64 + pthread_t int64 + pthread_key_t int32 + + // bionic's stack_t is struct { void *ss_sp; int ss_flags; size_t ss_size; } + stack_t struct { + ss_sp uintptr + ss_flags int32 + _pad int32 + ss_size size_t + } + + pthread_cond_t [12]uint32 // Bionic's int32[12], align 4. + pthread_mutex_t [10]uint32 // Bionic's int32[10], align 4. +) + +var ( + PTHREAD_COND_INITIALIZER = pthread_cond_t{} + PTHREAD_MUTEX_INITIALIZER = pthread_mutex_t{} +) + +type sighow int32 + +const ( + SIG_BLOCK sighow = 0 + SIG_UNBLOCK sighow = 1 + SIG_SETMASK sighow = 2 +) + +type G struct { + _ structs.HostLayout + stacklo uintptr + stackhi uintptr +} + +type ThreadStart struct { + _ structs.HostLayout + g *G + tls *uintptr + fn uintptr +} diff --git a/internal/fakecgo/libcgo_linux.go b/internal/fakecgo/libcgo_linux.go index 78cd999..503a15b 100644 --- a/internal/fakecgo/libcgo_linux.go +++ b/internal/fakecgo/libcgo_linux.go @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2022 The Ebitengine Authors // SPDX-FileCopyrightText: 2025-2026 Andrey Kolkov and GoGPU Contributors -//go:build !cgo +//go:build !cgo && linux && !android package fakecgo diff --git a/internal/fakecgo/symbols.go b/internal/fakecgo/symbols.go index cb7668a..1a6a353 100644 --- a/internal/fakecgo/symbols.go +++ b/internal/fakecgo/symbols.go @@ -4,7 +4,7 @@ // SPDX-FileCopyrightText: 2022 The Ebitengine Authors // SPDX-FileCopyrightText: 2025-2026 Andrey Kolkov and GoGPU Contributors -//go:build !cgo && (darwin || freebsd || linux || netbsd) +//go:build !cgo && (darwin || freebsd || linux || netbsd) && !android package fakecgo diff --git a/internal/fakecgo/symbols_android.go b/internal/fakecgo/symbols_android.go new file mode 100644 index 0000000..6f74e0d --- /dev/null +++ b/internal/fakecgo/symbols_android.go @@ -0,0 +1,211 @@ +// Code generated by 'go generate' with gen.go. DO NOT EDIT. + +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && android && arm64 + +package fakecgo + +import ( + "syscall" + "unsafe" +) + +// setg_trampoline calls setg with the G provided +func setg_trampoline(setg uintptr, G uintptr) + +// call5 takes fn the C function and 5 arguments and calls the function with those arguments +func call5(fn, a1, a2, a3, a4, a5 uintptr) uintptr + +//go:nosplit +//go:norace +func malloc(size uintptr) unsafe.Pointer { + ret := call5(mallocABI0, uintptr(size), 0, 0, 0, 0) + // this indirection is to avoid go vet complaining about possible misuse of unsafe.Pointer + return *(*unsafe.Pointer)(unsafe.Pointer(&ret)) +} + +//go:nosplit +//go:norace +func free(ptr unsafe.Pointer) { + call5(freeABI0, uintptr(ptr), 0, 0, 0, 0) +} + +//go:nosplit +//go:norace +func setenv(name *byte, value *byte, overwrite int32) int32 { + return int32(call5(setenvABI0, uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(value)), uintptr(overwrite), 0, 0)) +} + +//go:nosplit +//go:norace +func unsetenv(name *byte) int32 { + return int32(call5(unsetenvABI0, uintptr(unsafe.Pointer(name)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func sigfillset(set *sigset_t) int32 { + return int32(call5(sigfillsetABI0, uintptr(unsafe.Pointer(set)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func nanosleep(ts *syscall.Timespec, rem *syscall.Timespec) int32 { + return int32(call5(nanosleepABI0, uintptr(unsafe.Pointer(ts)), uintptr(unsafe.Pointer(rem)), 0, 0, 0)) +} + +//go:nosplit +//go:norace +func abort() { + call5(abortABI0, 0, 0, 0, 0, 0) +} + +//go:nosplit +//go:norace +func sigaltstack(ss *stack_t, old_ss *stack_t) int32 { + return int32(call5(sigaltstackABI0, uintptr(unsafe.Pointer(ss)), uintptr(unsafe.Pointer(old_ss)), 0, 0, 0)) +} + +//go:nosplit +//go:norace +func write(fd int32, buf unsafe.Pointer, count size_t) int { + return int(call5(writeABI0, uintptr(fd), uintptr(buf), uintptr(count), 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_attr_init(attr *pthread_attr_t) int32 { + return int32(call5(pthread_attr_initABI0, uintptr(unsafe.Pointer(attr)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_create(thread *pthread_t, attr *pthread_attr_t, start unsafe.Pointer, arg unsafe.Pointer) int32 { + return int32(call5(pthread_createABI0, uintptr(unsafe.Pointer(thread)), uintptr(unsafe.Pointer(attr)), uintptr(start), uintptr(arg), 0)) +} + +//go:nosplit +//go:norace +func pthread_detach(thread pthread_t) int32 { + return int32(call5(pthread_detachABI0, uintptr(thread), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_sigmask(how sighow, ign *sigset_t, oset *sigset_t) int32 { + return int32(call5(pthread_sigmaskABI0, uintptr(how), uintptr(unsafe.Pointer(ign)), uintptr(unsafe.Pointer(oset)), 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_attr_getstacksize(attr *pthread_attr_t, stacksize *size_t) int32 { + return int32(call5(pthread_attr_getstacksizeABI0, uintptr(unsafe.Pointer(attr)), uintptr(unsafe.Pointer(stacksize)), 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_attr_destroy(attr *pthread_attr_t) int32 { + return int32(call5(pthread_attr_destroyABI0, uintptr(unsafe.Pointer(attr)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_mutex_lock(mutex *pthread_mutex_t) int32 { + return int32(call5(pthread_mutex_lockABI0, uintptr(unsafe.Pointer(mutex)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_mutex_unlock(mutex *pthread_mutex_t) int32 { + return int32(call5(pthread_mutex_unlockABI0, uintptr(unsafe.Pointer(mutex)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_cond_broadcast(cond *pthread_cond_t) int32 { + return int32(call5(pthread_cond_broadcastABI0, uintptr(unsafe.Pointer(cond)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_setspecific(key pthread_key_t, value unsafe.Pointer) int32 { + return int32(call5(pthread_setspecificABI0, uintptr(key), uintptr(value), 0, 0, 0)) +} + +//go:linkname _malloc _malloc +var _malloc uint8 +var mallocABI0 = uintptr(unsafe.Pointer(&_malloc)) + +//go:linkname _free _free +var _free uint8 +var freeABI0 = uintptr(unsafe.Pointer(&_free)) + +//go:linkname _setenv _setenv +var _setenv uint8 +var setenvABI0 = uintptr(unsafe.Pointer(&_setenv)) + +//go:linkname _unsetenv _unsetenv +var _unsetenv uint8 +var unsetenvABI0 = uintptr(unsafe.Pointer(&_unsetenv)) + +//go:linkname _sigfillset _sigfillset +var _sigfillset uint8 +var sigfillsetABI0 = uintptr(unsafe.Pointer(&_sigfillset)) + +//go:linkname _nanosleep _nanosleep +var _nanosleep uint8 +var nanosleepABI0 = uintptr(unsafe.Pointer(&_nanosleep)) + +//go:linkname _abort _abort +var _abort uint8 +var abortABI0 = uintptr(unsafe.Pointer(&_abort)) + +//go:linkname _sigaltstack _sigaltstack +var _sigaltstack uint8 +var sigaltstackABI0 = uintptr(unsafe.Pointer(&_sigaltstack)) + +//go:linkname _write _write +var _write uint8 +var writeABI0 = uintptr(unsafe.Pointer(&_write)) + +//go:linkname _pthread_attr_init _pthread_attr_init +var _pthread_attr_init uint8 +var pthread_attr_initABI0 = uintptr(unsafe.Pointer(&_pthread_attr_init)) + +//go:linkname _pthread_create _pthread_create +var _pthread_create uint8 +var pthread_createABI0 = uintptr(unsafe.Pointer(&_pthread_create)) + +//go:linkname _pthread_detach _pthread_detach +var _pthread_detach uint8 +var pthread_detachABI0 = uintptr(unsafe.Pointer(&_pthread_detach)) + +//go:linkname _pthread_sigmask _pthread_sigmask +var _pthread_sigmask uint8 +var pthread_sigmaskABI0 = uintptr(unsafe.Pointer(&_pthread_sigmask)) + +//go:linkname _pthread_attr_getstacksize _pthread_attr_getstacksize +var _pthread_attr_getstacksize uint8 +var pthread_attr_getstacksizeABI0 = uintptr(unsafe.Pointer(&_pthread_attr_getstacksize)) + +//go:linkname _pthread_attr_destroy _pthread_attr_destroy +var _pthread_attr_destroy uint8 +var pthread_attr_destroyABI0 = uintptr(unsafe.Pointer(&_pthread_attr_destroy)) + +//go:linkname _pthread_mutex_lock _pthread_mutex_lock +var _pthread_mutex_lock uint8 +var pthread_mutex_lockABI0 = uintptr(unsafe.Pointer(&_pthread_mutex_lock)) + +//go:linkname _pthread_mutex_unlock _pthread_mutex_unlock +var _pthread_mutex_unlock uint8 +var pthread_mutex_unlockABI0 = uintptr(unsafe.Pointer(&_pthread_mutex_unlock)) + +//go:linkname _pthread_cond_broadcast _pthread_cond_broadcast +var _pthread_cond_broadcast uint8 +var pthread_cond_broadcastABI0 = uintptr(unsafe.Pointer(&_pthread_cond_broadcast)) + +//go:linkname _pthread_setspecific _pthread_setspecific +var _pthread_setspecific uint8 +var pthread_setspecificABI0 = uintptr(unsafe.Pointer(&_pthread_setspecific)) diff --git a/internal/fakecgo/symbols_android_imports.go b/internal/fakecgo/symbols_android_imports.go new file mode 100644 index 0000000..5840053 --- /dev/null +++ b/internal/fakecgo/symbols_android_imports.go @@ -0,0 +1,28 @@ +// Code generated by 'go generate' with gen.go. DO NOT EDIT. + +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && android && arm64 + +package fakecgo + +//go:cgo_import_dynamic purego_malloc malloc "libc.so" +//go:cgo_import_dynamic purego_free free "libc.so" +//go:cgo_import_dynamic purego_setenv setenv "libc.so" +//go:cgo_import_dynamic purego_unsetenv unsetenv "libc.so" +//go:cgo_import_dynamic purego_sigfillset sigfillset "libc.so" +//go:cgo_import_dynamic purego_nanosleep nanosleep "libc.so" +//go:cgo_import_dynamic purego_abort abort "libc.so" +//go:cgo_import_dynamic purego_sigaltstack sigaltstack "libc.so" +//go:cgo_import_dynamic purego_write write "libc.so" +//go:cgo_import_dynamic purego_pthread_attr_init pthread_attr_init "libc.so" +//go:cgo_import_dynamic purego_pthread_create pthread_create "libc.so" +//go:cgo_import_dynamic purego_pthread_detach pthread_detach "libc.so" +//go:cgo_import_dynamic purego_pthread_sigmask pthread_sigmask "libc.so" +//go:cgo_import_dynamic purego_pthread_attr_getstacksize pthread_attr_getstacksize "libc.so" +//go:cgo_import_dynamic purego_pthread_attr_destroy pthread_attr_destroy "libc.so" +//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "libc.so" +//go:cgo_import_dynamic purego_pthread_mutex_unlock pthread_mutex_unlock "libc.so" +//go:cgo_import_dynamic purego_pthread_cond_broadcast pthread_cond_broadcast "libc.so" +//go:cgo_import_dynamic purego_pthread_setspecific pthread_setspecific "libc.so" diff --git a/internal/fakecgo/symbols_linux.go b/internal/fakecgo/symbols_linux.go index 5d35a76..b5c7dfa 100644 --- a/internal/fakecgo/symbols_linux.go +++ b/internal/fakecgo/symbols_linux.go @@ -4,7 +4,7 @@ // SPDX-FileCopyrightText: 2022 The Ebitengine Authors // SPDX-FileCopyrightText: 2025-2026 Andrey Kolkov and GoGPU Contributors -//go:build !cgo +//go:build !cgo && !android package fakecgo diff --git a/internal/fakecgo/trampolines_stubs.s b/internal/fakecgo/trampolines_stubs.s index 4bbeb89..9bfcccb 100644 --- a/internal/fakecgo/trampolines_stubs.s +++ b/internal/fakecgo/trampolines_stubs.s @@ -4,7 +4,7 @@ // SPDX-FileCopyrightText: 2022 The Ebitengine Authors // SPDX-FileCopyrightText: 2025-2026 Andrey Kolkov and GoGPU Contributors -//go:build !cgo && (darwin || freebsd || linux || netbsd) +//go:build !cgo && (darwin || freebsd || linux || netbsd) && !android #include "textflag.h" diff --git a/internal/fakecgo/trampolines_stubs_android.s b/internal/fakecgo/trampolines_stubs_android.s new file mode 100644 index 0000000..dd1e515 --- /dev/null +++ b/internal/fakecgo/trampolines_stubs_android.s @@ -0,0 +1,86 @@ +// Code generated by 'go generate' with gen.go. DO NOT EDIT. + +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && android && arm64 + +#include "textflag.h" + +// these stubs are here because it is not possible to go:linkname directly the C functions on darwin arm64 + +TEXT _malloc(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_malloc(SB) + RET + +TEXT _free(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_free(SB) + RET + +TEXT _setenv(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_setenv(SB) + RET + +TEXT _unsetenv(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_unsetenv(SB) + RET + +TEXT _sigfillset(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_sigfillset(SB) + RET + +TEXT _nanosleep(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_nanosleep(SB) + RET + +TEXT _abort(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_abort(SB) + RET + +TEXT _sigaltstack(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_sigaltstack(SB) + RET + +TEXT _write(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_write(SB) + RET + +TEXT _pthread_attr_init(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_attr_init(SB) + RET + +TEXT _pthread_create(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_create(SB) + RET + +TEXT _pthread_detach(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_detach(SB) + RET + +TEXT _pthread_sigmask(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_sigmask(SB) + RET + +TEXT _pthread_attr_getstacksize(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_attr_getstacksize(SB) + RET + +TEXT _pthread_attr_destroy(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_attr_destroy(SB) + RET + +TEXT _pthread_mutex_lock(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_mutex_lock(SB) + RET + +TEXT _pthread_mutex_unlock(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_mutex_unlock(SB) + RET + +TEXT _pthread_cond_broadcast(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_cond_broadcast(SB) + RET + +TEXT _pthread_setspecific(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_setspecific(SB) + RET From 53d06d7c96e7624eb6a69bd764e8c5cefca8a4ff Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 15 Jul 2026 21:02:31 +0300 Subject: [PATCH 3/4] test: add Android probes and preview documentation --- .github/workflows/ci.yml | 42 +++++- README.md | 11 +- docs/ANDROID.md | 54 +++++++ ffi/struct_e2e_test.go | 13 +- internal/fakecgo/android_abi_test.go | 56 ++++++++ scripts/check-android-arm64.sh | 203 +++++++++++++++++++++++++++ testdata/android_abi_probe.c | 28 ++++ 7 files changed, 397 insertions(+), 10 deletions(-) create mode 100644 docs/ANDROID.md create mode 100644 internal/fakecgo/android_abi_test.go create mode 100755 scripts/check-android-arm64.sh create mode 100644 testdata/android_abi_probe.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1606c4..1b308c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: fi echo "All files are properly formatted ✓" - # Cross-compilation - Verify all 7 supported platforms compile + # Cross-compilation - Verify all 8 supported desktop platforms compile cross-compile: name: Cross-Compile runs-on: ubuntu-latest @@ -164,6 +164,41 @@ jobs: fi echo "✅ All examples compile successfully" + # Android arm64/API 29+ source, ABI, cgo-mode, and ELF regression gates. + # Keep both supported Go patch lines: runtime/cgo startup and TLS details + # are part of this platform contract, so a single floating toolchain is not + # sufficient evidence. + android-cross: + name: Android arm64 (Go ${{ matrix.go }}) + runs-on: ubuntu-latest + needs: [lint, formatting] + strategy: + fail-fast: false + matrix: + go: ['1.25.12', '1.26.5'] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + cache: true + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Install Android NDK r29 + shell: bash + run: | + yes | sdkmanager --licenses >/dev/null || true + sdkmanager "ndk;29.0.14206865" + echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/29.0.14206865" >> "$GITHUB_ENV" + + - name: Check Android arm64 + run: scripts/check-android-arm64.sh + # Unit tests - Platform-specific (Linux + Windows + macOS AMD64) # Tested under both CGO_ENABLED=0 (fakecgo path) and CGO_ENABLED=1 (real # runtime/cgo path). Both modes must pass identically; CGO_ENABLED=0 is the @@ -363,7 +398,7 @@ jobs: # Final status - All checks passed ci-success: name: CI Success - needs: [lint, formatting, cross-compile, test, benchmarks, quality-gate] + needs: [lint, formatting, cross-compile, android-cross, test, benchmarks, quality-gate] runs-on: ubuntu-latest if: success() steps: @@ -372,7 +407,8 @@ jobs: echo "✅ All CI checks passed!" echo "✅ Lint: PASSED" echo "✅ Formatting: PASSED" - echo "✅ Cross-Compile: PASSED (7 platforms)" + echo "✅ Cross-Compile: PASSED (8 desktop targets)" + echo "✅ Android arm64: PASSED (API 29+, Go 1.25/1.26, cgo=0/1)" echo "✅ Tests: PASSED (CGO_ENABLED=0 and CGO_ENABLED=1)" echo " - Linux AMD64 (ubuntu-latest)" echo " - Windows AMD64 (windows-latest)" diff --git a/README.md b/README.md index 0398abf..c366b9f 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ _, _ = ffi.CallFunction(cif, sym, unsafe.Pointer(&result), args) |---|---------|---------| | **Zero CGO** | Pure Go | No C compiler needed. `go get` and build. | | **Fast** | 88–114 ns/op | Pre-computed CIF, zero per-call allocations | -| **Cross-platform** | 8 targets | Windows, Linux, macOS, FreeBSD × AMD64 + ARM64 | -| **Callbacks** | C→Go safe | `crosscall2` integration, struct args, works from any C thread | +| **Cross-platform** | 8 desktop targets + Android preview | Windows, Linux, macOS, FreeBSD × AMD64 + ARM64; Android arm64/API 29+ candidate pending physical-device startup proof | +| **Callbacks** | C→Go safe where validated | `crosscall2` integration on desktop targets; Android callbacks fail explicitly until a physical-thread proof exists | | **Type-safe** | Runtime validation | 5 typed error types with `errors.As()` support | | **Struct pass/return** | Full ABI | Args: INTEGER/SSE classification. Returns: ≤8B (RAX/XMM0), 9–16B (4 modes: RAX/XMM × RAX/XMM), >16B (sret) | | **Variadic** | `printf`/`sprintf` | `PrepareVariadicCallInterface` — Apple ARM64 stack-force included | @@ -46,6 +46,11 @@ _, _ = ffi.CallFunction(cif, sym, unsafe.Pointer(&result), args) ## Quick Start +Android arm64/API 29+ is a preview candidate in both CGO modes. Cross-build, +ABI, and ELF probes pass, but physical-device startup proof is still pending; +see [docs/ANDROID.md](docs/ANDROID.md) for the runtime ABI, NDK probe, and the +intentional callback limitation. + ### Installation ```bash @@ -276,7 +281,7 @@ if err != nil { | Context support | Timeouts/cancellation | No | No | | C-thread callbacks | crosscall2 | crosscall2 | Full | | String/bool/slice args | Raw pointers only | Auto-marshaling | Full | -| Platform breadth | 8 targets | 8 GOARCH / 20+ OS×ARCH | All | +| Platform breadth | 8 desktop targets + Android preview | 8 GOARCH / 20+ OS×ARCH | All | | AMD64 overhead | 88–114 ns | Not published | ~140 ns (Go 1.26 claims ~30% reduction) | **Choose goffi** for GPU/real-time workloads: struct passing, zero per-call overhead, callback float returns, typed errors. diff --git a/docs/ANDROID.md b/docs/ANDROID.md new file mode 100644 index 0000000..554468b --- /dev/null +++ b/docs/ANDROID.md @@ -0,0 +1,54 @@ +# Android arm64 preview candidate + +goffi carries an Android arm64/API 29+ preview candidate. Cross-build, ABI, +and ELF probes pass, but physical-device startup validation is still required +before this can be described as released Android support. + +The implementation follows the pinned Go runtime's Android AAPCS64 startup +contract. The four-argument `_cgo_init` entry point and TLS setup are +irreducible: the runtime passes `(g, setg_gcc, &runtime.tls_g, TLS base)` before +ordinary `runtime.cgocall` is available, then reads the Go `g` pointer from +Bionic's `TLS_SLOT_APP` (slot 2). The fakecgo trampoline must preserve all four +registers, validate API/TLS first, and use Bionic's LP64 pthread and signal +layouts; a generic Linux startup path would corrupt runtime state before Go +could report an error. + +Both build modes are candidate build surfaces: + +```sh +# No C compiler is needed for this mode. +GOOS=android GOARCH=arm64 CGO_ENABLED=0 go build ./... + +# For applications that already use cgo, point CC at the API-29 NDK driver. +CC="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang" \ +GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./... +``` + +The cgo=0 path uses direct Bionic `libc.so`/`libdl.so` dynamic imports and the +fakecgo startup path. The cgo=1 path uses small NDK C wrappers so external +linking never emits an AAPCS64 branch relocation to a `cgo_import_dynamic` +symbol. Both paths reject glibc sonames and `__errno_location`. + +Android callback trampolines are deliberately unavailable. `ffi.NewCallback` +panics with a stable message instead of exposing a pointer whose foreign-thread +startup path has not been validated on a physical device. Vulkan/WebGPU users +should use polling or an application-owned native callback bridge until that +evidence exists. + +Dynamic-library handles are retained for process lifetime. `RTLD_NOW | RTLD_LOCAL +| RTLD_NODELETE` makes that policy explicit, and `FreeLibrary` is safe to call +but does not unload code that may still have function pointers in use. + +## Regression probe + +The NDK header, Go layout, cgo=0/cgo=1 cross-build, and ELF dependency checks +are reproducible without a device: + +```sh +ANDROID_NDK_HOME=/path/to/android-ndk-r29 scripts/check-android-arm64.sh +``` + +The audited source/ABI matrix is Go 1.25.12 and Go 1.26.5 with Android NDK +r29 (`29.0.14206865`). Keep both Go lines in CI when runtime startup files or +TLS offsets change upstream. Passing this probe is not physical-device +startup evidence. diff --git a/ffi/struct_e2e_test.go b/ffi/struct_e2e_test.go index a80d240..3e98234 100644 --- a/ffi/struct_e2e_test.go +++ b/ffi/struct_e2e_test.go @@ -19,10 +19,15 @@ import ( var structTestLib unsafe.Pointer func TestMain(m *testing.M) { - if err := buildStructTestLib(); err != nil { - // If gcc not available, skip struct e2e tests gracefully. - // Other tests still run. - structTestLib = nil + // Android test binaries run on-device, where invoking a host compiler is + // neither meaningful nor available. Keep the pure validation tests active + // and let only the host-built shared-library cases skip via requireStructLib. + if runtime.GOOS != "android" { + if err := buildStructTestLib(); err != nil { + // If gcc is not available, skip struct e2e tests gracefully. + // Other tests still run. + structTestLib = nil + } } code := m.Run() if structTestLib != nil { diff --git a/internal/fakecgo/android_abi_test.go b/internal/fakecgo/android_abi_test.go new file mode 100644 index 0000000..7d42f42 --- /dev/null +++ b/internal/fakecgo/android_abi_test.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build android && arm64 && !cgo + +package fakecgo + +import ( + "testing" + "unsafe" +) + +// These compile-time array pairs turn an ABI drift into a build failure. The +// runtime assertions below keep the expected layout visible in test output +// when a physical Android runner is available. +var ( + _ [56 - unsafe.Sizeof(pthread_attr_t{})]byte + _ [unsafe.Sizeof(pthread_attr_t{}) - 56]byte + _ [48 - unsafe.Sizeof(pthread_cond_t{})]byte + _ [unsafe.Sizeof(pthread_cond_t{}) - 48]byte + _ [40 - unsafe.Sizeof(pthread_mutex_t{})]byte + _ [unsafe.Sizeof(pthread_mutex_t{}) - 40]byte + _ [8 - unsafe.Sizeof(sigset_t(0))]byte + _ [unsafe.Sizeof(sigset_t(0)) - 8]byte + _ [24 - unsafe.Sizeof(stack_t{})]byte + _ [unsafe.Sizeof(stack_t{}) - 24]byte +) + +func TestAndroidBionicABI(t *testing.T) { + tests := []struct { + name string + got uintptr + want uintptr + }{ + {"pthread_attr_t size", unsafe.Sizeof(pthread_attr_t{}), 56}, + {"pthread_attr_t align", unsafe.Alignof(pthread_attr_t{}), 8}, + {"pthread_cond_t size", unsafe.Sizeof(pthread_cond_t{}), 48}, + {"pthread_cond_t align", unsafe.Alignof(pthread_cond_t{}), 4}, + {"pthread_mutex_t size", unsafe.Sizeof(pthread_mutex_t{}), 40}, + {"pthread_mutex_t align", unsafe.Alignof(pthread_mutex_t{}), 4}, + {"sigset_t size", unsafe.Sizeof(sigset_t(0)), 8}, + {"sigset_t align", unsafe.Alignof(sigset_t(0)), 8}, + {"stack_t size", unsafe.Sizeof(stack_t{}), 24}, + {"stack_t ss_size offset", unsafe.Offsetof(stack_t{}.ss_size), 16}, + {"pthread_t size", unsafe.Sizeof(pthread_t(0)), 8}, + {"pthread_key_t size", unsafe.Sizeof(pthread_key_t(0)), 4}, + {"tls_g offset", androidTLSGOffset, 16}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.got != tt.want { + t.Fatalf("got %d, want %d", tt.got, tt.want) + } + }) + } +} diff --git a/scripts/check-android-arm64.sh b/scripts/check-android-arm64.sh new file mode 100755 index 0000000..1b2138f --- /dev/null +++ b/scripts/check-android-arm64.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Cross-build and inspect Android arm64/API 29+ artifacts. The script is +# intentionally device-free: compile-time Go/C ABI checks and ELF dependency +# checks catch the portability regressions that a host runner can observe. + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +NDK=${ANDROID_NDK_HOME:-} +if [[ -z "$NDK" ]]; then + cat >&2 <<'EOF' +ANDROID_NDK_HOME must point at an Android NDK r29 installation. +EOF + exit 2 +fi + +case "$(uname -s)-$(uname -m)" in + Darwin-arm64|Darwin-x86_64) host_tag=darwin-x86_64 ;; + Linux-x86_64) host_tag=linux-x86_64 ;; + *) echo "unsupported NDK host: $(uname -s)-$(uname -m)" >&2; exit 2 ;; +esac + +toolchain="$NDK/toolchains/llvm/prebuilt/$host_tag/bin" +cc="$toolchain/aarch64-linux-android29-clang" +readelf="$toolchain/llvm-readelf" +[[ -x "$cc" ]] || { echo "missing Android compiler: $cc" >&2; exit 2; } +[[ -x "$readelf" ]] || { echo "missing llvm-readelf: $readelf" >&2; exit 2; } + +# This port deliberately depends on the Android arm64 startup ABI in the Go +# runtime. Fail closed when the selected toolchain is outside the audited +# lines, or when any audited source invariant changes. +go_version=$(go env GOVERSION) +case "$go_version" in + go1.25.12|go1.26.5) ;; + *) + echo "unsupported Go runtime source for Android fakecgo: $go_version" >&2 + echo "audit the new runtime/cgo Android arm64 startup ABI before extending this gate" >&2 + exit 2 + ;; +esac + +goroot=$(go env GOROOT) +runtime_asm="$goroot/src/runtime/asm_arm64.s" +runtime_tls="$goroot/src/runtime/tls_arm64.s" +runtime_android_cgo="$goroot/src/runtime/cgo/gcc_android.c" + +require_source_pattern() { + local file=$1 + local pattern=$2 + local description=$3 + grep -Eq "$pattern" "$file" || { + echo "Go $go_version runtime drift: missing $description in $file" >&2 + exit 1 + } +} + +android_callsite=$(awk ' + /^#ifdef GOOS_android$/ { capture = 1 } + capture { print } + capture && /BL[[:space:]]+\(R12\)/ { exit } +' "$runtime_asm") +for pattern in \ + 'MRS_TPIDR_R0' \ + 'MOVD[[:space:]]+R0,[[:space:]]*R3' \ + 'MOVD[[:space:]]+\$runtime·tls_g\(SB\),[[:space:]]*R2' \ + 'MOVD[[:space:]]+\$setg_gcc<>\(SB\),[[:space:]]*R1' \ + 'MOVD[[:space:]]+g,[[:space:]]*R0' \ + 'BL[[:space:]]+\(R12\)' +do + grep -Eq "$pattern" <<<"$android_callsite" || { + echo "Go $go_version runtime drift in Android _cgo_init callsite: $pattern" >&2 + exit 1 + } +done +require_source_pattern "$runtime_tls" 'DATA runtime·tls_g\+0\(SB\)/8,[[:space:]]*\$16' 'API-29 TLS slot-2 offset' +require_source_pattern "$runtime_android_cgo" '^#define TLS_SLOT_APP 2$' 'Bionic TLS_SLOT_APP contract' +require_source_pattern "$runtime_android_cgo" 'dlsym\(handle, "android_get_device_api_level"\)' 'Android API-level startup probe' + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +echo "Checking generated Android fakecgo sources" +generator_dir="$tmp/fakecgo-generator" +mkdir -p "$generator_dir" +cp "$ROOT/internal/fakecgo/gen.go" "$generator_dir/gen.go" +( + cd "$generator_dir" + go run gen.go +) +for generated in \ + symbols.go \ + symbols_android.go \ + symbols_android_imports.go \ + symbols_darwin.go \ + symbols_freebsd.go \ + symbols_linux.go \ + symbols_netbsd.go \ + trampolines_stubs.s \ + trampolines_stubs_android.s +do + cmp "$ROOT/internal/fakecgo/$generated" "$generator_dir/$generated" || { + echo "generated fakecgo source is stale: $generated" >&2 + exit 1 + } +done + +echo "Checking NDK C ABI headers" +"$cc" -std=c11 -Wall -Werror -fsyntax-only "$ROOT/testdata/android_abi_probe.c" + +for cgo in 0 1; do + echo "Building Android arm64 (CGO_ENABLED=$cgo)" + if [[ "$cgo" == 1 ]]; then + CC="$cc" GOOS=android GOARCH=arm64 CGO_ENABLED=1 \ + go test -exec=true ./... 2>&1 + CC="$cc" GOOS=android GOARCH=arm64 CGO_ENABLED=1 \ + go vet ./... + CC="$cc" GOOS=android GOARCH=arm64 CGO_ENABLED=1 \ + go test -c -o "$tmp/ffi-cgo.test" ./ffi + else + GOOS=android GOARCH=arm64 CGO_ENABLED=0 \ + go test -exec=true ./... 2>&1 + GOOS=android GOARCH=arm64 CGO_ENABLED=0 \ + go vet ./... + GOOS=android GOARCH=arm64 CGO_ENABLED=0 \ + go test -c -o "$tmp/ffi-nocgo.test" ./ffi + fi + + artifact="$tmp/ffi-$([[ "$cgo" == 1 ]] && echo cgo || echo nocgo).test" + file_header=$("$readelf" -h "$artifact") + grep -q 'Machine:.*AArch64' <<<"$file_header" || { + echo "unexpected machine in $artifact" >&2 + exit 1 + } + dynamic=$("$readelf" -d "$artifact") + if grep -Eiq 'GLIBC|libpthread|\.so\.2|libc\.so\.6|libdl\.so\.2|__errno_location' <<<"$dynamic"; then + echo "forbidden non-Bionic dependency in $artifact" >&2 + grep -Ei 'GLIBC|libpthread|\.so\.2|libc\.so\.6|libdl\.so\.2|__errno_location' <<<"$dynamic" >&2 + exit 1 + fi + dynsyms=$("$readelf" --dyn-syms "$artifact") + if grep -Eiq '__errno_location|callback|trampoline' <<<"$dynsyms"; then + echo "forbidden callback/glibc symbol in $artifact" >&2 + grep -Ein '__errno_location|callback|trampoline' <<<"$dynsyms" >&2 + exit 1 + fi + grep -q 'Shared library: \[libc\.so\]' <<<"$dynamic" || { + echo "missing Bionic libc dependency in $artifact" >&2 + exit 1 + } + if [[ "$cgo" == 0 ]]; then + # The fakecgo startup trampoline must preserve the four AAPCS64 input + # registers and tail-call the Go implementation indirectly. The Go + # implementation must enter the API/TLS guard before any ordinary cgo + # path; fakecgo x_cgo_init must not call runtime.cgocall itself. + trampoline=$(go tool objdump -s '^x_cgo_init_trampoline$' "$artifact") + grep -q 'CALL (R9)' <<<"$trampoline" || { + echo "x_cgo_init trampoline is not an indirect AAPCS64 call" >&2 + exit 1 + } + if awk ' + /MOVD/ { + line = $0 + gsub(/[^[:alnum:]]/, " ", line) + count = split(line, fields) + for (i = 1; i <= count; i++) { + if (fields[i] ~ /^R[0-3]$/) found = 1 + } + } + END { exit found ? 0 : 1 } + ' <<<"$trampoline"; then + echo "x_cgo_init trampoline clobbers an AAPCS64 input register" >&2 + exit 1 + fi + startup=$(go tool objdump -s '^github.com/go-webgpu/goffi/internal/fakecgo.x_cgo_init$' "$artifact") + grep -q 'CALL github.com/go-webgpu/goffi/internal/fakecgo.x_cgo_inittls' <<<"$startup" || { + echo "x_cgo_init does not enter the Android TLS guard" >&2 + exit 1 + } + guard_line=$(grep -n 'CALL github.com/go-webgpu/goffi/internal/fakecgo.x_cgo_inittls' <<<"$startup" | head -n1 | cut -d: -f1) + call5_line=$(grep -n 'CALL github.com/go-webgpu/goffi/internal/fakecgo.call5.abi0' <<<"$startup" | head -n1 | cut -d: -f1 || true) + if [[ -n "$call5_line" && "$guard_line" -ge "$call5_line" ]]; then + echo "x_cgo_init reaches a Bionic call before the Android TLS guard" >&2 + exit 1 + fi + if grep -q 'runtime.cgocall' <<<"$startup"; then + echo "x_cgo_init reached runtime.cgocall before startup guard" >&2 + exit 1 + fi + + # Loader failures must capture and duplicate dlerror before the one + # runtime.cgocall returns; dlerror state is local to that OS thread. + open_wrapper=$(go tool objdump -s '^androidDlopenWrapper$' "$artifact") + sym_wrapper=$(go tool objdump -s '^androidDlsymWrapper$' "$artifact") + open_calls=$(grep -c 'CALL (R10)' <<<"$open_wrapper") + sym_calls=$(grep -c 'CALL (R10)' <<<"$sym_wrapper") + if [[ "$open_calls" -ne 3 || "$sym_calls" -ne 4 ]]; then + echo "Android loader wrapper no longer captures dlerror in-call" >&2 + exit 1 + fi + fi + done + +echo "Android arm64/API 29+ checks passed" diff --git a/testdata/android_abi_probe.c b/testdata/android_abi_probe.c new file mode 100644 index 0000000..f073813 --- /dev/null +++ b/testdata/android_abi_probe.c @@ -0,0 +1,28 @@ +/* + * Compile-only ABI probe for Android arm64/API 29+. + * Keep this independent from Go so NDK header drift fails before a device run. + */ +#include +#include +#include +#include + +_Static_assert(sizeof(sigset_t) == 8, "Bionic LP64 sigset_t must be one word"); +_Static_assert(_Alignof(sigset_t) == 8, "Bionic LP64 sigset_t alignment changed"); +_Static_assert(sizeof(pthread_attr_t) == 56, "Bionic LP64 pthread_attr_t changed"); +_Static_assert(_Alignof(pthread_attr_t) == 8, "Bionic LP64 pthread_attr_t alignment changed"); +_Static_assert(sizeof(pthread_cond_t) == 48, "Bionic LP64 pthread_cond_t changed"); +_Static_assert(_Alignof(pthread_cond_t) == 4, "Bionic LP64 pthread_cond_t alignment changed"); +_Static_assert(sizeof(pthread_mutex_t) == 40, "Bionic LP64 pthread_mutex_t changed"); +_Static_assert(_Alignof(pthread_mutex_t) == 4, "Bionic LP64 pthread_mutex_t alignment changed"); +_Static_assert(sizeof(pthread_t) == 8, "Bionic LP64 pthread_t changed"); +_Static_assert(sizeof(pthread_key_t) == 4, "Bionic pthread_key_t changed"); +_Static_assert(sizeof(stack_t) == 24, "Bionic LP64 stack_t changed"); +_Static_assert(offsetof(stack_t, ss_size) == 16, "Bionic stack_t layout changed"); +_Static_assert(RTLD_NOW == 2, "Bionic RTLD_NOW changed"); +_Static_assert(RTLD_LOCAL == 0, "Bionic RTLD_LOCAL changed"); +_Static_assert(RTLD_NODELETE == 0x1000, "Bionic RTLD_NODELETE changed"); + +int main(void) { + return pthread_detach((pthread_t)0) == 0; +} From a8a33c781c0f0ab4de96b2f81834f9913e2d2a11 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 21 Jul 2026 09:11:25 +0300 Subject: [PATCH 4/4] fix: address Android runtime review feedback Document and assert the runtime.iscgo wiring selected by Android's implicit linux build tag, keep RTLD policy internal, split TLS startup diagnostics, and align all Android fakecgo imports with the goffi_ prefix introduced by #63. --- docs/ANDROID.md | 8 ++++ ffi/dl_android.go | 14 ++----- internal/fakecgo/android_abi_test.go | 29 +++++++++++++++ internal/fakecgo/android_dl.go | 4 +- internal/fakecgo/android_dl_stubs_arm64.s | 4 +- internal/fakecgo/doc.go | 7 ++-- internal/fakecgo/go_android_arm64.go | 7 +++- internal/fakecgo/iscgo.go | 4 ++ internal/fakecgo/symbols_android.go | 1 + internal/fakecgo/symbols_android_imports.go | 39 ++++++++++---------- internal/fakecgo/trampolines_stubs_android.s | 39 ++++++++++---------- scripts/check-android-arm64.sh | 16 ++++++++ 12 files changed, 115 insertions(+), 57 deletions(-) diff --git a/docs/ANDROID.md b/docs/ANDROID.md index 554468b..530cc08 100644 --- a/docs/ANDROID.md +++ b/docs/ANDROID.md @@ -29,6 +29,14 @@ fakecgo startup path. The cgo=1 path uses small NDK C wrappers so external linking never emits an AAPCS64 branch relocation to a `cgo_import_dynamic` symbol. Both paths reject glibc sonames and `__errno_location`. +`runtime.iscgo` is intentionally true in the cgo=0 path. Android builds also +satisfy Go's `linux` build term, so the shared `iscgo.go`, `callbacks.go`, and +`setenv.go` wiring is selected. `runtime.cgocall` rejects ordinary Unix targets +when `iscgo` is false; when it is true, the runtime also selects its cgo-aware +thread, TLS, signal, traceback, and extra-M paths. Android fakecgo supplies the +init, thread-start, environment, pthread-key, and bind hooks those paths expect. +This runtime wiring is separate from goffi's public callback policy. + Android callback trampolines are deliberately unavailable. `ffi.NewCallback` panics with a stable message instead of exposing a pointer whose foreign-thread startup path has not been validated on a physical device. Vulkan/WebGPU users diff --git a/ffi/dl_android.go b/ffi/dl_android.go index c227a54..c957e24 100644 --- a/ffi/dl_android.go +++ b/ffi/dl_android.go @@ -12,18 +12,12 @@ import ( "github.com/go-webgpu/goffi/internal/dl" ) -// Bionic keeps the object mapped after dlclose when RTLD_NODELETE is set. -// This matches goffi's process-lifetime function-pointer policy. -const ( - RTLD_NOW = dl.RTLD_NOW - RTLD_LOCAL = dl.RTLD_LOCAL - RTLD_NODELETE = dl.RTLD_NODELETE -) - // LoadLibrary loads a public Android shared library with eager, private -// symbol resolution. Android support is arm64/API 29+ only. +// symbol resolution. Bionic keeps the object mapped after dlclose because +// goffi's function pointers have process lifetime. Android support is +// arm64/API 29+ only. func LoadLibrary(name string) (unsafe.Pointer, error) { - handle, err := dl.Dlopen(name, RTLD_NOW|RTLD_LOCAL|RTLD_NODELETE) + handle, err := dl.Dlopen(name, dl.RTLD_NOW|dl.RTLD_LOCAL|dl.RTLD_NODELETE) if err != nil { return nil, &LibraryError{Operation: "load", Name: name, Err: err} } diff --git a/internal/fakecgo/android_abi_test.go b/internal/fakecgo/android_abi_test.go index 7d42f42..08b4613 100644 --- a/internal/fakecgo/android_abi_test.go +++ b/internal/fakecgo/android_abi_test.go @@ -54,3 +54,32 @@ func TestAndroidBionicABI(t *testing.T) { }) } } + +func TestAndroidFakeCGORuntimeWiring(t *testing.T) { + // GOOS=android also satisfies linux build constraints. These references + // deliberately prove that iscgo.go, callbacks.go, and setenv.go are part + // of the Android build even though ffi callbacks remain unsupported. + if !_iscgo { + t.Fatal("runtime.iscgo must be true for outbound runtime.cgocall") + } + + requiredHooks := []struct { + name string + ptr *byte + }{ + {"_cgo_init", _cgo_init}, + {"_cgo_thread_start", _cgo_thread_start}, + {"_cgo_notify_runtime_init_done", _cgo_notify_runtime_init_done}, + {"_cgo_bindm", _cgo_bindm}, + {"_cgo_setenv", _cgo_setenv}, + {"_cgo_unsetenv", _cgo_unsetenv}, + } + for _, hook := range requiredHooks { + if hook.ptr == nil { + t.Errorf("%s runtime hook is nil", hook.name) + } + } + if _cgo_pthread_key_created == nil { + t.Error("_cgo_pthread_key_created runtime hook is nil") + } +} diff --git a/internal/fakecgo/android_dl.go b/internal/fakecgo/android_dl.go index 99b4169..40fe8e1 100644 --- a/internal/fakecgo/android_dl.go +++ b/internal/fakecgo/android_dl.go @@ -11,8 +11,8 @@ import "unsafe" // the API-level marker with direct AAPCS64 calls instead of internal/dl so the // pre-Q guard cannot recurse through an uninitialized cgocall path. // -//go:cgo_import_dynamic purego_android_dlopen dlopen "libdl.so" -//go:cgo_import_dynamic purego_android_dlsym dlsym "libdl.so" +//go:cgo_import_dynamic goffi_android_dlopen dlopen "libdl.so" +//go:cgo_import_dynamic goffi_android_dlsym dlsym "libdl.so" //go:cgo_import_dynamic _ _ "libdl.so" // The assembly stubs are kept separate from the generated libc wrappers: the diff --git a/internal/fakecgo/android_dl_stubs_arm64.s b/internal/fakecgo/android_dl_stubs_arm64.s index 2573877..6e3f292 100644 --- a/internal/fakecgo/android_dl_stubs_arm64.s +++ b/internal/fakecgo/android_dl_stubs_arm64.s @@ -6,9 +6,9 @@ #include "textflag.h" TEXT _android_dlopen(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_android_dlopen(SB) + JMP goffi_android_dlopen(SB) RET TEXT _android_dlsym(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_android_dlsym(SB) + JMP goffi_android_dlsym(SB) RET diff --git a/internal/fakecgo/doc.go b/internal/fakecgo/doc.go index 968df4f..b7e3e14 100644 --- a/internal/fakecgo/doc.go +++ b/internal/fakecgo/doc.go @@ -16,9 +16,10 @@ // // # Support // -// Currently, fakecgo supports Linux, macOS, FreeBSD, and NetBSD on amd64 & arm64. -// It cannot be used with -buildmode=c-archive because that requires special -// initialization that fakecgo does not implement at the moment. +// Currently, fakecgo supports Linux, macOS, FreeBSD, and NetBSD on amd64 & arm64, +// plus Android arm64/API 29+ as a guarded preview. It cannot be used with +// -buildmode=c-archive because that requires special initialization that fakecgo +// does not implement at the moment. // // # Usage // diff --git a/internal/fakecgo/go_android_arm64.go b/internal/fakecgo/go_android_arm64.go index e31285c..7c92736 100644 --- a/internal/fakecgo/go_android_arm64.go +++ b/internal/fakecgo/go_android_arm64.go @@ -94,8 +94,11 @@ func x_cgo_inittls(tlsg *uintptr, tlsbase unsafe.Pointer) { if !androidAPI29() { androidFatal("fakecgo: Android API 29 or newer is required") } - if tlsg == nil || *tlsg != androidTLSGOffset { - androidFatal("fakecgo: Android runtime.tls_g offset mismatch") + if tlsg == nil { + androidFatal("fakecgo: Android runtime did not provide runtime.tls_g") + } + if *tlsg != androidTLSGOffset { + androidFatal("fakecgo: Android runtime.tls_g offset mismatch (want 16)") } } diff --git a/internal/fakecgo/iscgo.go b/internal/fakecgo/iscgo.go index 12e5214..32e4372 100644 --- a/internal/fakecgo/iscgo.go +++ b/internal/fakecgo/iscgo.go @@ -10,6 +10,10 @@ // but those depend on dynamic linker magic to get initialized // correctly, and sometimes they break. This variable is a // backup: it depends only on old C style static linking rules. +// +// Android builds also satisfy the linux build term, so this declaration is +// intentionally selected for Android/arm64. Outbound runtime.cgocall requires +// iscgo even though goffi rejects Android C-to-Go callbacks at its public API. package fakecgo diff --git a/internal/fakecgo/symbols_android.go b/internal/fakecgo/symbols_android.go index 6f74e0d..0899800 100644 --- a/internal/fakecgo/symbols_android.go +++ b/internal/fakecgo/symbols_android.go @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors +// SPDX-FileCopyrightText: 2025-2026 Andrey Kolkov and GoGPU Contributors //go:build !cgo && android && arm64 diff --git a/internal/fakecgo/symbols_android_imports.go b/internal/fakecgo/symbols_android_imports.go index 5840053..8e298dc 100644 --- a/internal/fakecgo/symbols_android_imports.go +++ b/internal/fakecgo/symbols_android_imports.go @@ -2,27 +2,28 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors +// SPDX-FileCopyrightText: 2025-2026 Andrey Kolkov and GoGPU Contributors //go:build !cgo && android && arm64 package fakecgo -//go:cgo_import_dynamic purego_malloc malloc "libc.so" -//go:cgo_import_dynamic purego_free free "libc.so" -//go:cgo_import_dynamic purego_setenv setenv "libc.so" -//go:cgo_import_dynamic purego_unsetenv unsetenv "libc.so" -//go:cgo_import_dynamic purego_sigfillset sigfillset "libc.so" -//go:cgo_import_dynamic purego_nanosleep nanosleep "libc.so" -//go:cgo_import_dynamic purego_abort abort "libc.so" -//go:cgo_import_dynamic purego_sigaltstack sigaltstack "libc.so" -//go:cgo_import_dynamic purego_write write "libc.so" -//go:cgo_import_dynamic purego_pthread_attr_init pthread_attr_init "libc.so" -//go:cgo_import_dynamic purego_pthread_create pthread_create "libc.so" -//go:cgo_import_dynamic purego_pthread_detach pthread_detach "libc.so" -//go:cgo_import_dynamic purego_pthread_sigmask pthread_sigmask "libc.so" -//go:cgo_import_dynamic purego_pthread_attr_getstacksize pthread_attr_getstacksize "libc.so" -//go:cgo_import_dynamic purego_pthread_attr_destroy pthread_attr_destroy "libc.so" -//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "libc.so" -//go:cgo_import_dynamic purego_pthread_mutex_unlock pthread_mutex_unlock "libc.so" -//go:cgo_import_dynamic purego_pthread_cond_broadcast pthread_cond_broadcast "libc.so" -//go:cgo_import_dynamic purego_pthread_setspecific pthread_setspecific "libc.so" +//go:cgo_import_dynamic goffi_malloc malloc "libc.so" +//go:cgo_import_dynamic goffi_free free "libc.so" +//go:cgo_import_dynamic goffi_setenv setenv "libc.so" +//go:cgo_import_dynamic goffi_unsetenv unsetenv "libc.so" +//go:cgo_import_dynamic goffi_sigfillset sigfillset "libc.so" +//go:cgo_import_dynamic goffi_nanosleep nanosleep "libc.so" +//go:cgo_import_dynamic goffi_abort abort "libc.so" +//go:cgo_import_dynamic goffi_sigaltstack sigaltstack "libc.so" +//go:cgo_import_dynamic goffi_write write "libc.so" +//go:cgo_import_dynamic goffi_pthread_attr_init pthread_attr_init "libc.so" +//go:cgo_import_dynamic goffi_pthread_create pthread_create "libc.so" +//go:cgo_import_dynamic goffi_pthread_detach pthread_detach "libc.so" +//go:cgo_import_dynamic goffi_pthread_sigmask pthread_sigmask "libc.so" +//go:cgo_import_dynamic goffi_pthread_attr_getstacksize pthread_attr_getstacksize "libc.so" +//go:cgo_import_dynamic goffi_pthread_attr_destroy pthread_attr_destroy "libc.so" +//go:cgo_import_dynamic goffi_pthread_mutex_lock pthread_mutex_lock "libc.so" +//go:cgo_import_dynamic goffi_pthread_mutex_unlock pthread_mutex_unlock "libc.so" +//go:cgo_import_dynamic goffi_pthread_cond_broadcast pthread_cond_broadcast "libc.so" +//go:cgo_import_dynamic goffi_pthread_setspecific pthread_setspecific "libc.so" diff --git a/internal/fakecgo/trampolines_stubs_android.s b/internal/fakecgo/trampolines_stubs_android.s index dd1e515..7a50243 100644 --- a/internal/fakecgo/trampolines_stubs_android.s +++ b/internal/fakecgo/trampolines_stubs_android.s @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2022 The Ebitengine Authors +// SPDX-FileCopyrightText: 2025-2026 Andrey Kolkov and GoGPU Contributors //go:build !cgo && android && arm64 @@ -10,77 +11,77 @@ // these stubs are here because it is not possible to go:linkname directly the C functions on darwin arm64 TEXT _malloc(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_malloc(SB) + JMP goffi_malloc(SB) RET TEXT _free(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_free(SB) + JMP goffi_free(SB) RET TEXT _setenv(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_setenv(SB) + JMP goffi_setenv(SB) RET TEXT _unsetenv(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_unsetenv(SB) + JMP goffi_unsetenv(SB) RET TEXT _sigfillset(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_sigfillset(SB) + JMP goffi_sigfillset(SB) RET TEXT _nanosleep(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_nanosleep(SB) + JMP goffi_nanosleep(SB) RET TEXT _abort(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_abort(SB) + JMP goffi_abort(SB) RET TEXT _sigaltstack(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_sigaltstack(SB) + JMP goffi_sigaltstack(SB) RET TEXT _write(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_write(SB) + JMP goffi_write(SB) RET TEXT _pthread_attr_init(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_pthread_attr_init(SB) + JMP goffi_pthread_attr_init(SB) RET TEXT _pthread_create(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_pthread_create(SB) + JMP goffi_pthread_create(SB) RET TEXT _pthread_detach(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_pthread_detach(SB) + JMP goffi_pthread_detach(SB) RET TEXT _pthread_sigmask(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_pthread_sigmask(SB) + JMP goffi_pthread_sigmask(SB) RET TEXT _pthread_attr_getstacksize(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_pthread_attr_getstacksize(SB) + JMP goffi_pthread_attr_getstacksize(SB) RET TEXT _pthread_attr_destroy(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_pthread_attr_destroy(SB) + JMP goffi_pthread_attr_destroy(SB) RET TEXT _pthread_mutex_lock(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_pthread_mutex_lock(SB) + JMP goffi_pthread_mutex_lock(SB) RET TEXT _pthread_mutex_unlock(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_pthread_mutex_unlock(SB) + JMP goffi_pthread_mutex_unlock(SB) RET TEXT _pthread_cond_broadcast(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_pthread_cond_broadcast(SB) + JMP goffi_pthread_cond_broadcast(SB) RET TEXT _pthread_setspecific(SB), NOSPLIT|NOFRAME, $0-0 - JMP purego_pthread_setspecific(SB) + JMP goffi_pthread_setspecific(SB) RET diff --git a/scripts/check-android-arm64.sh b/scripts/check-android-arm64.sh index 1b2138f..df0e392 100755 --- a/scripts/check-android-arm64.sh +++ b/scripts/check-android-arm64.sh @@ -76,6 +76,22 @@ require_source_pattern "$runtime_tls" 'DATA runtime·tls_g\+0\(SB\)/8,[[:space:] require_source_pattern "$runtime_android_cgo" '^#define TLS_SLOT_APP 2$' 'Bionic TLS_SLOT_APP contract' require_source_pattern "$runtime_android_cgo" 'dlsym\(handle, "android_get_device_api_level"\)' 'Android API-level startup probe' +# GOOS=android also satisfies linux build constraints. The shared fakecgo +# wiring must therefore remain selected: iscgo=true admits outbound cgocall, +# while the hooks satisfy the runtime paths enabled by that state. Public +# Android callbacks are rejected separately in ffi. +echo "Checking Android fakecgo runtime wiring" +android_fakecgo_files=$( + GOOS=android GOARCH=arm64 CGO_ENABLED=0 \ + go list -f '{{range .GoFiles}}{{println .}}{{end}}' ./internal/fakecgo +) +for required in callbacks.go iscgo.go setenv.go; do + grep -Fxq "$required" <<<"$android_fakecgo_files" || { + echo "Android fakecgo runtime wiring omitted $required" >&2 + exit 1 + } +done + tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT