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.