Weak-linking in Swift Package Manager (plugin architecture)

I'm implementing a project that uses a plugin architecture, where plugins are shared libraries (.dylibs/.sos) that are dynamically loaded at runtime using dlopen. I want to distribute the main ("host") application as a standalone, single-file executable that provides the symbols needed by plugins:

  • Package Lib defines func foo() in source
  • Package Host adds Lib as a dependency and links to it statically, providing the foo symbol in its binary
  • Package Plugin adds Lib as a dependency and references foo in source without actually dynamically linking to it, and without knowledge of Host

With Clang, this is possible by using -undefined dynamic_lookup when building the plugin and not passing a library to link to — see my example here.

How can I recreate this setup (ie. disable linking to a dependency) in Swift Package Manager while still being able to refer to that dependency in source? Using unsafeFlags here is OK because the host and plugin are end products, but they need to be compatible with Linux too. Or, as a newbie to linkage, is there a better way to go about this?

Thanks for your help!

2 Likes

Wow, I wasn't even aware that such a thing is possible, but upon googling it, I found something that might just answer your question :D Not the most swift-y solution though, the very first thing I would do is to write an abstraction layer so I don't have to see the scary UnsafeMutableRawPointer stuff

1 Like

Thank you! Unfortunately that article builds a shared library that is dynamically linked to both the host and the plugin:

You can run the application ./CoreApp wihtout further issues. The app will print out A without the warning message, since the Swift package manager is recognizing that you would like to link the libPluginInterface framework as a dynamic framework, so it won't be embedded into the application binary. Of course you have to set up the right plugin path in the core application.

I want to have the library statically linked to the host, but the plugin dynamically linked to the host — basically I want the host to be an "executable .dylib or .so", so that I am able to ship a single file instead of two. When I try to edit the LC_LOAD_DYLIB values inside the plugin .dylib/.so to point to the executable, I am unable to load the plugin at all (dlopen returns a null pointer).

Hmmmm...

Interesting approach.

Not sure if this is at all possible, but what you should be able to do is: have the host app and the interface lib located in the same Package as different products. As far as I can see, the interface lib has to be a shared dynamic library so the plugins can see it, but it might help with dependency management. I'm just guessing though.

Yeah, I may have to end up doing that, but I would love to get this approach working if possible so I can distribute a "zero-dependencies" binary. I even want to link the Swift standard library into the binary with --static-swift-stdlib on Linux, as well as embed resources into the binary so I can have a fully-portable executable, but I'm not sure how to make plugins link to the standalone executable instead of shared libraries :(

If anyone else knows how to approach this, please share! For context, I am trying to port a Rust application to Swift, targeting platforms including the Raspberry Pi. My Raspberry Pis are being used for robotics and can't connect to the internet, so installing and updating packages is a pain and I would rather copy over a single executable containing everything. By default, Rust links everything statically and embeds resources directly into the binary, but the problem is that plugins will have their own copy of the "interface" library, which could get out of sync with the "host" application and cause strange behavior. Ideally, the host would contain both the executable code and the interface library (as well as all the other Swift runtime libraries), and plugins would link to those at runtime.

3 Likes

This is related to the discussion we had in Swift dynamic loading API - #18 by STREGAsGate also - it is definitely a desirable feature for many types of deployment.