Checking at runtime for weakly linked symbols from third-party framework in Swift

746 Views Asked by At

On macOS, I use an external framework (written in C) that must be installed by the user. In Swift, I need to check at runtime if it exists, and I can't use #available() since it is for OS-related features, and I am trying to track down an external framework. Also, NSClassFromString() is not useful since it is not an Objective-C framework.

I have been trying to understand how to replicate the Objective-C equivalent for checking for a weakly-linked symbol such as:

if ( anExternalFunction == NULL ) {
    // fail graciously
} else {
    // do my thing
}

but in Swift, this does not seem to work: the compiler states that since anExternalFunction is not optional, I will always get != nil, which make "Swift sense" but does not help me one bit.

I have found two solutions, but they stink up my code like you wouldn't believe:

Option 1, create an Objective-C file with a function called isFrameworkAvailable() doing the work, and calling from Swift

Option 2, actually checking for the library with the following Swift code:

let libHandle = dlopen("/Library/Frameworks/TheLib.framework/TheLib", RTLD_NOW)

if (libHandle != nil) {
    if dlsym(libHandle, "anExternalFunction") != nil {
        return true
    }
}
return false

I have been unable to get Option 2 to work nicely with RTLD_DEFAULT since for some reason, it is defined in dlfcn.h (-2) but does not seem to be imported in Swift (like all negative pointers in that header: RTLD_NEXT, RTLD_DEFAULT, RTLD_SELF, RTLD_MAIN_ONLY). I have found this ugliest hack to make it work:

if dlsym(unsafeBitCast(-2, to: UnsafeMutableRawPointer.self), "anExternalFunction") != nil {
    // library installed
}

So for option 2, I require the path or a hack, which I find particularly ugly (but it works) and Option 1 is not very "Swifty" and makes little sense to me: what is the proper way of doing this in Swift?

Edits: clarified question, explained Option 2 better.

1

There are 1 best solutions below

0
Joe Susnick On

I've found two ways of doing this:

    1.
if let _ = dlopen("/Library/Frameworks/MyLibrary.framework/MyLibrary", RTLD_NOW) {
  print("Can open my library")
}
    2.
#if canImport(MyLibrary)
  print("Can import my library")
#endif

The second seems clearly better from just a readability and type safety standpoint but I'm not sure what the tradeoffs are between the two.