applecontainer: embed bridge dylib and dlopen at runtime (PR-A2) by bilby91 · Pull Request #59 · crunchloop/devcontainer · GitHub
Skip to content

applecontainer: embed bridge dylib and dlopen at runtime (PR-A2)#59

Merged
bilby91 merged 3 commits into
mainfrom
m6/pr-a2-embed-dlopen
May 15, 2026
Merged

applecontainer: embed bridge dylib and dlopen at runtime (PR-A2)#59
bilby91 merged 3 commits into
mainfrom
m6/pr-a2-embed-dlopen

Conversation

@bilby91

@bilby91 bilby91 commented May 15, 2026

Copy link
Copy Markdown
Member

Summary

Second PR of M6. Replaces PR-A's build-time cgo LDFLAGS rpath into
the SwiftPM output directory with a single-binary embed + dlopen
distribution path, per design decision §13.4. The Go binary now travels
with the dylib bytes; consumers don't need a separate file or any
install step beyond brew install container for the daemon itself.

Mechanism

  • shim.c / shim.h — small cgo C shim. ac_load(path, errbuf, errlen)
    does dlopen(RTLD_NOW | RTLD_LOCAL) + dlsym for every exported
    symbol into static function-pointer slots. Per-export wrappers
    (ac_version_p, ac_ping_p, ac_free_p) call through those pointers.
    Error reporting uses a caller-provided buffer (no malloc/free dance).
  • embed_darwin_arm64.gogo:embed embed/libACBridge.dylib. A
    sync.Once loader writes the bytes to a per-user cache file keyed
    by sha256(bridgeDylib) (so multiple bridge versions coexist
    without manual invalidation), then calls ac_load. Write-to-temp
    • atomic rename, so a partial write from a crashed process can't
      be picked up by a concurrent reader.
  • runtime_darwin_arm64.go — drops the build-time LDFLAGS rpath;
    every entry point (New, Ping, bridgeVersion) calls
    ensureLoaded() before invoking the bridge.

Build pipeline

  • make bridge now also copies the built dylib into
    runtime/applecontainer/embed/libACBridge.dylib. The global
    *.dylib gitignore keeps it out of the tree.
  • make test and make test-integration gain a bridge
    dependency. On darwin/arm64 this means go:embed always has a
    file to read; on other platforms bridge is a no-op (the existing
    uname guard from PR-A's review-feedback commit), so the dependency
    is free for Linux CI.

Cache location

os.UserCacheDir()/devcontainer-go/applecontainer/<sha256>.dylib
on macOS that's ~/Library/Caches/devcontainer-go/applecontainer/.
Hash-named so bridge bumps don't require manual cache invalidation.

Test plan

  • make bridge — builds the dylib and copies into embed dir
  • go test ./runtime/applecontainer/... — both smoke tests still pass;
    cache file appears at the expected path with correct sha256
  • GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build ./runtime/applecontainer/...
    — stub still builds; no cgo / embed dependency on non-darwin/arm64
  • go vet ./runtime/applecontainer/... clean

Summary by CodeRabbit

  • New Features

    • Apple Silicon (darwin/arm64) now embeds the native bridge into builds and performs a safe, idempotent runtime extraction with hash‑based caching to ensure reliable dynamic loading on ARM Macs.
    • Runtime now ensures the embedded bridge is loaded before bridge-dependent operations.
  • Chores

    • Build/test workflow updated so unit and integration tests require the embedded bridge; build copies the embedded library and clean removes the copied artifact.

Review Change Stack

PR-A2 replaces PR-A's build-time cgo LDFLAGS rpath into the SwiftPM
output directory with a single-binary embed + dlopen path, per
design/runtime-applecontainer.md §13.4.

Mechanism:
- shim.c / shim.h: small cgo C shim with ac_load (dlopen + dlsym for
  every exported symbol into static function-pointer slots) and per-
  export wrappers (ac_version_p, ac_ping_p, ac_free_p) that call
  through those pointers.
- embed_darwin_arm64.go: go:embed of runtime/applecontainer/embed/
  libACBridge.dylib. ensureLoaded() (sync.Once) writes the bytes to a
  per-user cache file keyed by sha256 of the embedded payload, then
  invokes ac_load. The hash key means multiple bridge versions
  coexist without manual invalidation, and a partial write from a
  crashed process can't be picked up (write-to-temp-then-rename).
- runtime_darwin_arm64.go: drop the build-time LDFLAGS rpath; call
  through the shim. Every entry point (New, Ping, bridgeVersion)
  calls ensureLoaded() before invoking the bridge.

Distribution outcome: the Go binary travels with the dylib bytes; no
separate file or install step on the consumer side. The daemon
(`brew install container` + `container system start`) is still
required, same as PR-A.

Build pipeline: `make bridge` now also copies the built dylib into
runtime/applecontainer/embed/. `make test` and `make test-integration`
gain a bridge dependency so go:embed has a file to read on
darwin/arm64; on other platforms bridge is a no-op so the
dependency is free for Linux CI.

Test plan:
- `make bridge` builds dylib and copies to embed dir
- `go test ./runtime/applecontainer/...` — both smoke tests pass;
  cache file appears at ~/Library/Caches/devcontainer-go/
  applecontainer/<sha256>.dylib
- `GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build ./runtime/applecontainer/...`
  — stub still compiles, no embed/cgo on non-darwin/arm64
- `go vet ./runtime/applecontainer/...` clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@runtime/applecontainer/shim.c`:
- Around line 43-47: The symbol-resolution failure path currently returns -1
without closing the dlopen handle or clearing partially resolved pointers;
update the failure branch where p_ac_version, p_ac_ping, or p_ac_free are
checked to call dlclose(h) before returning, and reset the function pointers
(p_ac_version, p_ac_ping, p_ac_free) and the handle variable h to NULL/0 to
avoid leaks and stale state; keep the existing error reporting via
copy_err(errbuf, errlen, ...).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84b3b92f-f2c7-47e9-aa57-75d70887979f

📥 Commits

Reviewing files that changed from the base of the PR and between 388d2e0 and e06112e.

📒 Files selected for processing (5)
  • Makefile
  • runtime/applecontainer/embed_darwin_arm64.go
  • runtime/applecontainer/runtime_darwin_arm64.go
  • runtime/applecontainer/shim.c
  • runtime/applecontainer/shim.h

Comment thread runtime/applecontainer/shim.c
bilby91 and others added 2 commits May 14, 2026 23:26
CI on Linux failed with:

  package github.com/crunchloop/devcontainer/runtime/applecontainer:
  C source files not allowed when not using cgo or SWIG: shim.c

The stub file on non-darwin/arm64 doesn't use cgo, so an untagged
shim.c in the package directory tripped Go's "C without cgo" guard.
Add `//go:build darwin && arm64` to shim.c so the C source is only
considered on the platforms that actually link the bridge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If any of ac_version / ac_ping / ac_free fails to resolve via dlsym,
return without closing the dlopen handle or zeroing the partially
resolved function pointers. Once-only via sync.Once today, but cheap
to make robust now so a future retry-on-failure refactor doesn't
inherit half-loaded state.

Reset all pointers to NULL and dlclose the handle before returning
the error to Go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant