[ad_1]
Learn to create a plugin system utilizing dynamic libraries and the facility of Swift, aka. modular frameworks on the server-side.
Swift
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 by utilizing varied invocation factors and hooks. The one downside with that strategy is you can’t actually activate or off modules on-the-fly, since we normally construct our apps in a static method.
A very good plugin system ought to allow us to alter the conduct of our code at runtime. WordPress plugins are extraordinarily profitable, as a result of you may add additional performance to the CMS with out recompiling or altering the core. Exterior the Apple ecosystem, there’s a large world that would benefit from this idea. Sure, I’m speaking about Swift on the server and backend purposes.
My thought right here is to construct an open-source modular CMS that may be quick, protected and extensible via plugins. Luckily now we now have this wonderful type-safe programming language that we are able to use. Swift is quick and dependable, it’s the good selection for constructing backend apps on the long run. ✅
On this article I want to present you a how one 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 exhibiting me how one can use dlopen
and different related features. 🙏
The magic of dynamic linking
Handmade iOS frameworks are normally bundled with the appliance itself, you may be taught just about all the pieces a few framework if you understand some command line instruments. This time we’re solely going to deal with static and dynamic linking. By default Swift bundle dependencies are linked statically into your utility, however you may change this if you happen to 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 bundle = Package deal(
title: "PluginInterface",
merchandise: [
.library(name: "PluginInterface", type: .dynamic, targets: ["PluginInterface"]),
],
targets: [
.target(name: "PluginInterface", dependencies: []),
]
)
This dynamic PluginInterface
bundle can produce a .dylib
or .so
file, quickly there will probably be a .dll
model as effectively, based mostly on the working system. All of the code bundled into this dynamic library will 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 are able to use a brand new summary class for this objective.
open class PluginBuilder {
public init() {}
open func construct() -> PluginInterface {
fatalError("It's important to override this methodology.")
}
}
That is our dynamic plugin interface library, be at liberty 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 bundle = Package deal(
title: "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 after all implement the PluginInterface
protocol. You’ll be able to lengthen this protocol based mostly in your wants, it’s also possible to 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 avoid wasting our operate below the “createPlugin” image title.
import PluginInterface
@_cdecl("createPlugin")
public func createPlugin() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(PluginABuilder()).toOpaque()
}
remaining class PluginABuilder: PluginBuilder {
override func construct() -> PluginInterface {
PluginA()
}
}
We are able to construct the plugin utilizing the command line, simply run swift construct
within the venture folder. Now you will discover the dylib file below the binary path, be at liberty to run swift construct --show-bin-path
, it will output the required folder. We are going to want each .dylib information for later use.
Loading the plugin at runtime
The core utility may also use the plugin interface as a dependency.
import PackageDescription
let bundle = Package deal(
title: "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 predominant.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 are able to use the dlopen
operate to open the dynamic library file, then we are attempting to get the createPlugin image utilizing the dlsym
methodology. If we now have a pointer we nonetheless must forged that into a legitimate PluginBuilder
object, then we are able to name the construct methodology and return the plugin interface.
Working the app
Now if you happen to attempt to run this utility utilizing Xcode you may get a warning like this:
Class _TtC15PluginInterface13PluginBuilder is applied in each…
One of many two will probably be used. Which one is undefined.
That is associated to an outdated bug, however luckily that’s already resolved. This time Xcode is the dangerous man, since it’s making an attempt to hyperlink all the pieces as a static dependency. Now if you happen to construct the appliance via the command line (swift construct) and place the next information in the identical folder:
- CoreApp
- libPluginA.dylib
- libPluginInterface.dylib
You’ll be able to run the appliance ./CoreApp
wihtout additional points. The app will print out A
with out the warning message, because the Swift bundle supervisor is recognizing that you just want to hyperlink the libPluginInterface
framework as a dynamic framework, so it will not be embedded into the appliance binary. In fact it’s important to arrange the precise plugin path within the core utility.
[ad_2]