Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a0c4e73
feat: add goffi_static build tag for fully static binaries
unxed Aug 22, 2026
561ebf7
Minor
unxed Aug 22, 2026
8db5daf
gofmt
unxed Aug 22, 2026
a6f07cc
feat: add goffi_musl build tag for Alpine and other musl systems
unxed Aug 22, 2026
5094553
fix: route callback pointer arguments through one nocheckptr helper
refaim Aug 23, 2026
e18d972
Merge pull request #1 from refaim/fix/checkptr-callback
unxed Aug 23, 2026
20f5a56
Merge branch 'go-webgpu:main' into main
unxed Sep 1, 2026
1a015a9
WIP: Profile U universal (musl+glibc) FFI — scaffolding
Sep 1, 2026
60ee648
WIP(2): universal re-exec bridge logic, tooling, probe — compiles
Sep 1, 2026
9734459
docs: add PROFILE_U_PLAN.md — full task/plan/status hand-off
Sep 2, 2026
163669e
fakecgo: add setupUniversalTLS shim — fix first-launch TLS fault
Sep 2, 2026
4767268
fakecgo: bind on glibc via memfd interp-restore — universal now green…
Sep 2, 2026
227e007
gofmt
unxed Sep 2, 2026
5865f9b
loader: add internal/loader package + public HostLoader/HostLibC/Libc…
Sep 2, 2026
a507f73
audit: add cmd/goffi-audit + ffi universal DT_NEEDED test
Sep 2, 2026
09ce585
ci: add both-libc universal workflow + purego coexistence
Sep 2, 2026
c6a4fd0
docs: user-facing PROFILE_U.md + attribution (static-everywhere, pg83…
Sep 2, 2026
ff80377
ci: make bare go vet match the repo's govet exclusions; pin toolchain
unxed Sep 2, 2026
5bdf933
docs: close out the Profile U plan; record the purego/pureffi position
unxed Sep 2, 2026
7dc87b5
ci: fix golangci-lint errcheck/govet/staticcheck issues
Sep 2, 2026
d650a9a
ci: fix Android fakecgo staleness gate and example builds
Sep 2, 2026
f295fd5
ci: disable unsafeptr in the Android arm64 bare go vet
Sep 2, 2026
11dbee2
test: cover callback pointer paths and the unsupported-arch guard
Sep 2, 2026
6bf5dbf
ci: run the Android arm64 gate on Go 1.25.12 as well
Sep 2, 2026
a172b9e
fakecgo: survive a host with no dynamic loader instead of faulting
Sep 2, 2026
fea4dfc
fakecgo: record argv[0] and the executable path before the re-exec ta…
Sep 3, 2026
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
64 changes: 55 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: CI
# Testing Strategy:
# - Tests run on Linux and Windows (AMD64 only - macOS planned for v0.5.0)
# - goffi uses platform-specific assembly (System V vs Win64 ABI)
# - Go 1.25+ required (matches go.mod requirement)
# - Go 1.26+ required (matches go.mod requirement)
# - Race detector critical for low-level FFI code
#
# Branch Strategy (Git Flow):
Expand Down Expand Up @@ -41,7 +41,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
go-version: '1.26.x'
cache: true

- name: Download dependencies
Expand Down Expand Up @@ -69,7 +69,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
go-version: '1.26.x'
cache: true

- name: Check formatting
Expand Down Expand Up @@ -97,7 +97,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
go-version: '1.26.x'
cache: true

- name: Cross-compile all platforms
Expand Down Expand Up @@ -168,14 +168,60 @@ jobs:
# 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.
# Static builds - the goffi_static tag must strip every
# //go:cgo_import_dynamic directive, otherwise the linker keeps emitting an
# ELF interpreter and the artifact will not run on Alpine or scratch.
# See docs/STATIC_BUILDS.md and unxed/f4#693.
static-build:
name: Static Build (goffi_static)
runs-on: ubuntu-latest
needs: [lint, formatting]
env:
CGO_ENABLED: 0
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26.x'
cache: true

- name: Check static build mode
run: scripts/check-static.sh

# musl builds - the goffi_musl tag must swap the glibc SONAMEs and the
# ELF interpreter for their musl equivalents, otherwise the binary cannot
# start on Alpine. Link-time checks run for amd64 and arm64; the runtime
# probe executes inside a real Alpine userland. See docs/MUSL.md.
musl-build:
name: musl Build (goffi_musl)
runs-on: ubuntu-latest
needs: [lint, formatting]
env:
CGO_ENABLED: 0
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26.x'
cache: true

- name: Check musl build mode
run: scripts/check-musl.sh

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']
go: ['1.25.12', '1.26.5', '1.26.x']
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down Expand Up @@ -229,7 +275,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
go-version: '1.26.x'
cache: true
cache-dependency-path: go.sum

Expand Down Expand Up @@ -298,7 +344,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
go-version: '1.26.x'
cache: true

- name: Download dependencies
Expand Down Expand Up @@ -340,7 +386,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
go-version: '1.26.x'

- name: Download coverage reports
uses: actions/download-artifact@v4
Expand Down Expand Up @@ -408,7 +454,7 @@ jobs:
echo "✅ Lint: PASSED"
echo "✅ Formatting: PASSED"
echo "✅ Cross-Compile: PASSED (8 desktop targets)"
echo "✅ Android arm64: PASSED (API 29+, Go 1.25/1.26, cgo=0/1)"
echo "✅ Android arm64: PASSED (API 29+, Go 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)"
Expand Down
117 changes: 117 additions & 0 deletions .github/workflows/universal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: universal

on:
push:
branches: [ "**" ]
pull_request:

permissions:
contents: read

jobs:
build:
name: build + Profile U contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
# Pinned to match .github/workflows/ci.yml: the goffi_musl build below
# depends on the toolchain's cgo_import_dynamic restrictions, which
# the -gcflags=...=-std workaround is calibrated against.
go-version: '1.26.x'
- name: Build every mode, vet and unit tests
run: |
set -euo pipefail
CGO_ENABLED=0 go build ./...
CGO_ENABLED=0 go build -tags goffi_universal ./...
CGO_ENABLED=0 go build -tags goffi_static ./...
CGO_ENABLED=0 go build -tags goffi_musl -gcflags=github.com/go-webgpu/goffi/internal/dl=-std ./...
# -unsafeptr=false: goffi reconstructs pointers from native call
# registers and stack slots by design (see ffi/callback_pointer.go's
# //go:nocheckptr contract). ci.yml runs vet through golangci-lint,
# which excludes govet on exactly those paths (.golangci.yml); this
# flag is the bare-vet equivalent of that exclusion.
CGO_ENABLED=0 go vet -unsafeptr=false ./...
CGO_ENABLED=0 go test ./...
CGO_ENABLED=0 go test -tags goffi_universal ./internal/...
- name: Build the universal probe and check the ELF contract
run: |
set -euo pipefail
bash scripts/build-universal.sh -o universal-probe ./cmd/universal-probe
go run ./cmd/goffi-audit universal-probe
- name: Upload universal probe
uses: actions/upload-artifact@v4
with:
name: universal-probe
path: universal-probe

run-glibc:
name: run on glibc (${{ matrix.image }})
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
image: [ "debian:stable-slim", "ubuntu:24.04" ]
container:
image: ${{ matrix.image }}
steps:
- uses: actions/download-artifact@v4
with:
name: universal-probe
- name: Run the same universal binary on glibc
run: |
set -eu
chmod +x universal-probe
./universal-probe | tee out.txt
grep -q UNIVERSAL-PROBE-OK out.txt

run-musl:
name: run on musl (alpine)
needs: build
runs-on: ubuntu-latest
container:
image: alpine:latest
steps:
- uses: actions/download-artifact@v4
with:
name: universal-probe
- name: Run the same universal binary on musl
shell: sh
run: |
set -eu
chmod +x universal-probe
./universal-probe | tee out.txt
grep -q UNIVERSAL-PROBE-OK out.txt

purego-coexistence:
name: purego coexistence (CGO-free, -tags nofakecgo)
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: goffi
- uses: actions/setup-go@v5
with:
go-version: '1.26.x'
- name: Build and run a program importing goffi and purego together
run: |
set -euo pipefail
mkdir coexist && cd coexist
printf '%s\n' \
'package main' \
'' \
'import (' \
' _ "github.com/ebitengine/purego"' \
' "github.com/go-webgpu/goffi/ffi"' \
')' \
'' \
'func main() { println("coexist ffi.Available =", ffi.Available()) }' \
> main.go
go mod init coexist
go mod edit -replace github.com/go-webgpu/goffi=../goffi
GOFLAGS=-mod=mod go get github.com/go-webgpu/goffi/ffi
GOFLAGS=-mod=mod go get github.com/ebitengine/purego
CGO_ENABLED=0 go build -tags nofakecgo -o coexist .
./coexist
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,5 @@ RESEARCH*.md
# Go modules (don't ignore go.mod and go.sum)
!go.mod
!go.sum
/goffi-audit
/def_probe
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ experiment: prototype macOS Apple Silicon support

### Prerequisites

- **Go 1.25 or later** (required for latest runtime.cgocall features)
- **Go 1.26 or later** (required for latest runtime.cgocall features)
- **golangci-lint** (code quality)
- **GCC or Clang** (for race detector, optional but recommended)
- **Platform access**:
Expand All @@ -241,7 +241,7 @@ go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

# Verify installation
golangci-lint --version
go version # Should be 1.25+
go version # Should be 1.26+
```

### Running Tests
Expand Down
10 changes: 10 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ under the Apache License, Version 2.0. Original copyright:

The Apache-2.0 licensed files retain their original SPDX headers
with dual copyright attribution.

--------------------------------------------------------------------------------
Profile U (universal glibc/musl) build
--------------------------------------------------------------------------------
goffi's universal build ports the "Profile U" concept from
unxed/static-everywhere (https://github.com/unxed/static-everywhere): a single
CGO-free binary with no PT_INTERP and no DT_NEEDED that reaches the host libc
through the host's own dynamic loader. The full in-process foreign-libc loader
pg83/solo (https://github.com/pg83/solo) is referenced but not vendored. See
docs/PROFILE_U.md.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,47 @@ if err != nil {

---

## Static Builds

Binaries that import goffi are dynamically linked by default: the
`//go:cgo_import_dynamic` directives behind `dlopen`/`dlsym` make the Go linker
emit an ELF interpreter and `DT_NEEDED` entries even under `CGO_ENABLED=0`, and
`-extldflags '-static'` cannot change that because no external linker runs.

Build with `-tags goffi_static` to drop those directives:

```bash
CGO_ENABLED=0 go build -tags goffi_static ./...
```

The result is a genuinely static binary that runs on Alpine and in `scratch`
containers. The trade is unavoidable — `dlopen` is a service of the dynamic
loader, which a static executable does not have — so in that mode `LoadLibrary`,
`GetSymbol` and `CallFunction` return an error wrapping `ffi.ErrStaticBuild`.

The API is identical in both modes, so one source tree can produce both
artifacts. Branch on `ffi.Available()`, a compile-time constant, to keep the
pure-Go path and let the linker drop the rest:

```go
if ffi.Available() {
backend = newAcceleratedBackend()
} else {
backend = newPureGoBackend()
}
```

The tag is a no-op on Windows and Android, which are dynamically linked by
construction; `Available()` stays `true` there, so a cross-platform build matrix
can pass the tag everywhere. See [docs/STATIC_BUILDS.md](docs/STATIC_BUILDS.md).

For Alpine and other musl-based distros there is a third flavor: the default
build hardcodes glibc SONAMEs and the glibc loader path, so it cannot start
under musl at all. Build with `-tags goffi_musl` (plus one `-gcflags` line) to
target musl with **full FFI** — see [docs/MUSL.md](docs/MUSL.md).

---

## Platform Support

| Platform | Arch | ABI | Since | CI |
Expand Down Expand Up @@ -483,3 +524,9 @@ MIT — see [LICENSE](LICENSE).
---

*goffi v0.4.1 | [GitHub](https://github.com/go-webgpu/goffi) | [pkg.go.dev](https://pkg.go.dev/github.com/go-webgpu/goffi) | [Dev.to](https://dev.to/kolkov/goffi-zero-cgo-foreign-function-interface-for-go-how-we-call-c-libraries-without-a-c-compiler-ca5)*

## Universal build (glibc + musl)

One CGO-free binary can do FFI on both glibc and musl systems — see
[docs/PROFILE_U.md](docs/PROFILE_U.md). Attribution for the Profile U concept
(unxed/static-everywhere, pg83/solo) is in [NOTICE](NOTICE).
Loading
Loading