1

Context

I have an instance of class called Solution and I have a function name as a string functionName that I want to call on the Solution instance solutionInstance. I have the parameters for the function in an array and I'd like to pass those as well.

I am using the Swift compiler to compile all of my .swift files together (swiftc with a files enumerated and then -o and the output file name) then I run the final output.

Python Example

Here is how I do this in Python:

method = getattr(solutionInstance, functionName) # get method off of instance for function
programOutput = method(*testInputsParsed) # pass the list of parameters & call the method

Purpose

This is server-side code that runs in a container to run a user's code. This code lives in a "Driver" main.swift file that calls the methods and orchestrates testing.

Problem

Swift is statically typed and I've been searching around and most sources say there is limited reflection support in Swift (and suggest to "reach into Objective-C" to get the functionality desired).

Swift is not my native language (TypeScript/JavaScript, Java, Python strongest, then C# and C++ mild, then just implementing Swift code for this feature now) so I'm not sure what that means and I haven't been able to find a definitive answer.

Question

How can I call a function by its name on a Solution class instance (it implements no protocols, at least by me) and pass an array of parameters in Swift (using reflection)? How does my setup need to change to make this happen (importing libraries, etc.)

Thank you!

Referenced Posts
Benyam Ephrem
  • 448
  • 6
  • 20

1 Answers1

1

First of all, as you noted Swift doesn't have full reflection capabilities and rely on the coexisting ObjC to provide these features.

So even if you can write pure Swift code, you will need Solution to be a subclass of NSObject (or implement NSObjectProtocol).

Playground sample:

class Solution: NSObject {

    @objc func functionName(greeting: String, name: String) {
        print(greeting, name)
    }

}

let solutionInstance = Solution() as NSObject
let selector = #selector(Solution.functionName)
if solutionInstance.responds(to: selector) {
    solutionInstance.perform(selector, with: "Hello", with: "solution")
}

There are other points of concern here:

  • Swift's perform is limited to 2 parameters
  • you need to have the exact signature of the method (#selector here)

If you can stick an array in the first parameters, and alway have the same signature then you're done. But if you really need to go further you have no choice than to go with ObjC, which doesn't work in Playground.

You could create a Driver.m file of the like:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

id call (NSObject *callOn, NSString *callMethod, NSArray <NSObject *>*callParameters)
{
    void *result = NULL;
    unsigned int index, count;

    Method *methods = class_copyMethodList(callOn.class, &count);
    for (index = 0; index < count; ++index)
    {
        Method method = methods[index];

        struct objc_method_description *description = method_getDescription(method);
        NSString *name = [NSString stringWithUTF8String:sel_getName(description->name)];
        if ([name isEqualToString:callMethod])
        {
            NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:description->types];
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];

            NSObject *parameters[callParameters.count];
            for (int p = 0; p < callParameters.count; ++p) {
                parameters[p] = [callParameters objectAtIndex:p];
                [invocation setArgument:&parameters[p] atIndex:p + 2]; // 0 is self 1 is SEL
            }
            [invocation setTarget:callOn];
            [invocation setSelector:description->name];
            [invocation invoke];
            [invocation getReturnValue:&result];
            break;
        }
    }
    free(methods);

    return (__bridge id)result;
}

Add it to a bridging-header (for Swift to know about what is in ObjC):

// YourProjectName-Bridging-Header.h
id call (NSObject *callOn, NSString *callMethod, NSArray *callParameters);

And call it with a Solution.swift like this:

import Foundation

class Solution: NSObject {

    override init() {
        super.init()
        // this should go in Driver.swift
        let result = call(self, "functionNameWithGreeting:name:", ["Hello", "solution"])
        print(result as Any)
    }

    @objc
    func functionName(greeting: String, name: String) -> String {
        print(greeting, name)
        return "return"
    }

}

output:

Hello solution
Optional(return)

Edit: compilation

To compile both ObjC and Swift on the command line you can first compile ObjC to an object file:

$ cc -O -c YouObjCFile.m

Then compile your Swift project with the bridging header and the object file:

$ swiftc -import-objc-header ../Your-Bridging-Header.h YouObjCFile.o AllYourSwiftFiles.swift -o program

working sample

blld
  • 1,030
  • 10
  • 19