3

I found article describing how to create plugin using Swift and Cocoa. It uses NSBundle to load plugin, but that, as far as I know, is not available in pure swift (no Cocoa). Is there way how to achieve same result without using Cocoa?

More info:

In case it's relevant, here is what I want to achieve. I create app in swift that runs on linux server. User can connect to it using their browser. I want to be able to have other people write "plugins" that will implement functionality itself (what user can see and do once they connect), from printing out hello world, through chat programs to games without having to worry about low level stuff provided by my app. Some sort of dll, that my server application loads and runs.

Lope
  • 5,388
  • 4
  • 30
  • 40

2 Answers2

3

Solution to this is not trivial, but it's not impossible to do either. I prefer to use swift package manager to manage dependencies and Xcode as IDE. This combination is not perfect as it needs a lot of tinkering but there is not any other useable free swift IDE as of now.

You will need to set up two projects, let's call them Plugin (3rd party library) and PluginConsumer (app that uses other people plugins). You will also need to decide on API, for now we will use simple

TestPluginFunc()

Create Plugin.swift file with TestPluginFunc implementation in your Plugin project:

public func TestPluginFunc() {
    print("Hooray!")
}

Set the project to build framework, not executable and build[1]. You will get Plugin.framework file which contains your plugin.

Now switch to your PluginConsumer project

Copy Plugin.framework from your Plugin project somewhere where you can easily find it. To actually load the framework and use it:

// we need to define how our plugin function looks like
typealias TestPluginFunc = @convention(c) ()->()
// and what is its name
let pluginFuncName = "TestPluginFunc"

func loadPlugin() {
    let pluginName = "Plugin"
    let openRes = dlopen("./\(pluginName).framework/\(pluginName)", RTLD_NOW|RTLD_LOCAL)
    if openRes != nil {
        // this is fragile
        let symbolName = "_TF\(pluginName.utf8.count)\(pluginName)\(initFuncName.utf8.count)\(initFuncName)FT_T_"
        let sym = dlsym(openRes, symbolName)
        if sym != nil {
            // here we load func from framework based on the name we constructed in "symbolName" variable
            let f: TestPluginFunc = unsafeBitCast(sym, to: TestPluginFunc.self)

            // and now all we need to do is execute our plugin function
            f()

        } else {
            print("Error loading \(realPath). Symbol \(symbolName) not found.")
            dlclose(openRes)
        }
    } else {
        print("error opening lib")
    }
}

If done correctly, you should see "Hooray!" being printed to your log.

There is a lot of room for improvement, first thing you should do is replace Plugin.framework string with parameter, preferably using some file library (I am using PerfectLib). Another thing to look at is defining plugin API in your PluginConsumer project as a protocol or base class, creating framework out of that, importing that framework in your plugin project and basing your implementation on that protocol/base class. I am trying to figure out exactly how to do that. I will update this post if I mange to do it properly.

[1]: I usually do this by creating Package.swift file and creating xcode project out of it using swift package generate-xcodeproj. If your project doesn't contain main.swift, xcode will create framework instead of executable

Lope
  • 5,388
  • 4
  • 30
  • 40
0

What you will want to do is create a folder your program will look in. Let's say it's called 'plugins'. It should make a list of names from the files in there, and then iterate through using them, passing parameters to the files and getting the output and making use of that in some way.

Activating a program and getting output:

func runCommand(cmd : String, args : String...) -> (output: [String], error: [String], exitCode: Int32) {

    var output : [String] = []
    var error : [String] = []

    let task = Process()
    task.launchPath = cmd
    task.arguments = args

    let outpipe = Pipe()
    task.standardOutput = outpipe
    let errpipe = Pipe()
    task.standardError = errpipe

    task.launch()

    let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
    if var string = String(data: outdata, encoding: .utf8) {
        string = string.trimmingCharacters(in: .newlines)
        output = string.components(separatedBy: "\n")
    }

    let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
    if var string = String(data: errdata, encoding: .utf8) {
        string = string.trimmingCharacters(in: .newlines)
        error = string.components(separatedBy: "\n")
    }

    task.waitUntilExit()
    let status = task.terminationStatus

    return (output, error, status)
}

`

Here is how a swift plugin would accept arguments:

    for i in 1..C_ARGC {
    let index = Int(i);

    let arg = String.fromCString(C_ARGV[index])
       switch arg {
        case 1:
            println("1");

        case 2:
            println("2")

        default:
            println("3)
       }
    }

So once you have the program and plugin communicating you just have to add handling in your program based on the output so the plugins output can do something meaningful. Without cocoa libraries this seems the way to go, though if you use C there are a couple of other options available there as well. Hope this helps.

Community
  • 1
  • 1
Milam
  • 176
  • 6