Skip to content

Instantly share code, notes, and snippets.

@leonbreedt
Last active January 21, 2021 17:00
Show Gist options
  • Save leonbreedt/f242f9423af8d65a84cefadcf6a70945 to your computer and use it in GitHub Desktop.
Save leonbreedt/f242f9423af8d65a84cefadcf6a70945 to your computer and use it in GitHub Desktop.
JavaScriptCore custom property JSValue lifetime
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