{{ message }}
applecontainer: embed bridge dylib and dlopen at runtime (PR-A2)#59
Merged
Conversation
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>
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
Makefileruntime/applecontainer/embed_darwin_arm64.goruntime/applecontainer/runtime_darwin_arm64.goruntime/applecontainer/shim.cruntime/applecontainer/shim.h
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>
This was referenced May 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Second PR of M6. Replaces PR-A's build-time cgo
LDFLAGSrpath intothe 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 containerfor the daemon itself.Mechanism
shim.c/shim.h— small cgo C shim.ac_load(path, errbuf, errlen)does
dlopen(RTLD_NOW | RTLD_LOCAL)+dlsymfor every exportedsymbol 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.go—go:embed embed/libACBridge.dylib. Async.Onceloader writes the bytes to a per-user cache file keyedby
sha256(bridgeDylib)(so multiple bridge versions coexistwithout manual invalidation), then calls
ac_load. Write-to-tempbe picked up by a concurrent reader.
runtime_darwin_arm64.go— drops the build-timeLDFLAGSrpath;every entry point (
New,Ping,bridgeVersion) callsensureLoaded()before invoking the bridge.Build pipeline
make bridgenow also copies the built dylib intoruntime/applecontainer/embed/libACBridge.dylib. The global*.dylibgitignore keeps it out of the tree.make testandmake test-integrationgain abridgedependency. On darwin/arm64 this means
go:embedalways has afile to read; on other platforms
bridgeis a no-op (the existinguname 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 dirgo 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/...cleanSummary by CodeRabbit
New Features
Chores