Last active
January 21, 2021 17:00
-
-
Save leonbreedt/f242f9423af8d65a84cefadcf6a70945 to your computer and use it in GitHub Desktop.
JavaScriptCore custom property JSValue lifetime
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Cocoa | |
import JavaScriptCore | |
/// Something that can be represented as a `JSValue`. | |
protocol JSValueRepresentable { | |
/// The `JSValue` for this thing. | |
var jsValue: JSValue { get } | |
} | |
/// Represents a mutable property, with a `JSValue` that is not copied every time | |
/// the property is accessed, unlike the default `JSExport` behavior. | |
class ExtensionProperty { | |
/// The property name. | |
let name: String | |
/// The `JSValue` for the property. | |
var value: JSValue | |
init(name: String, value: JSValue) { | |
self.name = name | |
self.value = value | |
} | |
} | |
/// Represents an object that will be passed into JavaScript extensions. | |
/// Intentionally not using `JSExport` protocol, because its default behavior is | |
/// to always create a copy of a property on access, which does not yield a great | |
/// API for extensions, especially not for things like header mutation. | |
class ExtensionObject: NSObject, JSValueRepresentable { | |
let context: JSContext | |
let properties: [ExtensionProperty] | |
let proxyClass: JSClassRef | |
var target: AnyObject? | |
/// Creates a new `ExtensionObject` with a list of mutable properties. Any attempts | |
/// to access properties not found in the list will be forwarded on to a target object. | |
/// | |
/// - parameter context: The `JSContext` in which this object should live. | |
/// - parameter properties: The list of mutable properties. | |
/// - parameter target: The object to forward any non-mutable property requests to. | |
init(context: JSContext, properties: [ExtensionProperty], target: AnyObject?) { | |
self.context = context | |
self.properties = properties | |
self.target = target | |
self.proxyClass = ExtensionObject.defineProxyClass() | |
super.init() | |
} | |
deinit { | |
JSClassRelease(proxyClass) | |
} | |
// MARK: - API | |
var jsValue: JSValue { | |
/// We want to keep `self` alive until the JSValue gets GC'd. | |
let object = JSObjectMake(context.JSGlobalContextRef, proxyClass, retainedPointerFor(self)) | |
return JSValue(JSValueRef: object, inContext: context) | |
} | |
// MARK: - Private | |
private static func defineProxyClass() -> JSClassRef { | |
var definition = kJSClassDefinitionEmpty | |
definition.getProperty = { context, object, name, exception in | |
guard let proxy = JSObjectGetPrivate(object).dataValueOf(type: ExtensionObject.self) else { | |
return JSValueMakeNull(context) | |
} | |
let name = (JSStringCopyCFString(kCFAllocatorDefault, name) as NSString) as String | |
return proxy.getProperty(named: name).JSValueRef | |
} | |
definition.setProperty = { context, object, name, value, exception in | |
guard let proxy = JSObjectGetPrivate(object).dataValueOf(type: ExtensionObject.self) else { | |
return false | |
} | |
let name = (JSStringCopyCFString(kCFAllocatorDefault, name) as NSString) as String | |
return proxy.setProperty(named: name, | |
toValue: JSValue(JSValueRef: value, inContext: proxy.context)) | |
} | |
definition.finalize = { object in | |
JSObjectGetPrivate(object).releasePointer() | |
} | |
return JSClassCreate(&definition) | |
} | |
private func getProperty(named name: String) -> JSValue { | |
if let customProperty = properties.filter({$0.name == name}).first { | |
return customProperty.value | |
} else { | |
guard let targetProperty = Mirror(reflecting: target!).children.filter({ $0.label == name }).first where target != nil else { | |
return JSValue(nullInContext: context) | |
} | |
return anyAsJSValue(targetProperty.value, inContext: context) | |
} | |
} | |
private func setProperty(named name: String, toValue value: JSValue) -> Bool { | |
if let customProperty = properties.filter({$0.name == name}).first { | |
customProperty.value = value | |
return true | |
} else { | |
if let target = target as? NSObject { | |
let startIndex = name.startIndex | |
let firstCharacter = name.substringWithRange(startIndex...startIndex).uppercaseString | |
let remainderOfName = name.substringWithRange(startIndex.advancedBy(1)...name.endIndex.predecessor()) | |
if target.respondsToSelector(Selector("set\(firstCharacter)\(remainderOfName):")) { | |
target.setValue(value.toObject(), forKey: name) | |
} | |
return true | |
} | |
} | |
return false | |
} | |
} | |
/// Returns a `JSValue` for a particular value. Supports integral, boolean, floating point | |
/// and object values. | |
/// | |
/// - parameter any: A value for which to produce `JSValue`. | |
/// - parameter context: The `JSContext` in which the value should live. | |
/// - returns: A `JSValue` for the suppplied value. | |
func anyAsJSValue(any: Any, inContext context: JSContext) -> JSValue { | |
var jsValue = JSValue(nullInContext: context) | |
switch any { | |
case let int as Int: | |
jsValue = JSValue(int32: Int32(int), inContext: context) | |
case let uint as UInt: | |
jsValue = JSValue(UInt32: UInt32(uint), inContext: context) | |
case let uint8 as UInt8: | |
jsValue = JSValue(UInt32: UInt32(uint8), inContext: context) | |
case let uint16 as UInt16: | |
jsValue = JSValue(UInt32: UInt32(uint16), inContext: context) | |
case let uint32 as UInt32: | |
jsValue = JSValue(UInt32: UInt32(uint32), inContext: context) | |
case let uint64 as UInt64: | |
jsValue = JSValue(UInt32: UInt32(uint64), inContext: context) | |
case let bool as Bool: | |
jsValue = JSValue(bool: bool, inContext: context) | |
case let float as Float: | |
jsValue = JSValue(double: Double(float), inContext: context) | |
case let double as Double: | |
jsValue = JSValue(double: double, inContext: context) | |
case let point as CGPoint: | |
jsValue = JSValue(point: point, inContext: context) | |
case let range as NSRange: | |
jsValue = JSValue(range: range, inContext: context) | |
case let rect as CGRect: | |
jsValue = JSValue(rect: rect, inContext: context) | |
case let size as CGSize: | |
jsValue = JSValue(size: size, inContext: context) | |
case let object as AnyObject: | |
jsValue = JSValue(object: object, inContext: context) | |
default: | |
break | |
} | |
return jsValue | |
} | |
/// Increases the retain count of an object, and returns an `UnsafeMutablePointer<Void>` | |
/// that can be passed into native code. | |
/// | |
/// - parameter value: The object to retain. | |
///.- returns: An `UnsafeMutablePointer<Void>` that can be passed into native code. | |
private func retainedPointerFor(value: AnyObject) -> UnsafeMutablePointer<Void> { | |
return UnsafeMutablePointer(Unmanaged.passRetained(value).toOpaque()) | |
} | |
private extension UnsafeMutablePointer { | |
/// Returns the object associated with a pointer previously returned by `retainedPointerFor()`. Does | |
/// not affect retain count. | |
/// | |
/// - parameter type: The type of the object to return. | |
/// - returns: The object, or `nil` if this pointer is `nil`. | |
func dataValueOf<T: AnyObject>(type type: T.Type) -> T? { | |
guard self != nil else { return nil } | |
return Unmanaged<T>.fromOpaque(COpaquePointer(self)).takeUnretainedValue() | |
} | |
/// Decreases the retain count of an object associated with a pointer previously returned by `retainedPointerFor()`. | |
func releasePointer() { | |
guard self != nil else { return } | |
let _ = Unmanaged<AnyObject>.fromOpaque(COpaquePointer(self)).takeRetainedValue() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment