JavaScriptCore
Whether you love it or hate it, JavaScript has become the most important language for developers in the world today. Yet despite any efforts we may take to change or replace it we’d be hard-pressed to deny its usefulness.
What is JavaScriptCore
?
JavaScriptCore is a framework that provides a JavaScript engine for macOS and iOS. It allows developers to run JavaScript code within their applications. JavaScriptCore is a part of the WebKit framework and is used by Safari, Mail, and many other applications on macOS and iOS.
JavaScriptCore / Objective-C / Swift Types
JavaScriptCore uses a set of types to represent JavaScript values in Objective-C and Swift. These types are used to interact with JavaScript code and pass values between JavaScript and native code.
Here is a table that shows the mapping between JavaScript types and Objective-C/Swift types:
JavaScript Type | JSValue method | Objective-C Type | Swift Type |
---|---|---|---|
string | toString | NSString | String! |
boolean | toBool | BOOL | Bool |
number | toNumber | NSNumber | NSNumber! |
number | toDouble | double | Double |
number | toInt32 | int32_t | Int32 |
number | toUInt32 | uint32_t | UInt32 |
date | toDate | NSDate | Date? |
array | toArray | NSArray | [Any]! |
object | toDictionary | NSDictionary | [AnyHashable: Any]! |
class | toObject | custom type | custom type |
Simple Example
Here’s a simple example that evaluates a JavaScript expression and prints the result:
import JavaScriptCore
let context = JSContext()!
let result = context.evaluateScript("1 + 2 + 3")
result?.toInt32() // 6
Handling Exceptions
If a script throws an exception, JavaScriptCore calls the exception handler you provide. The handler receives the context and the exception object, which you can use to get the exception message.
below is an example how to create an exception handler:
import JavaScriptCore
let context = JSContext()!
context.exceptionHandler = { context, exception in
print(exception!.toString())
}
context.evaluateScript("**INVALID**")
// Prints "SyntaxError: Unexpected token '**'"
Note: You can’t tell whether a script evaluated successfully based on its return value. For instance, both variable assignment and syntax errors produce undefined return values.
(Hacky) Way to dertimine if a script was evaluated successfully
If we want to determine if a script was evaluated successfully, we can set a variable in the script and then check if that variable exists in the context.
if the variable exists, the script was evaluated successfully.
import JavaScriptCore
let context = JSContext()!
let result = context.evaluateScript("1 + 2 + 3; var didRun = true;")
let didRun = context.objectForKeyedSubscript("didRun")?.toBool()
Virtual Machines and Contexts
JavaScriptCore uses a virtual machine to manage memory and resources. A virtual machine can have multiple contexts, each of which is a separate JavaScript environment. Contexts are isolated from each other, so they can’t share variables or functions.
let queue = DispatchQueue(label: "myVirtualMachineQueue")
let vm = queue.sync { JSVirtualMachine()! }
let context = JSContext(virtualMachine: vm)!
Creating a function in JavaScript
JavaScript evaluation isn’t limited to single statements. When you evaluate code that declares a function or variable, it’s saved into the context’s object space.
context.evaluateScript("""
function twoTimes(number) {
return number * 2;
}
""")
context.evaluateScript("twoTimes(5)")?
.toInt32() // 10
Getting JavaScript Context Values from Swift
You can access named values from a JSContext by calling the objectForKeyedSubscript(_:)
method. This method returns a JSValue object that represents the value in the context.
context.evaluateScript("var twoTimesFive = twoTimes(5)")
context.objectForKeyedSubscript("twoTimesFive")?
.toInt32() // 10
Setting Swift Values on a JavaScript Context
You can set a Swift value in a JSContext by calling the setObject(_:forKeyedSubscript:)
method. This method creates a new variable in the context with the given name and value.
context.setObject(10, forKeyedSubscript: "swiftValue")
context.evaluateScript("swiftValue")?
.toInt32() // 10
Passing Functions between Swift and JavaScript
You can pass Swift functions to JavaScript by creating a JSValue object from a block. This object can be called from JavaScript as if it were a regular JavaScript function.
let swiftFunction: @convention(block) (Int) -> Int = {
return $0 * 2
}
let jsFunction = JSValue(object: swiftFunction, in: context)
context.setObject(swiftFunction,
forKeyedSubscript: "swiftFunction" as NSString)
context.evaluateScript("swiftFunction(5)")?
.toInt32() // 10
context.objectForKeyedSubscript("swiftFunction")?
.call(withArguments: [6]) // 12
Passing Swift Objects between Swift and JavaScript
To improve interoperability between language contexts, JavaScriptCore provides the JSExport protocol, which allows native classes to be mapped and initialized directly.
import JavaScriptCore
@objc protocol MyExport: JSExport {
var value: Int { get set }
func doubleValue() -> Int
func doubleTheValue(value: Int) -> Int
}
class MyObject: NSObject, MyExport {
var value: Int = 0
func doubleValue() -> Int {
return value * 2
}
func doubleTheValue(value: Int) -> Int {
return value * 2
}
}
let context = JSContext()!
context.setObject(
MyObject.self,
forKeyedSubscript: "myObject" as NSString
)
context.evaluateScript("""
var obj = new myObject();
obj.value = 5;
obj.doubleValue(); // 10
""")
context.evaluateScript("""
myObject.doubleTheValue(5) // 10
""")
Conclusion
JavaScriptCore is a powerful framework that allows you to run JavaScript code within your applications.
JavaScriptCore provides a simple API for evaluating JavaScript code, handling exceptions, creating functions, and passing values between Swift and JavaScript. By using JavaScriptCore, you can add dynamic scripting capabilities to your applications and create powerful, interactive user experiences.
Resources:
- https://developer.apple.com/documentation/javascriptcore
- https://github.com/0xWDG/AuroraEditor/tree/development/AuroraEditor/Core/ExtensionSystem/Models/JavaScript%20Support
Read more
- Deep linking in SwiftUI • 4 minutes reading time.
- iCloud Drive Tips & Tricks • 8 minutes reading time.
- Implementing Sign in with Apple • 6 minutes reading time.
Share
Share Mastodon Twitter LinkedIn Facebook