Skip to content

Instantly share code, notes, and snippets.

@leonbreedt
Last active January 21, 2021 17:00
Show Gist options
  • Select an option

  • Save leonbreedt/f242f9423af8d65a84cefadcf6a70945 to your computer and use it in GitHub Desktop.

Select an option

Save leonbreedt/f242f9423af8d65a84cefadcf6a70945 to your computer and use it in GitHub Desktop.

Revisions

  1. leonbreedt revised this gist Jun 11, 2016. 1 changed file with 29 additions and 29 deletions.
    58 changes: 29 additions & 29 deletions CustomPropertyJSValueLifetime.swift
    Original file line number Diff line number Diff line change
    @@ -62,35 +62,6 @@ class ExtensionObject: NSObject, JSValueRepresentable {
    return JSValue(JSValueRef: object, inContext: context)
    }

    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)
    }
    }

    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
    }

    // MARK: - Private

    private static func defineProxyClass() -> JSClassRef {
    @@ -118,6 +89,35 @@ class ExtensionObject: NSObject, JSValueRepresentable {

    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
  2. leonbreedt revised this gist Jun 11, 2016. 1 changed file with 42 additions and 39 deletions.
    81 changes: 42 additions & 39 deletions CustomPropertyJSValueLifetime.swift
    Original file line number Diff line number Diff line change
    @@ -50,12 +50,49 @@ class ExtensionObject: NSObject, JSValueRepresentable {
    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)
    }

    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)
    }
    }

    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
    }

    // MARK: - Private

    private static func defineProxyClass() -> JSClassRef {
    var definition = kJSClassDefinitionEmpty

    @@ -81,40 +118,6 @@ class ExtensionObject: NSObject, JSValueRepresentable {

    return JSClassCreate(&definition)
    }

    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)
    }
    }

    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 obj = target as? NSObject {
    let first = name.substringWithRange(name.startIndex...name.startIndex).uppercaseString
    let remainder = name.substringWithRange(name.startIndex.advancedBy(1)...name.endIndex.predecessor())
    let selector = Selector("set\(first)\(remainder):")
    if obj.respondsToSelector(selector) {
    obj.setValue(value.toObject(), forKey: name)
    }
    return true
    }
    }
    return false
    }

    deinit {
    log.debug("Deinitializing \(self)")
    JSClassRelease(proxyClass)
    }
    }

    /// Returns a `JSValue` for a particular value. Supports integral, boolean, floating point
    @@ -129,15 +132,15 @@ func anyAsJSValue(any: Any, inContext context: JSContext) -> JSValue {
    case let int as Int:
    jsValue = JSValue(int32: Int32(int), inContext: context)
    case let uint as UInt:
    jsValue = JSValue(int32: Int32(uint), inContext: context)
    jsValue = JSValue(UInt32: UInt32(uint), inContext: context)
    case let uint8 as UInt8:
    jsValue = JSValue(int32: Int32(uint8), inContext: context)
    jsValue = JSValue(UInt32: UInt32(uint8), inContext: context)
    case let uint16 as UInt16:
    jsValue = JSValue(int32: Int32(uint16), inContext: context)
    jsValue = JSValue(UInt32: UInt32(uint16), inContext: context)
    case let uint32 as UInt32:
    jsValue = JSValue(int32: Int32(uint32), inContext: context)
    jsValue = JSValue(UInt32: UInt32(uint32), inContext: context)
    case let uint64 as UInt64:
    jsValue = JSValue(int32: Int32(uint64), inContext: context)
    jsValue = JSValue(UInt32: UInt32(uint64), inContext: context)
    case let bool as Bool:
    jsValue = JSValue(bool: bool, inContext: context)
    case let float as Float:
  3. leonbreedt created this gist Jun 11, 2016.
    190 changes: 190 additions & 0 deletions CustomPropertyJSValueLifetime.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,190 @@
    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()
    }

    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)
    }

    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)
    }

    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)
    }
    }

    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 obj = target as? NSObject {
    let first = name.substringWithRange(name.startIndex...name.startIndex).uppercaseString
    let remainder = name.substringWithRange(name.startIndex.advancedBy(1)...name.endIndex.predecessor())
    let selector = Selector("set\(first)\(remainder):")
    if obj.respondsToSelector(selector) {
    obj.setValue(value.toObject(), forKey: name)
    }
    return true
    }
    }
    return false
    }

    deinit {
    log.debug("Deinitializing \(self)")
    JSClassRelease(proxyClass)
    }
    }

    /// 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(int32: Int32(uint), inContext: context)
    case let uint8 as UInt8:
    jsValue = JSValue(int32: Int32(uint8), inContext: context)
    case let uint16 as UInt16:
    jsValue = JSValue(int32: Int32(uint16), inContext: context)
    case let uint32 as UInt32:
    jsValue = JSValue(int32: Int32(uint32), inContext: context)
    case let uint64 as UInt64:
    jsValue = JSValue(int32: Int32(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()
    }

    }