Constructing and loading dynamic libraries at runtime in Swift

[ad_1]

Discover ways 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 through the use of numerous invocation factors and hooks. The one drawback with that method is that you may’t actually activate or off modules on-the-fly, since we often construct our apps in a static approach.

An excellent 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 possibly can add additional performance to the CMS with out recompiling or altering the core. Exterior the Apple ecosystem, there’s a enormous 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 way of plugins. Luckily now we’ve got 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 the best way to 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 the best way to use dlopen and different comparable features. 🙏



The magic of dynamic linking

Handmade iOS frameworks are often bundled with the applying itself, you possibly can be taught just about all the things a couple of framework if you recognize some command line instruments. This time we’re solely going to give attention to static and dynamic linking. By default Swift bundle dependencies are linked statically into your utility, however you possibly can change this should 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 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 likely be a .dll model as properly, based mostly on the working system. All of the code bundled into this dynamic library may 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 are going to want one thing like a builder to assemble the specified object. We are able to use a brand new summary class for this goal.


open class PluginBuilder {
    
    public init() {}

    open func construct() -> PluginInterface {
        fatalError("It's important to override this technique.")
    }
}

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 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 in fact implement the PluginInterface protocol. You possibly can prolong this protocol based mostly in your wants, you can even use different frameworks as dependencies.

import PluginInterface

struct PluginA: PluginInterface {

    func foo() -> String {
        return "A"
    }
}

We’ve 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 title.

import PluginInterface

@_cdecl("createPlugin")
public func createPlugin() -> UnsafeMutableRawPointer {
    return Unmanaged.passRetained(PluginABuilder()).toOpaque()
}

closing 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 mission folder. Now yow will discover the dylib file underneath the binary path, be happy to run swift construct --show-bin-path, it will output the required folder. We’ll want each .dylib recordsdata 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 essential.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 technique. If we’ve got a pointer we nonetheless must solid that into a legitimate PluginBuilder object, then we are able to name the construct technique and return the plugin interface.



Operating the app

Now should you attempt to run this utility utilizing Xcode you will 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 fortuitously that’s already resolved. This time Xcode is the dangerous man, since it’s making an attempt to hyperlink all the things as a static dependency. Now should you construct the applying by way of the command line (swift construct) and place the next recordsdata in the identical folder:

  • CoreApp
  • libPluginA.dylib
  • libPluginInterface.dylib

You possibly can run the applying ./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 applying binary. In fact you need to arrange the best plugin path within the core utility.


[ad_2]

Leave a Reply