Why ought to we make a plugin system?
Within the modules and hooks article I used to be writing about how modules (plugins) can work collectively through the use of numerous invocation factors and hooks. The one downside with that method is that you would be able to’t actually activate or off modules on-the-fly, since we often construct our apps in a static manner.
plugin system ought to allow us to alter the habits of our code at runtime. WordPress plugins are extraordinarily profitable, as a result of you’ll be able to add further performance to the CMS with out recompiling or altering the core. Outdoors the Apple ecosystem, there’s a large world that would reap the benefits of this idea. Sure, I’m speaking about Swift on the server and backend purposes.
My concept right here is to construct an open-source modular CMS that may be quick, protected and extensible by means of plugins. Fortuitously now we’ve got this superb type-safe programming language that we are able to use. Swift is quick and dependable, it’s the excellent selection for constructing backend apps on the long run. ✅
On this article I want to present you a how you can construct a dynamic plugin system. The entire idea relies on Lopdo‘s GitHub repositories, he did fairly a tremendous job implementing it. Thanks very a lot for displaying me how you can use dlopen
and different comparable features. 🙏
The magic of dynamic linking
Handmade iOS frameworks are often bundled with the applying itself, you’ll be able to be taught just about every part a few framework if you already know some command line instruments. This time we’re solely going to concentrate on static and dynamic linking. By default Swift package deal dependencies are linked statically into your utility, however you’ll be able to change this in case you outline a dynamic library product.
First we’re going to create a shared plugin interface containing the plugin API as a protocol.
import PackageDescription
let package deal = Package deal(
identify: "PluginInterface",
merchandise: [
.library(name: "PluginInterface", type: .dynamic, targets: ["PluginInterface"]),
],
targets: [
.target(name: "PluginInterface", dependencies: []),
]
)
This dynamic PluginInterface
package deal can produce a .dylib
or .so
file, quickly there will likely be a .dll
model as properly, primarily based on the working system. All of the code bundled into this dynamic library could be shared between different purposes. Let’s make a easy protocol.
public protocol PluginInterface {
func foo() -> String
}
Since we’re going to load the plugin dynamically we’ll want one thing like a builder to assemble the specified object. We will use a brand new summary class for this objective.
open class PluginBuilder {
public init() {}
open func construct() -> PluginInterface {
fatalError("It's a must to override this methodology.")
}
}
That is our dynamic plugin interface library, be happy to push this to a distant repository.
Constructing a dynamic plugin
For the sake of simplicity we’ll construct a module known as PluginA
, that is the manifest file:
import PackageDescription
let package deal = Package deal(
identify: "PluginA",
merchandise: [
.library(name: "PluginA", type: .dynamic, targets: ["PluginA"]),
],
dependencies: [
.package(url: "path/to/the/PluginInterface/repository", from: "1.0.0"),
],
targets: [
.target(name: "PluginA", dependencies: [
.product(name: "PluginInterface", package: "PluginInterface")
]),
]
)
The plugin implementation will in fact implement the PluginInterface
protocol. You may lengthen this protocol primarily based in your wants, you too can use different frameworks as dependencies.
import PluginInterface
struct PluginA: PluginInterface {
func foo() -> String {
return "A"
}
}
We’ve got to subclass the PluginBuilder
class and return our plugin implementation. We’re going to use the @_cdecl
attributed create operate to entry our plugin builder from the core app. This Swift attribute tells the compiler to save lots of our operate underneath the “createPlugin” image identify.
import PluginInterface
@_cdecl("createPlugin")
public func createPlugin() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(PluginABuilder()).toOpaque()
}
ultimate class PluginABuilder: PluginBuilder {
override func construct() -> PluginInterface {
PluginA()
}
}
We will construct the plugin utilizing the command line, simply run swift construct
within the challenge folder. Now you will discover the dylib file underneath the binary path, be happy to run swift construct --show-bin-path
, this may output the required folder. We’ll want each .dylib
information for later use.
Loading the plugin at runtime
The core utility may even use the plugin interface as a dependency.
import PackageDescription
let package deal = Package deal(
identify: "CoreApp",
dependencies: [
.package(url: "path/to/the/PluginInterface/repository", from: "1.0.0"),
],
targets: [
.target(name: "CoreApp", dependencies: [
.product(name: "PluginInterface", package: "PluginInterface")
]),
]
)
That is an executable goal, so we are able to place the loading logic to the fundamental.swift
file.
import Basis
import PluginInterface
typealias InitFunction = @conference(c) () -> UnsafeMutableRawPointer
func plugin(at path: String) -> PluginInterface {
let openRes = dlopen(path, RTLD_NOW|RTLD_LOCAL)
if openRes != nil {
defer {
dlclose(openRes)
}
let symbolName = "createPlugin"
let sym = dlsym(openRes, symbolName)
if sym != nil {
let f: InitFunction = unsafeBitCast(sym, to: InitFunction.self)
let pluginPointer = f()
let builder = Unmanaged<PluginBuilder>.fromOpaque(pluginPointer).takeRetainedValue()
return builder.construct()
}
else {
fatalError("error loading lib: image (symbolName) not discovered, path: (path)")
}
}
else {
if let err = dlerror() {
fatalError("error opening lib: (String(format: "%s", err)), path: (path)")
}
else {
fatalError("error opening lib: unknown error, path: (path)")
}
}
}
let myPlugin = plugin(at: "path/to/my/plugin/libPluginA.dylib")
let a = myPlugin.foo()
print(a)
We will use the dlopen
operate to open the dynamic library file, then we try to get the createPlugin image utilizing the dlsym
methodology. If we’ve got a pointer we nonetheless have to forged that into a legitimate PluginBuilder
object, then we are able to name the construct methodology and return the plugin interface.
Operating the app
Now in case you attempt to run this utility utilizing Xcode you may get a warning like this:
Class _TtC15PluginInterface13PluginBuilder is carried out in each… One of many two will likely be used. Which one is undefined.
That is associated to an outdated bug, however thankfully that’s already resolved. This time Xcode is the unhealthy man, since it’s attempting to hyperlink every part as a static dependency. Now in case you construct the applying by means of the command line (swift construct) and place the next information in the identical folder:
- CoreApp
- libPluginA.dylib
- libPluginInterface.dylib
You may run the applying ./CoreApp
with out additional points. The app will print out A
with out the warning message, because the Swift package deal supervisor is recognizing that you simply want to hyperlink the libPluginInterface framework as a dynamic framework, so it will not be embedded into the applying binary. In fact you need to arrange the appropriate plugin path within the core utility.