From 25cb546fa7248640e4d12a54a7437165cd5a8764 Mon Sep 17 00:00:00 2001 From: Sergei Date: Tue, 28 Jul 2026 21:25:47 +0200 Subject: [PATCH] feat(ffi): let a host ask which moss it loaded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A host resolves the shared library by path at runtime, so an old library sitting next to a new host is an ordinary mistake — and its symptoms are transport bugs the host has no way to attribute. Nothing exposed the version: not the FFI, not GetMeshInfo. Moss_Version returns the build stamp. The release workflow passes -ldflags "-X main.buildVersion="; anything built another way reports "dev" rather than claiming a version it cannot know. Callers should treat a missing symbol as "older than v0.8.17". New symbol only — no existing signature changes, so every current host keeps working untouched. --- .github/workflows/release-main.yml | 8 +++- cmd/moss-ffi/main.go | 19 ++++++++ cmd/moss-ffi/version_test.go | 69 ++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 cmd/moss-ffi/version_test.go diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index 4aece70..f8c0cb9 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -68,7 +68,13 @@ jobs: mingw-w64-ucrt-x86_64-gcc - name: Build shared library - run: go build -buildmode=c-shared -o ${{ matrix.output }} ./cmd/moss-ffi + # Stamp the tag so Moss_Version reports it. A host loads this library by + # path at runtime, so without the stamp there is no way to tell which + # build it actually got. + run: >- + go build -buildmode=c-shared + -ldflags "-X main.buildVersion=${{ github.ref_name }}" + -o ${{ matrix.output }} ./cmd/moss-ffi - name: Package shared artifact shell: pwsh diff --git a/cmd/moss-ffi/main.go b/cmd/moss-ffi/main.go index 6da5ca9..eecfd7a 100644 --- a/cmd/moss-ffi/main.go +++ b/cmd/moss-ffi/main.go @@ -80,6 +80,11 @@ import ( const relayFFITimeout = 5 * time.Second +// buildVersion is stamped at link time by the release workflow +// (-ldflags "-X main.buildVersion=v0.8.17"). A library built any other way says +// so rather than claiming a version it cannot know. +var buildVersion = "dev" + var ( handleCounter atomic.Int64 registryMu sync.RWMutex @@ -371,6 +376,20 @@ func Moss_GetNATType(handle C.MossHandle) *C.char { // allocated C string (free with Moss_Free), or NULL if the handle is unknown. // Call it before Moss_Stop, which removes the handle from the registry. // +// Moss_Version returns the version this library was built at, as a newly +// allocated C string (free with Moss_Free). Release builds carry their tag; +// anything else reports "dev". +// +// A host loads moss by path at runtime, so nothing stops an old library from +// sitting next to a new host — and the symptoms of that are transport bugs the +// host cannot diagnose. This lets a host say which library it got instead of +// guessing. Callers must treat a missing symbol as "older than v0.8.17". +// +//export Moss_Version +func Moss_Version() *C.char { + return C.CString(buildVersion) +} + //export Moss_LastError func Moss_LastError(handle C.MossHandle) *C.char { node, code := getNode(int64(handle)) diff --git a/cmd/moss-ffi/version_test.go b/cmd/moss-ffi/version_test.go new file mode 100644 index 0000000..a582fc9 --- /dev/null +++ b/cmd/moss-ffi/version_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +// An unstamped build must say "dev" rather than claim a version it cannot know. +// The release workflow is the only thing that knows the tag, so this is the +// value a hand-built library carries — and a host can tell the two apart. +func TestUnstampedBuildReportsDev(t *testing.T) { + if buildVersion != "dev" { + t.Fatalf("an unstamped build reports %q; the default must stay %q so a "+ + "library built outside the release pipeline never claims a release", + buildVersion, "dev") + } +} + +// The stamp has to survive the linker flag the release workflow passes, in a +// real c-shared build rather than a plain test binary. Without it Moss_Version +// reports "dev" for every published release and a host cannot tell which +// library it loaded — which is the whole reason the symbol exists. +func TestVersionStampReachesTheSharedLibrary(t *testing.T) { + if testing.Short() { + t.Skip("skipping shared build in short mode") + } + const stamp = "v9.9.9-stamp-probe" + + outDir := t.TempDir() + var libraryName string + switch runtime.GOOS { + case "windows": + libraryName = "moss.dll" + case "darwin": + libraryName = "libmoss.dylib" + default: + libraryName = "libmoss.so" + } + output := filepath.Join(outDir, libraryName) + cacheDir := filepath.Join(outDir, "gocache") + tmpDir := filepath.Join(outDir, "gotmp") + if err := os.MkdirAll(cacheDir, 0o755); err != nil { + t.Fatalf("mkdir cache failed: %v", err) + } + if err := os.MkdirAll(tmpDir, 0o755); err != nil { + t.Fatalf("mkdir tmp failed: %v", err) + } + + cmd := exec.Command("go", "build", "-buildmode=c-shared", + "-ldflags", "-X main.buildVersion="+stamp, "-o", output, ".") + cmd.Dir = "." + cmd.Env = append(os.Environ(), "GOCACHE="+cacheDir, "GOTMPDIR="+tmpDir) + if combined, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("c-shared build failed: %v\n%s", err, combined) + } + + built, err := os.ReadFile(output) + if err != nil { + t.Fatalf("reading the built library failed: %v", err) + } + if !bytes.Contains(built, []byte(stamp)) { + t.Fatal("the linker stamp did not reach the shared library — " + + "Moss_Version would report \"dev\" for every release") + } +}