Search code examples
javascriptswiftmacosjavascriptcore

JavaScriptCore methods with parameters not being exported from class


I'm running into some weird issues with JavaScriptCore. Whenever I add a method to my class that has a parameter, it doesn't seem to be exported (or is exported with a weird name). For example:

import Cocoa
import JavaScriptCore

@objc
public protocol MyObjectExport: JSExport {
    var property: Int { get set }
    init()
    func test() -> String
    func doNothing(num: Int)
    func squared(num: Int) -> Int
    func sum(a:Int, _ b: Int) -> Int
}

@objc
public class MyObject: NSObject, MyObjectExport {
    public var property: Int {
        get {
            return 5
        }
        set {
            print("New value \(newValue)")
        }
    }

    public required override init() {

    }

    public func test() -> String {
        return "Tested"
    }

    public func doNothing(num: Int) {

    }

    public func squared(num: Int) -> Int {
        return num * num
    }

    public func sum(a: Int, _ b: Int) -> Int {
        return a + b
    }
}

class ViewController: NSViewController {

    override func viewDidLoad() {
        let context = JSContext()!

        context.exceptionHandler = { context, exc in
            print("Exception \(exc)")
        }

        context.setObject(MyObject.self, forKeyedSubscript: NSString(string: "MyObject"))
        context.evaluateScript("var obj = new MyObject()")
        print(context.evaluateScript("obj"))
        print(context.evaluateScript("obj.test()"))
        print(context.evaluateScript("obj.doNothing(5)"))
        print(context.evaluateScript("obj.squared(5)"))
        print(context.evaluateScript("obj.sum(5,5)"))

    }
}

Note that this has to be tested in a macOS app. JavaScriptCore acts even weirder in a playground.

When this is ran, I get the output

<TestProject.MyObject: 0x608000001180>
Tested
Exception Optional(TypeError: obj.doNothing is not a function. (In 'obj.doNothing(5)', 'obj.doNothing' is undefined))
undefined
Exception Optional(TypeError: obj.squared is not a function. (In 'obj.squared(5)', 'obj.squared' is undefined))
undefined
Exception Optional(TypeError: obj.sum is not a function. (In 'obj.sum(5,5)', 'obj.sum' is undefined))
undefined

As you can see, the initiator works and so does the method test. However, all other methods with parameters do not seem to be exported or they are exported under some other method name I can not find.

Any help would be much appreciated.


Solution

  • Since Swift 3, the first parameter is no longer longer unnamed by default.

    You cannot call doNothing, squared, and sum from Swift code like this:

    doNothing(5)
    squared(5)
    sum(5,5)
    

    You must include the argument names:

    doNothing(num: 5)
    squared(num: 5)
    sum(a: 5,5)    //argument name not required for 'b' because it is '_'
    

    Those methods get the Objective-C selectors doNothingWithNum:, squaredWithNum:, and sumWithA::. These are exported to JavaScript with the following rules, according to the documentation for JSExport:

    When exporting a selector that takes one or more arguments, JavaScriptCore generates a corresponding function name using the following conversion:

    • All colons are removed from the selector.

    • Any lowercase letter that had followed a colon is capitalized.

    So doNothing, squared, and sum are called doNothingWithNum(), squaredWithNum(), and sumWithA(). You have to either change your JavaScript to call the methods with those names:

    print(context.evaluateScript("obj.doNothingWithNum(5)"))
    print(context.evaluateScript("obj.squaredWithNum(5)"))
    print(context.evaluateScript("obj.sumWithA(5,5)"))
    

    Or change your class & protocol definitions to remove the parameter names:

    @objc
    public protocol MyObjectExport: JSExport {
        var property: Int { get set }
        init()
        func test() -> String
        func doNothing(_ num: Int)
        func squared(_ num: Int) -> Int
        func sum(_ a: Int, _ b: Int) -> Int
    }
    
    @objc
    public class MyObject: NSObject, MyObjectExport {
        public var property: Int {
            get {
                return 5
            }
            set {
                print("New value \(newValue)")
            }
        }
    
        public required override init() {
    
        }
    
        public func test() -> String {
            return "Tested"
        }
    
        public func doNothing(_ num: Int) {
    
        }
    
        public func squared(_ num: Int) -> Int {
            return num * num
        }
    
        public func sum(_ a: Int, _ b: Int) -> Int {
            return a + b
        }
    }