Explicit dynamic loading of a framework in macOS - recommended approach?

I am working on a cross-platform application where, on Android and Windows, I explicitly load dynamic libraries at runtime (e.g., LoadLibrary/GetProcAddress on Windows and equivalent mechanisms on Android). This allows me to control when and how modules are loaded, and to transfer execution flow from the main executable into the dynamically loaded library.

I want to follow a similar approach on macOS (and also iOS) and explicitly load a framework (instead of relying on implicit linking via import).

From my exploration so far, I have come across the following options:

  • Using Bundle (NSBundle) - Load framework using:
let bundle = Bundle(path: path)
try bundle?.load()

Access functionality via NSPrincipalClass and @objc methods (class-based entry)

  • Using dlopen + dlsym

Load the framework binary and resolve symbols:

let handle = dlopen(path, RTLD_NOW)
let sym = dlsym(handle, "EntryPoint")

Expose Swift functions using @_cdecl

  • Using a hybrid approach (Bundle + dlsym) - Use Bundle for loading and dlsym for symbol access

From what I understand: Bundle works well for class-based/plugin-style designs using the Objective-C runtime while dlopen/dlsym works at the symbol level and is closer to what I am doing on other platforms

However, my requirement is specifically:

  • Explicit runtime loading (not compile-time linking)
  • Ability to transfer execution flow from the main executable into the dynamically loaded framework

**What is the recommended approach on macOS for this kind of explicit dynamic loading, or is implicit loading the way to go? Also, would it differ for interactive and non-interactive apps? **

In what scenarios would Apple recommend using Bundle instead of dlopen?

Is there any other methods best for this explicit loading of frameworks on Apple?

Answered by DTS Engineer in 881260022

I want to be clear about terminology here:

  • A load-time import is one that’s statically declared in the binary via the LC_LOAD_DYLIB load command.
  • A run-time import is one that you do from code, using dlopen or some API layered on top of that.

Beyond that, I’m going to use the terms from an An Apple Library Primer, and you should read before continuing.

What is the recommended approach on macOS for this kind of explicit dynamic loading … ?

My recommendation is that you not do this. Apple platforms generally prefer load-time imports because it enables optimisations in the dynamic linker. Specifically, the dynamic linker can build and cache a closure for an app, reusing that closure the next time the app launches.

Note We discussed this in detail in WWDC 2017 Session 413 App Startup Time: Past, Present, and Future. Sadly, it’s no longer available from Apple, but the core info is still available from third-party sources.

I don’t think this closure feature will make a big difference on macOS, but our preference for load-time imports applies to all platforms.

Additionally, load-time imports are:

  • Much faster
  • Easier to debug
  • Encourage correct layering [1]

I see folks avoid load-time imports because of initialisation order issues, and that’s especially true of folks coming from other platforms. On Apple platforms we recommend lazy initialisation. That is, set up your libraries so that they run no initialisation code at load time, but instead defer that initialisation until they’re actually called.

(FWIW, Swift makes this really easy because all Swift global variables are lazily initialised.)

Apple’s static linker, ld, has a -no_inits option to help you find and quash non-lazy initialisation.


Having said that, run-time imports are supported, and they’re even the right option in some cases, for example, when working with a library whose on-disk path you discover at runtime.

In that case you have three options:

  • dlopen and friends
  • CFBundle
  • NSBundle

Which you choose depends on your specific requirements:

  • If you’re working with bundled code [2], it’s best to use one of the bundle APIs [3].
  • If you’re importing an Objective-C class, put it in a bundle and use NSBundle.
  • If you’re importing C functions or data from bundled code, using CFBundle [4].
  • For non-bundled code, use dlopen.

Expose Swift functions using @_cdecl

Be aware that @_cdecl isn’t an official part of the language (hence the leading underscore). It’s been replaced by SE-0495 C compatible functions and enums. That’s part of Swift 6.3, so only available in Xcode 26.4 (currently a release candidate). If you’re using an older Xcode, it’s fine to use @_cdecl but you should also plan update your code once you have access to @c.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] By outlawing dependency loops.

[2] That term is defined in Creating distribution-signed code for macOS.

[3] The bundle APIs can work with non-bundled code but you have to jump through hoops and it’s generally not worth doing so.

[4] This avoids you having to navigate the bundle hierarchy, which is a surprisingly tricky task.

I want to be clear about terminology here:

  • A load-time import is one that’s statically declared in the binary via the LC_LOAD_DYLIB load command.
  • A run-time import is one that you do from code, using dlopen or some API layered on top of that.

Beyond that, I’m going to use the terms from an An Apple Library Primer, and you should read before continuing.

What is the recommended approach on macOS for this kind of explicit dynamic loading … ?

My recommendation is that you not do this. Apple platforms generally prefer load-time imports because it enables optimisations in the dynamic linker. Specifically, the dynamic linker can build and cache a closure for an app, reusing that closure the next time the app launches.

Note We discussed this in detail in WWDC 2017 Session 413 App Startup Time: Past, Present, and Future. Sadly, it’s no longer available from Apple, but the core info is still available from third-party sources.

I don’t think this closure feature will make a big difference on macOS, but our preference for load-time imports applies to all platforms.

Additionally, load-time imports are:

  • Much faster
  • Easier to debug
  • Encourage correct layering [1]

I see folks avoid load-time imports because of initialisation order issues, and that’s especially true of folks coming from other platforms. On Apple platforms we recommend lazy initialisation. That is, set up your libraries so that they run no initialisation code at load time, but instead defer that initialisation until they’re actually called.

(FWIW, Swift makes this really easy because all Swift global variables are lazily initialised.)

Apple’s static linker, ld, has a -no_inits option to help you find and quash non-lazy initialisation.


Having said that, run-time imports are supported, and they’re even the right option in some cases, for example, when working with a library whose on-disk path you discover at runtime.

In that case you have three options:

  • dlopen and friends
  • CFBundle
  • NSBundle

Which you choose depends on your specific requirements:

  • If you’re working with bundled code [2], it’s best to use one of the bundle APIs [3].
  • If you’re importing an Objective-C class, put it in a bundle and use NSBundle.
  • If you’re importing C functions or data from bundled code, using CFBundle [4].
  • For non-bundled code, use dlopen.

Expose Swift functions using @_cdecl

Be aware that @_cdecl isn’t an official part of the language (hence the leading underscore). It’s been replaced by SE-0495 C compatible functions and enums. That’s part of Swift 6.3, so only available in Xcode 26.4 (currently a release candidate). If you’re using an older Xcode, it’s fine to use @_cdecl but you should also plan update your code once you have access to @c.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] By outlawing dependency loops.

[2] That term is defined in Creating distribution-signed code for macOS.

[3] The bundle APIs can work with non-bundled code but you have to jump through hoops and it’s generally not worth doing so.

[4] This avoids you having to navigate the bundle hierarchy, which is a surprisingly tricky task.

Explicit dynamic loading of a framework in macOS - recommended approach?
 
 
Q