diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 954202a..789ab12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,6 +164,13 @@ jobs: fi echo "✅ All examples compile successfully" + # Targets whose NewCallback uses goffi's own assembly trampolines must + # link ffi.callbackTrampoline as code. A .s file with a narrower build + # constraint than its .go file still compiles and links, so only the + # symbol type of a linked binary shows the breakage. + - name: Check callback trampolines link as code + run: scripts/check-callback-trampolines.sh + # 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 @@ -186,8 +193,15 @@ jobs: go-version: ${{ matrix.go }} cache: true + # setup-android installs `tools platform-tools` by default. Google's SDK + # repository no longer lists the obsolete `tools` package, so + # `sdkmanager tools` exits 1 and fails the step + # (android-actions/setup-android#537). Nothing here uses `tools`; request + # only platform-tools. The NDK is installed explicitly below. - name: Set up Android SDK uses: android-actions/setup-android@v3 + with: + packages: platform-tools - name: Install Android NDK r29 shell: bash diff --git a/CHANGELOG.md b/CHANGELOG.md index 7618146..e2a552b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- **FreeBSD amd64: callbacks pointed into a data page** — `ffi/callback_amd64.s` carried `(linux || darwin) && amd64` while `ffi/callback.go` carried `(linux || darwin || freebsd) && amd64`, so on freebsd/amd64 the assembly trampoline table was never built and the `//go:linkname` placeholder variable became `ffi.callbackTrampoline`. The build succeeded, but `NewCallback` returned addresses in the data segment (`go tool nm`: `D`, not `T`). The assembly now builds on FreeBSD too. +- CI: `scripts/check-callback-trampolines.sh` links a consumer for every asm-trampoline target (linux, darwin, freebsd × amd64, arm64) and fails unless `ffi.callbackTrampoline` is a text symbol. + ## [0.6.4] - 2026-09-10 ### Added diff --git a/ffi/callback_amd64.s b/ffi/callback_amd64.s index 0c381d7..1a268bd 100644 --- a/ffi/callback_amd64.s +++ b/ffi/callback_amd64.s @@ -1,4 +1,4 @@ -//go:build (linux || darwin) && amd64 +//go:build (linux || darwin || freebsd) && amd64 #include "textflag.h" #include "go_asm.h" diff --git a/scripts/check-callback-trampolines.sh b/scripts/check-callback-trampolines.sh new file mode 100755 index 0000000..f5bc63d --- /dev/null +++ b/scripts/check-callback-trampolines.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# check-callback-trampolines.sh — assert that every target whose NewCallback is +# built from goffi's own assembly links ffi.callbackTrampoline as code. +# +# ffi/callback.go and ffi/callback_arm64.go take the address of the trampoline +# table through a //go:linkname *variable*. If the matching .s file is excluded +# by a narrower build constraint than the .go file, that variable itself +# becomes the definition of ffi.callbackTrampoline: the package still compiles +# and links, `go tool nm` reports D (data) instead of T (text), and +# NewCallback hands C code a pointer into a data page. freebsd/amd64 was built +# that way while callback_amd64.s lacked `freebsd` in its constraint, and no +# compile step could notice. +# +# The check links a real consumer binary per target, because building ./... +# links nothing, and inspects the symbol type. +# +# Usage: scripts/check-callback-trampolines.sh + +set -euo pipefail + +MODULE="github.com/go-webgpu/goffi" +SYMBOL="${MODULE}/ffi.callbackTrampoline" +FAKECGO_STD="-gcflags=${MODULE}/internal/fakecgo=-std" + +# goos/goarch|extra go build flags +TARGETS=( + "linux/amd64|" + "linux/arm64|" + "darwin/amd64|" + "darwin/arm64|" + "freebsd/amd64|${FAKECGO_STD}" + "freebsd/arm64|${FAKECGO_STD}" +) + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +GO_VERSION="$(cd "${ROOT}" && go list -m -f '{{.GoVersion}}')" + +PROBE="$(mktemp -d)" +trap 'rm -rf "${PROBE}"' EXIT + +cat >"${PROBE}/go.mod" < ${ROOT} +EOF + +cat >"${PROBE}/main.go" <<'EOF' +package main + +import "github.com/go-webgpu/goffi/ffi" + +func main() { + // A live reference keeps the trampoline table in the binary. Without it, + // dead-code elimination drops the symbol and the check has nothing to see. + _ = ffi.NewCallback(func(a uintptr) uintptr { return a }) +} +EOF + +FAILED=0 +for entry in "${TARGETS[@]}"; do + IFS='|' read -r target flags <<<"${entry}" + goos="${target%%/*}" + goarch="${target##*/}" + bin="${PROBE}/probe-${goos}-${goarch}" + + # shellcheck disable=SC2086 # flags is either empty or a single word + if ! (cd "${PROBE}" && CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \ + go build ${flags} -o "${bin}" .); then + echo "❌ ${target}: consumer probe failed to build" + FAILED=1 + continue + fi + + kind="$(go tool nm "${bin}" | awk -v s="${SYMBOL}" '$NF == s { print $(NF-1) }')" + if [ "${kind}" = "T" ]; then + echo "✅ ${target}: ffi.callbackTrampoline is code (T)" + else + echo "❌ ${target}: ffi.callbackTrampoline has type '${kind:-missing}', want T" + echo " Check that the build constraint of the callback .s file covers this target." + FAILED=1 + fi +done + +if [ "${FAILED}" -ne 0 ]; then + exit 1 +fi +echo "✅ Callback trampolines link as code on all asm-trampoline targets"