Android arm64 preview candidate

July 21, 2026 ยท View on GitHub

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:

# 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.

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 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:

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.