Monday, March 27, 2023
HomeiOS DevelopmentConstructing and loading dynamic libraries at runtime in Swift

Constructing and loading dynamic libraries at runtime in Swift


Learn to create a plugin system utilizing dynamic libraries and the ability 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 method is that you could’t actually activate or off modules on-the-fly, since we often construct our apps in a static approach.

A great 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 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 functions.


My thought right here is to construct an open-source modular CMS that may be quick, secure and extensible by way of plugins. Fortuitously now we have now this wonderful type-safe programming language that we will 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 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 use dlopen and different related capabilities. 🙏



The magic of dynamic linking

Handmade iOS frameworks are often bundled with the appliance itself, you’ll be able to be taught just about the whole lot a couple of framework if you recognize 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 software, however you’ll be able to 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 package deal = Package deal(
    title: "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 can be a .dll model as nicely, based mostly on the working system. All of the code bundled into this dynamic library may be shared between different functions. 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 function.


open class PluginBuilder {
    
    public init() {}

    open func construct() -> PluginInterface {
        fatalError("It's a must 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 package deal = 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’ll be able to prolong this protocol based mostly in your wants, you too can use different frameworks as dependencies.

import PluginInterface

struct PluginA: PluginInterface {

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

We now have to subclass the PluginBuilder class and return our plugin implementation. We’re going to use the @_cdecl attributed create perform to entry our plugin builder from the core app. This Swift attribute tells the compiler to avoid wasting our perform underneath 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 challenge folder. Now you could find the dylib file underneath the binary path, be happy 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 software will even use the plugin interface as a dependency.


import PackageDescription

let package deal = 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 will 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 are able to use the dlopen perform to open the dynamic library file, then we try to get the createPlugin image utilizing the dlsym technique. If we have now a pointer we nonetheless have to forged that into a sound PluginBuilder object, then we will name the construct technique and return the plugin interface.



Working the app

Now if you happen to attempt to run this software utilizing Xcode you will get a warning like this:

Class _TtC15PluginInterface13PluginBuilder is carried out in each…
One of many two can be used. Which one is undefined.

That is associated to an previous bug, however luckily that’s already resolved. This time Xcode is the dangerous man, since it’s attempting to hyperlink the whole lot as a static dependency. Now if you happen to construct the appliance by way of 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 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 appliance binary. After all you must arrange the fitting plugin path within the core software.


RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments