When accessing resources from my code, I sometimes need to specify the current bundle, but the existing API isn’t great. Here’s an alternative.
Apps typically load resources via the Main Bundle (Bundle.main
), but sometimes this is not what we want. For example, in plugins or frameworks, the code of the framework will need the bundle for that framework. Another example is with unit tests: I often load mock data from json files, packaged in the .xctest
bundle. In the context of the unit tests, the Main Bundle is the xcunit binary (or the app, for UI tests).
The existings solutions to find the current bundle are:
Bundle(identifier: String)
, which is OK, but a bit tedious;Bundle(for: AnyClass)
, which is mostly what we want. It uses the Objective-C internal tables to find which loaded bundle contains the code for the passed class.
The problem with Bundle(for: AnyClass)
is that sometimes, there is no class
; in Objective-C, this was somewhat of an exception, but a Swift framework could really be just struct
s and enum
s.
The workaround, that I had been using for years, is to create a dummy class; in Swift, it can even be a local type:
func foo() {
class _k {}
let bundle = Bundle(for: _k.self)
...
}
It’s smart, and it works, but it’s a bit too smart. Really, it’s just a workaround to please the API.
I just want to write let bundle = Bundle.current
.
Luckily, Nicolas Bachschmidt just gave me the right idea last week:
- Use
backtrace()
to find the function pointer of the caller; - Use
dladdr()
to find the executable image path containing this function; - Find the loaded bundle with this executable path.
I was afraid Step 1 was impossible in Swift, as the backtrace()
function is not available, but Thread.callStackReturnAddresses()
works just as well.
Step 3 is more difficult. Given the executable path, we could rewind the directories and find the containing Bundle. However, the internal structure of mac apps, iOS apps, plugins, frameworks, and unit tests is complex. Frameworks, for example, use symlinks internally to reference the executable.
Instead, we’re going to use Bundle.allBundles
and Bundle.allFrameworks
, which will give the full list of loaded Bundles, and find the one matching the .executablePath
.
Finally, since resolving the executablePath
of a Bundle is a bit expensive, we’re going to cache the result.
Here’s the code, as a Bundle
extension:
extension Bundle {
class var current: Bundle {
let caller = Thread.callStackReturnAddresses[1]
if let bundle = _cache.object(forKey: caller) {
return bundle
}
var info = Dl_info(dli_fname: "", dli_fbase: nil, dli_sname: "", dli_saddr: nil)
dladdr(caller.pointerValue, &info)
let imagePath = String(cString: info.dli_fname)
for bundle in Bundle.allBundles + Bundle.allFrameworks {
if let execPath = bundle.executableURL?.resolvingSymlinksInPath().path,
imagePath == execPath {
_cache.setObject(bundle, forKey: caller)
return bundle
}
}
fatalError("Bundle not found for caller \"\(String(cString: info.dli_sname))\"")
}
private static let _cache = NSCache<NSNumber, Bundle>()
}
I was surprised how easy it is to call low-level C functions from Swift; in fact, I wrote a first version in Objective-C, but it turned out to be more more awkward.
With this, I can finally write Bundle.current
everywhere, instead of Bundle.main
or Bundle(for: AnyClass)
.
🎉
-
Let’s just use the
NS
prefix in the title, for reference. And I can’t let it go. Yet. ↩