diff --git a/.github/workflows/bindings-go.yml b/.github/workflows/bindings-go.yml index 18471f0e..92fa84b3 100644 --- a/.github/workflows/bindings-go.yml +++ b/.github/workflows/bindings-go.yml @@ -76,6 +76,11 @@ jobs: - "1.18" - "oldstable" - "stable" + # CGo's handling of C constants differs between compilers, so make sure + # the bindings build with both of the mainstream C compilers. + cc: + - gcc + - clang runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -92,7 +97,11 @@ jobs: go-version: ${{ matrix.go-version }} check-latest: true # Run smoke-tests. + # NOTE: CC is only set here (and not for the whole job) so that we don't + # affect how libpathrs itself is built. - run: make -C examples/go smoke-test + env: + CC: ${{ matrix.cc }} go-complete: needs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 36f7cba9..c44be786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). library installation were incorrectly called `--disable-dynamic` rather than autoconf's `--disable-shared`. We have added new `--{enable,disable}-shared` flags and the old ones are kept for compatibility. +- go-pathrs: Fix a build failure when building the Go bindings with Clang. + CGo's handling of the signedness of C constants is compiler-dependent, and + the internal sanity check of the `PATHRS_PROC_*` values only handled the way + GCC does it (which made Clang builds fail with "untyped int constant + ... overflows"). (#401) ## [0.2.5] - 2026-06-17 ## diff --git a/go-pathrs/internal/libpathrs/libpathrs_linux.go b/go-pathrs/internal/libpathrs/libpathrs_linux.go index d610a3be..7c907737 100644 --- a/go-pathrs/internal/libpathrs/libpathrs_linux.go +++ b/go-pathrs/internal/libpathrs/libpathrs_linux.go @@ -219,10 +219,12 @@ func InRootHardlink(oldRootFd uintptr, oldPath string, newRootFd uintptr, newPat // ProcBase is pathrs_proc_base_t (uint64_t). type ProcBase C.pathrs_proc_base_t -// FIXME: We need to open-code the constants because CGo unfortunately will -// implicitly convert any non-literal constants (i.e. those resolved using gcc) -// to signed integers. See for some -// more information on the underlying issue (though. +// FIXME: We need to open-code the constants because CGo's handling of +// non-literal constants (i.e. those resolved using the C compiler) that don't +// fit in an int64 is compiler-dependent -- GCC gives us implicitly-converted +// (negative) signed integers while Clang gives us the actual unsigned values. +// See for some more information on +// the underlying issue. const ( // ProcRoot is PATHRS_PROC_ROOT. ProcRoot ProcBase = 0xFFFF_FFFE_7072_6F63 // C.PATHRS_PROC_ROOT @@ -246,16 +248,27 @@ func assertEqual[T comparable](a, b T, msg string) { } } +// u64Mask is used to convert the C constants below into plain uint64 values, +// regardless of how CGo decided to interpret their signedness. +// +// As explained above, CGo gives us the PATHRS_PROC_* values as negative +// (signed) constants with GCC and as (unsigned) constants that overflow int64 +// with Clang, so neither int64 nor uint64 can hold them with both compilers. +// However, Go constant expressions are arbitrary-precision and bitwise +// operations on negative constants operate on their infinite two's complement +// representation, so masking off the lower 64 bits yields the same uint64 +// value no matter which compiler CGo used. +const u64Mask = 0xFFFF_FFFF_FFFF_FFFF + // Verify that the values above match the actual C values. Unfortunately, Go -// only allows us to forcefully cast int64 to uint64 if you use a temporary -// variable, which means we cannot do it in a const context and thus need to do -// it at runtime (even though it is a check that fundamentally could be done at +// has no way of doing compile-time assertions, so we need to do it at runtime +// (even though it is a check that fundamentally could be done at // compile-time)... func init() { var ( - actualProcRoot int64 = C.PATHRS_PROC_ROOT - actualProcSelf int64 = C.PATHRS_PROC_SELF - actualProcThreadSelf int64 = C.PATHRS_PROC_THREAD_SELF + actualProcRoot uint64 = C.PATHRS_PROC_ROOT & u64Mask + actualProcSelf uint64 = C.PATHRS_PROC_SELF & u64Mask + actualProcThreadSelf uint64 = C.PATHRS_PROC_THREAD_SELF & u64Mask ) assertEqual(ProcRoot, ProcBase(actualProcRoot), "PATHRS_PROC_ROOT") @@ -263,8 +276,8 @@ func init() { assertEqual(ProcThreadSelf, ProcBase(actualProcThreadSelf), "PATHRS_PROC_THREAD_SELF") var ( - actualProcBaseTypeMask uint64 = C.__PATHRS_PROC_TYPE_MASK - actualProcBaseTypePid uint64 = C.__PATHRS_PROC_TYPE_PID + actualProcBaseTypeMask uint64 = C.__PATHRS_PROC_TYPE_MASK & u64Mask + actualProcBaseTypePid uint64 = C.__PATHRS_PROC_TYPE_PID & u64Mask ) assertEqual(ProcBaseTypeMask, ProcBase(actualProcBaseTypeMask), "__PATHRS_PROC_TYPE_MASK")