Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ffi/callback_amd64.s
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build (linux || darwin) && amd64
//go:build (linux || darwin || freebsd) && amd64

#include "textflag.h"
#include "go_asm.h"
Expand Down
91 changes: 91 additions & 0 deletions scripts/check-callback-trampolines.sh
Original file line number Diff line number Diff line change
@@ -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" <<EOF
module goffitrampolineprobe

go ${GO_VERSION}

require ${MODULE} v0.0.0

replace ${MODULE} => ${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"
Loading